diff --git a/.github/workflows/eval-with-entra-id.yml b/.github/workflows/eval-with-entra-id.yml index ba10d72fc..41ae23606 100644 --- a/.github/workflows/eval-with-entra-id.yml +++ b/.github/workflows/eval-with-entra-id.yml @@ -1,9 +1,9 @@ name: eval-with-entra-id on: push: - branches: [main] + branches: [main, v3] pull_request: - branches: [main] + branches: [main, v3] permissions: contents: read diff --git a/.github/workflows/pr-eval-shell-tests.yml b/.github/workflows/pr-eval-shell-tests.yml index c4e42933c..ed36e2a72 100644 --- a/.github/workflows/pr-eval-shell-tests.yml +++ b/.github/workflows/pr-eval-shell-tests.yml @@ -1,9 +1,9 @@ name: pr-eval-shell-tests on: push: - branches: [main] + branches: [main, v3] pull_request: - branches: [main] + branches: [main, v3] permissions: contents: read diff --git a/.github/workflows/pr-l1-static-fast.yml b/.github/workflows/pr-l1-static-fast.yml index be454ca57..1ff095460 100644 --- a/.github/workflows/pr-l1-static-fast.yml +++ b/.github/workflows/pr-l1-static-fast.yml @@ -4,9 +4,9 @@ name: pr-l1-static-fast on: pull_request: - branches: [main] + branches: [main, v3] push: - branches: [main] + branches: [main, v3] permissions: contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index b745d112a..587b066d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,32 @@ deprecations ship one minor release before removal. ## [Unreleased] +### Added + +- Proposed ADR 0046 and its complete, documentation-only normative + specification set for the d2b 3.0 Provider control plane, under user review. + The atomic set has 55 member specs (28 foundation, resource, cross-cutting, + and closing specs plus a 27-Provider dossier catalog) indexed by + `docs/specs/README.md` and `docs/specs/providers/README.md` and bound by the + generated `ADR-046-spec-set.json` and `ADR-046-work-items.json` manifests. + It specifies Zone-local resources over the 19 standard ResourceTypes + (including `Endpoint`), independently packaged multi-process Providers + selected by `{ artifactId, config }`, an asynchronous embedded redb resource + plane with owner-driven reconciliation and commit-gated expedited reconcile, + status-first component state with optional Volumes, layered base-plus-provider + ResourceType specs with three-layer status, resource currency/upgrade/recycle + with CLI projections, Guest-resident Entrablau identity custody, fast hermetic + tests with integration-only slow coverage, and Noise-protected + ComponentSession/d2b-bus channels on the `d2bus.org` public namespace, + together with the security/threat-model, reset/cutover, feasibility, + validation and delivery, and streamline contracts. Everything remains + Proposed; no crates, services, controllers, or Providers are created. + +### Changed + +- Enabled the required Layer-1, eval-shell, and Entra example PR gates for + changes targeting the `v3` branch as well as `main`. + ## [1.4.1] - 2026-07-12 ### Added diff --git a/docs/adr/0046-d2b-3-provider-control-plane.md b/docs/adr/0046-d2b-3-provider-control-plane.md new file mode 100644 index 000000000..a6d5ec0d3 --- /dev/null +++ b/docs/adr/0046-d2b-3-provider-control-plane.md @@ -0,0 +1,545 @@ +# ADR 0046: d2b 3.0 Provider control plane + +- Status: Proposed +- Date: 2026-07-22 +- Baseline: protected `v3` commit + `b5ddbed67867d9244bf33390868101bd9b053e49` +- Related: [ADR 0028](0028-guest-control-plane-over-vsock.md) + (typed guest control), [ADR 0034](0034-storage-lifecycle-restart-and-synchronization.md) + (storage/restart ownership), [ADR 0037](0037-local-hypervisor-runtime-seam.md) + (runtime Provider seam), [ADR 0043](0043-realm-native-control-plane.md) + (pre-v3 Realm architecture), and + [ADR 0044](0044-unsafe-local-runtime-provider.md) + (unsafe-local Provider) + +## Status and output + +This ADR defines the d2b v3 architecture from the protected pre-ADR-0045 +branch. Main-branch ADR 0045 is not ancestry or current v3 behavior, but its +code may be copied/adapted without restriction when an exact work item proves +the source, selected behavior, destination, integration, tests, and excluded +ADR 0045 assumptions. + +The decision is a concise parent for the normative +[`docs/specs/ADR-046-*`](../specs/README.md) specification set. The set, its +indexes, and this ADR are accepted as one unit. + +This decision PR delivers documentation only. It does not create v3 crates, +dependencies, Nix modules, services, controllers, Providers, state stores, or +reset behavior. Future implementation requires a separate request after the +set is Accepted. + +Every normative ResourceType/Provider spec defines its Nix authoring form, +canonical rendered ResourceSpec, and NixOS eval/build schema/reference +validation. Nix mirrors the ResourceSpec `spec` shape directly; only name, +Zone, and apiVersion are derived/defaulted, and status is controller-owned. Nix +emits an integrity-pinned per-Zone resource generation. +Removing a configured resource activates the new generation immediately and +requests asynchronous owner/finalizer-safe deletion, with visible Degraded +cleanup status; dynamic controller-owned resources are not broadly swept. + +## Context + +The v3 baseline has useful foundations: + +- typed Provider traits in `d2b-realm-provider`; +- production ACA and Relay integrations; +- codec-neutral Realm operations, idempotency, targets, capabilities, and + stream state machines; +- typed guest control over ttrpc/vsock; +- typed process DAGs, minijail profiles, broker effects, pidfd adoption, and + generated storage/synchronization contracts; +- normalized Nix indexes and integrity-pinned bundle artifacts; +- user-session process/scope precedents in unsafe-local; +- separate journald/OTEL and authoritative audit paths. + +It does not have a live generic Provider registry, universal ComponentSession, +Provider toolkit/client, native resource API/store, generic Provider state, or +production Realm peer frontend. Several Provider/transport/codec/client crates +are test-only or unwired. Realm artifacts and the allocator remain explicitly +metadata-only. + +A kcp feasibility spike proved useful object semantics—typed spec/status, +revisions, watches, optimistic conflicts, owner/finalizer behavior, hierarchy, +and controller clients—but measured approximately 490 MiB RSS and a 176 MiB +executable. That footprint is unsuitable for recursive host/Guest/Zone use. + +## Decision + +### Zones and resources + +`Zone` replaces Realm as the v3 public isolation, policy, routing, resource, +state, and audit unit. Every Zone owns: + +- one Zone runtime; +- one embedded redb database and `d2b.resource.v3` service; +- one authoritative `Zone/` self resource; +- one fixed core-controller process; +- Zone-local Provider, Host, Guest, controller, policy, and ordinary resources. + +Zone.spec is empty. Zone-wide ceilings and emergency controls are separate +Quota and EmergencyPolicy resources with their own controllers/status. + +Every resource belongs to one Zone. A parent represents a child with a local +`ZoneLink/` resource and accesses the child's resources through the child +Zone API. Ordinary resource references never cross Zones. + +### Resource references and ownership + +Every `*Ref` field is: + +```text +/ +``` + +It resolves a named resource in the same Zone. Plain enums and inline values do +not use a `Ref` suffix. Standard ResourceTypes are short and Zone-unique; +vendor ResourceTypes are qualified; API binding rejects collisions. + +Every resource has zero or one `ownerRef`. Any committed child mutation +triggers reconciliation of the owner through a reverse owner index. The owner +controller relists its children and restores the complete desired child set and +configuration. Owner cycles fail, deletion is child-first/finalizer-aware, and +immutable UIDs prevent delete/recreate name confusion. + +### Standard ResourceTypes + +The frozen catalog (D035) has **19 standard, unqualified, Zone-unique +ResourceTypes**: + +- control plane: `Zone`, `ZoneLink`, `Provider`, `Role`, `RoleBinding`, + `Quota`, `EmergencyPolicy`; +- execution: `Host`, `Guest`, `Process`, `EphemeralProcess`, `User`; +- primitives: `Volume`, `Network`, `Device`, `Credential`; +- connectivity: `Endpoint` (added by **D092**); +- cross-Zone sharing: `ResourceExport`, `ResourceImport` (added by **D096**). + +`Endpoint` promotes stable managed endpoint identities (TPM/GPU/Wayland/vhost/ +QMP/guest-control/service/listener endpoints) from opaque IDs to first-class +resources referenced by `Endpoint/` refs; `ProcessSpec` no longer carries +an inline `endpoints` field, and per-session/high-churn handles +(pidfd, fd index, named-stream id, transport byte-stream handle) stay internal. +A general promotion test (D092) decides ResourceType vs opaque handle for every +entity. `ResourceExport`/`ResourceImport` (D096/D098) share only a qualified semantic +owner Service across Zones. The frozen provider-neutral pairs are +`audio.d2bus.org.AudioService` + `audio.d2bus.org.AudioBinding`, +`security-key.d2bus.org.SecurityKeyService` + +`security-key.d2bus.org.SecurityKeyBinding`, +`telemetry.d2bus.org.TelemetryService` + +`telemetry.d2bus.org.TelemetryBinding`, and `usb.d2bus.org.UsbService` + +`usb.d2bus.org.UsbBinding`. Core creates one same-type projection Service per +import; Nix/operators author Bindings, and Provider controllers realize their +Process/Endpoint children. Device, Endpoint, and Binding are never projections. + +Every ResourceType is **layered**: a Provider implements the ResourceType +**base spec plus a strict provider extension** (**D089**), and status is the +**three-layer** common/base/provider shape (**D088**). Vendor ResourceTypes and +every Provider spec/status extension schema are qualified on the project's +public domain **`d2bus.org`** — `.d2bus.org.` and +`.d2bus.org//spec` (**D080**); there is no legacy +project-domain alias (clean reset). API binding rejects ResourceType collisions. + +```yaml +apiVersion: ... +type: ... +metadata: + name: ... + zone: ... + uid: ... + generation: ... + revision: ... + ownerRef: ... + createdAt: ... + updatedAt: ... +spec: {} +status: + observedGeneration: ... + phase: ... + conditions: [] + lastReconciledAt: ... + startedAt: ... + completedAt: ... + outcome: ... +``` + +`spec` and `status` are always present. Common identity, Zone, ownership, +revision, and timestamps remain in metadata. Status is a separately authorized +controller-owned subresource. It stores the latest bounded observation: +numeric `observedGeneration`, closed phase, conditions and transition/reconcile +datetimes, stable outcome code, optional process exit code, detailed bounded +redacted message, retryability, and ResourceType-specific fields. Earlier +status versions are revision-log history, not an embedded unbounded array. +The common phase is +`Pending|Ready|Succeeded|Degraded|Failed|Deleted|Unknown`; conditions carry +starting, deleting, retrying, and other transition detail. + +### Providers and controllers + +A Provider must be installed as a Zone-local `Provider/` resource before +it can be selected; a `providerRef` resolves only a `Ready` `Provider` resource +in the same Zone, and package presence alone is not installation. Provider +ResourceSpec is exactly `{ artifactId, config }`: `artifactId` selects a signed +Nix artifact-catalog/manifest entry, and `config` is validated against the +Provider's generated settings schema. The set defines **27 Providers**, each an +independently buildable crate and signed, separately-built multi-process +package that may contain several separately sandboxed controller, service, and +worker binaries but declares one Provider identity. They are indexed in +[`docs/specs/providers/README.md`](../specs/providers/README.md). + +Component state is core-governed and **status-first** (**D086/D087**): bounded, +non-secret Provider/controller state lives in the owning resource's `status` +subresource by default (revisioned, size/cardinality bounded). A component +declares a separate `Volume` — created and deleted by **core ProviderDeployment**, +never by the semantic controller — only when its state is secret/sensitive +private data, large or binary/file content, or otherwise unsuitable for +revisioned API/status churn. Stateless and status-sufficient components declare +no state Volume. When one is declared it is `Provider/volume-local`-backed, +`persistent`, carries a nonzero quota and identity marker, and is `Ready` before +the component Process starts. `Provider/volume-local` is the sole `Volume` +reconciler and owns Host source-side storage; `Provider/volume-virtiofs` owns +the virtiofsd Process and the qualified `virtiofs.d2bus.org.Export` attachment +and never adds `Volume` to its exported ResourceTypes. + +Semantic Provider controllers compose behavior by creating owned primitive +resources and by calling typed `EffectPort` interfaces whose host-mutating +implementations are resolved by core and executed through the privileged +broker; controllers never call spawn, systemd, minijail, broker, filesystem, +network, or device effects directly. + +Controller processes own their watch/coalescing queues and retry decisions. +Core validates signed watch plans, filters by API/scope/ownership/dependencies, +suppresses irrelevant or already-converged changes, coalesces revisions, and +delivers bounded reconcile hints immediately after durable commit. All +reconciliation APIs are asynchronous. A dedicated watch task keeps reading +while per-resource tasks reconcile; independent resources run in parallel and +long-running Process effects do not block the next ready Process. There is no +fixed poll, debounce, or inter-resource sleep. A committed UX-affecting mutation +gets a commit-gated **expedited reconcile** (**D090**). All controllers +implement the standard reconciliation contract and commit optimistic +`ResourceMutationBatch` values. Every resource carries a currency/disruptive +upgrade/recycle lifecycle with CLI projections (**D091**). + +### Hosts, Guests, and process placement + +Physical/local host execution contexts are `Host/` resources reconciled +by Provider/system-core. A Zone may declare several policy/budget-separated +Hosts. VM, sandbox, cloud-host, and remote execution contexts are +`Guest/` resources reconciled by their installed runtime Providers. + +Host and Guest share one `ExecutionPolicy`: + +- `providerRef`; +- `defaultDomain = system|user`; +- `allowedDomains`; +- `defaultUserRef` when its default domain is user. +- budgets and Volume/Network/Device attachment defaults. + +Current unsafe-local becomes a user-only Host under +Provider/system-core, not a v3 Provider. Its no-isolation warning remains +explicit in status and UI. + +Every ordinary `Process` declares: + +- `providerRef`, selecting an installed Process Provider; +- `executionRef`, selecting `Host/` or `Guest/`; +- optional `domain`, inheriting from the referenced ExecutionPolicy; +- `userRef` when required by user domain; +- exact sandbox, principal, budget, state, filesystem, endpoint, network, + device, config, and telemetry resource refs. + +There are no Host/Guest-specific Process ResourceTypes. Provider controller +descriptors declare which Host/Guest Provider capabilities and domains their +instances support. + +### System Providers and pidfd + +The initial system Provider family is: + +- `Provider/system-core` for Host reconciliation and local User + discovery/status only; +- `Provider/system-systemd` for systemd-backed Process/scope lifecycle; +- `Provider/system-minijail` for broker/minijail-backed Process lifecycle. + +The fixed core-controller process is also the `Provider/system-core` +controller. `Provider/system-minijail` is a second fixed bootstrap controller +because the first Process controller cannot be launched by itself. It +reconciles all later Process resources, including the system-systemd controller. +Every other Provider/controller is represented by a Process resource under a +Host or Guest. + +Both Process Providers implement the same common execution schema and +conformance for long-lived `Process` and one-shot `EphemeralProcess`. +EphemeralProcess reports its terminal outcome directly and does not reference a +Process child. It retains successful results for `successfulTtl` (default +`1h`) and failed results for `failedTtl` (default `24h`), measured from +status.completedAt, before revision/finalizer-safe cleanup. Every Process +Provider must obtain and locally retain a verified pidfd. +Minijail receives it from `clone3(CLONE_PIDFD)` and owns wait/reap. The systemd +implementation binds a non-forking transient unit/scope by +InvocationID+cgroup+MainPID/start-time and opens a pidfd, while systemd owns +wait/reap. Pidfds never persist or cross d2b-bus. + +### Resource plane + +Each Zone embeds one redb database in its Zone runtime. redb supplies ACID +transactions, one writer, concurrent MVCC readers, crash safety, and B-tree +storage. d2b owns: + +- ResourceType schemas/API exports/bindings; +- resources and derived indexes; +- native Role/Binding authorization; +- revisions/change log/watches; +- operations/idempotency; +- Zone-link cursors; +- compaction, backup, restore, upgrade, and quarantine; +- controller registration and reconcile hinting. + +Concurrent clients are supported. Writes serialize through a bounded fair +coordinator. Non-conflicting queued mutations may use bounded group commit: +one redb transaction/fsync and Zone revision with ordered mutation ordinals, +while each caller receives its own result. Every mutation has expected-revision +preconditions; stale writes return conflict and never merge silently. + +The aggregate Zone resource service/store plus mandatory system-core and +system-minijail controller processes must satisfy the normative footprint/ +startup/latency gates, including +p95 durable commit-to-controller-handler <=5 ms and p95 ready Process +commit-to-launch-attempt <=20 ms. + +### Communication + +All controller control-plane traffic uses: + +```text +ResourceClient + -> d2b-bus + -> ComponentSession + -> selected d2b transport + -> owning Zone d2b.resource.v3 + -> redb coordinator +``` + +This applies to local, user, guest, remote, and nested controller registration, +get/list/watch, mutations, status/finalizers, reconcile hints/checkpoints, and +conflicts. Providers/controllers never receive a redb handle/path, direct store +client, HTTP control plane, or alternate ambient socket. + +ComponentSession uses v3 Noise-based authentication and record protection. +Authenticated peers map to canonical Zone-local resource subjects. The same +native Role/RoleBinding engine authorizes session connect, service invoke, +stream open, and resource verbs. A handshake cannot self-assert roles; +authorization leases bind policy revisions and revoke when RBAC changes. + +Credential Providers may deliver raw token bytes only over a dedicated +end-to-end Noise_KK ComponentSession to a fully enrolled authorized consumer +Provider/component. Bus/Zone/relay intermediaries authorize and forward opaque +protected records without decrypting them. Tokens never enter resource +spec/status/store/revision/audit/telemetry, NN, or bootstrap sessions. + +The `selected d2b transport` is supplied by a transport-only Provider that owns +no Zone ResourceType: `Provider/transport-unix` (same-host), +`Provider/transport-vsock` (host↔Guest and delegation), and +`Provider/transport-azure-relay` (remote). A parent reaches a child Zone only +through its local `ZoneLink/` resource, which carries the ComponentSession +over the selected transport; ordinary resource references never cross Zones. +Consistent with [ADR 0032](0032-d2b-v2-constellation-control-plane.md), realm +relay/provider credentials, remote node registries, and realm audit stay in a +per-realm gateway Guest and never enter the host daemon, broker, or host +bundle; a relay-authenticated peer is never mapped to a local lifecycle role. +`Provider/credential-entra` is a Guest-resident adapter to an Entrablau-enabled +identity Guest `Endpoint` (**D093**): login/token/TPM state stays in the Guest, +login is a typed CLI flow, and there is no Host ambient authentication. + +A primitive is a complete standard low-level ResourceSpec. The model is kept +small: Host; Guest; Process/EphemeralProcess; Volume; and only independently +lifecycle-managed/shared/substitutable User, Network, Device, and Credential +resources approved by the normative resource catalog. + +Budgets, cgroups, sandbox, namespaces, capabilities, environment, mounts, +endpoints, ports, and telemetry are fields of Host/Guest/Process specs. Files, +directories, state, ACLs, views, and mount lifecycle are one Volume spec. +Locks/leases remain internal transaction/controller mechanics unless a later +independent shared lifecycle proves a ResourceType is required. + +Volume preserves fine-grained storage policy: anchored relative layout entries, +owner/group refs, mode, access/default ACLs, no-follow/inheritance/repair/ +cleanup rules, named views, and Process mounts. Same-Zone Volume attachments +can bridge an authorized Host source to a Guest mount using virtiofs; the +Volume controller owns any virtiofsd Process and reports per-attachment status. + +Helpers, syscalls, broker operations, pidfds, and sandbox fragments are +implementation mechanisms, not primitives. + +Semantic Provider controllers compose behavior by creating owned primitive +resources. They do not call spawn, systemd, minijail, broker, filesystem, +network, or device effects directly. + +## Rejected alternatives + +### kcp/etcd/Kubernetes API + +Rejected for runtime footprint and unnecessary workload-cluster machinery. +Useful resource semantics are retained in the native API. + +### One global resource database + +Rejected because Zone ownership, backup, failure, authorization, and state +must remain independently bounded. + +### Separate Process ResourceTypes for Host and Guest + +Rejected because location belongs to executionRef/provider/domain selection. +Duplicated Process schemas would drift and prevent implementation substitution. + +### Direct controller store access + +Rejected because it bypasses ComponentSession identity, d2b-bus routing, native +authorization, audit, limits, and cross-Host/Guest transport parity. + +### One Provider process or dynamic library + +Rejected because components need distinct UIDs, Hosts/Guests, user domains, +sandboxes, resources, and failure boundaries. + +## Consequences + +Benefits: + +- one resource/controller model for physical hosts, users, guests, sandboxes, + remote hosts, and nested Zones; +- independently packaged and later independently hosted Provider repositories; +- exact ownership-driven reconciliation after child drift; +- implementation-neutral Process resources with systemd/minijail substitution; +- smaller control-plane target than kcp; +- language-neutral contracts and one authenticated communication channel. + +Costs: + +- d2b must implement and maintain resource storage, watches, RBAC, schemas, + backup/upgrade, controller scheduling support, and conformance; +- many current implementation-shaped roles require explicit migration work + items; +- every ResourceType, core controller, and Provider needs a complete normative + spec; +- more processes and controller/resource status surfaces increase operational + complexity; +- the d2b 3.0 cutover remains destructive. + +## Normative specifications + +The authoritative set has **55 members** — 28 foundation, resource, +cross-cutting, and closing specs plus 27 Provider dossiers — indexed by +[`docs/specs/README.md`](../specs/README.md) and bound by the generated +`docs/specs/ADR-046-spec-set.json` and `docs/specs/ADR-046-work-items.json` +manifests. The generated implementation DAG +(`docs/specs/ADR-046-implementation-graph.json` and its human view +`docs/specs/ADR-046-implementation-graph.md`, decision D095) maps every member +spec and work item to a dependency-ordered `W0`–`W7` launch wave and a +file-disjoint parallel group; like the manifests it is a generated non-member +artifact and does not change the 55-member count. This decision and every member +are `Proposed` and reviewed as one atomic unit; the PR delivers documentation +only. + +Foundation and platform (15): resource object model / three-layer status +([`ADR-046-resource-object-model`](../specs/ADR-046-resource-object-model.md)), +async embedded redb resource plane +([`ADR-046-resource-store-redb`](../specs/ADR-046-resource-store-redb.md)), API +and native Role/RoleBinding authorization +([`ADR-046-resource-api-and-authorization`](../specs/ADR-046-resource-api-and-authorization.md)), +asynchronous owner-driven reconciliation with commit-gated expedited reconcile +([`ADR-046-resource-reconciliation`](../specs/ADR-046-resource-reconciliation.md)), +primitive composition +([`ADR-046-primitive-resource-composition`](../specs/ADR-046-primitive-resource-composition.md)), +ComponentSession/Noise/d2b-bus +([`ADR-046-componentsession-and-bus`](../specs/ADR-046-componentsession-and-bus.md)), +Zone-link routing +([`ADR-046-zone-routing`](../specs/ADR-046-zone-routing.md)), Provider model and +`{artifactId,config}` packaging +([`ADR-046-provider-model-and-packaging`](../specs/ADR-046-provider-model-and-packaging.md)), +status-first Provider state +([`ADR-046-provider-state`](../specs/ADR-046-provider-state.md)), fixed core +controllers +([`ADR-046-core-controllers`](../specs/ADR-046-core-controllers.md)), the +component/process/sandbox model +([`ADR-046-components-processes-and-sandbox`](../specs/ADR-046-components-processes-and-sandbox.md)), +Nix direct-ResourceSpec authoring +([`ADR-046-nix-configuration`](../specs/ADR-046-nix-configuration.md)), +terminology +([`ADR-046-terminology-and-identities`](../specs/ADR-046-terminology-and-identities.md)), +the current-code migration map +([`ADR-046-current-code-migration-map`](../specs/ADR-046-current-code-migration-map.md)), +and the resolved decision register +([`ADR-046-decision-register`](../specs/ADR-046-decision-register.md)). + +Resource catalog (6): the 19 standard ResourceTypes (including `Endpoint`, +`ResourceExport`, and `ResourceImport`) are +specified in +[`ADR-046-resources-zone-control`](../specs/ADR-046-resources-zone-control.md), +[`ADR-046-resources-host-guest-process-user`](../specs/ADR-046-resources-host-guest-process-user.md), +[`ADR-046-resources-volume`](../specs/ADR-046-resources-volume.md), +[`ADR-046-resources-network`](../specs/ADR-046-resources-network.md), +[`ADR-046-resources-device`](../specs/ADR-046-resources-device.md), and +[`ADR-046-resources-credential`](../specs/ADR-046-resources-credential.md). + +Cross-cutting (3): +[`ADR-046-cli-and-operations`](../specs/ADR-046-cli-and-operations.md), +[`ADR-046-telemetry-audit-and-support`](../specs/ADR-046-telemetry-audit-and-support.md), +and the threat model +[`ADR-046-security-and-threat-model`](../specs/ADR-046-security-and-threat-model.md). + +Closing (4): destructive reset/cutover +([`ADR-046-reset-and-cutover`](../specs/ADR-046-reset-and-cutover.md)), +pre-acceptance feasibility proofs/spikes +([`ADR-046-feasibility-and-spikes`](../specs/ADR-046-feasibility-and-spikes.md)), +validation and delivery waves with fast hermetic tests and integration-only +slow coverage (**D094**) +([`ADR-046-validation-and-delivery`](../specs/ADR-046-validation-and-delivery.md)), +and the efficiency/streamline contract +([`ADR-046-streamline`](../specs/ADR-046-streamline.md)). + +Provider dossiers (27): one dossier per installed `Provider/`, indexed +with owned/exported ResourceTypes and component placement in +[`docs/specs/providers/README.md`](../specs/providers/README.md). + +The resolved decisions include the status-first state default (D086/D087), +three-layer status (D088), layered base spec + strict provider extension (D089), +commit-gated expedited reconcile (D090), resource currency/disruptive +upgrade/recycle with CLI projections (D091), the `Endpoint` ResourceType and +promotion criterion (D092), Guest-resident Entrablau identity custody (D093), +fast-test/legacy-retirement delivery (D094), and the global `d2bus.org` +public-contract namespace (D080). + +Foundation specs are authored first. Once stable, all ready file-disjoint +resource, core-controller, cross-cutting, and Provider dossier specs are +authored in parallel. No dependent spec may invent a missing foundation choice. + +## Review and acceptance + +The same ADR/spec PR has two required human review gates: + +1. approval before the immutable final panel snapshot; +2. approval after unanimous panel signoff. + +Any content change invalidates validation and panel evidence. The changed +candidate is revalidated, repaneled, and reviewed again. + +The set cannot become Accepted while it contains: + +- an unresolved decision; +- a missing ResourceType/core-controller/Provider dossier; +- an undefined ref, owner, controller, process, state, limit, error, or test; +- a work item without exact v3 source and future destination paths; +- a claim that proposed v3 implementation is already live. + +## Current-code fit + +| Item | ADR 0046 treatment | +| --- | --- | +| Current anchor | ADR 0043 Realm model; `d2b-realm-*`; live CLI/daemon/guest-control; process DAG/broker; normalized Nix/bundle/storage contracts; ACA/Relay/gateway; unsafe-local user scopes | +| Evidence class | Mixed; exact classifications live in the authoring evidence ledger and owning specs | +| Behavior retained | Typed/fail-closed contracts, positive capabilities, idempotency, bounded streams, pidfd identity, broker mediation, generated storage ownership, argv/secret redaction, OTEL/audit separation | +| Required delta | Entire native Zone resource plane, ComponentSession/d2b-bus production path, Provider resources/toolkit, reconciliation, Primitive ResourceSpecs, Provider process packaging, Provider state, Zone routing, and reset | +| Reuse path | Copy/extract/adapt exact symbols named by each spec work item; current code is canon for current behavior | +| Replacement/deletion | No current path is removed until its resource/controller/Provider successor is integrated and tested | +| Feasibility proof | redb/resource/reconciliation/process/bus/package/state/route/security spikes specified by the normative set | +| Future owner | Exact `ADR046-*` work items in the spec-set manifest | diff --git a/docs/adr/README.md b/docs/adr/README.md index e97eb9fc1..c3e686e0e 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -50,3 +50,4 @@ for the broader design narrative, see | [0043. Realm-native control plane](0043-realm-native-control-plane.md) | Accepted | 2026-07-05 | Supersedes ADR 0032 with realm-as-control-plane architecture: one `d2bd` plus broker/socket/state/audit boundary per realm, strict parent/child routing, dynamic relay discovery, realm-qualified VM addresses, and migration from current `home`/`dev`/`work` groups into first-class realms. | | [0044. Unsafe-local runtime provider](0044-unsafe-local-runtime-provider.md) | Accepted | 2026-07-09 | Adds an explicit `unsafe-local` provider for host-user/session/network workloads with Wayland-proxy identity rails, first-class desktop launch metadata, and host-local persistent shell UX while preserving the no-isolation security warning. | | [0045. Provider and transport framework](0045-provider-and-transport-framework.md) | Proposed | 2026-07-10 | Separates provider authorities from transport and protocol seams, selects Noise-authenticated component sessions with ttrpc/protobuf control services, models controllers as parent-owned workloads, and authorizes direct peer streams over shared transport fabrics without bypassing realm-tree policy. | +| [0046. d2b 3.0 Provider control plane](0046-d2b-3-provider-control-plane.md) | Proposed | 2026-07-22 | Rebuilds d2b 3.0 from the pre-ADR45 v3 baseline around Zone-local resources over 19 standard ResourceTypes (incl. `Endpoint`), independently packaged multi-process Providers selected by `{artifactId,config}`, an async embedded redb resource plane with owner-driven reconciliation, status-first component state with optional Volumes, layered base+provider ResourceType specs, and Noise/d2b-bus ComponentSession channels on the `d2bus.org` namespace. | diff --git a/docs/specs/ADR-046-cli-and-operations.md b/docs/specs/ADR-046-cli-and-operations.md new file mode 100644 index 000000000..1c6d5e78a --- /dev/null +++ b/docs/specs/ADR-046-cli-and-operations.md @@ -0,0 +1,2649 @@ +# ADR 0046 CLI and operations + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-cli-and-operations` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `packages/d2b` binary crate, Zone runtime client layer | +| Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-api-and-authorization`, `ADR-046-provider-model-and-packaging`, `ADR-046-components-processes-and-sandbox`, `ADR-046-componentsession-and-bus` | +| Supersedes | Current v3 `d2b` CLI contract (`packages/d2b/src/lib.rs` at baseline) | + +## Purpose + +This spec defines every public-facing CLI command, argument, output format, exit +code, stream/TTY contract, completion/help surface, and dynamic descriptor +behavior for the d2b 3.0 CLI binary. It maps every current v3 CLI verb and +target to its retained, successor, or deletion outcome. It specifies how the CLI +discovers the nearest Zone runtime, constructs canonical ResourceRefs, routes +through the resource API, handles async operations and their lifecycle, and +enforces all limits without embedding Provider logic. + +## Guiding invariants + +1. The CLI is a typed client. It constructs resource API requests and interprets + resource/operation responses. It contains no Provider code, no controller + logic, no broker operations, and no sandbox compilation. +2. Every mutating verb routes through the Zone resource API over ComponentSession + and d2b-bus. Every read-only verb prefers the Zone resource API and documents + its graceful fallback. +3. ResourceRefs are canonical Zone-local references of the form + `/`. The CLI parses, validates, and serializes + them but does not invent, guess, or alias them. +4. Output is stable: `--json` emits newline-terminated JSON objects with a + frozen schema; human output is for terminals only and is not machine-parseable. +5. Exit codes are stable. The guest process exit code passes through `vm exec` + transparently; the `--json` envelope disambiguates collisions. +6. Dynamic descriptors (shell completion, help, Provider-advertised commands) + are bounded and fetched with hard deadlines. A slow or absent Zone runtime + never blocks the CLI binary startup. +7. Provider code never runs in the CLI binary or subprocess at CLI invocation + time. +8. No SSH, no bash fallback, no realm/workload terminology in new surfaces. + +## Baseline terminology mapping + +The v3 codebase at baseline `b5ddbed6` uses pre-ADR 0046 names. This table +maps every cited current symbol to its ADR 0046 target. Current-source evidence +citations throughout this spec use the old symbol names; target names appear in +the "target:" annotation. + +| Current baseline symbol | Package | Target name | +| --- | --- | --- | +| `RealmId`, `RealmPath`, `TargetName` | `d2b-realm-core/src/ids.rs`, `realm.rs`, `target.rs` | `Zone/` addressing; ancestry comes from sealed compiler-only `{ childZone, parentZone }` topology and authenticated route projections | +| `RealmControllerPlacement`, `EntrypointMode` | `d2b-realm-core/src/realm.rs` | Private Zone runtime bootstrap placement plus compiler-only parent topology; transport state remains in the child's local ZoneLink | +| `RealmEntrypointDocument`, `RealmEntrypointConfig` | `packages/d2b/src/lib.rs:5163` | Static `realm-entrypoints.json` retires; target is the read-only topology projection over sealed `{ childZone, parentZone }` rows and authenticated route state | +| `RealmPolicyOutputV1 { realm, mode, gateway_vm, gateway_target, gateway_state, cross_realm_policy, credential_boundary }` | `d2b-contracts/src/cli_output.rs:345` | Retired; successor rows expose compiler topology plus authenticated route/projection status, never child-local ZoneLink spec or status | +| `RealmListOutputV1 { realms: Vec }` | `d2b-contracts/src/cli_output.rs:285` | Target: `d2b zone list` topology-projection response | +| `RealmInspectOutputV1 { realm: RealmPolicyOutputV1 }` | `d2b-contracts/src/cli_output.rs:292` | Target: `d2b zone get ` topology/route projection response | +| `WorkloadId`, `WorkloadTarget` (`= RealmTarget`) | `d2b-realm-core/src/ids.rs`, `d2b-core/src/workload_identity.rs` | `Guest/` ResourceRef for VM/sandbox/cloud/remote; `Host/` for unsafe-local (NEVER `Guest`) | +| `WorkloadProviderKind::LocalVm` | `d2b-realm-core/src/workload.rs:16` | `Guest` under `Provider/runtime-cloud-hypervisor` | +| `WorkloadProviderKind::QemuMedia` | `d2b-realm-core/src/workload.rs:19` | `Guest` under `Provider/runtime-qemu-media` | +| `WorkloadProviderKind::UnsafeLocal` | `d2b-realm-core/src/workload.rs:22`; Nix option `kind = "unsafe-local"` in `nixos-modules/options-realms-workloads.nix:221`; Nix emitter `nixos-modules/unsafe-local-workloads-json.nix`; helper crate `packages/d2b-unsafe-local-helper/`; wire protocol `packages/d2b-contracts/src/unsafe_local_wire.rs` | Target: `Host/` resource with `defaultDomain=user`, `allowedDomains=[user]`, `defaultUserRef=User/`; reconciled by `Provider/system-core` (NOT itself a Provider and NOT a Guest); child processes use normal Process Providers; no-isolation posture (`IsolationPosture::UnsafeLocal`) MUST be preserved as explicit warnings in Host status, CLI output, UI, and audit/telemetry | +| `WorkloadProviderKind::ProviderManaged` | `d2b-realm-core/src/workload.rs:21` | `Guest` or `Host` depending on Provider type; decision-required per provider dossier | +| `IsolationPosture::UnsafeLocal` | `d2b-realm-core/src/workload.rs:33`; `WorkloadExecutionPosture { isolation: IsolationPosture::UnsafeLocal, environment: EnvironmentPosture::SystemdUserManagerAmbient, execution_identity: ExecutionIdentityPosture::AuthenticatedRequesterUid }` (test at workload.rs:207); emitted in `WorkloadPublicSummary.execution_posture` via `public_wire.rs:267` | Target: `Host` resource `status.isolationPosture: "none"` field; this field MUST be present and non-empty in all Host status outputs, `--json` envelopes, CLI `--human` tables, and audit events for unsafe-local Hosts; a missing or silent posture field is a correctness violation | +| `WorkloadSummary`, `WorkloadState` | `d2b-realm-core/src/workload.rs:130,156` | Guest resource status/phase (LocalVm/QemuMedia/ProviderManaged); for UnsafeLocal → Host resource status/phase | +| `ListEntry { vm: String, lifecycle: VmLifecycle, workload_identity: Option, … }` | `d2b-contracts/src/public_wire.rs:2495` | Guest resource envelope; `ListEntry.vm` = `WorkloadId`; `workload_identity` = current realm-native canonical target | +| `VmStatus { vm: String, lifecycle: VmLifecycle, … }` | `d2b-contracts/src/public_wire.rs:2530` | Guest resource status | +| `VmLifecycleState::Stopped/Starting/Booted/Running/Stopping/Restarting/Failed/Unknown` | `d2b-contracts/src/public_wire.rs:2605` | Guest resource `phase`; target phases are `Pending\|Ready\|Succeeded\|Degraded\|Failed\|Deleted\|Unknown`; `Starting`/`Stopping`/`Restarting` map to conditions/reasons on `Pending`/`Degraded`, not separate phases | +| `VmTargetRoute::Local { vm }` | `packages/d2b/src/lib.rs:5577` | `Guest/` ResourceRef in the current Zone | +| `VmTargetRoute::Gateway { realm, gateway_vm }` | `packages/d2b/src/lib.rs:5578` | Cross-Zone through `ZoneRouteEngine` using sealed topology and an authenticated admitted route; no parent-local ZoneLink lookup | +| `ProcessRole::Virtiofsd` | `d2b-core/src/processes.rs:199` | `Process` under volume-virtiofs Provider | +| `ProcessRole::Swtpm` | `d2b-core/src/processes.rs:203` | `Process` under device-tpm Provider | +| `ProcessRole::CloudHypervisorRunner` | `d2b-core/src/processes.rs:213` | `Process` under runtime-cloud-hypervisor Provider | +| `ProcessRole::QemuMediaRunner` | `d2b-core/src/processes.rs:215` | `Process` under runtime-qemu-media Provider | +| `ProcessRole::WaylandProxy` | `d2b-core/src/processes.rs:249` | `Process` under display-wayland Provider | +| `ProcessRole::Gpu`/`GpuRenderNode` | `d2b-core/src/processes.rs:209,211` | `Process` under `Provider/device-gpu` | +| `ProcessRole::Audio` | `d2b-core/src/processes.rs:213` | `Process` under audio-pipewire Provider | +| `ProcessRole::HostReconcile` | `d2b-core/src/processes.rs:196` | Realm controller bootstrap step; not a standalone CLI-visible Process | +| `ProcessRole::GuestControlHealth` | `d2b-core/src/processes.rs:235` | Guest readiness probe integrated into Guest controller reconcile loop; not a standalone Process ResourceType | +| `ProcessRole::GuestSshReadiness` | `d2b-core/src/processes.rs:228` | Deleted at v3 clean cutover; no compatibility retention | +| `VmProcessDag` | `d2b-core/src/processes.rs` | Per-Guest/Provider `Process` resource DAG; target managed by Provider controller | +| `RealmControllersJson`, `RealmControllerLocalWorkload { vm_name, identity }` | `d2b-core/src/realm_controller_config.rs` | Zone Nix-authored Guest resources; `vm_name` → `Guest/` resource name; `identity.canonical_target` → `WorkloadId` = `Guest/` resource name | +| `ShellAttachArgs { vm: String }`, `ShellOp`, `ShellOpResponse` | `d2b-contracts/src/public_wire.rs:1319,1394,1527` | Target: ShellSession resource + named stream over ComponentSession; `vm` arg = current `WorkloadId` (= target `executionRef: Host/\|Guest/`) | + +## Zone context and nearest-runtime discovery + +### Zone context selection + +The CLI selects a Zone context in this priority order: + +1. `--zone ` flag on any command where it applies. +2. `D2B_ZONE` environment variable. +3. Nearest-runtime discovery: the CLI walks the runtime socket hierarchy + (see below) and selects the first reachable Zone runtime whose + `Zone/` self resource is Ready. +4. If no Zone runtime is reachable, read-only verbs that accept static sources + fall back to those sources with a visible degraded warning; mutating verbs + fail closed with exit code 1 and a stable `zone-unavailable` error class. + +The selected zone name and socket path are visible in every `--json` output +envelope under `zoneRef` (e.g. `"zoneRef": "Zone/dev"`). + +### Nearest-runtime socket paths + +For a system-domain (default) context, the CLI probes these paths in order, +stopping at the first socket that accepts a local ComponentSession connect: + +```text +/run/d2b/zones//public.sock # per-Zone socket (multi-Zone host) +/run/d2b/public.sock # v3 canonical single-Zone socket +``` + +For a user-domain context (`--domain user` or inferred from `$XDG_RUNTIME_DIR` +availability and the target ResourceRef): + +```text +$XDG_RUNTIME_DIR/d2b/zones//public.sock +$XDG_RUNTIME_DIR/d2b/public.sock +``` + +Environment overrides (retained from v3 baseline for compatibility): + +| Variable | Purpose | +| --- | --- | +| `D2B_ZONE` | Override zone name selection | +| `D2B_PUBLIC_SOCKET` | Override public socket path directly | +| `D2B_BROKER_SOCKET` | Override broker socket path (host prepare/destroy only) | + +Discovery never falls back from a zone-qualified socket to the single-Zone +socket when `--zone` or `D2B_ZONE` is set. + +`--zone` is a global option available to all commands. Purely local +host-maintenance commands (`host prepare`, `host destroy`, `host install`, +`host reconcile`, `host doctor`, `host check`) do not consult Zone context; +they operate on the local host only and explicitly document this in their +`--help` text. Providing `--zone` to these commands is not an error; the flag +is accepted and documented as having no effect on host-maintenance operations. + +## Canonical ResourceRef argument parsing + +### Accepted forms + +Standard ResourceRef positional or `--` flag argument: + +```text +/ # canonical full form + # short form for commands that declare a default ResourceType +``` + +Short form rules: + +- The command declares one and only one default ResourceType. There is no + implicit disambiguation across types. +- Short form is accepted only when the command's `--help` documents the default + type. +- If the name contains a `/`, the full form is required. + +Validation: + +- `resource_name` matches `^[a-z][a-z0-9-]*$`. +- `ResourceType` is a known Zone-registered type or a qualified API name. +- Validation failure yields exit code 2 with a `ref-invalid` error class. + +ResourceRef values that name resources that do not exist return exit code 1 with +a `resource-not-found` error class. + +**ResourceType validation:** The frozen standard ResourceType set (Zone, +ZoneLink, Provider, Role, RoleBinding, Host, Guest, Process, EphemeralProcess, +Volume, Network, Device, User, Credential, Endpoint, ResourceExport, +ResourceImport, Quota, EmergencyPolicy) is +validated locally at compile time against the names defined in this spec +(19 standard types; `Endpoint` added by D092; `ResourceExport` and +`ResourceImport` added by D096). A syntactically +valid type name that contains an API qualifier (e.g. +`widgets.example.org.WidgetResource`) +is passed through to the live Zone catalog; the API returns +`resource-schema-invalid` if the Zone does not recognize it. Any other +unrecognized type name fails locally with exit code 2 and class `ref-invalid` +without a Zone round-trip. `Endpoint/` is accepted by the local +ResourceRef parser exactly like every other standard type; consumers reference +endpoints only through the `Endpoint/` ResourceRef and never through a raw +transport locator. `ResourceExport/` and `ResourceImport/` are +likewise accepted by the local parser. Qualified semantic/provider-neutral +`*Service` and `*Binding` refs pass through the live Zone catalog and do not enlarge the +19-standard-type set. A `ResourceExport.resourceRef` must resolve to the +factory-bound local owner Service; Device/Endpoint/Binding targets reject. A +`ResourceImport` names only a local `zoneLinkRef` plus bounded `exportKey`; the +parser/API reject every cross-Zone Ref (D096). + +## Nix configuration, ResourceSpec JSON, and generation lifecycle + +> Full Nix option surface, eval/build validation rules, bundle emission format, +> schema-digest binding, and assertion catalogue are owned by the Nix +> configuration spec (ADR-046-nix-configuration.md). This section covers the +> canonical resource envelope shape as exposed by the CLI and the CLI-visible +> activation/cleanup commands. + +### Resource shape and Nix authoring + +Zone resources are declared in NixOS modules through a unified structure that +mirrors the canonical ResourceSpec schema directly: + +``` +d2b.zones..resources. = { + type = ""; + metadata = { # optional user-authored fields only + ownerRef = null; # optional; names a declared resource in the same bundle + labels = { }; # optional; presentation/selector labels only + }; + spec = { /* exact ResourceTypeSchema field names — no renaming or extra nesting */ }; +}; +``` + +**Derived on emit:** `metadata.name` from `` attr key; +`metadata.zone` from `` attr key; `apiVersion` defaults to +`"resources.d2bus.org/v3"`. + +**Omitted in authoring (runtime-filled):** `status`, `uid`, `revision`, +`generation`, `finalizers`, `deletionRequestedAt`, `createdAt`, `updatedAt`, +`managedBy`, `configurationGeneration`. The Nix module rejects these as unknown +options if authored. + +`spec` sub-options per `type` are generated from the same ResourceTypeSchema +(and signed Provider schema for `type = "Provider"`) that governs the API. +Field names in `spec` are identical to the schema; there is no second vocabulary +and no extra nesting. Vendor-qualified types (containing `.`) are admitted when +a matching schema is installed. + +**Minimal schema-valid example:** + +```nix +# Derivations are declared globally; ResourceSpecs carry only catalog IDs. +d2b.artifacts = { + provider-runtime-cloud-hypervisor = { + package = inputs.d2b-provider-runtime-cloud-hypervisor.packages.${system}.default; + type = "provider"; + }; + work-vm-system = { + package = inputs.nixpkgs.lib.nixosSystem { + inherit system; + modules = [ ./guests/work-vm.nix ]; + }.config.system.build.toplevel; + type = "nixos-system"; + }; +}; + +d2b.zones.local-root = {}; +d2b.zones.main = { + parentZone = "local-root"; + resources = { + main-host = { + type = "Host"; + spec.providerRef = "Provider/system-core"; + }; + runtime-cloud-hypervisor = { + type = "Provider"; + spec = { + artifactId = "provider-runtime-cloud-hypervisor"; + config.controllerExecutionRef = "Host/main-host"; + }; + }; + work-vm = { + type = "Guest"; + spec = { + allowedDomains = [ "system" ]; + budget = {}; + defaultDomain = "system"; + defaultUserRef = null; + deviceAttachments = []; + networkAttachments = []; + providerRef = "Provider/runtime-cloud-hypervisor"; + provider = { + schemaId = "runtime-cloud-hypervisor.d2bus.org/Guest/spec"; + schemaVersion = "1.0.0"; + settings = {}; + }; + systemArtifactId = "work-vm-system"; + volumeAttachmentDefaults = []; + }; + }; + }; +}; +``` + +`Zone/main`, `Provider/system-core`, and `Provider/system-minijail` are +runtime-created; none is authored in the resource map. Every authored reference +resolves: the system Provider owns `Host/main-host`, the installed runtime +Provider names that Host as its controller execution target, and +`Guest/work-vm` names both that Provider and a `nixos-system` artifact. The +compact example omits unrelated types rather than using placeholder fields. + +**Evidence class:** `ADR-only` — v3 Nix modules do not exist at baseline +`b5ddbed6`. Current source: `nixos-modules/options-realms-workloads.nix`, +`nixos-modules/options-realms.nix`, `nixos-modules/unsafe-local-workloads-json.nix` +(pre-ADR 0046 names). Target implemented by work item ADR046-cli-011. + +### Canonical ResourceSpec JSON shape + +Every `d2b get`, `d2b list` element, and `d2b watch` event carries a canonical +resource envelope inside the outer `--json` response. The Guest `spec` below is +a field-for-field mirror of the authored Nix after deterministic defaults; +the runtime adds store-generated metadata and status. Optional presentation +metadata and the optional Provider status extension are omitted. + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Guest", + "metadata": { + "name": "work-vm", + "zone": "main", + "uid": "01930a4f-2b3d-7e8f-9c1a-4d5e6f708192", + "generation": 3, + "revision": "", + "ownerRef": null, + "finalizers": [], + "deletionRequestedAt": null, + "createdAt": "2026-07-22T00:00:00Z", + "updatedAt": "2026-07-22T10:30:00Z", + "managedBy": "configuration", + "configurationGeneration": 7 + }, + "spec": { + "allowedDomains": ["system"], + "budget": {}, + "defaultDomain": "system", + "defaultUserRef": null, + "deviceAttachments": [], + "networkAttachments": [], + "providerRef": "Provider/runtime-cloud-hypervisor", + "provider": { + "schemaId": "runtime-cloud-hypervisor.d2bus.org/Guest/spec", + "schemaVersion": "1.0.0", + "settings": {} + }, + "systemArtifactId": "work-vm-system", + "volumeAttachmentDefaults": [] + }, + "status": { + "observedGeneration": 3, + "phase": "Ready", + "conditions": [], + "lastReconciledAt": "2026-07-22T10:30:05Z", + "startedAt": "2026-07-22T10:30:05Z", + "completedAt": null, + "outcome": null, + "update": { + "state": "Current", + "reasons": [], + "observedGeneration": 3, + "targetGeneration": 3, + "disruption": "None", + "preserveState": true, + "operationId": null, + "lastAssessedAt": "2026-07-22T10:30:05Z", + "owned": { "count": 1, "refs": ["Process/work-vm-vmm"] }, + "dependencies": { "count": 1, "refs": ["Provider/runtime-cloud-hypervisor"] } + }, + "resource": { + "providerPhase": "running", + "bootstrapReady": true, + "guestIdentityDigest": "8d5f7b1a", + "activeProcessCount": 1 + } + } +} +``` + +**Core management metadata** (runtime-filled; never authored in Nix): + +| Field | Value | Meaning | +| --- | --- | --- | +| `managedBy` | `"configuration"` | Core-set closed enum; `"controller"` and `"api"` are the other values | +| `configurationGeneration` | `7` (integer) | Bundle generation that last confirmed this configuration-managed resource; absent on controller/API-managed resources | + +**Cleanup authority:** Core identifies resources eligible for deletion from a +new bundle by `managedBy == "configuration"` AND absence from the new canonical +configured set. `managedBy == "controller"` and `managedBy == "api"` resources +are **never** deleted by bundle application. This invariant is a correctness +requirement. + +**Owner controller responsibility:** When a configuration-managed parent resource is +deleted, its owning controller enqueues deletion of declared children (child +Processes, EphemeralProcess sessions, Volume attachments). It does not delete +unrelated resources. + +**List response** (`d2b list Guest`): + +```json +{ + "ok": true, + "zoneRef": "Zone/main", + "operationId": "", + "schemaVersion": 1, + "items": [ { /* ResourceSpec envelope */ }, ... ], + "nextPageToken": null, + "totalCount": 3 +} +``` + +**Status-only response** (`d2b status Guest/work-vm`): + +```json +{ + "ok": true, + "zoneRef": "Zone/main", + "operationId": "", + "schemaVersion": 1, + "resourceRef": "Guest/work-vm", + "status": { /* status sub-object only */ } +} +``` + +### CLI-visible activation and cleanup + +Full activation-provider command surface is in +[§ `d2b activation`](#d2b-activation--activation-nixos-provider-commands). + +**Apply a new bundle generation:** + +``` +d2b activation apply [--zone ] [--dry-run] [--json | --human] +``` + +Sends the built bundle (`/etc/d2b/zones//resource-bundle.json`) to the +fixed core-controller configuration service. +Exits 0 when the apply phase completes (creates/updates present resources). +Does not wait for deletion of absent Nix resources. Output includes applied +resource count and pending-cleanup count. + +**Absent-resource deletion:** Resources with `managedBy == "configuration"` absent from +the new bundle receive asynchronous Delete. The Zone enters +`Degraded/pending-cleanup` until all complete. + +**Observe cleanup progress:** + +``` +d2b zone status [] [--zone ] [--watch] [--deadline ] +``` + +With `--watch`, streams `pending-cleanup` condition transitions: + +``` +$ d2b zone status main --watch +Zone/main Degraded pending-cleanup: 2 resources pending deletion +Zone/main Degraded pending-cleanup: 1 remaining (Guest/old-vm done) +Zone/main Ready (cleanup complete) +``` + +**Per-resource deletion progress:** + +``` +d2b status / [--watch] +``` + +**Prior generation retention:** count-based; default 3; range 1–16. The +retention count is configured by the Zone-level Nix/compiler setting +`d2b.zones..retainedGenerations`, outside the empty `Zone.spec`. +Generations beyond the count become eligible for pruning after cleanup +completes. Generations within the count are retained for rollback regardless of +elapsed time; there is no TTL. + +``` +d2b activation gc [--zone ] [--dry-run] [--json | --human] +``` + +Prunes generation bundles and hardlink-farm snapshots beyond the retention count. + +**Rollback:** + +``` +d2b activation rollback [--to-generation ] [--zone ] [--dry-run] [--apply] +``` + +Re-applies a retained prior generation bundle as a new (higher) +`bundleGeneration`. The generation counter is never reversed. + +**Audit:** Each deletion dispatched from bundle application emits +`resource-delete-dispatched`; each completion emits +`resource-delete-completed` or `resource-delete-failed`. + +### CLI-visible tests for activation and cleanup + +| Layer | Test | What it proves | +| --- | --- | --- | +| Runtime (integration) | Apply bundle → absent `managedBy=configuration` resource receives async Delete | Cleanup dispatch uses `managedBy` | +| Runtime (integration) | `managedBy=controller` and `managedBy=api` resources absent from bundle → NOT deleted | `managedBy` cleanup invariant | +| Runtime (integration) | Zone status = `Degraded/pending-cleanup` during outstanding deletions | Status accuracy | +| Runtime (integration) | Zone status = `Ready` after all cleanup completes | Status accuracy | +| Runtime (integration) | Generations within retention count retained (no TTL) | Count-based retention | +| Runtime (integration) | `d2b activation gc` prunes beyond retention count | GC correctness | +| Runtime (integration) | `d2b activation rollback` re-applies prior bundle as new bundleGeneration | Rollback increments counter | +| Runtime (integration) | Stuck finalizer leaves resource Degraded and requires controller repair or full Zone reset | No force-finalizer path | +| Runtime (integration) | Old `bundleGeneration` replay rejected | Generation monotonicity | + +## Standard resource verbs + +The following subcommand structure applies to all standard ResourceTypes that +declare a CLI surface. The exact set of verbs implemented per ResourceType is +specified in the per-ResourceType section below. + +### `d2b get /` + +Fetch the current resource envelope. + +``` +d2b get / [--zone ] [--json | --human] +``` + +Returns the complete resource envelope (`metadata`, `spec`, `status`). + +**Output format:** JSON envelope or human summary table row with phase/condition +summary. + +**Exit codes:** +- 0: resource returned +- 1: resource not found or zone unavailable (`resource-not-found`, + `zone-unavailable`) +- 2: usage/validation error + +### `d2b list [filters]` + +List resources of one type. + +``` +d2b list [--zone ] + [--phase ] + [--label-selector =] + [--updates] + [--page-token ] + [--limit ] + [--json | --human] +``` + +Returns a bounded page of matching resources. Pagination uses `--page-token` +from the previous response. + +**Output format:** JSON array envelope with `items`, `nextPageToken` +(absent if last page), `snapshotRevision`; human table with status column. Every +`get`/`list` output includes the resource's `status.update` currency (state, +disruption, and bounded owned/dependency update aggregates) so operators always +see self/owned/dependency update state (D091). `--updates` filters/sorts to +resources whose `status.update.state` is not `Current` and expands the +owned/dependency update breakdown; the `--json` shape is stable. + +**Exit codes:** +- 0: list returned (may be empty) +- 1: zone unavailable or filter error +- 2: usage error + +### `d2b watch [filters]` + +Stream resource change events after a starting revision. + +``` +d2b watch [--zone ] + [--since-revision ] + [--phase ] + [--label-selector =] + [--json] + [--deadline ] +``` + +Streams newline-separated JSON change envelopes (`type`, `object`) to stdout. + +**Output format:** JSON change stream only (`--human` is not valid for watch). + +**Deadline:** `--deadline ` (e.g. `30s`, `5m`) sets a hard wall +deadline. Exit code 0 on clean close; 1 on deadline expiry with +`deadline-exceeded` class; 3 on signal/cancel. + +**Exit codes:** +- 0: clean close by server +- 1: stream error, zone unavailable +- 2: usage error +- 3: cancelled by SIGINT/SIGTERM + +### `d2b create [--spec-file | --spec-stdin]` + +Create a resource from a JSON spec file or stdin. + +``` +d2b create [--zone ] + [--spec-file | --spec-stdin] + [--wait-for-reconcile [--reconcile-deadline ]] + [--json | --human] +``` + +With `--wait-for-reconcile` (expedited mode, D090; authorized UX mutation only), +the command returns after the durable commit plus one expedited reconcile pass +and reports the committed resource, the post-pass projected layered status, +`disposition` (`Converged|Progressing|Blocked|UpgradeRequired|Failed`), +`statusPersistence` (`pending|committed`), and `lastPersistedStatusRevision`. A +durable commit is never undone if the pass times out; a timeout returns a +committed-but-reconcile-pending outcome. When the change is disruptive the +disposition is `UpgradeRequired` and the response carries the upgrade projection +(D091). + +**Exit codes:** +- 0: created +- 1: already exists (`resource-already-exists`), validation error, zone + unavailable +- 2: usage error + +### `d2b update-spec /` + +Full spec replacement. + +``` +d2b update-spec / [--zone ] + [--revision ] + [--spec-file | --spec-stdin] + [--wait-for-reconcile [--reconcile-deadline ]] + [--json | --human] +``` + +`--wait-for-reconcile` behaves as for `create` (D090): committed resource + +one expedited reconcile pass disposition + `statusPersistence`. A disruptive +change returns `UpgradeRequired` rather than applying in place (D091). + +**Exit codes:** +- 0: updated +- 1: conflict, not found, validation error, zone unavailable +- 2: usage error + +### `d2b delete /` + +Request deletion. + +``` +d2b delete / [--zone ] + [--revision ] + [--wait-for-reconcile [--reconcile-deadline ]] + [--json | --human] +``` + +Triggers deletion (sets `deletionRequestedAt`). Does not wait for finalizers to +complete unless `--wait` is given. With `--wait-for-reconcile` (expedited, D090) +the response is an event-only Deleted projection / not-found outcome after the +commit plus one expedited pass. + +**Exit codes:** +- 0: deletion accepted +- 1: not found, conflict, zone unavailable +- 2: usage error + +### `d2b status /` + +Fetch the status subresource only. + +``` +d2b status / [--zone ] + [--watch] + [--deadline ] + [--json | --human] +``` + +With `--watch`, streams status change events until Ready/Succeeded/Failed/ +Deleted or deadline. + +**Exit codes (without `--watch`):** +- 0: returned +- 1: not found, zone unavailable +- 2: usage error + +**Exit codes (with `--watch`):** +- 0: phase reached Ready or Succeeded +- 1: phase reached Failed, Deleted, or zone unavailable +- 2: usage error +- 3: cancelled or deadline exceeded + +### `d2b upgrade /` + +Assess and apply a disruptive/currency upgrade (D091). Plans by default; applies +only with explicit `--apply`. + +``` +d2b upgrade / [--zone ] + [--recursive] + [--apply] + [--reconcile-deadline ] + [--json | --human] +``` + +Without `--apply`, prints the bounded upgrade plan (disruption class, +`preserveState`, and the topological owned/dependency drain→recycle→restart set) +and makes no change. `--recursive` includes owned/dependent resources in the +plan. `--recursive --apply` executes the planned upgrade, preserving Resource +UID/spec identity and durable/state/secret Volumes and TPM identity where +possible (`preserveState`), recycling only realization and owned ephemeral +Processes/endpoints. The `--json` plan/result shape is stable. + +**Exit codes:** +- 0: plan produced (no `--apply`) or upgrade applied +- 1: blocked, conflict, not found, or zone unavailable +- 2: usage error +- 3: cancelled or deadline exceeded + +### `d2b reconcile /` + +Admin action that requests one expedited reconcile pass (D090) on an existing +resource via the shared priority lane, without changing spec. + +``` +d2b reconcile / [--zone ] + [--reconcile-deadline ] + [--json | --human] +``` + +Returns the resource, the post-pass projected layered status, `disposition`, +`statusPersistence`, and `lastPersistedStatusRevision`. Requires admin/core +authorization; a non-authorized caller is rejected with `expedited-not-authorized`. + +**Exit codes:** +- 0: expedited pass reached a disposition +- 1: committed-but-reconcile-pending (timeout), blocked, not found, or zone unavailable +- 2: usage error +- 3: cancelled or deadline exceeded + +## `d2b host` — Host resource commands + +Maps to the `Host` ResourceType. Default ResourceType context for positional +name argument. + +### `d2b host get ` + +Equivalent to `d2b get Host/`. + +### `d2b host list` + +Equivalent to `d2b list Host`. + +**Current v3 source:** no current command lists physical/local Host contexts. +`cmd_list`/`cmd_status` list VM workloads and therefore inform the target +`Guest` command shape, not Host phase semantics. `Host` as a distinct +ResourceType and `d2b host list` are ADR-only. + +### `d2b host status ` + +Equivalent to `d2b status Host/`. + +### `d2b host prepare` + +Reconcile host-side infrastructure (bridges, nftables, sysctls, cgroups). +Routes through the Zone resource API's Host reconcile operation. `--dry-run` +returns a plan; `--apply` executes. + +``` +d2b host prepare [--zone ] [--dry-run | --apply] [--json | --human] +``` + +**Current v3 source:** `cmd_host_prepare` at +`packages/d2b/src/lib.rs:4468`; dispatches through `public.sock`/broker. +Evidence class: `implemented-and-reachable`. + +**Retained behavior:** dry-run/apply distinction, broker-mediated mutation, +ownership markers, fail-closed on foreign markers. + +**Required delta:** Route through Zone resource API Host reconcile operation +instead of raw broker seqpacket. + +### `d2b host destroy` + +``` +d2b host destroy [--zone ] [--dry-run | --apply] [--json | --human] +``` + +**Current v3 source:** `cmd_host_destroy` at `packages/d2b/src/lib.rs:4549`. +Evidence class: `implemented-and-reachable`. + +### `d2b host doctor` + +Read-only deep diagnostics. + +``` +d2b host doctor [--zone ] [--read-only] [--json | --human] +``` + +**Current v3 source:** `cmd_host_doctor` at `packages/d2b/src/lib.rs:4752`. +Evidence class: `implemented-and-reachable`. + +**Retained behavior:** reads daemon state dir, metrics URL reachability, +pidfd-table, kernel-module-report, autostart-report, storage-lifecycle-report. +Daemon-state-dir and metrics-URL overrides retained via `D2B_DAEMON_STATE_DIR` +and `D2B_METRICS_URL`. + +**Required delta:** Read from Zone runtime status resources rather than local +files where available; fall back to local state files when Zone API is +unavailable. + +### `d2b host check` + +``` +d2b host check [--read-only] [--json | --human] +``` + +**Current v3 source:** `cmd_host_check` at `packages/d2b/src/lib.rs:4271`. +Evidence class: `implemented-and-reachable`. Host-check exit code 3 retained +for compatibility. + +### `d2b host install` + +``` +d2b host install [--dry-run | --apply] [--enable] [--start] [--no-start] + [--json | --human] +``` + +**Current v3 source:** `cmd_host_install` at `packages/d2b/src/lib.rs:5007`. +Evidence class: `implemented-and-reachable`. + +### `d2b host reconcile` + +``` +d2b host reconcile [--dry-run | --apply] [--json | --human] +``` + +**Current v3 source:** `cmd_host_reconcile` at `packages/d2b/src/lib.rs:5070`. +Evidence class: `implemented-and-reachable`. + +### `d2b host validate` + +``` +d2b host validate [--dry-run | --apply] [--wave ] + [--evidence-dir ] [--scripts-dir ] + [--operator-signature ] + [--json | --human] +``` + +**Current v3 source:** `cmd_host_validate` at `packages/d2b/src/lib.rs:4941`. +Evidence class: `implemented-and-reachable`. + +### Unsafe-local Host resources — isolation posture requirements + +`Host` resources with `WorkloadProviderKind::UnsafeLocal` lineage have +`defaultDomain=user`, `allowedDomains=[user]`, and `defaultUserRef=User/`. +They are reconciled by `Provider/system-core`; they are NOT Provider resources +themselves, and they are NEVER `Guest` resources. Their child processes run +through normal Process Providers. + +The no-isolation posture (`IsolationPosture::UnsafeLocal` in current baseline, +`status.isolationPosture: "none"` in target Host resource) MUST be preserved +explicitly in: + +1. **Host status** (`d2b host status`, `d2b host get`, `d2b host list`): the + `status.isolationPosture` field MUST be `"none"` in `--json` output and + presented as a visible `[no isolation]` annotation in `--human` table rows. +2. **CLI/UI**: `d2b shell open Host/` and `d2b exec run Host/ -- ...` + MUST emit a one-line `warning: no isolation boundary — this process runs as + your host user` line to stderr before attaching. The warning has NO + suppression flag; it appears unconditionally in human output. JSON output + carries `isolationPosture: "none"` in the response envelope. +3. **Audit**: every `Shell` and `EphemeralProcess` event on a Host resource + MUST carry `isolationPosture: "none"` as a fixed closed label in audit + records. This posture MUST NOT appear in OTEL metric labels, span attributes, + or structured log fields; it is status/UI/audit-surface only. + +A missing or silent posture field is a correctness violation. Reviewers MUST +reject any diff that adds a code path serving a Host/unsafe-local resource +without propagating the isolation posture through all three surfaces above. + +**Current v3 evidence:** +- `IsolationPosture::UnsafeLocal` at `d2b-realm-core/src/workload.rs:33` +- `WorkloadExecutionPosture { isolation: IsolationPosture::UnsafeLocal, environment: EnvironmentPosture::SystemdUserManagerAmbient, execution_identity: ExecutionIdentityPosture::AuthenticatedRequesterUid }` at `workload.rs:207` +- `WorkloadPublicSummary.execution_posture: WorkloadExecutionPosture` at `packages/d2b-contracts/src/public_wire.rs:267` (public inventory carries isolation posture today) +- `nixos-modules/options-realms-workloads.nix:233` — "Host-user process runtime with no isolation boundary. Requires explicit realm policy opt-in." +- `nixos-modules/unsafe-local-workloads-json.nix` — emits `runtimeKind = "unsafe-local"` and `providerId = "unsafe-local"` in private bundle +- `packages/d2b-unsafe-local-helper/src/` — runtime helper; `packages/d2b-contracts/src/unsafe_local_wire.rs` — wire protocol + +Evidence class: `implemented-and-reachable` for isolation posture data path; +`ADR-only` for the `Host` resource API shape and the `Provider/system-core` reconciler. + +## `d2b guest` — Guest resource commands + +Maps to the `Guest` ResourceType. Unsafe-local workloads are `Host` resources, +never `Guest` — see §Unsafe-local Host resources above. + +### `d2b guest get ` + +Equivalent to `d2b get Guest/`. + +### `d2b guest list` + +Equivalent to `d2b list Guest`. + +**Current v3 source:** `cmd_list`/`cmd_vm_list` at `packages/d2b/src/lib.rs:3560`; returns +`ListResponse { vms: Vec }` from `packages/d2b-contracts/src/public_wire.rs:2152`. +Each `ListEntry` has `vm: String` (= current `WorkloadId`; target: `Guest/` resource +name), `lifecycle.state: VmLifecycleState`, `workload_identity: Option` (realm- +native canonical target, additive field tolerated absent by old daemons), and `runtime_capabilities: +Vec` (current: `WorkloadProviderKind` encoded; target: Provider family). Evidence class: +`implemented-and-reachable` for the list/lifecycle data path; `ADR-only` for `Guest` resource API. + +### `d2b guest status ` + +Equivalent to `d2b status Guest/`. With `--watch`, streams status events. + +**Successor to:** `d2b vm status ` (see migration table below). + +**Current v3 source:** `cmd_vm_status`/`cmd_status` at `packages/d2b/src/lib.rs:6868,3587`; +returns `StatusResponse { entries: Vec }` from `public_wire.rs:2158`. `VmStatus.vm: +String` = current `WorkloadId`; `VmStatus.lifecycle.state: VmLifecycleState` maps to target +`Guest` phase (`Stopped` → `Pending`, `Booted`/`Running` → `Ready`, `Failed` → `Failed`, +`Starting`/`Stopping`/`Restarting` → conditions/reasons on `Pending`/`Degraded`, +not separate phases). Evidence class: +`implemented-and-reachable` for status/lifecycle data path; `ADR-only` for `Guest` resource API. + +### `d2b guest start ` + +Request that the Guest reach Ready phase. + +``` +d2b guest start [--zone ] + [--dry-run | --apply] + [--no-wait-ready] + [--json | --human] +``` + +`--no-wait-ready` exits 0 on accepted (not waiting for Ready phase). +Default behavior waits for phase Ready or Failed with exit codes 0/1. + +**Successor to:** `d2b vm start `, `d2b up `. + +**Current v3 source:** `cmd_vm_start` at `packages/d2b/src/lib.rs:6675`; +dry-run/apply/no-wait-api pattern at `VmStartArgs`. +Evidence class: `implemented-and-reachable`. + +**Required delta:** Route through Zone resource API Guest lifecycle instead +of raw daemon verb; replace `EXIT_API_TIMEOUT` (33) with `deadline-exceeded` +error class. + +### `d2b guest stop ` + +``` +d2b guest stop [--zone ] + [--dry-run | --apply] + [-f | --force] + [--json | --human] +``` + +**Successor to:** `d2b vm stop `, `d2b down `. + +**Current v3 source:** `cmd_vm_stop` at `packages/d2b/src/lib.rs:6690`. +Evidence class: `implemented-and-reachable`. + +### `d2b guest restart ` + +``` +d2b guest restart [--zone ] + [--dry-run | --apply] + [-f | --force] + [--json | --human] +``` + +**Successor to:** `d2b vm restart `, `d2b restart `. + +### `d2b guest create` + +``` +d2b guest create [--zone ] + [--spec-file | --spec-stdin] + [--json | --human] +``` + +### `d2b guest update-spec ` + +### `d2b guest delete ` + +**Note:** The following v3 Guest verbs (`build`, `generations`, `switch`, +`boot`, `test`, `rollback`) are Guest-activation operations that interact with +the activation Provider (`activation-nixos`). They are specified under +`d2b activation` below. + +## `d2b process` — Process resource commands + +Maps to the `Process` ResourceType. + +### `d2b process get ` + +Equivalent to `d2b get Process/`. + +### `d2b process list` + +Equivalent to `d2b list Process [--execution-ref /] + [--domain ]`. + +### `d2b process status ` + +Equivalent to `d2b status Process/`. With `--watch`, streams until Ready +or terminal. + +### `d2b process start ` + +Request that the Process reach Ready phase (sets desired lifecycle). + +``` +d2b process start [--zone ] + [--dry-run | --apply] + [--no-wait-ready] + [--json | --human] +``` + +### `d2b process stop ` + +``` +d2b process stop [--zone ] + [--dry-run | --apply] + [-f | --force] + [--json | --human] +``` + +### `d2b process create`, `update-spec`, `delete` + +Standard resource verbs. + +## `d2b exec` — EphemeralProcess (one-shot exec) + +`d2b exec` creates an `EphemeralProcess` resource and manages its lifecycle. +This replaces the `d2b vm exec` sub-verb for the one-shot asynchronous exec +use case. + +### `d2b exec run -- [args...]` + +Create a detached `EphemeralProcess` and print its resource name. + +``` +d2b exec run [--zone ] + [--name ] + [--domain ] + [--user ] + [--provider ] + [--env KEY=VALUE]... + [--cwd ] + [--deadline ] + [--json | --human] + -- [args...] +``` + +`executionRef` is a canonical ResourceRef of type `Host` or `Guest`. + +Returns the created `EphemeralProcess/` resource ref and initial status. + +**Exit codes:** +- 0: EphemeralProcess created and accepted +- 1: execution target unavailable, authorization denied, zone unavailable +- 2: usage error + +**Successor to:** `d2b vm exec -d -- ` (detached form). + +**Current v3 source:** `VmExecArgs.detach` in `cmd_vm_exec` at +`packages/d2b/src/lib.rs:7166`; `exec_client::EXIT_EXEC_*` exit codes at +`packages/d2b/src/exec_client.rs`. +Evidence class: `implemented-and-reachable` for detached vm exec. + +### `d2b exec attach /` + +Attach stdin/stdout/stderr to a running EphemeralProcess. + +``` +d2b exec attach / [--zone ] + [-i | --interactive] + [-t | --tty] + [--json] + [--deadline ] +``` + +`-t` puts the host terminal in raw mode. `-i` forwards host stdin. `-it` +together yields an interactive shell session. `--tty` is incompatible with +`--json`. + +**Successor to:** `d2b vm exec -it -- bash` (attached interactive form) and +`d2b vm exec -- ` (attached non-interactive form). + +**Current v3 source:** `cmd_vm_exec` attached path at +`packages/d2b/src/lib.rs:7166`; `exec_client.rs` FSM; `terminal_client.rs` +TTY abstractions. Evidence class: `implemented-and-reachable`. + +**Retained behavior:** host terminal raw mode; RAII termios restore on every +exit/error/panic; guest owns PTY; host only flips termios; `--json` envelope +with `source`/`reason`/`guestExitCode`/`signal`/`exitCode` fields; +`--json` is non-interactive (incompatible with `-t`). + +### `d2b exec wait /` + +Wait for completion and exit with the process exit code. + +``` +d2b exec wait / [--zone ] + [--deadline ] + [--json] +``` + +**Exit codes:** guest `WIFEXITED` code passes through (0–255). Reserved exit +codes (see table below) are only generated by transport/protocol failures. + +### `d2b exec status /` + +``` +d2b exec status / [--zone ] + [--watch] + [--deadline ] + [--json | --human] +``` + +**Successor to:** `d2b vm exec status `. + +### `d2b exec list []` + +``` +d2b exec list [] [--zone ] + [--phase ] + [--json | --human] +``` + +**Successor to:** `d2b vm exec list`. + +### `d2b exec logs /` + +``` +d2b exec logs / [--zone ] + [--stdout-offset ] + [--stderr-offset ] + [--max-len ] + [--json] +``` + +**Successor to:** `d2b vm exec logs `. + +**Current v3 source:** `VmExecLogsArgs` in `packages/d2b/src/lib.rs:789`; +`cmd_vm_exec_management` at line 7397. Evidence class: `implemented-and-reachable`. + +### `d2b exec kill /` + +``` +d2b exec kill / [--zone ] + [--signal ] + [--json | --human] +``` + +Sends `ExecCancel` SIGTERM/grace/SIGKILL sequence. + +**Successor to:** `d2b vm exec kill `. + +## `d2b shell` — ShellSession (persistent terminal sessions) + +Shell commands interact with the `shell-terminal` Provider's `ShellSession` +ResourceType and attach to persistent named sessions. + +### `d2b shell open ` + +Open a new persistent shell session or attach to an existing one. + +``` +d2b shell open [--zone ] + [--name ] + [--force] + [--json | --human] +``` + +Puts the host terminal in raw mode for the attached session. With `--json`, +emits the session resource ref and initial status without attaching. + +**Successor to:** `d2b shell ` / `d2b shell attach`. + +**Current v3 source:** `cmd_shell` and `cmd_shell_attach` at +`packages/d2b/src/lib.rs:1788,1957`; shell FSM in `run_shell_fsm` at line +2092; `ShellOwnerTransport`/`TerminalTransport` traits; `exec_client.rs` +signal/TTY machinery. Evidence class: `implemented-and-reachable`. + +**Current target argument:** The current `ShellArgs.vm: String` accepts a +`WorkloadId` (local VM name) or a `RealmTarget` of the form +`..d2b`. The CLI dispatches through `route_vm_target()` (at +`lib.rs:5544`) which resolves to `VmTargetRoute::Local { vm }` (local shell) or +`VmTargetRoute::Gateway { realm, gateway_vm }` (cross-realm shell). The target +v3 arg is `executionRef: Host/ | Guest/`. + +**Gateway shell fails closed:** `cmd_gateway_shell` at `lib.rs:1770` returns +`shell_gateway_attach_failure()` (error class `gateway-shell-attach-unavailable`) +for `ShellAction::Attach`; only management verbs (list/detach/kill) route through +the gateway today. The v3 target removes this restriction via cross-Zone +ComponentSession. + +**Retained behavior:** host terminal raw mode; RAII termios restore; SIGINT/ +SIGTERM/SIGSTOP/SIGHUP signal forwarding; SIGWINCH window resize; shell session +supervision in a verified transient user scope; disconnect detaches but does not +kill; kill targets only the exact re-verified transient scope. + +### `d2b shell attach /` + +Attach to an existing named session. + +``` +d2b shell attach / [--zone ] + [--force] +``` + +**Successor to:** `d2b shell attach --name `. + +### `d2b shell list []` + +``` +d2b shell list [] [--zone ] + [--json | --human] +``` + +**Successor to:** `d2b shell list`. + +**Current v3 source:** `ShellAction::List` in `cmd_shell`; shell list dispatch +at `packages/d2b/src/lib.rs:1788`. Evidence class: `implemented-and-reachable`. + +### `d2b shell detach /` + +``` +d2b shell detach / [--zone ] + [--json | --human] +``` + +**Successor to:** `d2b shell detach --name `. + +### `d2b shell kill /` + +``` +d2b shell kill / [--zone ] + [--json | --human] +``` + +Terminates the session's transient user scope. + +**Successor to:** `d2b shell kill --name `. + +**Current v3 source:** `ShellAction::Kill` in `cmd_shell`. Exit code 1 when +`--name` is absent for kill retained. Evidence class: `implemented-and-reachable`. + +### `d2b shell status /` + +``` +d2b shell status / [--zone ] + [--watch] + [--deadline ] + [--json | --human] +``` + +## `d2b volume` — Volume resource commands + +Maps to the `Volume` ResourceType. + +``` +d2b volume get +d2b volume list +d2b volume status [--watch] [--deadline ] +d2b volume create [--spec-file | --spec-stdin] +d2b volume update-spec [--revision ] [--spec-file | --spec-stdin] +d2b volume delete [--revision ] +``` + +All accept `[--zone ] [--json | --human]`. + +### `d2b volume verify ` + +Verify the Volume's backing state (hardlink farm integrity check for +`volume-local`). + +``` +d2b volume verify [--zone ] + [--repair] + [--json | --human] +``` + +**Successor to:** `d2b store verify `. + +**Current v3 source:** `cmd_store_verify`; `StoreVerifyArgs` at +`packages/d2b/src/lib.rs:973`. Evidence class: `implemented-and-reachable`. + +**Retained behavior:** non-destructive by default; `--repair` opts in to +repairs; exit code reflects verification result. + +## `d2b network` — Network resource commands + +Maps to the `Network` ResourceType. + +``` +d2b network get +d2b network list +d2b network status [--watch] [--deadline ] +d2b network create [--spec-file | --spec-stdin] +d2b network update-spec [--revision ] [--spec-file | --spec-stdin] +d2b network delete [--revision ] +``` + +All accept `[--zone ] [--json | --human]`. + +## `d2b device` — Device resource commands + +Maps to the `Device` ResourceType. + +``` +d2b device get +d2b device list [--type ] +d2b device status [--watch] [--deadline ] +``` + +All accept `[--zone ] [--json | --human]`. + +### `d2b device usb attach ` + +Bind a host USB busid to a declared Device resource. + +``` +d2b device usb attach / + [--zone ] + [--dry-run | --apply] + [--json | --human] +``` + +**Successor to:** `d2b usb attach `. + +**Current v3 source:** `cmd_usb_attach` at `packages/d2b/src/lib.rs`; UsbAttachArgs. +Evidence class: `implemented-and-reachable`. + +### `d2b device usb detach ` + +**Successor to:** `d2b usb detach `. + +**Current v3 source:** `cmd_usb_detach`. Evidence class: `implemented-and-reachable`. + +### `d2b device usb probe` + +``` +d2b device usb probe [--zone ] [--json | --human] +``` + +**Successor to:** `d2b usb probe`. + +**Current v3 source:** `cmd_usb_probe`. Evidence class: `implemented-and-reachable`. + +### `d2b device security-key status` + +``` +d2b device security-key status [--zone ] [--json | --human] +``` + +**Successor to:** `d2b usb security-key status`. + +**Current v3 source:** `cmd_usb_sk_status`. Evidence class: `implemented-and-reachable`. + +### `d2b device security-key sessions` + +**Successor to:** `d2b usb security-key sessions`. + +### `d2b device security-key cancel` + +``` +d2b device security-key cancel [ | --current] + [--dry-run | --apply] + [--json | --human] +``` + +**Successor to:** `d2b usb security-key cancel`. + +### `d2b device security-key test ` + +``` +d2b device security-key test / [--zone ] + [--dry-run] [--json | --human] +``` + +**Successor to:** `d2b usb security-key test `. + +## `d2b endpoint` — Endpoint resource commands + +Maps to the `Endpoint` ResourceType (D092). Endpoints are provider-neutral +managed endpoint identities produced by a `Process`, `Device`, `Guest`, or +`Host` (or a qualified resource); the CLI surface is read-and-reference only and +never exposes a raw path, address, CID, port, fd, or credential. + +``` +d2b endpoint get +d2b endpoint list [--endpoint-class ] +d2b endpoint watch [--endpoint-class ] +d2b endpoint status [--watch] [--deadline ] +d2b endpoint resolve +``` + +All accept `[--zone ] [--json | --human]`. + +- `get`/`list`/`watch`/`status` use the standard resource verbs on + `Endpoint/`. Output is the base Endpoint envelope only: base spec + (`providerRef`, `producerRef`, `endpointClass`, `transport`, `purpose`, + service/schema fingerprint or capability class, locality/visibility, + attachment support/policy, consumer policy/ref bounds, lifecycle/recycle + policy) and base status (readiness, observed producer/resource generation, + connection/lease availability, endpoint generation, closed + capability/locality/transport observations, `status.update` currency, + conditions). Any `status.provider` projection is bounded, redacted, and + deny-unknown; **no raw locator** (path/address/CID/port/fd/credential) appears + in any field. +- `d2b endpoint resolve ` resolves the `Endpoint/` ResourceRef and + prints its provider-neutral resolution projection — `producerRef`, + `endpointClass`, transport class, readiness, and capability/locality + observations. It NEVER prints or returns a private transport handle or raw + locator; actual resolution to a private transport/FD happens only inside + Core/ProviderSupervisor through EffectPort/LaunchTicket, never through the CLI. +- Endpoints are controller-created by their signed component/semantic owner; + `d2b endpoint create`/`update-spec`/`delete` are rejected for the operator + surface except for statically Nix/API-authored Endpoints whose schema permits + it. `status.update` currency and `d2b upgrade Endpoint/` visibility + follow the standard resource verbs and D091, so operators see endpoint update + state and disruption without any raw locator exposure. + +## `d2b export` — ResourceExport commands (cross-Zone sharing, D096) + +Maps to the `ResourceExport` ResourceType. Runs in the owner/authority Zone. A +`ResourceExport` shares one qualified owner `*Service` with child Zones through +a single Provider authority. The Service/Binding namespace is semantic and +provider-neutral; `providerRef` separately selects its implementation. +`resourceRef` MUST target that local Service, never its Device/Endpoint/backend, +a Binding, or a remote Ref. + +``` +d2b export get +d2b export list [--exported-type ] +d2b export watch [--exported-type ] +d2b export status [--watch] [--deadline ] +d2b export create [--spec-file | --spec-stdin] +d2b export update-spec [--revision ] [--spec-file | --spec-stdin] +d2b export delete [--revision ] +``` + +All accept `[--zone ] [--json | --human]`. `status` shows advertised/ready/ +revoking state, active/pending consumer counts, and bounded per-consumer lease +summaries, owner-Service type/generation, and projection/factory fingerprints — +**no backing Ref, raw bytes, path, device node, socket, secret, or token**. +`create`/`update-spec` reject a non-Service target, absent/mismatched signed +factory, cross-Zone Ref, unauthorized Zone, or non-exportable Provider. +`device-usbip` additionally requires its policy gate. `delete` revokes leases +and degrades every bound import/projection Service without deleting the owner +Service. + +## `d2b import` — ResourceImport commands (cross-Zone sharing, D096) + +Maps to the `ResourceImport` ResourceType. Runs in the consumer (child) Zone. A +`ResourceImport` binds a local `zoneLinkRef` and a bounded `exportKey` to a +core-owned same-qualified-type projection `*Service`. It never projects a +Device, Endpoint, or `*Binding`. Operators/Nix separately author matching local +Bindings that reference the projection Service and a consuming Guest/User/Zone. +Binding spec is desired consumer intent only; observations appear only in status. + +``` +d2b import get +d2b import list [--expected-type ] +d2b import watch [--expected-type ] +d2b import status [--watch] [--deadline ] +d2b import projection +d2b import graph +d2b import create [--spec-file | --spec-stdin] +d2b import update-spec [--revision ] [--spec-file | --spec-stdin] +d2b import delete [--revision ] +``` + +All accept `[--zone ] [--json | --human]`. `status` shows +pending/reachable/bound/degraded/revoked, observed remote export generation and +factory/projection fingerprints, local projection-Service Ref, lease state/ +count, and `status.update` currency — no remote Ref or raw locator. +`d2b import projection ` prints that Service Ref and bounded base status. + +`d2b import graph ` renders the deterministic local dependency graph: + +```text +ResourceImport/mic-import +└─ audio.d2bus.org.AudioService/host-audio [projection, ownerRef=ResourceImport/mic-import] + └─ audio.d2bus.org.AudioBinding/work-mic [authored, targetRef=Guest/workstation] + ├─ Process/ + └─ Endpoint/ +``` + +Human output and versioned JSON include refs, ownership/dependency edges, +readiness, and bounded generations only. They distinguish the authored Binding +from controller-owned children and never render backing refs, remote refs, +session/stream IDs, FDs, secrets, raw paths, locators, or bytes. +`create`/`update-spec` reject remote refs, missing/mismatched factory metadata, +semantic-type changes, Provider implementation details in the base, or +capabilities outside the ceiling. Service and Binding output always uses the +semantic type preserved by export/import; an implementation selection appears +only as `providerRef` and a bounded/redacted `status.provider` projection. +`delete` marks the projection draining and +waits for Bindings to be deleted/retargeted; it never deletes a Binding. It then +releases the lease and deletes only the projection Service/provider-owned +children. + +## `d2b resource authorities` — authority index (D097) + +Reports the D097 authority index: which authority owns each scarce or singleton +backing, its holders, and any conflict. + +``` +d2b resource authorities [--zone ] [--scope ] [--json | --human] +d2b resource authorities holders [--zone ] [--json | --human] +d2b resource authorities conflict [--zone ] [--json | --human] +``` + +- `d2b resource authorities` lists every authority-bearing Resource with its + `authorityScope`, `cardinality`, `arbitration`, `available`, current holder + count, and queue depth (the provider-neutral status base). No raw identity is + shown — the `authorityKey` appears only as a bounded opaque digest. +- `holders ` lists the current holders/leases of one authority + (bounded summaries only: consumer scope, lease state — no raw locator/token). +- `conflict ` shows whether a create/activation was rejected with + `duplicateConflict` and prints the incumbent **owner digest** (the same digest + a `Degraded` config activation reports), with no raw path/serial/address. + +Read verbs require ordinary `get`/`list` authorization on the owning +ResourceType. This surface never creates or mutates an authority. + +## `d2b user` — User resource commands + +Maps to the `User` ResourceType. + +``` +d2b user get +d2b user list +d2b user status +``` + +All accept `[--zone ] [--json | --human]`. + +## `d2b credential` — Credential resource commands + +Maps to the `Credential` ResourceType. Credential bytes are never surfaced +through the CLI; only opaque status/lease metadata is returned. + +``` +d2b credential get +d2b credential list +d2b credential status [--watch] [--deadline ] +d2b credential delete +d2b credential login [--attach] [--deadline ] +d2b credential cancel-login +``` + +All accept `[--zone ] [--json | --human]`. + +`d2b credential login ` (D093) starts (`BeginLogin`) and, with `--attach`, +attaches the typed interactive login stream for an identity-Guest-grounded +credential (e.g. `credential-entra`). The interactive UI itself executes **inside +the identity Guest** (Entrablau); the CLI attaches a named ComponentSession +stream direct to that Guest login service and never handles tokens, URLs, +cookies, device codes conferring authority, or user PII. `get`/`list`/`status` +show the Credential base `interactionState` +(`NotRequired|Required|Starting|AwaitingUser|Authenticated|Failed|Unknown`) and +`status.update` currency; `cancel-login` cancels the in-flight session +(`CancelLogin`). Errors are actionable (`identity-guest-unavailable`, +`login-endpoint-unavailable`, `login-deadline-exceeded`, `login-cancelled`). +`--deadline` bounds the interactive wait; the command never blocks past the +login deadline. + +**Current v3 source:** No direct current CLI mapping. +Evidence class: `ADR-only`. + +## `d2b provider` — Provider resource commands + +### `d2b provider list` + +``` +d2b provider list [--zone ] + [--package-only] + [--json | --human] +``` + +`--package-only` lists the offline package catalog without contacting the Zone +runtime. + +**Current v3 source:** No direct mapping; closest is `cmd_list` which lists +runtime VMs. Evidence class: `ADR-only`. + +### `d2b provider get ` + +``` +d2b provider get [--zone ] + [--json | --human] +``` + +Returns the Provider resource envelope including component/service status. + +### `d2b provider status ` + +``` +d2b provider status [--zone ] + [--watch] + [--deadline ] + [--json | --human] +``` + +### `d2b provider inspect ` + +Return full Provider descriptor: exported ResourceTypes, schemas, services, +process templates, permission claims, CLI projection, dependency aliases. + +``` +d2b provider inspect [--zone ] + [--json | --human] +``` + +**Relationship to dynamic descriptors:** The CLI fetches the CLI projection +sub-field of the Provider descriptor and renders the custom commands it +declares. See §Custom provider commands below. + +**Current v3 source:** No direct mapping. Evidence class: `ADR-only`. + +### Custom provider commands + +A Provider may declare a bounded `cliProjection` in its descriptor. The +projection contains: + +- a top-level subcommand name (e.g. `audio`, `display`); +- one or more sub-verb descriptors, each with: name, description, arguments + (typed, bounded), required/optional flags, output schema; +- maximum total projection byte size: 64 KiB per Provider. + +The CLI discovers projections by calling `InspectSchema` on each Ready +Provider with a `--deadline 2s` hard deadline per call. Missing or slow +Providers produce a visible warning but do not block CLI startup. + +Custom commands are rendered as: + +``` +d2b [args...] +``` + +The declared top-level subcommand name is the command name; the Provider +resource name is not part of the invocation. For example, `audio-pipewire` may +declare `audio`, rendered as `d2b audio status`, and `display-wayland` may +declare `display`, rendered as `d2b display list`. + +**Canonical built-in command registry:** The implementation maintains exactly +one registry of built-in top-level command names. Parser construction, help, +completion, and Provider projection binding all consume that registry; no +callsite keeps a separate reserved-name list. The registry is: + +`get`, `list`, `watch`, `create`, `update-spec`, `delete`, `status`, `upgrade`, +`reconcile`, `host`, `guest`, `process`, `exec`, `shell`, `volume`, `network`, +`device`, `endpoint`, `export`, `import`, `resource`, `user`, `credential`, +`provider`, `zone`, `quota`, `emergency-policy`, `activation`, `audit`, `op`, +`auth`, and `complete`. + +Adding or removing a built-in command updates this registry; projection binding +therefore inherits the change without a second reserved-name edit. + +**Single collision authority:** Provider install/bind time is the only +authority for projected top-level name collisions. Binding rejects a declared +name found in the canonical built-in registry or already bound by another +Provider. The CLI never resolves a collision at dispatch time and exposes no +collision-specific fallback command. + +**Migrated Provider commands:** The following current v3 Provider-specific +commands retain their operator-facing names through declared CLI projections; +the serial console instead moves to the built-in `guest` command: + +| Current v3 command | Owner | Successor | +| --- | --- | --- | +| `d2b audio status/mic/speaker/off` | `audio-pipewire` | `d2b audio status/mic/speaker/off` via the declared `audio` projection | +| `d2b clipboard arm` | `clipboard-wayland` | `d2b clipboard arm` via the declared `clipboard` projection | +| `d2b console ` | Guest serial console | `d2b guest console ` | +| `d2b vm display list/close` | `display-wayland` | `d2b display list/close` via the declared `display` projection | + +**Current v3 source (audio):** `cmd_audio` at `packages/d2b/src/lib.rs`; `AudioArgs`, +`AudioCommand`. Evidence class: `implemented-and-reachable`. + +**Current v3 source (clipboard):** `cmd_clipboard_arm` at line 2555. Evidence +class: `implemented-and-reachable`. + +**Provider projection loading:** Provider CLI projections are loaded lazily on +demand. A projection is fetched from the Provider's InspectSchema endpoint the +first time that Provider's projected subcommand is invoked or included in +top-level `d2b --help` output. Each fetch is bounded by the per-Provider +2-second deadline. The result is cached for the duration of the current CLI +invocation only. No disk cache or cross-invocation cache is maintained. Startup +latency for non-provider commands is zero. + +## `d2b zone` — Zone topology and self-resource commands + +``` +d2b zone get [] # omitting name fetches the current Zone self resource +d2b zone list # compiler topology plus authenticated route/projection status +d2b zone status [] [--watch] [--deadline ] +``` + +All accept `[--json | --human]`. + +**Successor to:** `d2b realm list`, `d2b realm inspect `. + +**Current v3 source:** `cmd_realm_list`, `cmd_realm_inspect` at +`packages/d2b/src/lib.rs:5942,5958`. These read the **static Nix-generated file** +`realm-entrypoints.json` (path constant in `lib.rs`) via `realm_policy_rows_raw()` — +NOT a live daemon API call. The file contains a `RealmEntrypointDocument { entries: +BTreeMap }` (from `d2b-realm-router` crate); entries +have `mode: host-resident|gateway-backed` and optional `gateway` VM name. +`cmd_realm_list` emits `RealmListOutputV1 { realms: Vec }`; +`cmd_realm_inspect` emits `RealmInspectOutputV1 { realm: RealmPolicyOutputV1 }`. +`RealmPolicyOutputV1` fields: `realm` (= `RealmId`), `mode`, `gateway_vm`, `gateway_target`, +`gateway_state`, `cross_realm_policy`, `credential_boundary` (from +`packages/d2b-contracts/src/cli_output.rs:345`). + +Evidence class: `implemented-and-reachable` for realm list/inspect (static file read); +`ADR-only` for the Zone self-resource API and Zone topology/route projection +(live daemon query replacing the static file). + +**Command algorithms and output boundary:** + +1. `d2b zone list` invokes the local `ZoneService` topology-projection method. + The service starts from the sealed, compiler-only sorted + `{ childZone, parentZone }` rows and joins only route/projection status + derived from currently authenticated, admitted advertisements. The result is + sorted by `(parentZone, childZone)`. +2. `d2b zone get` with no name, or with the current Zone name, fetches only the + current Zone's cardinality-one self resource. For another declared Zone, it + resolves the exact topology row and returns its authenticated route/projection + inspection; when the route is current, the service may augment that + inspection with an authenticated call to the target Zone's self resource. + Missing, withdrawn, stale, or unauthenticated route state is reported as + unavailable and is never replaced by static guesses. +3. `d2b zone status --watch` watches the current Zone self resource for the + current Zone and the topology/route projection for any child or descendant. + Reconnect resumes from the projection revision; it never opens a watch on a + parent-store ZoneLink row. + +The generated CLI DTOs and golden output fixtures contain the topology +`childZone`/`parentZone` pair and bounded authenticated route/projection status. +They contain no ZoneLink resource name, UID, spec, status, Provider ref, +fingerprint, transport setting, or parent-side link handle. A child's one +ZoneLink remains in that child's store and is reconciled by that child's +core-controller; a parent stores only sealed topology/allocation state and +authenticated route projections. + +**Replacement/deletion:** `d2b realm enter` and `d2b realm run` are replaced by +`d2b guest start ` plus `d2b exec run -- `. +See migration table. + +## `d2b quota` — Quota resource commands + +Maps to the `Quota` ResourceType. Quota resources define resource-consumption +limits for a Zone or a subset of its resources. Quota and EmergencyPolicy are +separate resources; quota commands do not operate on `Zone.spec` (which is empty). + +``` +d2b quota get [--zone ] [--json | --human] +d2b quota list [--zone ] [--json | --human] +d2b quota status [--zone ] [--watch] [--deadline ] [--json | --human] +d2b quota create [--zone ] [--spec-file | --spec-stdin] [--json | --human] +d2b quota update-spec [--zone ] [--revision ] [--spec-file | --spec-stdin] [--json | --human] +d2b quota delete [--zone ] [--revision ] [--json | --human] +``` + +**Successor to:** no current v3 equivalent. + +**Evidence class:** `ADR-only`. Target implemented by standard resource verb +infrastructure (ADR046-cli-002). + +## `d2b emergency-policy` — EmergencyPolicy resource commands + +Maps to the `EmergencyPolicy` ResourceType. EmergencyPolicy resources define +Zone-wide emergency operational modes (e.g. forced shutdown, isolation, +credential revocation). They are separate resources from Zone, Host, and Guest; +`Zone.spec` is empty and carries no policy fields. + +``` +d2b emergency-policy get [--zone ] [--json | --human] +d2b emergency-policy list [--zone ] [--json | --human] +d2b emergency-policy status [--zone ] [--watch] [--deadline ] [--json | --human] +d2b emergency-policy create [--zone ] [--spec-file | --spec-stdin] [--json | --human] +d2b emergency-policy update-spec [--zone ] [--revision ] [--spec-file | --spec-stdin] [--json | --human] +d2b emergency-policy delete [--zone ] [--revision ] [--json | --human] +``` + +**Successor to:** no current v3 equivalent. + +**Evidence class:** `ADR-only`. Target implemented by standard resource verb +infrastructure (ADR046-cli-002). + +**Help text** for both `d2b quota` and `d2b emergency-policy` MUST note that +`Zone.spec` is empty; quota limits and emergency policy are configured via +their own resource types, not through Zone fields. + +**Tests:** + +| Layer | Test | What it proves | +| --- | --- | --- | +| Unit | `d2b quota get/list/status/create/update-spec/delete` parse and route correctly | Quota resource noun wired | +| Unit | `d2b emergency-policy get/list/status/create/update-spec/delete` parse and route correctly | EmergencyPolicy resource noun wired | +| Unit | `Quota` and `EmergencyPolicy` accepted as frozen standard types (local validation) | Compile-time type list complete | +| Unit | `Endpoint` accepted as a frozen standard type; `Endpoint/` parses via the local ResourceRef parser without a Zone round-trip | Compile-time type list complete (19 types) | +| Unit | `ResourceExport` and `ResourceImport` accepted as frozen standard types; `ResourceExport/` and `ResourceImport/` parse locally without a Zone round-trip | Compile-time type list complete (19 types) | +| Unit | `d2b export get/list/watch/status/create/update-spec/delete` and `d2b import get/list/watch/status/projection/graph/create/update-spec/delete` parse and route correctly | ResourceExport/ResourceImport nouns wired (D096) | +| Unit | export/import reject a Device/Endpoint/Binding target, cross-Zone Ref, absent/mismatched factory/type/schema fingerprint, unauthorized Zone, forbidden Provider, USBIP policy denial, and capability-ceiling violation | Cross-Zone Service projection guards (D096) | +| Unit | `d2b import graph` renders import → projection Service → authored Binding → owned Process/Endpoint; Binding spec is intent-only, observations are status-only, and status/graph carry no backing/remote refs, sessions/streams, FDs, secrets, paths, locators, or bytes | Deterministic graph and no cross-Zone leakage (D096) | +| Unit | `d2b endpoint get/list/watch/status/resolve` parse and route correctly on `Endpoint/` | Endpoint resource noun wired | +| Unit | `d2b endpoint get`/`resolve` output carries no raw path/address/CID/port/fd/credential field; provider status projection is bounded/redacted | No raw locator exposure (D092) | +| Unit | `d2b list Endpoint --updates` and `d2b upgrade Endpoint/` expose `status.update` currency and disruption | Endpoint update visibility (D091) | +| Unit | `d2b get Quota/default` routes to Zone catalog without a round-trip for type validation | Local type validation path | +| Unit | `d2b get EmergencyPolicy/lockdown` routes correctly | Local type validation path | + +## `d2b activation` — activation-nixos Provider commands + +The `activation-nixos` Provider projects these CLI commands. They operate on +the `activation-nixos`-specific ResourceTypes. + +### `d2b activation build ` + +Non-destructive eval + build of the Guest NixOS toplevel. + +``` +d2b activation build / [--zone ] + [--json | --human] +``` + +**Successor to:** `d2b build `. + +**Current v3 source:** `cmd_build` at `packages/d2b/src/lib.rs`. Evidence +class: `implemented-and-reachable`. + +### `d2b activation generations ` + +``` +d2b activation generations / [--zone ] + [--json | --human] +``` + +**Successor to:** `d2b generations `. + +### `d2b activation switch ` + +Atomically activate a new closure. + +``` +d2b activation switch / [--zone ] + [--dry-run | --apply] + [--json | --human] +``` + +**Successor to:** `d2b switch `. + +### `d2b activation boot ` + +Stage a closure for next boot only. + +``` +d2b activation boot / [--zone ] + [--dry-run | --apply] + [--json | --human] +``` + +**Successor to:** `d2b boot `. + +### `d2b activation test ` + +Activate with rollback on reboot. + +``` +d2b activation test / [--zone ] + [--dry-run | --apply] + [--json | --human] +``` + +**Successor to:** `d2b test `. + +### `d2b activation rollback ` + +Roll back to previous generation. + +``` +d2b activation rollback / [--zone ] + [--dry-run | --apply] + [--json | --human] +``` + +**Successor to:** `d2b rollback `. + +### `d2b activation gc` + +Garbage-collect hardlink farms. + +``` +d2b activation gc [--zone ] + [--dry-run | --apply] + [--json | --human] +``` + +**Successor to:** `d2b gc`. + +### `d2b activation migrate` + +``` +d2b activation migrate [--zone ] + [--dry-run | --apply] + [--json | --human] +``` + +**Successor to:** `d2b migrate`. + +### `d2b activation keys` — managed SSH key lifecycle + +``` +d2b activation keys list [--json | --human] +d2b activation keys show [--json | --human] +d2b activation keys rotate [--dry-run | --apply] [--json | --human] +d2b activation trust [--json | --human] +d2b activation rotate-known-host [--json | --human] +``` + +**Successor to:** `d2b keys list/show/rotate`, `d2b trust `, +`d2b rotate-known-host `. + +**Current v3 source:** `cmd_keys_list`, `cmd_keys_show`, `cmd_keys_rotate`, +`cmd_keys_trust`, `cmd_keys_rotate_known_host` at +`packages/d2b/src/lib.rs:986ff`. Evidence class: `implemented-and-reachable`. + +### `d2b activation config` — guest-editable config lifecycle + +``` +d2b activation config sync [--dry-run] [--json] +d2b activation config diff --against [--json] +d2b activation config approve --to [--json] +d2b activation config reject [--json] +d2b activation config status [--json] +``` + +**Successor to:** `d2b config sync/diff/approve/reject/status`. + +**Current v3 source:** `ConfigCommand` variants; `cmd_config_sync` and +related at `packages/d2b/src/lib.rs:2699ff`. Evidence class: +`implemented-and-reachable`. + +**Retained behavior:** guest-control transport for sync (no SSH); staging file +lifecycle; diff/approve/reject workflow. + +### `d2b guest console ` + +Foreground serial console bridge for headless Guests. + +``` +d2b guest console [--zone ] +``` + +**Successor to:** `d2b console `. + +**Current v3 source:** `cmd_console` at `packages/d2b/src/lib.rs`. Evidence +class: `implemented-and-reachable`. + +## `d2b audit` — audit log streaming + +``` +d2b audit [--zone ] + [--strict] + [--deadline ] + [--json | --human] +``` + +**Successor to:** `d2b audit`. + +**Current v3 source:** `cmd_audit` at `packages/d2b/src/lib.rs`. Evidence +class: `implemented-and-reachable`. + +**Retained behavior:** streaming audit lines; `--strict` (exit non-zero on +parse error); bounded line lengths; redacted payloads. + +### `d2b zone audit export` + +```text +d2b zone audit export [--zone ] + [--after ] + [--before ] +``` + +The command invokes exactly `d2b.audit.v3.AuditService/Export` and requires the +admin-only `audit-export` session verb. CLI help describes it as a diagnostic +service grant, not a Zone resource verb; it supplies no resource `get`, `list`, +or mutation authority. + +### `d2b zone support-bundle` + +```text +d2b zone support-bundle [--zone ] +``` + +The command invokes exactly +`d2b.support.v3.SupportService/GenerateBundle` and requires the admin-only +`support-bundle` session verb. CLI help does not describe it as a resource read +grant: the admitted service performs bounded redacted internal reads and emits +NDJSON. + +## `d2b op` — operation inspection + +``` +d2b op inspect [--zone ] + [--operation-id ] + [--trace-id ] + [--json | --human] +``` + +**Successor to:** `d2b op inspect`. + +**Current v3 source:** `cmd_op_inspect` at `packages/d2b/src/lib.rs:6098`. +Evidence class: `implemented-and-reachable`. + +## `d2b auth` — authorization status + +``` +d2b auth status [--zone ] + [--json | --human] +``` + +**Successor to:** `d2b auth status`. + +**Current v3 source:** `cmd_auth_status` at `packages/d2b/src/lib.rs`. Evidence +class: `implemented-and-reachable`. + +**Retained behavior:** SO_PEERCRED group-membership check display; no test-uid +argument exposed in v3. + +## `d2b complete` — shell completion + +``` +d2b complete # emit completion script (bash | zsh | fish) +d2b complete --list-commands # emit available top-level commands as JSON +``` + +Completion discovery fetches Provider CLI projections with a hard 2s aggregate +deadline. Missing Providers produce a stable degraded list without error. +Completion output is bounded: maximum 256 KiB total. + +**Current v3 source:** No shell completion implemented in v3 baseline +(`packages/d2b/src/lib.rs` has no `clap_complete` reference). Evidence +class: `ADR-only`. + +## v2 command surface removed at 3.0 clean break + +All v2 aliases and predecessor commands are deleted at the d2b 3.0 clean break. +There are no executable aliases or migration-diagnostic command in 3.0. +Migration guidance is documentation-only. The table below records each removed +command and its v3 successor for documentation and test-removal tracking only. + +| Removed v2 command | v3 successor | +| --- | --- | +| `d2b up ` | `d2b guest start ` | +| `d2b down ` | `d2b guest stop ` | +| `d2b restart ` | `d2b guest restart ` | +| `d2b list` | `d2b guest list` | +| `d2b status []` | `d2b guest status ` | +| `d2b vm start/stop/restart/list/status` | `d2b guest start/stop/restart/list/status` | +| `d2b vm exec -- ` | `d2b exec run/attach` | +| `d2b vm exec list/logs/status/kill` | `d2b exec list/logs/status/kill` | +| `d2b realm list` | `d2b zone list` | +| `d2b realm inspect ` | `d2b zone get ` | +| `d2b realm enter ` | `d2b shell open ` | +| `d2b realm run -- ` | `d2b exec run -- ` | +| `d2b usb attach/detach/probe` | `d2b device usb attach/detach/probe` | +| `d2b usb security-key ` | `d2b device security-key ` | +| `d2b store verify ` | `d2b volume verify /` | +| `d2b keys list/show/rotate` | `d2b activation keys list/show/rotate` | +| `d2b trust ` | `d2b activation trust ` | +| `d2b rotate-known-host ` | `d2b activation rotate-known-host ` | +| `d2b build ` | `d2b activation build ` | +| `d2b generations ` | `d2b activation generations ` | +| `d2b switch/boot/test/rollback ` | `d2b activation switch/boot/test/rollback ` | +| `d2b gc` | `d2b activation gc` | +| `d2b migrate` | `d2b activation migrate` | +| `d2b config sync/diff/approve/reject/status` | `d2b activation config sync/...` | +| `d2b console ` | `d2b guest console ` | +| `d2b vm display list/close` | `d2b display list/close` | + +## Async operation lifecycle, status watch, and cancel + +### `d2b op inspect` and operation IDs + +Every mutating resource API call returns an operation ID in the JSON output +envelope as `operationId`. Long-running operations (Guest start/stop, Provider +lifecycle, activation switch) also emit it in human output. + +``` +d2b op inspect --operation-id [--zone ] + [--watch] + [--deadline ] + [--json | --human] +``` + +### Deadline and cancel flags + +All commands that interact with the Zone runtime accept: + +``` +--deadline # wall deadline (e.g. 30s, 5m); default per-command +--no-deadline # suppress default deadline (use with care) +``` + +Deadline exhausted: exit code 1, `deadline-exceeded` error class, `--json` +envelope with `errorClass: "deadline-exceeded"`. + +SIGINT / SIGTERM: attempt graceful cancel of the in-flight operation, emit a +cancel event envelope, restore terminal state, then exit with code 3. + +SIGHUP: detach from interactive sessions (shell/exec attach) without killing +the remote session. + +**Current v3 source:** Deadline partially implemented: `EXIT_API_TIMEOUT` (33) +for vm-start; exec transport deadline at `EXIT_EXEC_TRANSPORT` (69). Evidence +class: `implemented-but-unwired` for general deadline; `implemented-and-reachable` +for specific paths. + +**Required delta:** Unified `deadline-exceeded` error class across all resource +API paths; SIGINT/SIGTERM cancel dispatch; `--deadline` flag on all API-backed +commands. + +## Output format + +### `--json` envelope + +Every command's `--json` output is a single JSON object (for non-streaming +commands) or a stream of newline-separated JSON objects (for `watch`, `exec +attach`, `audit`). + +Common fields in every `--json` envelope: + +```json +{ + "ok": true, + "zoneRef": "Zone/dev", + "operationId": "", + "schemaVersion": 1, + "resource": { /* canonical ResourceSpec envelope — see §Canonical ResourceSpec JSON shape */ } +} +``` + +Error envelopes: + +```json +{ + "ok": false, + "zoneRef": "Zone/dev", + "errorClass": "resource-not-found", + "message": "bounded redacted operator detail", + "remediation": "optional actionable hint" +} +``` + +The `message` and `remediation` fields are bounded (max 4096 bytes each), +UTF-8 validated, and must not contain secrets, tokens, credential bytes, argv/ +environment, terminal bytes, or host paths. + +**Stability:** JSON field names are stable across patch and minor versions. +Additive new fields may appear; a consumer must use `deny_unknown_fields = false` +on JSON deserialization. + +**Versioning:** The `--json` envelope carries `"schemaVersion": 1` in the root. +Minor bumps add optional fields; major bumps increment the version. + +### `--human` output + +Human output is for interactive terminals only. Format is not machine-parseable +and may change between patch versions. + +**Format defaulting rule:** `--json` and `--human` are mutually exclusive. When +neither is given: if stdout is a TTY, human output is produced; if stdout is +not a TTY, JSON is produced (script-friendly default). `--human` explicitly +overrides to human output even in non-TTY contexts; `--json` explicitly +overrides to JSON even in TTY contexts. + +**Current v3 source:** `stdout_is_tty()` at +`packages/d2b/src/lib.rs:10302`. Evidence class: `implemented-and-reachable`. + +## Error classes and exit codes + +### Stable error classes + +All error class strings are stable lower-kebab-case machine values. + +| Error class | Meaning | +| --- | --- | +| `resource-not-found` | Target resource does not exist | +| `resource-already-exists` | Create precondition: resource exists | +| `resource-conflict` | Optimistic concurrency mismatch | +| `resource-schema-invalid` | Spec/status payload fails schema validation | +| `ref-invalid` | ResourceRef syntax or resolution failure | +| `authorization-denied` | RBAC or structural check denied the request | +| `zone-unavailable` | Zone runtime not reachable | +| `deadline-exceeded` | Wall deadline elapsed | +| `operation-cancelled` | SIGINT/SIGTERM graceful cancel | +| `provider-unavailable` | Target Provider is not Ready | +| `exec-transport-error` | Exec vsock/session transport unreachable or deadline | +| `exec-old-generation` | VM generation does not support guest-control exec | +| `exec-capacity` | Exec session table at capacity or rate-limited | +| `exec-protocol-error` | Malformed/out-of-contract guest response | +| `exec-auth-error` | Guest-control handshake rejected | +| `exec-internal-error` | CLI/daemon-internal failure | +| `shell-transport-error` | Shell session transport failure | +| `not-implemented` | Requested verb is not implemented in this runtime version | +| `internal-error` | Unexpected CLI/runtime internal failure | +| `bundle-integrity-failure` | Zone resource bundle SHA256 pin mismatch | +| `bundle-generation-replay` | Submitted bundleGeneration ≤ last applied generation | +| `bundle-schema-mismatch` | Bundle resourceTypeSchemaDigests do not match installed schemas | +| `resource-pending-cleanup` | Resource has outstanding deletion that must complete or be force-removed | + +### Stable exit codes + +| Exit code | Class | Condition | +| --- | --- | --- | +| 0 | success | Command completed successfully | +| 1 | error | Operational failure (not-found, unavailable, zone-down, etc.) | +| 2 | usage | Argument/parsing/validation error | +| 3 | cancelled | Cancelled by SIGINT/SIGTERM/deadline (interactive streams) | +| 33 | `deadline-exceeded` | API-ready wait timeout (retained from v3: `EXIT_API_TIMEOUT`) | +| 42 | `exec-internal-error` | CLI/daemon internal exec failure (`EXIT_EXEC_INTERNAL`) | +| 69 | `exec-transport-error` | Exec transport unreachable or deadline (`EXIT_EXEC_TRANSPORT`) | +| 70 | `exec-old-generation` | Old VM generation (`EXIT_EXEC_OLD_GENERATION`, `EXIT_GUEST_CONTROL_CONFIG`) | +| 75 | `exec-capacity` | Exec session table at capacity (`EXIT_EXEC_CAPACITY`) | +| 76 | `exec-protocol-error` | Malformed guest response (`EXIT_EXEC_PROTOCOL`) | +| 77 | `exec-auth-error` | Guest-control handshake rejected (`EXIT_EXEC_AUTH`) | +| 78 | `not-implemented` | Runtime returned `not-yet-implemented` | +| 128+N | signal | Terminated by signal N (non-TTY context only) | + +Guest WIFEXITED 0–255 pass through `exec attach`/`exec wait` and can collide +with the reserved codes above. The `--json` envelope disambiguates via +`source`/`reason`/`guestExitCode`/`transportExitCode`. + +**Host-check exit code 3:** `d2b host check` uses exit code 3 for usage errors +(retained from v3 baseline `is_host_usage` check at +`packages/d2b/src/lib.rs:2405`). This is the one documented exception to the +standard exit code 2 usage rule. + +**Current v3 source:** `EXIT_API_TIMEOUT=33`, `EXIT_GUEST_CONTROL_CONFIG=70`, +`EXIT_EXEC_TRANSPORT=69`, `EXIT_EXEC_OLD_GENERATION=70`, +`EXIT_EXEC_CAPACITY=75`, `EXIT_EXEC_PROTOCOL=76`, `EXIT_EXEC_AUTH=77`, +`EXIT_EXEC_INTERNAL=42` in +`packages/d2b/src/lib.rs:92,98` and +`packages/d2b/src/exec_client.rs:32-46`. +Evidence class: `implemented-and-reachable`. + +## Streams and TTY contract + +### Attached exec/shell streams + +When `-t` / `--tty` is given: + +1. The CLI verifies stdout is a TTY (`stdout.is_terminal()`). +2. The CLI enters raw mode on the host terminal (`tcsetattr(STDIN, TCSANOW)`). +3. An RAII guard restores termios on every code path (success, error, panic, + signal). +4. The guest owns the PTY. The host streams bytes bidirectionally without + interpretation. +5. SIGWINCH triggers a resize notification to the guest. +6. SIGINT/SIGTERM/SIGHUP are forwarded as typed signals to the guest session. +7. SIGHUP in a shell context detaches without killing. +8. On disconnect, raw mode is restored before any error message is printed to + stderr. + +`--tty` is incompatible with `--json` (the JSON envelope cannot capture raw +terminal bytes). + +**Current v3 source:** `exec_client::FdStateGuard`, `exec_client::install_signals`, +`exec_client::current_window_size`, `run_shell_fsm`, `ShellOwnerTransport` in +`packages/d2b/src/lib.rs:1957,2005,2092` and +`packages/d2b/src/exec_client.rs`. Evidence class: `implemented-and-reachable`. + +### Named streams + +The Zone resource API Watch and exec/shell attach operations use named streams +over ComponentSession. The CLI does not manage credit flow directly; that is +the ComponentSession layer's responsibility. + +### Audit and watch streams + +Non-TTY streaming commands (`d2b audit`, `d2b watch`, `d2b exec logs`) +write newline-terminated JSON objects to stdout continuously. Each object is +valid JSON on its own line. Buffering is flushed after each object. The stream +closes when the server closes, the deadline is reached, or a signal is received. + +## Completion and help + +### `--help` output + +Every command and subcommand emits a usage block followed by a description. +The description is bounded (max 2048 chars per command). Process-markers, wave +tags, or internal identifiers are not present in help text. + +Dynamic Provider-projected commands appear in the top-level `--help` listing +under a `PROVIDER COMMANDS` section with a `(provider: )` annotation and +a bounded description. + +### Shell completion + +Shell completion scripts (bash, zsh, fish) are generated by `d2b complete +`. Completion discovery: + +1. Fetches the list of Ready Providers from the Zone runtime (2s deadline). +2. For each Provider with a `cliProjection`, fetches the projection (2s + deadline per call). +3. Generates completion for all built-in + projected commands. +4. Result is bounded at 256 KiB. + +A timeout or unreachable Zone runtime produces completion for built-in commands +only, with no error. + +**Current v3 source:** No completion exists in v3 baseline. Evidence class: +`ADR-only`. + +## Dynamic descriptors — safety bounds + +Provider CLI projections are Provider-supplied content. The CLI applies these +bounds before rendering any projection: + +| Bound | Limit | +| --- | --- | +| Total bytes per projection | 64 KiB | +| Top-level subcommand name length | 32 bytes; `^[a-z][a-z0-9-]*$` | +| Sub-verb name length | 32 bytes; `^[a-z][a-z0-9-]*$` | +| Number of sub-verbs per Provider | 32 | +| Argument name length | 64 bytes | +| Number of arguments per sub-verb | 16 | +| Description length per sub-verb | 512 bytes | +| Fetch deadline per Provider | 2 s | +| Total fetch deadline (all Providers) | 10 s | + +A projection that violates any bound is silently skipped with a single +`d2b: provider cli-projection exceeded limit` line to stderr. No +Provider projection can cause the CLI to crash, loop, allocate unbounded memory, +or emit shell-injectable text. + +Completion strings are HTML/shell-escaped before inclusion in completion +scripts. Newlines in dynamic strings are replaced with a space. + +## No provider code in CLI + +The CLI binary must not: + +- import Provider implementation crates; +- import `d2bd`, broker, or Zone-store implementation crates; +- invoke Provider subprocesses; +- read Provider-specific configuration files (only the standard Zone socket and + optional env overrides); +- perform sandbox compilation or minijail argument construction; +- hold or transmit credential bytes. + +The CLI may import `d2b-contracts` for shared DTO types, `d2b-resource-api` for +the typed async client, and `d2b-session`/`d2b-session-unix` for +ComponentSession transport. + +## Current verb/target/contract migration table + +The following table maps every current v3 CLI verb to its v3 status. + +| Current verb | Status | v3 successor | Current source | Evidence class | +| --- | --- | --- | --- | --- | +| `d2b list` | Deleted at 3.0 | `d2b guest list` | `cmd_list` @ lib.rs:3560 | reachable | +| `d2b status []` | Deleted at 3.0 | `d2b guest status ` | `cmd_status` @ lib.rs:3587 | reachable | +| `d2b up ` | Deleted at 3.0 | `d2b guest start ` | `cmd_vm_start` via alias | reachable | +| `d2b down ` | Deleted at 3.0 | `d2b guest stop ` | `cmd_vm_stop` via alias | reachable | +| `d2b restart ` | Deleted at 3.0 | `d2b guest restart ` | `cmd_vm_restart` via alias | reachable | +| `d2b vm start ` | Deleted at 3.0 | `d2b guest start ` | `cmd_vm_start` @ lib.rs:6675 | reachable | +| `d2b vm stop ` | Deleted at 3.0 | `d2b guest stop ` | `cmd_vm_stop` @ lib.rs:6690 | reachable | +| `d2b vm restart ` | Deleted at 3.0 | `d2b guest restart ` | `cmd_vm_restart` @ lib.rs:6690 | reachable | +| `d2b vm list` | Deleted at 3.0 | `d2b guest list` | `cmd_vm_list` @ lib.rs | reachable | +| `d2b vm status ` | Deleted at 3.0 | `d2b guest status ` | `cmd_vm_status` @ lib.rs:6868 | reachable | +| `d2b vm exec -- ` | Deleted at 3.0 | `d2b exec run/attach` | `cmd_vm_exec` @ lib.rs:7166 | reachable | +| `d2b vm exec list` | Deleted at 3.0 | `d2b exec list` | `cmd_vm_exec_management` @ lib.rs:7397 | reachable | +| `d2b vm exec logs ` | Deleted at 3.0 | `d2b exec logs` | same | reachable | +| `d2b vm exec status ` | Deleted at 3.0 | `d2b exec status` | same | reachable | +| `d2b vm exec kill ` | Deleted at 3.0 | `d2b exec kill` | same | reachable | +| `d2b vm display list/close` | Provider-projected | `d2b display list/close` (`display` projection declared by `display-wayland`) | `cmd_vm_display` @ lib.rs | reachable | +| `d2b shell [attach]` | Deleted at 3.0 | `d2b shell open ` | `cmd_shell` @ lib.rs:1788 | reachable | +| `d2b shell list` | Deleted at 3.0 | `d2b shell list` | same | reachable | +| `d2b shell detach` | Deleted at 3.0 | `d2b shell detach` | same | reachable | +| `d2b shell kill` | Deleted at 3.0 | `d2b shell kill` | same | reachable | +| `d2b usb attach ` | Deleted at 3.0 | `d2b device usb attach` | `cmd_usb_attach` @ lib.rs | reachable | +| `d2b usb detach ` | Deleted at 3.0 | `d2b device usb detach` | `cmd_usb_detach` | reachable | +| `d2b usb probe` | Deleted at 3.0 | `d2b device usb probe` | `cmd_usb_probe` | reachable | +| `d2b usb security-key status` | Deleted at 3.0 | `d2b device security-key status` | `cmd_usb_sk_status` | reachable | +| `d2b usb security-key sessions` | Deleted at 3.0 | `d2b device security-key sessions` | `cmd_usb_sk_sessions` | reachable | +| `d2b usb security-key cancel` | Deleted at 3.0 | `d2b device security-key cancel` | `cmd_usb_sk_cancel` | reachable | +| `d2b usb security-key test ` | Deleted at 3.0 | `d2b device security-key test` | `cmd_usb_sk_test` | reachable | +| `d2b console ` | Deleted at 3.0 | `d2b guest console ` | `cmd_console` | reachable | +| `d2b audio status/mic/speaker/off` | Provider-projected | `d2b audio status/mic/speaker/off` (`audio` projection declared by `audio-pipewire`) | `cmd_audio` @ lib.rs | reachable | +| `d2b audit` | **Retained** | `d2b audit` | `cmd_audit` @ lib.rs | reachable | +| `d2b host check` | **Retained** | `d2b host check` | `cmd_host_check` @ lib.rs:4271 | reachable | +| `d2b host prepare` | **Retained** | `d2b host prepare` | `cmd_host_prepare` @ lib.rs:4468 | reachable | +| `d2b host destroy` | **Retained** | `d2b host destroy` | `cmd_host_destroy` @ lib.rs:4549 | reachable | +| `d2b host doctor` | **Retained** | `d2b host doctor` | `cmd_host_doctor` @ lib.rs:4752 | reachable | +| `d2b host migrate-storage` | Deleted after reset | *(none; storage ADR 0034 reset)* | `cmd_host_migrate_storage` @ lib.rs:4867 | reachable | +| `d2b host install` | **Retained** | `d2b host install` | `cmd_host_install` @ lib.rs:5007 | reachable | +| `d2b host reconcile` | **Retained** | `d2b host reconcile` | `cmd_host_reconcile` @ lib.rs:5070 | reachable | +| `d2b host validate` | **Retained** | `d2b host validate` | `cmd_host_validate` @ lib.rs:4941 | reachable | +| `d2b host shutdown-hook` | **Retained** internal | `d2b host shutdown-hook` (hidden) | `cmd_host_shutdown_hook` @ lib.rs:4644 | reachable | +| `d2b realm list` | Deleted at 3.0 | `d2b zone list` | `cmd_realm_list` @ lib.rs:5942 | reachable | +| `d2b realm inspect ` | Deleted at 3.0 | `d2b zone get ` | `cmd_realm_inspect` @ lib.rs:5958 | reachable | +| `d2b realm enter ` | Deleted at 3.0 | `d2b shell open ` | `cmd_realm_enter` @ lib.rs:6129 | reachable | +| `d2b realm run -- ` | Deleted at 3.0 | `d2b exec run -- ` | `cmd_realm_run` @ lib.rs:6143 | reachable | +| `d2b op inspect` | **Retained** | `d2b op inspect` | `cmd_op_inspect` @ lib.rs:6098 | reachable | +| `d2b auth status` | **Retained** | `d2b auth status` | `cmd_auth_status` @ lib.rs | reachable | +| `d2b store verify ` | Deleted at 3.0 | `d2b volume verify` | `cmd_store_verify` @ lib.rs | reachable | +| `d2b keys list/show/rotate` | Deleted at 3.0 | `d2b activation keys list/show/rotate` | `cmd_keys_*` @ lib.rs:986 | reachable | +| `d2b trust ` | Deleted at 3.0 | `d2b activation trust ` | `cmd_keys_trust` | reachable | +| `d2b rotate-known-host ` | Deleted at 3.0 | `d2b activation rotate-known-host` | `cmd_keys_rotate_known_host` | reachable | +| `d2b build ` | Deleted at 3.0 | `d2b activation build` | `cmd_build` | reachable | +| `d2b generations ` | Deleted at 3.0 | `d2b activation generations` | `cmd_generations` | reachable | +| `d2b switch ` | Deleted at 3.0 | `d2b activation switch` | `cmd_switch` | reachable | +| `d2b boot ` | Deleted at 3.0 | `d2b activation boot` | `cmd_boot` | reachable | +| `d2b test ` | Deleted at 3.0 | `d2b activation test` | `cmd_test` | reachable | +| `d2b rollback ` | Deleted at 3.0 | `d2b activation rollback` | `cmd_rollback` | reachable | +| `d2b gc` | Deleted at 3.0 | `d2b activation gc` | `cmd_gc` | reachable | +| `d2b migrate` | Deleted at 3.0 | `d2b activation migrate` | `cmd_migrate` | reachable | +| `d2b config sync/diff/approve/reject/status` | Deleted at 3.0 | `d2b activation config *` | `ConfigCommand` @ lib.rs:2705 | reachable | +| `d2b clipboard arm` | Provider-projected | `d2b clipboard arm` (`clipboard` projection declared by `clipboard-wayland`) | `cmd_clipboard_arm` @ lib.rs | reachable | +| `d2b clipboard picker` | **Deleted** (was already deprecated) | *(none)* | deprecated notice @ lib.rs:2424 | reachable | + +### Removal notes + +All commands listed as "Deleted at 3.0" in the full verb/target table +below are deleted at the 3.0 clean break. The removal criterion for each is: +its live v3 successor is implemented and tested, and the v2 source path +(`cmd_*` function) is deleted in the same wave. + +`d2b host migrate-storage` is deleted after the v3 storage reset completes; it +has no v3 successor because the layout cutover it served is a one-time v1→v2 +migration. + +`d2b clipboard picker` is already removed from the dispatch table in the v3 +baseline (only a deprecation notice remains at `lib.rs:2424`). + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `packages/d2b/src/lib.rs` (18 599 lines), `exec_client.rs`, `terminal_client.rs`, `target_routing.rs`, `human_render.rs`, `status_read_model.rs` at baseline `b5ddbed6`; supplementary: `packages/d2b-contracts/src/public_wire.rs` (`ListEntry`, `VmStatus`, `VmLifecycleState`, `ShellOp`, `ShellOpResponse`, `WorkloadPublicSummary.execution_posture`), `packages/d2b-contracts/src/cli_output.rs` (`RealmListOutputV1`, `RealmInspectOutputV1`, `RealmPolicyOutputV1`), `packages/d2b-contracts/src/unsafe_local_wire.rs` (unsafe-local helper wire protocol), `packages/d2b-unsafe-local-helper/src/` (runtime helper), `packages/d2b-realm-core/src/workload.rs` (`WorkloadProviderKind`, `IsolationPosture::UnsafeLocal`, `WorkloadExecutionPosture`), `packages/d2b-core/src/processes.rs` (`ProcessRole`, `VmProcessDag`), `nixos-modules/options-realms-workloads.nix` (`kind = "unsafe-local"`), `nixos-modules/unsafe-local-workloads-json.nix`. Note: baseline uses pre-ADR 0046 terminology throughout (Realm/WorkloadId/ProcessRole/VmProcessDag etc.); see "Baseline terminology mapping" above. Unsafe-local workloads are `Host` resources NEVER `Guest`; see §Unsafe-local Host resources. | +| Evidence class | All dispatched NativeCommand variants are `implemented-and-reachable`; Zone/resource API routing, Provider CLI projection, `d2b complete`, `d2b exec run/attach/wait`, `d2b shell open/attach`, `d2b guest/process/volume/network/device/user/credential/provider/zone/activation` are `ADR-only` | +| Behavior retained | All documented exit codes; `--json`/`--human` mutual exclusion with non-TTY JSON default and `--human` explicit override; TTY raw-mode RAII; SIGWINCH/SIGINT/SIGTERM forwarding; dry-run/apply pattern; daemon-down/not-implemented envelopes; host-check exit 3; bounded output; no bash fallback; no SSH for exec/shell | +| Required delta | Zone context discovery; ResourceRef argument parsing; resource API client routing; standard `get/list/watch/create/update-spec/delete/status` verb set; `d2b exec`, `d2b shell`, `d2b guest`, `d2b process`, `d2b volume`, `d2b network`, `d2b device`, `d2b user`, `d2b credential`, `d2b provider`, `d2b zone`, `d2b quota`, `d2b emergency-policy`, `d2b activation`, `d2b complete`; Provider CLI projection loading; unified deadline/cancel; JSON schema version field; `--zone` flag | +| Reuse path | Retain current `run()` entry, clap dispatch, `CliFailure`/`report_failure`, `exec_client.rs` FSM/signals/TTY, `terminal_client.rs` traits; adapt `Context` for Zone/socket discovery; replace seqpacket with ComponentSession resource API client | +| Replacement/deletion | Raw seqpacket send/recv helpers replaced only after resource API client paths are live and tested; old `public_wire` / `broker_wire` wire envelopes retired per owning resource ResourceType wave | +| Feasibility proof | Existing exec FSM/terminal/signal test suite; clap dispatch tests; all 150+ unit tests passing at baseline | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-cli-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-001` | +| Dependency/owner | ADR046-identities-001, ADR046-api-001; CLI crate owner | +| Current source | `packages/d2b/src/lib.rs`: `Context::from_env` (reads `D2B_PUBLIC_SOCKET` env or `/run/d2b/public.sock` default; single flat socket path — no zone-qualified path, no `ZoneContext`), `NativeCli`, `NativeCommand`, `dispatch`, `CliFailure`, `report_failure`, `stdout_is_tty`; socket path model: old `Context { public_socket, broker_socket }` → target: `ZoneContext { zone_name, socket_path, session_client }` | +| Reuse source | main `a1cc0b2d` — copy/adapt these exact symbols: (1) `packages/d2b-client/src/client.rs` `Client`, `ConnectedClient`, `CallOptions`, `CancellationToken`, `MetadataInput`, `RetryPolicy`, `Response` — copy unchanged as the async client foundation; (2) `packages/d2b-client/src/host_socket.rs` `HostSocketConnector::from_seqpacket_fd`, `local_daemon_endpoint_identity` — adapt: replace fixed `d2b.daemon.v2` service lookup with zone-scoped service identity; replace hardcoded `RealmPath::parse("local-root")` with `Zone/`-derived target; (3) `packages/d2b-daemon-access/src/component_session.rs` `LocalUnixDaemonAccess::connect_component_session()` connect chain — adapt: replace `TargetInput::LocalRoot(realm)` with the v3 Zone target variant; adapt socket path discovery for per-Zone paths; (4) `packages/d2b-client/src/session.rs` `ComponentSessionConnector`, `ConnectedSession`, `NamedStream`, `SessionCall`, `SessionReply`, `SharedDriver` — copy unchanged; (5) `packages/d2b-client/src/target.rs` `RouteTable`, `RouteRecord`, `TargetInput`, `ResolvedTarget`, `TransportKind`, `TransportSelection` — copy unchanged, excluding `TargetInput::Realm/Workload/Provider` variants which carry ADR 0045 assumptions; (6) `packages/d2b-contracts/src/v2_component_session.rs` `LimitProfile::local_default()` constants: `MAX_REQUEST_LIFETIME_MS=900000`, `LOCAL_HANDSHAKE_DEADLINE_MS=5000`, `MAX_RECONNECT_ATTEMPTS=10`, `MAX_ACTIVE_NAMED_STREAMS=128`, `MAX_LOGICAL_MESSAGE_BYTES=1048576`, `named_stream_queue_bytes=262144`, `aggregate_named_stream_queue_bytes=4194304` — copy unchanged; these bound every CLI deadline and stream operation | +| Reuse action | adapt | +| Destination | `packages/d2b/src/lib.rs`, `packages/d2b/src/context.rs`, `packages/d2b/src/dispatch.rs` | +| Detailed design | Introduce `ZoneContext` (zone name, socket path, ComponentSession client); implement `--zone`/`D2B_ZONE`/nearest-socket discovery using adapted `LocalUnixDaemonAccess::connect_component_session()` chain; introduce `ResourceRef` argument parser; introduce unified `--json`/`--human`/`--deadline` flag infrastructure bounded by `MAX_REQUEST_LIFETIME_MS=900s`; freeze `--json` schema version 1; stabilize exit code table. Excluded ADR 0045 assumptions: `TargetInput::Realm`, `TargetInput::Workload`, `TargetInput::Provider` variants; `RealmPath::parse("local-root")` / `RealmId::derive` pattern; `RealmPath`-based service owner types. | +| Integration | All command functions receive `ZoneContext`; resource API client (`Client`) is injected for testing | +| Data migration | None; context discovery replaces env-var path lookups | +| Validation | Zone-unavailable/fallback tests; ResourceRef parse/reject vectors; exit-code round-trip tests; TTY detection tests; adapt `client.rs:typed_routes_select_exact_transport_without_fallback` (line 1053), `connector_discovers_and_authenticates_the_driver_generation` (line 1254), `daemon_transport_rejects_ancillary_data_and_oversized_packets` (line 1312) | +| Removal proof | Old `Context` struct removed only after all command functions use `ZoneContext` | + +### ADR046-cli-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-002` | +| Dependency/owner | ADR046-cli-001, ADR046-api-001; CLI crate owner | +| Current source | `packages/d2b/src/lib.rs`: `cmd_vm_start`, `cmd_vm_stop`, `cmd_vm_restart`, `cmd_vm_status`, `cmd_vm_list`, `cmd_list`, `cmd_status`; wire types: `ListResponse { vms: Vec }`, `StatusResponse { entries: Vec }`, `VmLifecycleState` (old: Stopped/Starting/Booted/Running/Stopping/Restarting/Failed/Unknown) from `packages/d2b-contracts/src/public_wire.rs:2152,2158,2605`; `ListEntry.vm: String` = `WorkloadId`; `VmStatus.lifecycle.state: VmLifecycleState` → target Guest `phase` (Pending\|Ready\|Succeeded\|Degraded\|Failed\|Deleted\|Unknown; Starting/Stopping/Restarting → conditions/reasons); `WorkloadPublicSummary.execution_posture: WorkloadExecutionPosture` from `public_wire.rs:267` (carries `IsolationPosture`; unsafe-local entries have `IsolationPosture::UnsafeLocal`) | +| Reuse source | main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/daemon_service.rs` `DaemonClient::lifecycle()` (line 210), `DaemonClient::list_workloads()` (line 148), `DaemonClient::inspect()` (line 179); `DaemonMethod::Apply/Start/Stop/Restart/ListWorkloads` variants (lines 31-46) — adapt: replace `WorkloadLifecycleRequest`/`WorkloadName` with `Guest/` ResourceRef; replace `TargetInput::Workload`-scoped calls with zone-root resource API calls; (2) `packages/d2b-contracts/src/generated_v2_services/daemon.rs` `WorkloadLifecycleProjection`, `DeploymentProjection`, `RuntimeProjection` — adapt field mapping to Guest resource spec/status; (3) `packages/d2b/src/lib.rs` `cmd_launch` (`LaunchArgs`) — adapt: the typed ComponentSession target resolution pattern applies but realm/workload-model types (`RealmPath`, `WorkloadName`) are excluded; behavior selected: idempotent apply with dry-run/apply precondition | +| Reuse action | adapt | +| Destination | `packages/d2b/src/guest.rs` (`d2b guest start/stop/restart/list/status`); unsafe-local workloads go to `packages/d2b/src/host.rs` (`d2b host list/status/get`), NOT guest.rs | +| Detailed design | Route Guest lifecycle (WorkloadProviderKind: LocalVm/QemuMedia/ProviderManaged) through `d2b.resource.v3` Get/UpdateSpec/Watch; map dry-run/apply to resource API precondition; `--no-wait-ready` exits on accepted; with-wait uses `d2b status --watch` loop. WorkloadProviderKind::UnsafeLocal entries MUST route to `d2b host` commands only; any code path that would return an unsafe-local entry from `d2b guest list` is a correctness violation. v2 commands (`d2b up/down/restart/list/status`, `d2b vm start/stop/restart/list/status`) are deleted at 3.0; replacement guidance remains documentation-only, with no migration-diagnostic command. | +| Integration | ZoneContext → resource API client → Guest resource; status watch uses Watch stream | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Dry-run/apply/wait/no-wait-ready tests; zone-unavailable degraded path; JSON output schema tests; confirm v2 command paths are absent (compilation failure if any cmd_vm_start/stop alias re-introduced) | +| Removal proof | Old `cmd_vm_start/stop/restart` seqpacket paths removed after Guest resource API paths are live with full test coverage | + +### ADR046-cli-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-003` | +| Dependency/owner | ADR046-cli-001, ADR046-api-001; CLI crate owner | +| Current source | `packages/d2b/src/exec_client.rs` (entire FSM); `packages/d2b/src/terminal_client.rs`; `packages/d2b/src/lib.rs`: `cmd_vm_exec`, `cmd_vm_exec_management`, `VmExecArgs` | +| Reuse source | main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/daemon_service.rs` `DaemonClient::open_terminal(method, resource_id, operation_id, selection, options, cancellation)` returning `DaemonTerminal` (line 248) — copy-then-adapt: replaces the existing seqpacket Exec call; `DaemonMethod::Exec` (line 40) maps to `d2b exec run`; (2) `packages/d2b-contracts/src/generated_v2_services/terminal.rs` `TerminalOpenRequest`, `TerminalOpenResponse`, `TerminalStreamFrame`, `TerminalSelection`, `TerminalKind` — copy unchanged as the named-stream terminal wire protocol; (3) `packages/d2b-client/src/session.rs` `NamedStream` (`send`, `receive`, `cancel`, `close`, `is_terminal`) — copy unchanged; provides async stdio routing and cancel on disconnect; (4) `packages/d2b-client/src/daemon_service.rs` `GuestClient::inspect_exec()`, `cancel_exec()`, `open_exec_retained_log()` — adapt: rename from `WorkloadName`/`GuestClient` to `EphemeralProcess/` resource API; (5) `packages/d2b-session/src/streams.rs` `NamedStreamMux` limits (`MAX_ACTIVE_NAMED_STREAMS=128`, `named_stream_queue_bytes=262144`) — copy unchanged; bounds the exec stream pipeline; (6) `packages/d2b-session/src/cancellation.rs` `Cancellation`, `RequestRegistry` — copy unchanged; provides generation-bound per-request cancel; tests to adapt/import: `client.rs:terminal_uses_server_stream_and_validates_bidirectional_lifecycle`, `terminal_rejects_response_generation_and_non_server_stream_ids`, `invalid_terminal_selection_is_rejected_before_open_rpc`, `guest_exec_management_preserves_typed_state_and_cancel_correlation`, `guest_retained_log_open_binds_range_resource_and_selection`, `named_stream_fragments_over_queue_credit_and_has_terminal_actions`, `named_stream_grants_only_consumed_data_and_releases_blocked_sender`; excluded ADR 0045 assumptions: `GuestClient` internal `TargetInput::Workload`-scoped vsock routing (guest-control proxy uses old `WorkloadName`/`RealmPath` — these are excluded; v3 routes through resource API only) | +| Reuse action | adapt | +| Destination | `packages/d2b/src/exec.rs` (`d2b exec run/attach/wait/status/list/logs/kill`) | +| Detailed design | Map EphemeralProcess resource lifecycle; `exec run` creates resource and returns ref; `exec attach` opens named stream via adapted `DaemonClient::open_terminal(DaemonMethod::Exec, ...)` → `DaemonTerminal`; retain full `exec_client.rs` FSM and TTY machinery from baseline; retain `--json` envelope fields `source`/`reason`/`guestExitCode`/`signal`/`transportExitCode`; retain reserved exit codes 42/69/70/75/76/77. v2 commands (`d2b vm exec *`) are deleted at 3.0; no dispatch wiring. Excluded ADR 0045: `GuestClient` vsock/guest-control proxy path; `TargetInput::Workload`; old `WorkloadName`-keyed exec management. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-then-adapt. | +| Integration | ZoneContext → EphemeralProcess Create → named stream attach via `DaemonClient::open_terminal` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Full `exec_client.rs` test suite migrated; adapted tests from main `client.rs:terminal_*` and `guest_exec_*`; TTY/raw-mode/RAII/signal tests; `--json` envelope/disambiguation tests; capacity/transport/auth/protocol exit-code tests; confirm v2 `cmd_vm_exec` path is absent | +| Removal proof | Old `cmd_vm_exec` seqpacket path removed after `d2b exec` paths have equivalent coverage | + +### ADR046-cli-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-004` | +| Dependency/owner | ADR046-cli-001, `shell-terminal` Provider dossier; CLI crate owner | +| Current source | `packages/d2b/src/lib.rs`: `cmd_shell` (`ShellArgs.vm: String` = `WorkloadId` or `RealmTarget`; routes through `route_vm_target()` → `VmTargetRoute::Local\|Gateway`; gateway `Attach` fails closed via `shell_gateway_attach_failure()` with error class `gateway-shell-attach-unavailable` at lib.rs:1697,1780), `cmd_shell_attach`, `run_shell_fsm`, `ShellOwnerTransport`; wire: `ShellOp`, `ShellOpResponse`, `ShellAttachArgs { vm: String }`, `ShellListEntry`, `ShellSessionState` from `packages/d2b-contracts/src/public_wire.rs:1319,1394,1452,1409`; `exec_client.rs` signal/TTY machinery | +| Reuse source | main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/daemon_service.rs` `DaemonClient::open_terminal(DaemonMethod::Shell, ...)` returning `DaemonTerminal` — copy-then-adapt; same `TerminalOpenRequest`/`TerminalOpenResponse`/`DaemonTerminal` flow as cli-003, applied to shell open/attach; (2) `packages/d2b-contracts/src/generated_v2_services/shell.rs` and `shell_ttrpc.rs` `ShellService` methods: `ShellCreate`, `ShellAttach`, `ShellDetach`, `ShellList`, `ShellInspect`, `ShellKill`, `ShellCancel` (service definition); `ShellCreateRequest`/`ShellAttachRequest`/`ShellListResponse`/`ShellInspectResponse` — copy-then-adapt: these are the target ShellSession resource CRUD wire types; adapt field names from `workload_id`/`shell_name` to `Guest/` ResourceRef; (3) `packages/d2b-client/src/session.rs` `NamedStream` — copy unchanged; used for shell I/O stream; (4) `packages/d2b-session/src/cancellation.rs` `Cancellation`, `RequestRegistry` — copy unchanged; (5) `packages/d2b-session/src/deadline.rs` `DeadlineBudget` — copy unchanged; shell sessions use per-operation deadline tracking; tests to adapt/import: `client.rs:shell_management_uses_typed_selection_result_and_terminal_outcome`, `named_stream_fragments_over_queue_credit_and_has_terminal_actions`, `named_stream_grants_only_consumed_data_and_releases_blocked_sender`, `concurrent_named_streams_route_events_without_cross_consumption`; excluded ADR 0045 assumptions: `VmTargetRoute::Gateway` shell routing and `realm_router` relay path; old `ShellOp`/`ShellOpResponse` seqpacket wire; unsafe-local helper shell protocol v2 | +| Reuse action | adapt | +| Destination | `packages/d2b/src/shell.rs` (`d2b shell open/attach/list/detach/kill/status`) | +| Detailed design | Route ShellSession resource lifecycle through resource API using adapted `ShellService` generated types; `shell open` → `ShellCreate` → `DaemonClient::open_terminal(Shell)` → `DaemonTerminal`; retain FSM/TTY/signal/RAII behavior from `run_shell_fsm`; `--name` required for kill; SIGHUP detaches without kill. v2 commands (`d2b shell *`) are deleted at 3.0; no dispatch wiring. Excluded: gateway relay path (`VmTargetRoute::Gateway`); old `ShellOp`/`ShellOpResponse` seqpacket protocol; `TargetInput::Workload`-keyed realm routing. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-then-adapt. | +| Integration | ZoneContext → ShellSession Create via `DaemonClient::open_terminal` → `NamedStream` I/O | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Shell list/detach/kill/attach unit tests (adapted from existing); adapted `client.rs:shell_management_*` and `named_stream_*` tests; TTY RAII/signal tests; confirm v2 `cmd_shell` path is absent | +| Removal proof | Old `cmd_shell` seqpacket path removed after new shell commands have equivalent coverage | + +### ADR046-cli-005 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-005` | +| Dependency/owner | ADR046-cli-001, Provider model spec; CLI crate owner | +| Current source | `packages/d2b/src/lib.rs`: `cmd_audio`, `cmd_clipboard_arm`, `cmd_vm_display` | +| Reuse source | main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-provider/src/rpc.rs` `RpcProviderProxy` — adapt: the CLI uses the inverse side (client calling provider via `ConnectedClient::invoke`); `RpcCall`, `RpcPayload`, `RpcResponse`, `RpcOperation` define the typed call shape for dynamic provider commands; (2) `packages/d2b-contracts/src/generated_v2_services/provider_runtime.rs`, `provider_display.rs`, `provider_audio.rs`, `provider_infrastructure.rs` — adapt: the generated service method types show what CLI projection verbs can be mapped to typed service calls; use as shape reference for the first audio/clipboard/display migration; (3) `packages/d2b-provider-toolkit/src/conformance.rs` `check_provider_conformance`, `check_descriptor_conformance` — copy-then-adapt into CLI-side projection conformance validation (bounds: 64 KiB, 32 sub-verbs, 2s deadline, shell-escape, newline strip); (4) `packages/d2b-provider-toolkit/src/server.rs` `GeneratedProviderServiceServer::generated_services()` — server-side only; use as reference for what CLI InspectSchema receives; excluded ADR 0045 assumptions: `ProviderRegistry`/`ProviderAgentAdapter` are server-side and not used in CLI; `RpcProviderProxy` internal `AuthenticatedProviderRpc` pattern is server-side; tests to adapt: `conformance.rs:every_axis_passes_identical_in_process_and_rpc_conformance`, `generated_server_dispatches_closed_methods_over_authenticated_session` | +| Reuse action | adapt | +| Destination | `packages/d2b/src/provider.rs` (`d2b provider list/get/status/inspect`; dynamic projection loading) | +| Detailed design | `d2b provider list/get/status/inspect`; InspectSchema call returns dynamic projection descriptor using `ConnectedClient::invoke` with generated provider service types; projection bounds enforcement (64 KiB, 32 sub-verbs, 2s deadline, shell-escape, newline strip); define the single canonical built-in command registry consumed by parser/help/completion and Provider projection binding; make bind-time rejection the sole authority for built-in and Provider/Provider top-level-name collisions, with no dispatch fallback; render projected commands from their declared top-level subcommand names; audio/clipboard/display are the first providers to migrate their projections | +| Integration | ZoneContext → Provider resource + InspectSchema via `ConnectedClient::invoke` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Projection size/name/timeout bounds tests; registry-completeness test asserting exact equality with all built-in parser commands; bind-time built-in and Provider/Provider collision rejection tests; negative test proving no collision fallback dispatch exists; audio/clipboard/display projection conformance tests asserting `d2b audio ...`, `d2b clipboard ...`, and `d2b display ...` rendering; completion script safety tests; adapted `conformance.rs` tests | +| Removal proof | Built-in `cmd_audio`/`cmd_clipboard_arm`/`cmd_vm_display` removed only after Provider projection paths pass equivalence tests | + +### ADR046-cli-006 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-006` | +| Dependency/owner | ADR046-cli-001, ADR046-routing-012, ADR046-routing-016; CLI crate owner | +| Current source | None (no completion exists in v3 baseline) | +| Reuse source | Optional: clap_complete crate (version to be pinned); no main-branch source | +| Reuse action | adapt | +| Destination | `packages/d2b/src/complete.rs` (`d2b complete bash/zsh/fish`) | +| Detailed design | `d2b complete ` emits completion script; uses clap `CommandFactory::command()` plus dynamic projection fetch (2s per-Provider, 10s total); result bounded at 256 KiB; shell-escaped; newlines stripped Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged (clap_complete). | +| Integration | Standalone command; no Zone API required for static completion; Zone API used for dynamic Provider projection | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Completion script tests (bash/zsh/fish syntax valid); projection injection safety tests; deadline/partial-Provider tests | +| Removal proof | Not applicable | + +### ADR046-cli-007 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-007` | +| Dependency/owner | ADR046-cli-001; CLI/activation Provider owner | +| Current source | `packages/d2b/src/lib.rs`: `cmd_build`, `cmd_generations`, `cmd_switch`, `cmd_boot`, `cmd_test`, `cmd_rollback`, `cmd_gc`, `cmd_migrate`, `cmd_keys_*`, `cmd_keys_trust`, `cmd_keys_rotate_known_host`, `ConfigCommand` variants | +| Reuse source | main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/daemon_service.rs` `DaemonClient::lifecycle()` with `DaemonMethod::Apply`, `DaemonMethod::Start`, `DaemonMethod::Stop`, `DaemonMethod::Restart` — copy-then-adapt: the apply/lifecycle dispatch pattern maps cleanly to `d2b activation switch/boot/test/rollback`; retain idempotency token and dry-run/apply precondition from `DaemonMethod::Apply`; (2) `packages/d2b-contracts/src/generated_v2_services/activation.rs` activation service method types — adapt: map `ActivationBuildRequest`, `ActivationSwitchRequest`, `ActivationGenerationsRequest` to typed CLI args; excluded ADR 0045 assumptions: `DaemonMethod::ListRealms` / `DaemonMethod::ListWorkloads` are not used; old `WorkloadName`-keyed dispatch is excluded | +| Reuse action | adapt | +| Destination | `packages/d2b/src/activation.rs` (`d2b activation build/generations/switch/boot/test/rollback/gc/migrate/keys/trust/rotate-known-host/config`) | +| Detailed design | Route through `activation-nixos` Provider service via `ConnectedClient::invoke` using adapted `DaemonMethod::Apply`/lifecycle dispatch pattern; retain dry-run/apply; retain guest-control transport for config sync (no SSH). v2 top-level activation commands (`d2b build/switch/boot/test/rollback/gc/migrate/keys/trust/rotate-known-host/config`) are deleted at 3.0; no dispatch wiring. | +| Integration | ZoneContext → activation-nixos Provider service → resource API via `ConnectedClient::invoke` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | All existing switch/boot/test/rollback/keys tests adapted; config sync/diff/approve/reject tests; confirm v2 top-level activation paths are absent; adapted `client.rs:daemon_typed_list_preserves_projection_and_truncation` apply pattern | +| Removal proof | Old top-level activation verbs removed only after `d2b activation *` paths have equivalent coverage | + +### ADR046-cli-008 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-008` | +| Dependency/owner | ADR046-cli-001; CLI crate owner | +| Current source | `packages/d2b/src/lib.rs`: `cmd_host_check`, `cmd_host_prepare`, `cmd_host_destroy`, `cmd_host_doctor`, `cmd_host_install`, `cmd_host_reconcile`, `cmd_host_validate`; `host_validate.rs` | +| Reuse source | main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-daemon-access/src/component_session.rs` `LocalUnixDaemonAccess::connect_component_session()` connect chain — adapt: the zone-local host commands use the same connect chain as cli-001; `d2b host prepare` and `d2b host doctor` both require a live `ZoneContext`; (2) `packages/d2b-client/src/client.rs` `ConnectedClient::invoke()` with `CallOptions` and `CancellationToken` — copy unchanged; used for Host resource Get/UpdateSpec/Status calls and broker-op dispatch; (3) `packages/d2b-contracts/src/generated_v2_services/broker.rs` broker operation request/response types — adapt: `BrokerHostPrepareRequest`, `BrokerHostDestroyRequest`, `BrokerHostDoctorRequest` (or equivalent) types define the CLI argument shape; retain broker-mediated ownership-marker semantics from baseline; excluded ADR 0045 assumptions: `TargetInput::Workload`-scoped broker routing is excluded; broker operation routing uses zone-root LocalRoot pattern only | +| Reuse action | adapt | +| Destination | `packages/d2b/src/host.rs` (all `d2b host` subcommands) | +| Detailed design | Route `host prepare/destroy` through Zone resource API Host reconcile operation via `ConnectedClient::invoke`; retain broker-mediated mutation and ownership-marker semantics; `host doctor` prefers Zone resource API status, falls back to local state files; `host check` retains exit-code 3; `host validate` retains wave/evidence-dir/scripts-dir/signature | +| Integration | ZoneContext → Host resource; broker op path retained for emergency/shutdown-hook | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | All existing host-check/prepare/destroy/doctor/install/reconcile/validate tests; exit-code 3 regression; doctor Zone-fallback/local-state-fallback tests | +| Removal proof | Raw broker-socket paths removed only after Host resource API routes have equivalent coverage | + +### ADR046-cli-009 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-009` | +| Dependency/owner | ADR046-cli-001; CLI crate owner | +| Current source | `packages/d2b/src/lib.rs`: `cmd_realm_list` (reads static `realm-entrypoints.json` via `realm_policy_rows_raw()`), `cmd_realm_inspect`, `cmd_realm_enter` (→ `realm_gateway_exec_args` → `cmd_vm_exec` with `-it bash -l`), `cmd_realm_run` (→ `cmd_vm_exec` with caller argv); wire output types: `RealmListOutputV1 { realms: Vec }`, `RealmInspectOutputV1 { realm: RealmPolicyOutputV1 }` from `packages/d2b-contracts/src/cli_output.rs:285,292,345`; `RealmPolicyOutputV1` fields: `realm` (= `RealmId`), `mode`, `gateway_vm`, `gateway_target`, `gateway_state`, `cross_realm_policy`, `credential_boundary`; `target_routing.rs`: `Route::Local { vm }`, `Route::Gateway { gateway, target }`, `resolve_access_route()`, `VmTargetRoute`; `d2b-realm-router::RealmEntrypointTable` | +| Reuse source | main `a1cc0b2d` — reference only (no copy): `packages/d2b-realm-router/src/service_v2.rs` `RealmServiceServer`, `RealmServiceProcess`, `RealmMethod::Inspect`, `RealmMethod::ResolveRoute` — server-side multi-realm routing; this is the ADR 0045 multi-Zone topology and is **excluded** from v3 CLI as a direct reuse source; `packages/d2b-realm-router/src/remote_node.rs` `RemoteNodeRegistration`, `RemoteNodeEntry` — constellation remote routing; also excluded; note: `packages/d2b-client/src/daemon_service.rs` `DaemonClient::list_workloads()` and `DaemonMethod::ListRealms` are the closest live list-call patterns, but their zone/workload scoping uses `RealmPath`/`RealmId` types that are ADR 0045-specific; adapt `ConnectedClient::invoke()` with the v3 `ZoneService` topology-projection request instead; no main symbols are copied unchanged for cli-009; the projection type is an ADR-only deliverable owned by zone routing | +| Reuse action | adapt | +| Destination | `packages/d2b/src/zone.rs` (`d2b zone get/list/status`) | +| Detailed design | `d2b zone get []` fetches the current Zone self resource only for the local name; another declared name is inspected through the read-only Zone topology projection. `d2b zone list` lists sorted compiler-only `{ childZone, parentZone }` rows joined with authenticated route/projection status. `d2b zone status --watch` watches the corresponding projection revision. None of these commands lists or gets a parent-local ZoneLink: the sole ZoneLink resource and handler are child-local, while the parent owns only sealed allocator topology and authenticated route state. Generated DTO/golden tests reject ZoneLink names/spec/status/provider refs/fingerprints/transport settings in parent-side output. v2 commands (`d2b realm list/inspect/enter/run`) are deleted at 3.0; no dispatch wiring. Excluded ADR 0045: `RealmServiceServer`/`RealmServiceProcess` multi-realm service; `RemoteNodeRegistration` constellation routing; `TargetInput::Realm`; `RealmMethod::ResolveRoute`/`AuthorizeShortcut`/`RevokeShortcut`. | +| Integration | ZoneContext → local Zone self-resource Get or `ZoneService` topology/route projection via `ConnectedClient::invoke` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Zone self-get plus topology list/get/status/watch tests; disconnected and stale authenticated-route projections; golden output contains `{ childZone, parentZone }` and route/projection status but no ZoneLink fields; parent store has no ZoneLink row or watch; confirm v2 `cmd_realm_*` paths are absent | +| Removal proof | `cmd_realm_*` and `target_routing.rs` removed only after zone routes pass equivalence tests | + +### ADR046-cli-010 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-010` | +| Dependency/owner | ADR046-cli-001; CLI crate owner | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse source | main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/client.rs` `ConnectedClient::invoke()`, `ConnectedClient::invoke_with_attachments()`, `ConnectedClient::open_server_stream()` — copy unchanged; these are the three primitives for resource Get/List/Watch respectively; (2) `packages/d2b-client/src/session.rs` `NamedStream` (`send`, `receive`, `cancel`, `close`, `is_terminal`) — copy unchanged; Watch stream output arrives over a named stream; (3) `packages/d2b-session/src/deadline.rs` `DeadlineBudget` — copy unchanged; `--deadline` flag maps to `DeadlineBudget::admit_metadata` wall deadline; `MAX_REQUEST_LIFETIME_MS=900000` caps all Watch/List deadlines; (4) `packages/d2b-client/src/client.rs` `CancellationToken::cancel()` — copy unchanged; `SIGINT`/SIGTERM → `CancellationToken::cancel()` → propagated to `ConnectedClient::invoke` and `NamedStream`; (5) `packages/d2b-client/src/client.rs` `MetadataInput`, `RetryPolicy`, `CallOptions` — copy unchanged; `--idempotency-token` maps to `MetadataInput`; `RetryPolicy::mutating_once()` is the default for Create/UpdateSpec/Delete; tests to adapt/import: `client.rs:metadata_retries_and_cancellation_use_canonical_driver`, `mutating_retries_require_stable_idempotency`, `concurrent_named_streams_route_events_without_cross_consumption`, `named_stream_grants_only_consumed_data_and_releases_blocked_sender`; excluded ADR 0045 assumptions: `TargetInput::Workload/Realm/Provider` routing variants; `GuestClient` cross-realm proxy routing; old `DeploymentProjection`/`RuntimeProjection` ADR 0045-specific field types | +| Reuse action | adapt | +| Destination | `packages/d2b/src/resource.rs` (standard `d2b get/list/watch/create/update-spec/delete/status` top-level verbs) | +| Detailed design | Generic typed dispatch to resource API Get/List/Watch/Create/UpdateSpec/Delete using `ConnectedClient::invoke` (Get/List/Create/UpdateSpec/Delete) and `ConnectedClient::open_server_stream` + `NamedStream` (Watch); ResourceRef argument parsing and validation; page token pagination; `--phase`/`--label-selector` filters; `--deadline` bounded by `MAX_REQUEST_LIFETIME_MS=900s` via `DeadlineBudget`; Watch output streams resource events as JSON lines; JSON schema version field; `CancellationToken` wired to process signal handlers. Excluded: `GuestClient` vsock exec/shell routing; `TargetInput` realm/workload/provider variants. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-then-adapt. | +| Integration | ZoneContext → `ConnectedClient` → resource API | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Get/list/watch/create/update-spec/delete tests per ResourceType; pagination/filter/watch-deadline tests; error-class/exit-code tests; adapted `client.rs:metadata_retries_*` and `mutating_retries_*` and `concurrent_named_streams_*` tests | +| Removal proof | Not applicable (new surface) | + +### ADR046-cli-011 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-011` | +| Dependency/owner | ADR046-identities-002, ADR046-cli-001, ADR046-cli-002, ADR046-cli-007, ADR046-nix-014; Nix module owner + Zone runtime owner; `nixos-modules/assertions.nix` base migration precedes this item's unified-resource update | +| Current source | Nix emitters: `nixos-modules/options-realms-workloads.nix` (current `d2b.envs..vms..*`), `nixos-modules/options-realms.nix` (`d2b.realms.*`), `nixos-modules/unsafe-local-workloads-json.nix` (unsafe-local source), `nixos-modules/bundle-artifacts.nix`, `nixos-modules/manifest.nix`, `nixos-modules/assertions.nix`; JSON output: `/etc/d2b/processes.json` (old bundle), `/etc/d2b/realm-entrypoints.json` (static realm index); Zone runtime apply path: `packages/d2bd/src/` (activation apply handler — pre-ADR 0046 path through `cmd_host_prepare`/broker; no live resource bundle apply); cleanup: no current resource-deletion-on-bundle-apply path at baseline | +| Reuse source | None (new implementation; no main `a1cc0b2d` reuse — this is the Nix/Zone side, not the CLI client side) | +| Reuse action | replace | +| Destination | Nix: `nixos-modules/options-zones.nix` (unified `d2b.zones..resources` attrset; per-type `spec` sub-options generated from ResourceTypeSchema/Provider schema), `nixos-modules/bundle-emit.nix` (canonical JSON emit + SHA256 pin), `nixos-modules/assertions.nix` (updated); core controller: `packages/d2b-core-controller/src/configuration.rs`, `packages/d2b-core-controller/src/cleanup.rs`; Contracts: `packages/d2b-contracts/src/zone_bundle.rs` (new) | +| Detailed design | **Nix shape:** `d2b.zones..resources` is `attrsOf (submodule { type; optional metadata { ownerRef; labels; annotations }; spec })`. `spec` sub-options per `type` are generated from ResourceTypeSchema and signed Provider schemas; field names remain identical. `metadata.name`/`metadata.zone`/`apiVersion` are derived; status and all core metadata are rejected in input. Vendor-qualified types are admitted only when their schema is installed. **Nix emit:** `bundle-emit.nix` emits `/etc/d2b/zones//resource-bundle.json` plus its integrity pin with canonical resource ordering and schema digests. **Core-controller apply:** `configuration.rs` verifies bundle/catalog integrity, applies Create/Update/no-op intents with bounded async concurrency, refreshes `configurationGeneration` for unchanged configuration-managed resources without waking their controller, handles controller/API name collisions per-item without seizing them, and asynchronously deletes only persisted `managedBy=configuration` resources absent from the new configured set. `cleanup.rs` consumes `Deleted` revision watches and maintains `PendingCleanup`; it never force-removes finalizers. **Prior generation retention:** `d2b.zones..retainedGenerations`, default 3 and range 1–16, is a compiler setting outside `Zone.spec`; no TTL. Rollback reapplies a retained bundle as a new higher generation. | +| Integration | Nix build → per-Zone `resource-bundle.json` + global private artifact catalog → `d2b activation switch` → `d2b-core-controller` configuration service → resource API Create/Update/Delete → owner controllers → finalizer cascade → cleanup watcher → Zone status update | +| Data migration | Full reset from current manifest/processes/realm-entrypoints JSON format; prior Nix-generated artifacts (`/etc/d2b/processes.json`, `/etc/d2b/realm-entrypoints.json`) deleted after Zone resource bundle activates | +| Validation | Runtime integration: all CLI-visible cleanup/status/rollback/gc/audit tests (§CLI-visible tests for activation and cleanup), including no force-finalizer path; Nix unit and build tests owned by ADR-046-nix-configuration spec; canonical-example fixture validates every authored resource against the authoritative Host, Guest, and Provider schemas, resolves every artifact/resource reference, rejects unsupported fields, and proves the compiled Guest `spec` is field-for-field identical to the adjacent JSON | +| Removal proof | Old `nixos-modules/manifest.nix`, `nixos-modules/bundle-artifacts.nix` emitters removed only after `bundle-emit.nix` produces equivalent-or-superseding output and all downstream consumers of the old bundle format are migrated | + +### ADR046-cli-012 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-012` | +| Dependency/owner | ADR046-cli-001, ADR046-cli-010; CLI crate owner | +| Current source | None — net-new v3 work; `Endpoint` is a new standard ResourceType added by D092; no pre-ADR45 baseline equivalent | +| Reuse source | main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/client.rs` `ConnectedClient::invoke()` and `ConnectedClient::open_server_stream()` — copy unchanged; used for Endpoint Get/List/Status and Watch; (2) `packages/d2b-client/src/session.rs` `NamedStream` — copy unchanged; Watch stream output; (3) the ADR046-cli-010 generic resource-verb dispatch — reuse for `d2b endpoint get/list/watch/status`; the `d2b endpoint resolve` projection verb is net-new. No raw transport/FD handle type is imported into the CLI; resolution to a private transport/FD remains inside Core/ProviderSupervisor via EffectPort/LaunchTicket | +| Reuse action | adapt | +| Destination | `packages/d2b/src/endpoint.rs` (`d2b endpoint get/list/watch/status/resolve`) | +| Detailed design | Add `Endpoint` to the frozen standard ResourceType set (19 types; local ResourceRef parser accepts `Endpoint/` with no Zone round-trip). Implement `d2b endpoint get/list/watch/status` over the ADR046-cli-010 generic verbs and `d2b endpoint resolve ` as a provider-neutral resolution projection printing `producerRef`, `endpointClass`, transport class, readiness, and capability/locality observations. Endpoint output is the base envelope only (base spec + base status per D092); any `status.provider` projection is bounded, redacted, and deny-unknown. **No raw locator** (path/address/CID/port/fd/credential) appears in any CLI field or the `resolve` projection. `status.update` currency and `d2b upgrade Endpoint/` visibility follow the standard verbs and D091. `create`/`update-spec`/`delete` are rejected on the operator surface except for statically Nix/API-authored Endpoints whose schema permits it. | +| Integration | ZoneContext → `ConnectedClient` → resource API for `Endpoint`; consumers reference endpoints only through `Endpoint/` ResourceRefs | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | `Endpoint` accepted as a frozen standard type; `Endpoint/` parses locally without a Zone round-trip; `d2b endpoint get/list/watch/status/resolve` parse and route correctly; `get`/`resolve` output carries no raw path/address/CID/port/fd/credential; provider status projection is bounded/redacted; `d2b list Endpoint --updates` and `d2b upgrade Endpoint/` expose `status.update` currency and disruption; operator `create`/`update-spec`/`delete` rejected for controller-owned Endpoints | +| Removal proof | Not applicable (new surface) | + +### ADR046-cli-013 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cli-013` | +| Dependency/owner | ADR046-cli-001, ADR046-cli-010, ADR046-zone-control-019; CLI crate owner | +| Current source | None — net-new v3 work; `ResourceExport`/`ResourceImport` are new standard ResourceTypes added by D096; no pre-ADR45 baseline equivalent | +| Reuse source | ADR046-cli-010 generic resource-verb dispatch; `packages/d2b-client/src/client.rs` `ConnectedClient::invoke`/`open_server_stream` — copy unchanged | +| Reuse action | adapt | +| Destination | `packages/d2b/src/share.rs` (`d2b export …` and `d2b import …` nouns) | +| Detailed design | Add `ResourceExport` and `ResourceImport` to the frozen 19-type standard set while leaving qualified semantic/provider-neutral Service/Binding types dynamically cataloged. Implement export/import generic verbs, `d2b import projection`, and `d2b import graph`. Export accepts only a factory-bound owner Service. Import status exposes the same-semantic-type projection Service and verified factory/schema fingerprints; local `providerRef` selects the implementation. Graph renders import → projection Service → authored Bindings → controller-owned Process/Endpoint children with deterministic ownership/dependency edges. Binding spec is desired intent only and observations are status-only. Reject Device/Endpoint/Binding export targets, cross-Zone refs, absent/mismatched factory/type/fingerprints, implementation detail in the base, forbidden Providers, USB policy denial, unauthorized Zones, and capability-ceiling violations. No backing/remote refs, internal sessions/streams, FD, secret, path, locator, or bytes in output. Delete waits for authored Bindings to be removed/retargeted and never cascades them. | +| Integration | ZoneContext → resource API; signed Provider factory catalog; owner/dependency indexes for graph rendering; consumers use authored Binding → local projection Service, never ResourceImport directly | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Both standard types parse locally; qualified semantic Service/Binding resolve dynamically; verbs route; Service-only export and factory/type/fingerprint/policy/capability rejections; projection preserves semantic Service type across local implementation selection; canonical minimal base works without `spec.provider`; graph shape exact and bounded; Binding never auto-created/deleted; Binding spec intent-only/status observations; no implementation detail/backing/remote-ref/session/stream/FD/secret/path/locator/bytes leakage | +| Removal proof | Not applicable (new surface) | diff --git a/docs/specs/ADR-046-components-processes-and-sandbox.md b/docs/specs/ADR-046-components-processes-and-sandbox.md new file mode 100644 index 000000000..de522e7cb --- /dev/null +++ b/docs/specs/ADR-046-components-processes-and-sandbox.md @@ -0,0 +1,467 @@ +# ADR 0046 components, processes, and sandbox + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-components-processes-and-sandbox` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | Process Providers, ProviderSupervisor, broker/Host/Guest supervisors | +| Depends on | `ADR-046-provider-model-and-packaging`, `ADR-046-primitive-resource-composition`, `ADR-046-componentsession-and-bus` | +| Supersedes | Current ProcessRole/VmProcessDag as the public process model | + +## Bootstrap boundary + +Non-resource bootstrap is closed: + +- Zone runtime with embedded store/resource API/bus endpoint; +- Zone privileged broker where required; +- one fixed core-controller/Provider-system-core process; +- minimum Host/Guest supervisor/effect adapter; +- fixed user supervisor; +- fixed Provider/system-minijail controller process; +- transport/listener resources necessary to reach the owning Zone. + +Bootstrap mechanisms are not Primitive ResourceSpecs. They cannot be selected by +third-party Providers or widened by config. + +Provider/system-core and Provider/system-minijail are the only Provider +controllers without Process resources. system-minijail is fixed because no +Process controller exists to launch the first Process controller. It is +integrity-pinned, uses the compiled bootstrap authorization, and launches every +later Process controller, including Provider/system-systemd. + +### No bootstrap state Volume + +Provider state Volumes are optional and declared only under the storage-need +test (see "Static Provider deployment and optional component state Volumes" +below and `ADR-046-provider-state`). The fixed bootstrap components — the first +`Provider/volume-local` controller instance on each execution target, and +(where present) `Provider/system-core` and `Provider/system-minijail` — keep +their bounded non-secret operational state in the owning resource's `status` +subresource and the core Operation ledger, and declare **no** state Volume. +Because no component requires a state Volume before a `volume-local` instance +is Ready, there is no bootstrap state-Volume cycle, no per-execution-target +local bootstrap storage mechanism, and no bootstrap-storage exception (D086, +superseded by D087). There is no hidden bootstrap store. + +A fixed bootstrap component reaches Ready by adopting running processes and +re-deriving its observed state from `status`, the core Operation ledger, and +independent external observation (cgroup-leaf scanning, fresh pidfds, marker +reverification against external reality). If a future bootstrap component ever +needs secret or large private recovery state that cannot enter status, it must +be introduced through a new reviewed design that declares an ordinary optional +state Volume; it does not reintroduce a bootstrap-storage exception. + +After bootstrap: + +- every non-system-core controller is Process; +- every service is Process; +- every worker is Process or EphemeralProcess; +- every other Process Provider/controller is Process; +- every process has one executionRef/domain/user placement and selected Process + Provider. + +## Static Provider deployment and optional component state Volumes + +Core ProviderDeployment reads the signed manifest/catalog entry a Provider's +`artifactId` resolves to and creates the Provider's entire static +controller/service Process graph from it. A Provider controller never +bootstraps its own Process (the two fixed exceptions are system-core, which +has no Process, and system-minijail, which launches every later Process +controller including system-systemd's). The static graph is created once, at +Provider install/reconcile time, from the manifest's declared component +descriptors — never invented, widened, or self-launched by the Provider's own +code at runtime. + +Controllers may create authorized *dynamic* children beyond that static +graph: additional Process/EphemeralProcess resources, and other primitive or +vendor resources their descriptor authorizes (for example, volume-virtiofs +creating a virtiofsd Process per attachment). A service that needs worker +help never spawns a worker itself; it sends a typed internal request to its +owning controller, which creates the worker Process through the normal +ProviderDeployment/EffectPort path. + +Provider state Volumes are **optional**. Bounded non-secret operational state +belongs in the owning resource's `status` subresource and the core Operation +ledger by default (D087). A Provider component declares a state Volume only +when a specific payload passes the storage-need test: it is a secret or +sensitive private datum, is large/binary/file content, is private data unsafe +for status readers, or is bounded but revision-unsuitable with a demonstrated +recovery need (`ADR-046-provider-state`). A stateless component declares no +state Volume, receives none, and contributes none to the Provider's optional +**ProviderStateSet** (`ADR-046-provider-state`: the logical, query-time +grouping of the *declared* Volume resources in the Zone whose +`metadata.ownerRef` resolves to `Provider/`; the set is never a +ResourceType or a stored row, and it is empty for a Provider that declares no +state Volume). There is no empty identity-only Volume and no separate +compartment object distinct from an ordinary Volume. + +Each declared state Volume uses the canonical full Volume schema (see +`ADR-046-resources-volume`), extended with the +`stateSchema`/`persistenceClass`/`sensitivityClass` fields defined in +`ADR-046-provider-state`, and its layout is owned by a dedicated `User/` +principal drawn from a bounded, Nix-preprovisioned pool sized to the +Provider descriptor's fixed controller/service/worker/namespace counts — +never an ad hoc principal created at runtime. Core ProviderDeployment creates +every *declared* state Volume from the manifest's signed state declarations +before creating and launching the corresponding component Process, so the +component's `mounts` can reference an already-Ready Volume at launch. A +component mounts only its own declared view of its own state Volume (a +`mounts` entry naming that view's local dirfd); there is no cross-component or +cross-Provider sharing of another component's state Volume. Resource rows, +resource `status`, and the core Operation ledger remain the sole authority for +resource references, generation counters, backoff/idempotency state, and +session state — a component's state Volume payload never duplicates any of +that; it holds only the component's private secret/large/revision-unsuitable +working payload. Creating a declared state Volume normally requires a +`Provider/volume-local` controller instance to already be running on that same +execution target; because the fixed bootstrap components declare no state +Volume, no component needs a Volume before a `volume-local` instance is Ready, +so there is no bootstrap ordering exception (see "No bootstrap state Volume" +above). + +A worker Process has no ResourceClient, no d2b-bus/dependency-portal access, +no Credential access, no CLI, no broker access, and no authority to spawn +further children. Every resource reference, FD, and configuration value a +worker needs is inherited through its LaunchTicket at launch time. The one +narrow exception: a worker may be declared to own its exact workload child +process when that child is the worker's manifest-fixed data-plane purpose +(for example, a persistent-shell supervisor worker owning its shell PTY +child) — and only then, under an explicit descriptor policy naming that exact +child relationship. This exception never grants a worker broker, bus, or +arbitrary child-spawn authority beyond that one fixed relationship. + +## ProviderSupervisor and EffectPort + +No Provider process — including a primitive Process, Volume, Network, or +Device Provider — imports the broker crate, receives a broker socket or DTO, +directly opens a host path/device/systemd socket, or performs privileged +mutation itself. A Provider controller/service validates and decides semantics +only, then calls an injected async typed **EffectPort** trait using opaque +resource/intent/template/policy IDs. Core owns a small, fixed set of effect +adapters — one per effect domain — that privately map each EffectPort call to +the actual broker/allocator/systemd/user/guest/kernel operation. The broker +remains the sole privileged executor and independent audit owner of every +mutation; no effect adapter and no Provider bypasses it. + +**ProviderSupervisor** is the fixed effect adapter for the Process domain (the +`ProcessLaunchEffectPort`). It is a fixed local effect adapter, not a +Provider/resource controller. It receives a Process-controller-authenticated +LaunchTicket bound to: + +- Process/EphemeralProcess ref/UID/revision/generation; +- owner Provider/component/template; +- executionRef/domain/userRef; +- selected Process Provider; +- compiled sandbox/budget/mount/device/network/endpoint configuration digests; +- exact inherited FD table; +- operation/deadline/cancellation; +- expected process identity/readiness. + +It: + +1. verifies ticket/current resource/controller lease; +2. for every Endpoint, rejects any visibility outside + `owner|provider|zone`, applies that coarse scope, authenticates the exact + subject and signed Provider component, evaluates Role/RoleBinding, and + intersects the canonical `consumerPolicy.allowedSubjects`, + `allowedProviderComponents`, and `allowedOperations` allowlists; request + fields cannot select those identities, no visibility alias is accepted, and + any mismatch is `endpoint-resolve-denied`; +3. resolves only trusted package/template/resource outputs; +4. asks the local broker/systemd/user/Host/Guest effect owner to launch; +5. returns provider-specific stable process identity and mandatory pidfd + evidence; +6. observes/adopts/signals/stops only that exact identity; +7. reports bounded effects/status to the Process controller. + +It never interprets Provider root settings, chooses dependencies, reads sibling +state, or registers services/commands. + +The same pattern generalizes to the other primitive domains: volume-local and +volume-virtiofs call a `VolumeLayoutEffectPort`/`VolumeSourceEffectPort` with +the Volume resource UID, layout-entry index, and resolved semantic +owner/mode/ACL settings — never a raw host path or broker DTO — and the +volume-domain effect adapter performs the actual broker layout operation +(`ADR-046-resources-volume`). Network and Device Providers call the equivalent +`NetworkEffectPort`/`DeviceEffectPort` with opaque resource/intent IDs. Every +such effect adapter is, like ProviderSupervisor, a fixed core-owned component, +never a Provider or resource controller, and never widened by third-party +config. + +## Process common spec + +Common fields (exact frozen names; see `ADR-046-resources-host-guest-process-user` +for the full ExecutionSpec table): + +- `providerRef`; +- `executionRef`; +- `domain` and conditional `userRef`; +- `processClass` controller/service/worker; +- owning Provider/component, plain `template`; +- `configRef` and `credentialRefs`; +- `mounts` by Volume/view/path/access; +- `sandbox`; +- canonical nested `budget`; +- `networkUsage`; +- `deviceUsage`; +- `endpoints`; +- `telemetry`; +- Process/EphemeralProcess lifecycle fields (desired lifecycle, readiness/health, + restart/backoff, adoption/drain, one-shot deadlines/terminal TTLs). + +These are the only common field names. They are never renamed to `network`, +`devices`, a command/binary/argv field, an endpoint kind/path/service field, or +a custom budget/readiness/restart shape. + +No caller-controlled executable path, UID/GID, host path, cgroup path, +capability, raw seccomp, minijail argv, systemd property, broker operation, +socket address, credential byte, or environment escapes the signed Provider/ +Process/Volume schema. + +## Sandbox compilation + +Inline Process sandbox declares semantic requirements: + +- namespace isolation classes; +- capability classes; +- seccomp classes; +- mount/Volume views; +- Device/Network access; +- read-only root/store posture; +- environment classes; +- LSM labels/profile references where supported; +- no-new-privileges/start-root requirements; +- fd classes; +- umask/rlimits/oom policy; +- a semantic user-namespace `mappingClass` (never a numeric host UID/GID; see + `ADR-046-resources-host-guest-process-user`). + +The Process Provider controller never compiles or applies this plan itself. It +validates the declared SandboxSpec and calls the `ProcessLaunchEffectPort` +(ProviderSupervisor) with the resource UID, the selected Process Provider, and +the sandbox digest. ProviderSupervisor and the broker compile the semantic +request into its implementation: + +- system-minijail → ProviderSupervisor requests a broker-validated + minijail/clone3 plan; the broker compiles and applies it; +- system-systemd → ProviderSupervisor requests transient unit/scope hardening + and any approved sandbox wrapper from the fixed systemd effect owner; +- future Provider → same semantic conformance through its own fixed effect + owner. + +The Provider process itself never opens a systemd/D-Bus socket, calls the +broker, or issues `clone3`/`pidfd_open` directly. Raw policy fragments are +package/core-reviewed artifacts, never Provider config strings. + +## Pidfd and wait/reap + +Every Process Provider has a local verified pidfd. + +### system-minijail + +- broker `clone3(CLONE_PIDFD)` preferred; +- reviewed fallback only where contract permits; +- d2b owns wait/reap; +- process born directly in final cgroup; +- adoption verifies pid/start-time/cgroup/executable/template/generation before + `pidfd_open`; +- ambiguity → Unknown/quarantine, never broad kill/reuse. + +### system-systemd + +- non-forking transient unit/scope; +- bind InvocationID, cgroup, MainPID, start-time, Provider/template/generation; +- open verified pidfd; +- systemd owns wait/reap; +- no daemonizing/forking type; +- unit-name alone is not identity; +- adoption revalidates all stable identity before pidfd open. + +Pidfd: + +- is not persisted; +- is not public status; +- never crosses d2b-bus/Zone/Host/Guest transport; +- is closed/reopened across controller restart after identity verification. + +## User domain + +User-domain Process uses the same ResourceType: + +- executionRef; +- domain=user; +- exact userRef or inherited Host/Guest defaultUserRef; +- the referenced ExecutionPolicy allows user; +- system-systemd uses verified transient user scope via fixed user supervisor; +- system-minijail may support user domain only if descriptor/conformance says so; +- UserPortal needs are inline typed refs/settings; +- same-UID non-isolation is explicit status/security posture. + +User supervisor is a fixed local effect adapter, not another Provider. + +## Host/Guest locality + +A Process controller instance declares compatible Host/Guest Provider +capabilities. The ResourceType does not change for: + +- physical Host; +- VM Guest; +- ACA sandbox Guest; +- Azure/full-host Guest; +- remote/nested Guest. + +The Host/Guest local supervisor/effect adapter performs launch and reports to +the owning Zone over d2b-bus/ComponentSession. A Guest does not need a Zone +store unless it separately hosts a child Zone. + +## Process naming and cgroups + +Diagnostic process names: + +```text +z-@ +s-@ +u--@ +``` + +Opaque short IDs are derived from immutable UIDs, not human labels. Executable/ +template/generation remain separate verified identity. + +System cgroup shape: + +```text +z-/ + controller/ + broker/ + executions/ + e-/ + system/ + providers/ + p-/ + components/ + c-/ + process/ + workers/ + w-/ +``` + +Intermediate nodes are process-free. User processes live in exact user scopes +and are not misrepresented as system cgroup children. + +## Process status + +Common status plus: + +- process implementation/provider; +- process identity digest; +- wait/reap owner; +- executionRef/domain/userRef; +- config/sandbox/resource revision digests; +- readiness/health/restart/backoff; +- last start/exit class; +- optional exitCode in outcome; +- adoption/quarantine condition. + +No PID, pidfd, unit name, path, argv, environment, terminal bytes, or raw +Provider diagnostic is public status/audit. + +## Fast launch + +After durable ready Process commit: + +- direct post-commit hint; +- p95 handler start <=5 ms; +- p95 launch-attempt start <=20 ms; +- Process controller launches in independent async task; +- watch loop dispatches the next independent resource concurrently; +- readiness wait does not hold controller-wide queue; +- status transitions are async expected-revision writes. + +## Current-role disposition rule + +Every v3 ProcessRole/helper/unit is classified as: + +- fixed bootstrap; +- Process/EphemeralProcess under exact Provider; +- non-process controller/probe; +- deleted after successor. + +Each disposition names: + +- current source/emitter/runtime caller; +- ResourceType owner/Provider; +- Process common/Provider-specific fields; +- executionRef/domain/userRef; +- Volume/Network/Device dependencies; +- readiness/restart/adoption/finalizer; +- current tests copied/adapted; +- exact old path removal. + +No generic Process conversion may lose role-specific watchdog behavior, +pre-start effects, fine-grained ACL/device policy, or restart semantics. + +## Representative baseline mapping + +| Current role | Future owner | +| --- | --- | +| CloudHypervisorRunner | Runtime Cloud Hypervisor Provider owns a Process under Host | +| QemuMediaRunner | Runtime QEMU Provider owns Process | +| Virtiofsd | Volume Provider owns Process per attachment | +| Swtpm | TPM Device Provider owns Process plus Volume state | +| GPU/GpuRenderNode/Video | Device GPU Provider owns Processes; Display Provider consumes Device/endpoints | +| Audio | Audio Provider owns Process | +| WaylandProxy | Display/Wayland Provider owns Process | +| VsockRelay | Transport Provider owns Process | +| OtelHostBridge | Observability Provider owns Process | +| Usbip | USB Device Provider owns Process/EphemeralProcess | +| GuestControlHealth/readiness probes | controller observation, not Process | +| HostReconcile/store preflight | controller logic, not Process | + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `d2b-core/src/processes.rs`, minijail/profile/runtime; d2bd DAG/pidfd/adoption; broker SpawnRunner; unsafe-local helper; guestd/exec runner; Nix system/user/guest units | +| Evidence class | Current launch paths are reachable; generic Process Providers/supervisor are ADR-only | +| Behavior retained | Typed argv/profile, broker privilege, pidfd identity/adoption, direct cgroup placement, user scope, guest locality, readiness/watchdogs, redaction | +| Required delta | Common Process/EphemeralProcess, selected Provider, Host/Guest executionRef/domain placement, generic supervisor/async controller/status | +| Reuse path | Exact current/main source mappings in role/Provider dossiers | +| Replacement/deletion | No ProcessRole/unit/helper removal until successor behavior/test parity | +| Feasibility proof | systemd/minijail shared conformance; Host system/user and Guest execution; fast parallel launch | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-process-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0/W2; Process contracts/supervisor | +| Current source | `d2b-core/src/{processes,process_builder,minijail_profile}.rs`; `d2bd/src/supervisor/*`; broker `spawn_runner.rs` | +| Reuse source | Useful main ProviderSupervisor/session/process code named by sub-items | +| Reuse action | adapt | +| Destination | `packages/d2b-process/src/`, `packages/d2b-provider-supervisor/src/` | +| Detailed design | Common spec/status/tickets/pidfd/adoption/naming/cgroup/fast path Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Process Provider controllers → supervisor/broker/systemd → async status | +| Data migration | Full reset; no role snapshot import | +| Validation | Shared conformance/fault/latency tests | +| Removal proof | Role/DAG path removed only per role disposition | + +### ADR046-process-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-process-001; systemd/minijail Provider owners | +| Current source | unsafe-local helper runtime/systemd; guest exec systemd-run; broker SpawnRunner/minijail | +| Reuse source | Main process/session helpers if selected by exact sub-items | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-systemd/`, `packages/d2b-provider-system-minijail/` | +| Detailed design | Two Process/EphemeralProcess implementations, pidfd/wait ownership, system/user domains Primary reuse disposition: `adapt`. Preserved source-plan detail: extract/adapt. | +| Integration | Zone-installed Providers/controller instances per Host/Guest | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Identical schema/status conformance plus provider-specific adoption | +| Removal proof | Old helpers retained until host/user/guest parity | diff --git a/docs/specs/ADR-046-componentsession-and-bus.md b/docs/specs/ADR-046-componentsession-and-bus.md new file mode 100644 index 000000000..c671818f2 --- /dev/null +++ b/docs/specs/ADR-046-componentsession-and-bus.md @@ -0,0 +1,432 @@ +# ADR 0046 ComponentSession and d2b-bus + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-componentsession-and-bus` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-session`, transport adapters, `d2b-bus` | +| Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-api-and-authorization` | +| Supersedes | Current v3 Realm PeerSession and ad hoc guest/user/public IPC | + +## Source and reuse policy + +The pre-ADR45 v3 baseline has no ComponentSession. This spec deliberately +copies/adapts the implementation from main commit: + +```text +a1cc0b2da4a08ca3240a770a972fe4da6f912bef +ADR-0045 W9: coordinate toolkit and sibling cutover (#314) +``` + +Selected main sources: + +| Main source | Selected behavior | +| --- | --- | +| `packages/d2b-session/src/handshake.rs` | Strict canonical offer/preface, Noise prologue binding, NN/KK/IKpsk2 profiles, generation discovery, credentials, transcript | +| `packages/d2b-session/src/bootstrap.rs` | Zeroizing single-use operation/nonce/expiry-bound bootstrap PSK | +| `packages/d2b-session/src/record.rs` | Directional encrypted records, sequence/reconnect generation, replay detection, bounds | +| `packages/d2b-session/src/engine.rs` | Async establish/reconnect, timeout, cancellation, ttrpc, named streams, attachments | +| `packages/d2b-session/src/scheduler.rs` | Bounded priority/fair scheduling and named-stream round robin | +| `packages/d2b-session/src/streams.rs` | Credit-based named stream state machine | +| `packages/d2b-session/src/lifecycle.rs` | Keepalive/reconnect/close state | +| `packages/d2b-session/src/transport.rs` | Async owned transport abstraction | +| `packages/d2b-session-unix/src/adapter.rs` | Unix peer identity, atomic attachments, descriptor validation/credits | +| `packages/d2b-session-unix/src/vsock.rs` | Async framed vsock transport, expected CID, no attachments | +| `packages/d2b-session/tests/noise_vectors.rs` | Exact snow 0.10 NN/KK/IKpsk2 vectors and rejection mutations | +| `packages/d2b-session/tests/component_session.rs` | Integrated strict negotiation/record/lifecycle/limits | +| `packages/d2b-session-unix/tests/unix_session.rs` | SO_PEERCRED/SCM_RIGHTS/pidfd/object identity/credit tests | + +Unrelated ADR 0045 Provider types, endpoint roles, service inventory, realm +process model, generated v2 DTO names, and delivery assumptions are not copied +as v3 architecture. + +## Layering + +```text +generated async ttrpc services / named streams + -> d2b-bus exact addressed route/admission + -> ComponentSession + Noise authentication + record protection + schema/purpose/transport/limits/reconnect binding + cancellation/deadlines/keepalive + fair control/stream scheduling + local typed attachment validation + -> owned d2b transport + inherited socketpair / Unix seqpacket / Unix stream / vsock / + enrolled Zone/provider transport +``` + +d2b-transport provides carriage/evidence only. ComponentSession authenticates +and protects a session. d2b-bus resolves an exact service/Zone/Provider/ +controller route. Native Role/RoleBinding authorizes connect/invoke/stream/ +relay/resource verbs. No layer may widen the authority established below it. + +## Noise profiles + +### Local NN + +`Noise_NN_25519_ChaChaPoly_SHA256` is permitted only when: + +- purpose class is local; +- transport is inherited socketpair or Unix stream/seqpacket; +- transport provides directional peer identity evidence; +- endpoint policy is trusted/config-generated; +- authenticated Unix peer evidence maps to an exact canonical subject. + +NN supplies ephemeral encrypted record protection; peer authentication comes +from the bound Unix evidence, not a peer-supplied subject. + +### Enrolled KK + +`Noise_KK_25519_ChaChaPoly_SHA256` is used for enrolled peers: + +- both static public keys are known before handshake; +- local private keys are sealed/zeroizing; +- static key registry maps the authenticated remote key to one canonical + `Zone/*`, `Provider/*`, `Process/*`, `Host/*`, or `Guest/*` subject; +- route/purpose/service/schema/limits/channel binding are in the Noise prologue. + +### Bootstrap IKpsk2 + +`Noise_IKpsk2_25519_ChaChaPoly_SHA256` is used only for one-time bootstrap: + +- responder static key is known; +- single-use PSK is bound to operation ID, replay nonce, expected subject/ + Host/Guest/Provider/controller purpose, and expiry; +- PSK admission is consumed exactly once; +- successful enrollment replaces bootstrap with an enrolled identity/session; +- replay, expiry, wrong operation/subject/purpose fails closed. + +## Handshake policy + +The trusted endpoint policy binds: + +- endpoint purpose and purpose class; +- service package and exact schema fingerprint; +- initiator/responder evidence requirements; +- expected/authenticated subject constraints; +- selected Noise profile; +- transport class/locality/channel binding; +- reconnect generation; +- exact limit profile; +- attachment policy; +- authorization service/verb requirements, including the immutable forwarded + target verb and next-hop scope when the session carries a relay. + +The canonical preface+offer is the Noise prologue. Any mismatch changes the +transcript and fails. There is no negotiation down to another service, schema, +profile, transport, purpose, role/evidence class, limit, or attachment policy. + +Generation discovery remains local-only, exact-policy-bound, and unauthenticated +only to the extent required to learn the nonzero reconnect generation; it +cannot admit a request or authorize a service. + +## Authenticated subject + +ComponentSession never trusts a subjectRef/role from the peer payload. + +Evidence mapping: + +| Evidence | Subject source | +| --- | --- | +| Unix pathname/socketpair | trusted endpoint config + verified SO_PEERCRED/process identity maps uid/pid to exact Zone-local subject | +| Enrolled KK key | Zone identity registry maps static public key to exact subject | +| Bootstrap IKpsk2 | one-time admission record contains expected subject and operation | +| Native vsock | expected CID/Guest bootstrap identity plus IKpsk2/enrolled key maps to exact subject | + +The established session carries the shared +`AuthenticatedSubjectContext` from the terminology/identity spec, plus an +authorization lease revision/expiry. This spec owns the evidence-to-context +mapping, not a second identity schema. + +It is redacted in Debug/logs and cannot be changed without a new handshake. + +## Native RBAC integration + +After authentication and before service dispatch, the same native +Role/RoleBinding evaluator used by the resource API checks: + +```text +subject +Zone +session purpose/service +verb = connect | invoke | open-stream | relay | attach | cancel | observe + | audit-export | support-bundle +method/stream kind +target ResourceRef/Provider/Host/Guest +forwarded target verb and target Zone (relay only) +authenticated inbound ZoneLink and route-selected next hop (relay only) +Provider/controller/session generations +``` + +Resource API methods add their resource verb attributes. + +Rules may authorize ComponentSession connect/service/stream attributes in the +same Role resource as resource verbs. There is no endpoint-local parallel RBAC +language. + +`relay` is the closed session verb for one-hop forwarding. It permits only the +already-authenticated inbound ZoneLink/transport subject to pass an +already-admitted invocation or named stream to the route-selected next +authorized hop. The peer cannot supply or replace the subject, target verb, +target Zone, next hop, named resource or nameless `List`/`Watch` selector, +bounded filters, or authorization result. A relay grant conveys no resource +CRUD, identity mapping, capability widening, attachment, credential, or local +lifecycle authority. + +`audit-export` and `support-bundle` are the two admin-only diagnostic session +verbs. They bind exactly to `d2b.audit.v3.AuditService/Export` and +`d2b.support.v3.SupportService/GenerateBundle`, respectively. Neither may +appear in a rule's resource `verbs`, and neither implies any resource read or +mutation authority. + +Authorization: + +- is required for the session itself and each method/stream; +- for every forwarded method/stream, independently requires both `relay` for + the authenticated adjacent-Zone subject and the original target verb under + the same exact local scope; the destination rechecks the target verb; +- fails closed when relay, target-verb authorization, or policy state is + missing; +- is revision-bound to Role/RoleBinding/Provider/API/Zone policy; +- is cached only under exact attributes/short expiry; +- invalidates immediately after relevant durable policy commit; +- grants long-lived streams a short lease; +- closes/refuses new work when lease revalidation fails; +- preserves already admitted bounded work only to its original deadline. + +Noise authentication proves identity/channel; it does not itself grant a role. + +## Records + +The copied RecordProtector behavior is retained: + +- directional Noise transport keys; +- bounded u16 length-prefixed encrypted record; +- authenticated header containing record kind, channel, sequence, reconnect + generation, payload length; +- strict send/receive sequence; +- replay digest rejection; +- reconnect-generation rejection; +- no plaintext/status/credential Debug. + +The v3 contract versions all record and canonical offer encodings separately +from v2. Protobuf numbers and binary tags are regenerated/frozen; v2 and v3 do +not interoperate. + +## Channels and fairness + +Closed channel classes: + +- session control; +- ttrpc control; +- attachment control; +- named stream. + +Session/control/cancel/keepalive/status health traffic has reserved bounded +capacity. Named streams use per-stream and aggregate byte credit plus round- +robin fairness. One blocked terminal/watch stream cannot starve resource status, +cancel, or health. + +Controller resource watches and reconcile hints are named streams. Their +delivery/ack cursor remains resource revision, not session sequence. + +## Async behavior + +All transport/session/service methods are async. + +- one driver owns session transport/protector/scheduler; +- service handlers run as independent bounded tasks; +- named streams expose async read/write/credit/cancel; +- blocking fd/kernel/filesystem operations use explicit adapters; +- no nested runtime/block_on; +- cancellation/deadline propagates from bus to handler/effect; +- Process reconcile fast path cannot wait for unrelated stream traffic. + +## Attachments + +Attachments are local Unix only. They require: + +- packet-atomic seqpacket policy; +- encrypted descriptor bound to service/method/request/operation/generation; +- exact object/access/purpose; +- CLOEXEC; +- SO_PEERCRED/pidfd/object identity validation; +- duplicate kernel-object rejection unless explicitly permitted; +- atomic multi-scope credits and cleanup on every failure. + +Unix stream/vsock/remote Zone transports carry no SCM_RIGHTS. Destination +controllers re-origin local resources/effects. + +### ResourceExport and ResourceImport streams (D096) + +Cross-Zone `ResourceExport`/`ResourceImport` payload bytes use the existing +ComponentSession named-stream machinery only: bounded encrypted streams with +per-stream and aggregate credits/backpressure, cancel, deadline, and idempotency, +bound to a per-import session generation. No FD, SCM_RIGHTS attachment, or +resource grant crosses a Zone. Intermediate Zone controllers route only the +encrypted stream records and see ciphertext, never plaintext payload bytes, +device handles, paths, or tokens. + +### Fixed listener/endpoint authority (D097) + +A fixed listener binding (a stable Unix/vsock/TCP socket or a stable +`Endpoint`/port) is a D097 authority: exactly one authority service owns the +bind. The owning Provider/Resource declares an `AuthorityDescriptor` +(`authorityScope` matching the listener's reach, `cardinality: zero-or-one` per +`(scope, opaque bind-key digest)`, `arbitration: exclusive`), and core's +authority index rejects a second binder of the same listener with the typed +`duplicateConflict` before the bind. The `authorityKey` digests the bind +selector and is never a raw address/path/CID/port in status or audit. The d2b-bus +itself is an `exactly-one` per-Zone singleton authority (see the Zone-control +core authority table). Per-session named-stream and `OwnedTransport` handles +stay internal/high-churn (D092) and are not authorities. + +## d2b-bus + +d2b-bus is an exact addressed router, not pub/sub. + +Route key: + +```text +(Zone, service package, method/stream, target ResourceRef or Provider, + schema fingerprint, Provider/controller/session generation) +``` + +It: + +- resolves local/Host/Guest/parent-child Zone transport; +- binds authenticated subject and operation/deadline; +- checks session connect/service/stream RBAC; +- for a ZoneLink forward, checks the canonical `relay` verb and the immutable + target verb separately before opening the next-hop session; +- invokes exact generated ttrpc client/server; +- bridges named streams with bounded credit; +- preserves pinned reverse route/cancellation; +- exposes no global subscriber socket/topic namespace/route table to Providers. + +ResourceClient always uses d2b-bus, even beside the Zone runtime. There is no +direct-store shortcut. + +## Sensitive credential delivery + +Credential resources/status/store/revision/audit/OTEL remain free of token +bytes. A Credential Provider may deliver a raw token only through a dedicated +end-to-end sensitive ComponentSession: + +- initiator/responder are fully enrolled Provider/component identities; +- Noise profile is KK; NN and IKpsk2 are forbidden; +- consumerRef may name Provider/; its signed component descriptor and + Role/RoleBinding resolve the exact receiving component/Process; +- the offer/prologue binds Credential ref/UID/generation, Credential Provider + and consumer Provider/component generations, audience/operation class, + route, schema, limits, expiry/deadline, and authorization revisions; +- d2b-bus/Zone/relay intermediaries authorize route establishment but forward + opaque protected records and cannot terminate/decrypt the inner session; +- token payload has a strict small bound, zeroizing buffers, redacted Debug, + replay-safe sequence, no logging/audit/metrics, and immediate close/zeroize; +- ambiguous delivery never becomes success and is not automatically replayed + outside the credential method's explicit idempotency contract. + +This is the only initial cross-process secret-byte channel. It grants no generic +raw HTTP, signing, endpoint, or credential forwarding authority. + +## Lifecycle + +Retain/adapt main's: + +- Established/Disconnected/Reconnecting/Closing/Closed; +- monotonic keepalive; +- bounded reconnect window/attempts; +- reconnect generation increment; +- cancel all old-generation requests; +- new Noise handshake/protector after reconnect; +- explicit close reason/remediation. + +Resource/controller streams relist/resume using resource revisions after +reconnect; session generation alone is not durable state. + +## Errors and telemetry + +Stable errors include malformed/unsupported handshake, authentication, +transcript/schema/purpose/transport/evidence/subject mismatch, policy denial, +generation/replay/sequence, limit/backpressure, invalid attachment, timeout, +cancel, disconnect, and internal invariant. + +Metrics use closed labels: + +- purpose/service/transport/profile; +- operation class; +- outcome/error code; +- stream/attachment class. + +Never label subject/resource names, endpoint paths, keys, payload, terminal +bytes, credentials, PIDs, or Provider diagnostics. + +Audit records authenticated subject digest/ref where policy permits, endpoint/ +service/method, authorization revision/decision, operation/correlation, +transcript/session generation digest, route, and fixed outcome. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| v3 current anchor | `d2b-realm-router` PeerSession/SecurePeerSession/MuxSession and `d2b-realm-transport`; guest ttrpc/vsock/HMAC | +| v3 evidence class | Mostly implemented-but-unwired for Realm peer; guest control reachable; ComponentSession absent | +| Main reuse source | main `a1cc0b2d`, `d2b-session`, `d2b-session-unix`, v2 ComponentSession contracts/tests | +| Behavior retained | Strict Noise profiles/transcript, encrypted replay-safe records, async owned transports, fair streams, cancellation/reconnect, exact attachments | +| Required delta | v3 contract names/versions, shared AuthenticatedSubjectContext, Role/RoleBinding authorization, d2b-bus routing, Zone services | +| Excluded main assumptions | v2 EndpointRole/Realm/service inventory, Provider registry/process model, delivery/Nix ownership | +| Feasibility proof | Copy main tests, add subject/RBAC/revocation/resource-watch/latency integration on v3 | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-session-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0 shared contract root | +| Current source | v3 `d2b-realm-router/src/{session,secure_session,mux_session,lifecycle}.rs`, guest auth/transport | +| Reuse source | main `a1cc0b2d`: `d2b-contracts/src/v2_component_session.rs`, `d2b-session/src/{handshake,bootstrap,record,engine,scheduler,streams,lifecycle,transport}.rs`, Noise/component tests/vectors | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/component_session.rs`, `packages/d2b-session/` | +| Detailed design | Reversion canonical offer/records; retain NN/KK/IKpsk2; add canonical subject/authorization context hooks Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | d2b-bus, resource/controller/Provider services | +| Data migration | No v2 session compatibility; reconnect on v3 | +| Validation | Copied exact vectors/rejections plus subject/RBAC/revocation tests | +| Removal proof | v3 old Realm PeerSession removed only after all v3 peer routes move | + +### ADR046-session-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-session-001; transport owner | +| Current source | v3 guest vsock/local seqpacket implementations and d2b-realm-transport traits | +| Reuse source | main `a1cc0b2d`: `d2b-session-unix/src/{adapter,socket,descriptor,pidfd,vsock,systemd,credit}.rs`, `tests/unix_session.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-session-unix/`, future enrolled transport adapter crates | +| Detailed design | Unix/socketpair/vsock owned transports, peer evidence, fd/pidfd/object validation, credits Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | ProviderSupervisor/Host/Guest/Zone listeners hand owned transports to session | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Copied fd/peer/credit tests plus Host/Guest subject mapping | +| Removal proof | Ad hoc guest/public/helper transport removed only per service cutover | + +### ADR046-bus-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | Sessions + resource API; bus owner | +| Current source | v3 `d2b-realm-router`, target resolver, CLI routing, operation router | +| Reuse source | Any useful main d2b-client/provider/session routing symbols named by implementation sub-items | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/{router,registry,authorization,streams,operations}.rs` | +| Detailed design | Exact service/resource routes, RBAC, pinned reverse route, cancellation, named stream bridge, no wildcard pub/sub Primary reuse disposition: `adapt`. Preserved source-plan detail: extract/adapt. | +| Integration | Every ResourceClient/controller/Provider/CLI service | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Message isolation; closed session-verb enum including `relay`, `audit-export`, and `support-bundle`; exact diagnostic service/method binding with no implied resource grant; relay missing/target verb missing/provider self-assertion fail-closed vectors; named-target and nameless List/Watch selector/filter preservation at every hop; route/auth revocation; fairness; reconnect; no direct-store path | +| Removal proof | Old direct dispatch branches removed only after route parity | diff --git a/docs/specs/ADR-046-core-controllers.md b/docs/specs/ADR-046-core-controllers.md new file mode 100644 index 000000000..41d929ca9 --- /dev/null +++ b/docs/specs/ADR-046-core-controllers.md @@ -0,0 +1,363 @@ +# ADR 0046 core controllers + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-core-controllers` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | Fixed core-controller process, `Provider/system-core` | +| Depends on | `ADR-046-resource-store-redb`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-provider-model-and-packaging` | +| Supersedes | Current daemon-global hard-coded orchestration ownership | + +## Process model + +Each Zone has one fixed core-controller process. It hosts multiple isolated +controller handlers in one binary/runtime to reduce memory/process overhead. + +It is also the `Provider/system-core` controller. Provider/system-core and the +fixed Provider/system-minijail controller are the two Provider bootstrap +exceptions. It starts after the embedded store/resource API/bus endpoint is +ready and connects to those services through the same local +d2b-bus/ComponentSession path used by other controllers. Only the non-controller +store actor touches redb directly. + +The process: + +- has zero ambient host capability; +- cannot invoke arbitrary broker operations; +- owns no Provider payload state; +- uses typed internal core effect ports only for store lifecycle/emergency + operations that cannot be resources; +- has reserved resource API/bus capacity; +- runs handlers concurrently but serializes each resource; +- exposes aggregate health plus per-handler status. + +All other Provider controllers are Process resources under Hosts or Guests. + +## Handler catalog + +### Configuration publication + +Owns: + +- configured root resource generation; +- validation/staging/activation; +- prior generation rollback window. + +Algorithm: + +1. read integrity-pinned candidate bundle; +2. validate Provider packages/APIs/config/refs/owners/RBAC/budgets; +3. stage inactive resources in bounded transactions; +4. atomically activate one configuration revision; +5. trigger affected resources/providers/controllers; +6. request asynchronous Delete for prior configuration-owned resources omitted + from the new canonical set; +7. report Degraded/pending-cleanup without blocking activation; +8. retain prior generation for bounded rollback/drain; +9. prune only after ownership/finalizer checks. + +It does not overwrite controller-created children merely because root config +changes. + +### API catalog/binding + +Owns ResourceTypeSchema, ResourceApiExport, and ResourceApiBinding internal +control resources. + +It verifies: + +- Provider/package/signature; +- ResourceType short-name collision; +- exact schema/descriptor fingerprint; +- additive compatibility; +- permission claims narrowed by policy; +- controller implementation exists; +- API withdrawal has no unresolved resources/finalizers. + +It atomically swaps the in-memory validator/catalog after commit. + +### Authorization + +Owns Role/RoleBinding validation and revision-bound authorization indexes. + +It: + +- validates subjects/refs/scope/verbs; +- builds/switches indexes after durable commit; +- invalidates session/API authorization caches/leases immediately; +- triggers affected Provider/controller/resource reconcilers; +- never turns a deny into allow through fallback. + +### Provider lifecycle + +Owns Provider resources and aggregate status. + +It: + +- verifies package/trust/config/conformance; +- validates controller/service/worker graph; +- creates owned Process/EphemeralProcess and any *declared* optional state + Volume (`ADR-046-provider-state`: a component declares a state Volume only + when a payload passes the storage-need test; stateless components declare + none and get none) plus other required children; +- waits for required components/dependencies; +- publishes exported ResourceTypes/services only after ready; +- drains/withdraws/revokes components on update/disable/delete; +- aggregates exact child status without spawning directly. + +Provider/system-core is handled internally without a Process child. All others +use the same owned child-resource algorithm. + +### Controller registration and hints + +Owns: + +- signed descriptor validation; +- authenticated controller lease; +- ResourceType/provider/Host/Guest/domain binding; +- watch plan; +- reverse dependency indexes; +- hint filtering/coalescing; +- checkpoint/high-water tracking; +- lease withdrawal/replacement. + +It implements the <=5 ms commit-to-handler path with the store post-commit +dispatcher and d2b-bus. + +### Currency and upgrade aggregation (D091) + +Owns: + +- invoking `assess_update` triggers on core/Provider-generation, + artifact/image/NixOS-generation, immutable-spec, dependency, and + security-policy change; +- aggregating self/owned/dependency `status.update` currency into the bounded + `status.update.owned`/`status.update.dependencies` counts and truncated refs + for `Get`/`List` (including `List --updates`); +- topological dependency/owner planning for `plan_upgrade`/`execute_upgrade` + (drain → recycle → restart affected owned/dependent resources) via the reverse + dependency indexes; +- persisting each upgrade operation/idempotency/progress in the core Operation + ledger (status carries only the latest bounded plan/result), and resuming an + in-flight upgrade after crash/restart. + +The expedited (`waitForReconcile`) mutation path (D090) uses the same +post-commit dispatcher: core emits the typed `CommittedRevisionProof` only after +durable commit and routes the expedited request into the owning controller's +bounded priority lane, then aggregates the returned disposition/projection for +the API response without waiting for the asynchronous status write. + +### Ownership/finalizer + +Owns generic owner graph integrity and deletion ordering, not Provider-specific +finalizer logic. + +It: + +- validates singular ownerRef/UID/cycle/depth; +- emits owner hints for every child mutation; +- tracks child-first deletion; +- dispatches finalizer owners; +- reports blocked/ambiguous conditions; +- never clears another controller's finalizer or invents cleanup success. + +### Revision/watch maintenance + +Owns: + +- watch registration/high-water handoff; +- revision-log compaction policy; +- expired-cursor/relist; +- watch/stream quotas; +- owner/dependency live dispatch. + +The store actor performs redb transactions; this controller schedules policy and +reports status. + +### EphemeralProcess cleanup + +Owns terminal retention cleanup: + +- Succeeded uses successfulTtl, default 1h; +- Failed uses failedTtl, default 24h; +- starts at completedAt; +- writes/validates cleanupEligibleAt; +- respects finalizers, owner deletion, incident holds, expected revision; +- calls normal Delete; does not remove rows directly. + +### Zone link/delegation + +Owns ZoneLink: + +- parent/child Zone identity and transport/session requirements; +- child-local RoleBinding/authorization expectation; +- route/resource cursor status; +- reconnect/resync; +- local intents while disconnected; +- disable/revocation. + +It never stores child resources or credentials in the parent. + +### Budget/emergency policy + +Budgets are shared Host/Guest ExecutionPolicy and Process fields, not +ResourceTypes. This handler: + +- validates hierarchical Host/Guest allocations against Zone capacity/policy; +- tracks aggregate reservations/observations; +- blocks overcommit; +- narrows child Zone capacity; +- handles digest/Provider/Host/Guest/Zone/global emergency disable; +- revokes routes/sessions/grants and stops child Processes via normal resources; +- preserves incident-held Volume/Provider state. + +### Store lifecycle + +Coordinates: + +- backup request/admission; +- staged restore/internal schema upgrade; +- compaction; +- corruption quarantine; +- health/metrics; +- reset inventory. + +Actual redb transaction/file publication stays in the embedded store actor/ +storage owner. Normal controllers cannot call these internals. + +### system-core Host + +Provider/system-core reconciles Host: + +- validates it represents the local OS attached to this Zone runtime; +- validates defaultDomain/allowedDomains/defaultUserRef/budget/policy; +- reports local host availability/capabilities; +- exposes Host Provider capabilities used by Process/Volume/Network/Device + controller descriptors; +- never starts a Process directly. + +A Zone may have multiple policy/budget-separated Hosts, all +mapping to the same physical OS but accepting different Process domains/users. + +### system-core User + +Provider/system-core reconciles local User: + +- configured identity/name; +- NSS/passwd/group lookup or explicitly supported local source; +- observed UID/GID/groups/home/session manager availability; +- status only, no credential bytes; +- detects UID/name/group drift; +- does not create arbitrary users unless a later reviewed Provider implements + that behavior. + +User status supports Process userRef and Volume ACL owner/group refs. + +## Controller health/status + +Each handler exposes: + +- observed configuration/API/policy generation; +- phase/conditions/outcome; +- queue/running counts; +- last watch revision/checkpoint; +- lastReconciledAt; +- degraded/failed stable code; +- retry/backpressure; +- latency histogram without resource-name labels. + +The core-controller aggregate becomes Ready only when mandatory store/API/auth/ +configuration/Provider/controller/ownership handlers are Ready. Optional link/ +backup cleanup work can report Degraded without false total failure. + +## Startup + +1. Zone runtime validates/opens store. +2. Resource service and local d2b-bus/ComponentSession endpoint start. +3. Fixed system-core and system-minijail controller processes start and + authenticate as their exact Provider subjects. +4. The compiled bootstrap authorization grants only the closed verbs to exact + Provider/system-core and Provider/system-minijail subjects. +5. Handlers list/recover/checkpoint concurrently. +6. Configuration publishes/recovers active generation. +7. system-core Hosts/Users reconcile. +8. Other Provider controllers/processes launch through resources. +9. Zone readiness publishes after mandatory handlers are current. + +No handler requires all optional Providers to be ready. + +## Restart + +The core controller stores no authoritative private ledger outside resources/ +operations/revision log. The core Operation ledger owns in-flight idempotency, +retry, and transaction progress; resource `status` owns the latest bounded +result and checkpoint (D087). On restart it: + +- authenticates a new ComponentSession generation; +- relists owned resources; +- resumes from durable checkpoints where valid, treating recovered `status` as + observation and reverifying it against external reality before relying on it; +- revalidates Provider/controller leases; +- does not clean up before Process/Host/Guest/Volume owners observe/adopt; +- preserves Unknown/ambiguous states. + +## Security boundary + +Keeping handlers in one process does not union arbitrary Provider privilege: + +- only fixed core ResourceTypes/verbs; +- no Provider payload config/secrets/state; +- no broker socket/raw host paths; +- typed store-lifecycle/emergency internal port; +- all resource work still uses Role/RoleBinding and expected revisions; +- handler panic/failure is contained, surfaced, and may restart process, but + store remains independently owned by Zone runtime. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | d2bd startup/config/provider/process/operation/adoption owners; Realm access/allocator engines; Nix config generation; storage/sync contracts | +| Evidence class | Current owners are mixed reachable/generated; generic core-controller process is ADR-only | +| Behavior retained | Single owners, deterministic config, typed policy, idempotency, recover-before-cleanup, pidfd non-persistence, bounded audit | +| Required delta | Separate fixed core controller, ResourceType handlers, resource-only authority, async watches/checkpoints | +| Reuse path | Extract pure controller algorithms/validators from exact current/main sources per work item | +| Replacement/deletion | d2bd monolithic branches remain until each handler and Provider successor is integrated | +| Feasibility proof | One process with all handlers over local ComponentSession, fast hint path, crash/relist/revoke tests | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-core-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0/W1a; core-controller owner | +| Current source | `packages/d2bd/src/lib.rs`, `provider_registry.rs` if present on source ref, supervisor state, operations; `d2b-realm-core/src/{allocator_engine,identity_store}.rs` | +| Reuse source | Useful pure handler/toolkit code from main named in implementation sub-items | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/{main,configuration,api_catalog,authz,providers,controllers,ownership,watches,cleanup,zone_links,budgets,store}.rs` | +| Detailed design | One fixed process, isolated handlers, async ResourceClient, health/startup/restart Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Zone runtime local bus/session; Provider/system-core resource identity | +| Data migration | Full reset | +| Validation | Per-handler unit/property tests plus multi-process startup/restart | +| Removal proof | Current daemon branches removed after handler/Provider parity | + +### ADR046-core-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-core-001, ADR046-exec-003, ADR046-exec-004, ADR046-exec-005, ADR046-system-core-001; coordination-only owner | +| Current source | Canonical Host/User resource, bootstrap-order, and Provider-boundary work owned by ADR046-exec-003, ADR046-exec-004, ADR046-exec-005, and ADR046-system-core-001 | +| Reuse source | The canonical owner work items named above; this item reuses their public contracts but owns no Host/User implementation | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/tests/system_core_coordination.rs` | +| Detailed design | Coordination-only acceptance: prove the fixed core controller loads the manifest/audit boundary from ADR046-system-core-001 and routes Host/User reconciliation to the canonical ADR046-exec-003/004 handlers after ADR046-exec-005 bootstrap ordering. This item defines no schema, handler, status, capability, or `packages/d2b-provider-system-core/src/{host,user}.rs` destination. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt only the integration acceptance. | +| Integration | After all four canonical owner items are complete, exercise bootstrap Provider/system-core and prove other controllers resolve Host/Guest/User refs without a duplicate core-controller handler | +| Data migration | New v3 resources from Nix | +| Validation | Coordination test asserts one Host handler owner, one User handler owner, manifest load after bootstrap ordering, and no duplicate Host/User destination or reconcile path in core-controller | +| Removal proof | No independent implementation removal; canonical exec/system-core work items own legacy Host/User removal proofs | diff --git a/docs/specs/ADR-046-current-code-migration-map.md b/docs/specs/ADR-046-current-code-migration-map.md new file mode 100644 index 000000000..13007d16f --- /dev/null +++ b/docs/specs/ADR-046-current-code-migration-map.md @@ -0,0 +1,1156 @@ +# ADR-046 Current-Code Migration/Disposition Map + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-current-code-migration-map` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | Integrator; current-code migration/disposition map | +| Depends on | None | +| Supersedes | None | +| Foundation | `fd5b0067` — docs: establish d2b 3.0 Provider control-plane foundation | +| Main reuse | Permitted (copy/adapt); main is not current-state evidence | + +--- + +## §0 Purpose and Notation + +This document maps every identifiable current-code symbol, Nix module, generated artifact, +systemd unit, persistent path, CLI command, and test at baseline `b5ddbed6` to its ADR 0046 +disposition. Rows carry evidence class, current callers/tests, the target ResourceType or +controller destination, exact future file paths where fixed, integration work-item cross-reference, +and removal proof requirements. + +**Evidence classes:** + +| Class | Meaning | +|-------|---------| +| `production-reachable` | Live call path in a production binary at `b5ddbed6` | +| `dead-reachable` | Compiled into a production binary but no live call path found | +| `schema-only` | Used exclusively by `xtask` schema generation; not in any production binary | +| `test-only` | Appears only in `#[cfg(test)]` blocks or test binaries | +| `nix-emitted` | Generated by a Nix module; no Rust source equivalent | + +**Disposition codes:** + +| Code | Meaning | +|------|---------| +| `RETAIN` | Kept at current path with at most minor changes | +| `EXTRACT` | Moved to a new module/crate; old import site removed | +| `ADAPT` | Substantial rewrite in-place or at new path | +| `COPY/ADAPT` | Copy from main (`a1cc0b2d`) and adapt to v3 contracts | +| `REPLACE` | Current implementation deleted; new implementation at target | +| `DELETE` | Removed with no target counterpart | + +--- + +## §0.1 Canonical Terminology Mapping + +Evidence/definition columns use **current-baseline names** throughout this document. +Target columns use **ADR 0046 names**. This table is the authoritative translation key. + +| Current-baseline name | ADR 0046 target name | Notes | +|----------------------|---------------------|-------| +| `Realm` / `RealmId` / `RealmPath` | `Zone` / `ZoneId` (label-shaped) | `d2b-realm-core/src/realm.rs`, `ids.rs` | +| `realm controller` / `d2b.realms` | `Zone runtime` / `d2b.zones` | Nix option rename required | +| `realm-controllers.json` | Zone resource entry in `zones.json` | Generated artifact rename | +| `Workload` / `WorkloadId` (realm workload) | `Guest` ResourceType (VM / sandbox / cloud / remote execution parent) | D050 | +| `d2b.vms.` (legacy NixOS VM) | `Guest` ResourceSpec under `d2b.zones.` | VM execution → Guest | +| `d2b.realms..workloads.` (realm-native) | `Guest` / `Process` ResourceSpec under `d2b.zones.` | Classify per runtime.kind | +| `unsafe-local` workload (kind="unsafe-local" in `d2b.realms..workloads.`) | `Host` ResourceType (not Guest; not a separate Provider) reconciled by `Provider/system-core`; `defaultDomain=user`, `allowedDomains=[user]`, `defaultUserRef=User/`; child processes use normal Process Providers | D042 — unsafe-local IS the Host, it does not go inside a Guest | +| `IsolationPosture::UnsafeLocal` / `WorkloadProviderKind::UnsafeLocal` in public surface | Explicit no-isolation posture PRESERVED in `Host` status, CLI/UI warnings, and authoritative audit events only — must NOT be silently dropped in v3; must NOT be carried into OTEL metrics labels, span attributes, or log fields | D042 — no-isolation posture is a visible Host characteristic in status/CLI/audit; not a telemetry dimension | +| Physical local execution parent | `Host` ResourceType | New in ADR 0046; no current counterpart | +| `ProcessRole` / `VmProcessDag` runner | `Process` or `EphemeralProcess` ResourceType | Classify per evidence below | +| `storage.json` / filesystem-view / store-view | `Volume` layout/views/attachments | Where storage lifecycle applies; locks remain mechanisms | +| `NetEnv` (per-env network fabric) | `Network` resource + Host/Guest `networkAttachments` | `d2b-core/src/host.rs:293` | +| `VmRuntimeRow` (one per VM in `HostJson`) | `Guest` ResourceSpec per VM | `d2b-core/src/host.rs:158` | +| `HostJson` top-level fields | `Host` ResourceSpec (physical host) | `d2b-core/src/host.rs:109` | +| `WorkloadProviderKind` enum | **`UnsafeLocal` variant → `Host` ResourceType** (not `Guest.spec.providerRef`); `LocalVm`/`QemuMedia`/`ProviderManaged` variants → `Guest.spec.providerRef` pointing to respective Provider resource | D042/D050; UnsafeLocal selects Host not Guest | +| `WorkloadExecutionPosture` | `ExecutionPolicy` fields shared by Host and Guest | `d2b-realm-core/src/workload.rs:83` | +| Provider traits/adapters/factories | Zone-local `Provider` resource + controller/service/worker Processes | | +| guest-control ttrpc/vsock | current evidence of guestd vsock protocol; target is ComponentSession/d2b-bus | D001 — ComponentSession copies from main `a1cc0b2d` | +| `d2b-realm-router` session types | REPLACE target; current v3 session impl to be removed | Do NOT copy from v3 | +| `AllocatorEngine` / `HostResourceKind` leases | Zone bootstrap resource provisioning (Network/CgroupSubtree) | Currently schema-only | + +--- + +## §0.2 Universal Nix Configuration and Cleanup Contract + +Every work item that assigns or produces a spec (referenced in Work Item columns as `ADR046-*`) +**must** include each of the following sub-requirements in the spec itself. These apply +uniformly to every ResourceType, Provider, and Zone resource introduced in v3. + +### Nix configuration shape + +User Nix authoring mirrors the canonical ResourceSpec schema nearly identically. The single +universal option path is: + +```nix +d2b.zones..resources. = { + type = ""; # e.g. "Guest", "Network", "Volume", "Provider", "Process" + spec = { + # ...exact ResourceType spec fields as they appear in the ResourceTypeSchema... + # Provider-specific fields sit directly inside spec, not in a renamed sub-attrset + providerRef = "Provider/runtime-cloud-hypervisor"; + # example Guest spec field: + networkAttachments = [ "Network/lan" ]; + }; +}; +``` + +Rules: + +- `metadata.name` is derived from the Nix attr key (``); do not repeat it inside the attrset. +- `metadata.zone` is derived from the Zone attr key (``); do not repeat it. +- `apiVersion` is defaulted from the active ResourceTypeSchema version; users do not set it. +- `status` is omitted entirely from Nix; it is read-only and filled by the Zone runtime. Core + fills `uid`, `generation`, `revision`, `createdAt`, `updatedAt`, and all management metadata. +- Provider-specific fields live directly inside `spec.*`, mirroring their position in the + ResourceTypeSchema and Provider schema. No second bespoke Nix vocabulary, no renaming, no + extra nesting layer. +- Generated Nix option types, defaults, and documentation are derived from the same + ResourceTypeSchema and signed Provider schema used for build-time validation; the two sources + must never diverge (enforced by `make test-drift` / `xtask gen-nix-options` + schema drift gate). +- Derivation-valued inputs — Provider binary artifacts, guest NixOS system closures, and other + build derivations — are NOT permitted inside `spec.*`. Use `d2b.artifacts.` (see + §0.2 Artifact catalog below) and reference by `spec.artifactId = ""` (Provider) or + `spec.systemArtifactId = ""` (Guest). Placing a derivation directly in `spec` is a + Nix type error enforced by the generated option type. + +The spec must include all four of: + +1. **Eval-time validation** — The Nix module accepts `d2b.zones..resources.` as a + Nix attrset. At eval time: `type` is a known ResourceType in the Zone's locked + ResourceTypeSchema; every required `spec.*` field is present; `providerRef` is a valid + `Provider/` declared in the Zone; domain, owner, bounds, and conflict rules from the + ResourceTypeSchema are enforced; missing required fields produce actionable eval errors with + source location. + +2. **Build-time JSON validation** — The build renders the canonical JSON ResourceSpec (with + `metadata.name`, `metadata.zone`, `apiVersion` filled in; `status` absent) and validates it + against the committed ResourceTypeSchema (`docs/reference/schemas/v3/.json` pinned by + `xtask gen-schemas`). Provider-specific fields inside `spec` are additionally validated + against the **exact signed Provider schema** artifact from the Zone resource bundle. + `artifactId` and `systemArtifactId` fields are validated against the Nix artifact catalog: + the ID must exist in `d2b.artifacts`, and the catalog `type` must match the expected type for + the field (`spec.artifactId` in a Provider spec requires `type = "provider"`; `spec.systemArtifactId` + in a Guest spec requires `type = "nixos-system"`). Missing ID or type mismatch fails the + NixOS build with an actionable error identifying resource name, field, expected type, and + actual type. Store paths from the artifact catalog must not appear in the rendered + ResourceSpec, status, or audit events. + +3. **Credential refs for secrets** — Any spec field whose value is a secret byte sequence must + be a `Credential/` ResourceRef string. Inline secret bytes in Nix config are a + build-time error enforced by the signed Provider schema (`credentialRef: true` marker on + every secret-typed field in the schema). + +4. **Minimal Nix example + rendered JSON** — The spec includes a working Nix snippet using + `d2b.zones..resources. = { type = "..."; spec = { ... }; }` and the exact + canonical JSON the build renders from it, confirming field names are identical between Nix + and schema. If the spec uses `artifactId` or `systemArtifactId`, the example also shows the + matching `d2b.artifacts. = { package = ...; type = "..."; }` catalog entry and confirms + that the rendered ResourceSpec JSON contains only the bounded ID string, not any store path. + +### Artifact catalog + +Derivation-valued inputs are configured in a **separate named catalog**, never inside a +ResourceSpec. The universal option shape is: + +```nix +d2b.artifacts. = { + package = ; + type = "provider" | "nixos-system" | …; +}; +``` + +Authoring rules: + +- `` is a non-empty DNS-label-safe string, unique across `d2b.artifacts`. Duplicate IDs are + a Nix eval error. +- `type` must be one of the catalog's declared types. An undeclared type is a Nix eval error. +- Provider ResourceSpecs reference the Provider binary with `spec.artifactId = ""` where the + catalog entry has `type = "provider"`. +- Guest ResourceSpecs reference the guest NixOS system closure with + `spec.systemArtifactId = ""` where the catalog entry has `type = "nixos-system"`. +- `Artifact` is **not** a ResourceType. `artifactId` and `systemArtifactId` are plain bounded + ID strings, not ResourceRefs. The `*Ref` suffix must NOT be used for these fields. +- The build includes each derivation in the host closure, hashes it, validates + type/ID/duplicates/trust constraints, and emits a **private integrity-pinned artifact + catalog** installed at `root:d2bd` 0640 under `/etc/d2b/artifact-catalog.json`. The catalog + maps each ID to its `type`, SHA-256 content digest, and closure metadata. +- The artifact catalog is a private `root:d2bd` 0640 file. It contains a `storePath` field per + entry, validated by core at activation. Store paths are private catalog data: they must NOT + appear in any public ResourceSpec field, status field, audit event, log line, metric label, + or span attribute. +- A missing or wrong-type `artifactId` / `systemArtifactId` in a ResourceSpec fails the + NixOS build. The error message identifies the resource name, field name, expected catalog + type, and (if present) the actual catalog type of the referenced ID. + +### Bundle and generation emission + +1. The build collects all configured ResourceSpecs for a Zone, sorts them by canonical key + (`/`), and emits one **integrity-pinned Zone resource bundle** whose generation + ID equals the SHA-256 digest of the sorted canonical JSON of the full configured set. + +2. The build emits one **integrity-pinned Zone resource bundle** per generation — an immutable + artifact of canonical sorted ResourceSpec JSON installed at `root:d2bd` 0640 under + `/bundle/generation-.json`; `current-generation` names the active pointer. + The bundle contains only the plain ResourceSpec fields (type, spec, metadata.name, + metadata.zone, apiVersion). `metadata.managedBy` and `metadata.configurationGeneration` are + **not** in the bundle; core sets them only on persisted records at activation time. + +3. Provider-specific settings travel in a **separate signed artifact** per Provider. The Zone + runtime verifies the signature before activation; the signed artifact covers + `providerRef + provider-specific spec fields + generation`. + +### Cleanup contract + +1. **Configuration-managed vs controller-created vs API-created** — Core sets + `metadata.managedBy` only on persisted activated records, never in the Nix bundle itself: + - `managedBy: configuration` — resource originated from the Nix-authored bundle; subject to + generation-sweep Delete when absent from a new generation. + - `managedBy: controller` — resource created at runtime by an owner controller (dynamic + children, auto-provisioned Network/Volume/Device resources, etc.); **never** deleted solely + because absent from the Nix-configured set; reconciled and deleted by the owning controller + in response to parent resource changes. + - `managedBy: api` — resource created through an explicit API call (e.g. `resource apply`); + persists until an explicit `Delete` request; **never** generation-swept regardless of + whether the resource appears in a Nix bundle. + +2. **Absent-resource async Delete on generation change** — When a new generation activates, + the Zone runtime enqueues an asynchronous `Delete` for every `metadata.managedBy: + configuration` resource present in the prior generation but absent from the new generation. + The Delete follows the normal resource Delete path including finalizer and owner-child safe + deletion: children are deleted before the parent; finalizers are cleared first. + +3. **Non-blocking activation** — New generation activation does not block on cleanup. The Zone + reports `phase: Ready` (all pending cleanup complete) or `phase: Degraded` (cleanup in + progress) and emits condition `{type: PendingCleanup, status: True, reason: PendingCleanup, + message: " resources awaiting deletion"}` until all absent resources complete deletion and + their rows and index entries are removed atomically. Once the final resource is removed the + condition clears and the Zone transitions to `phase: Ready`. `d2b op inspect` surfaces the + degraded-state condition during cleanup. + +4. **Owner-child and finalizer safety** — When a resource is requested for deletion, + `deletionRequestedAt` is set and the resource enters `phase: Pending` (or `phase: Degraded` + if active finalizers or controller-managed children remain). The owning controller is + responsible for reconciling children to deletion in response to `deletionRequestedAt` being + set. The Zone runtime does not force-delete children; it signals the owner controller only. + Once all children and finalizers clear, one store transaction writes the + event-only `Deleted` revision and removes the resource row and index atomically. + `ResourceDeleted` audit is appended afterward from that committed revision with + dedup; there is no persisted `phase: Deleted` row. + +5. **Prior generation retention** — The Zone runtime retains prior generation bundles in the + Zone state store up to the per-Zone configured count (`d2b.zones..retainedGenerations`, + default `3`, range `1..16`). Bundles beyond that count are pruned oldest-first after all + cleanup for their generation is complete. Emergency rollback re-activates any retained prior + generation pointer without reprovisioning: resources that survived deletion are + re-reconciled; already-deleted resources are re-created by reconciliation. + +6. **Status, errors, and audit** — Every resource whose `deletionRequestedAt` is set due to + absence from the new generation must emit: + - A `ResourceDeletionRequested` audit event: fields `resource`, `zone`, `generation_removed`, + `reason: absent-from-configuration`. + - A resource status condition: + `{type: ConfigurationOwned, status: False, reason: AbsentFromGeneration, generation: }`. + - On successful deletion: a `ResourceDeleted` audit event; the resource row and index entry + are removed atomically at this point — there is no intermediate observable state after this + event. + - On deletion failure: `phase: Degraded, conditions: [{type: DeletionComplete, + status: False, reason: }]`; message fields are redacted (no paths, + identifiers, argv, or byte content). + +### Required tests per spec + +Every spec work item must include test coverage for: + +| Test Kind | Required Assertion | +|-----------|-------------------| +| Nix eval | Valid config evaluates without error; invalid `type`/`providerRef`/bounds produce the exact expected error message with source location | +| Build — schema | Rendered JSON passes ResourceTypeSchema validation; provider-specific `spec.*` fields pass signed Provider schema; inline secret bytes fail build | +| Build — bundle | Two identical configs produce identical generation IDs; field-order variation in Nix config does not change generation ID; any field change changes generation ID | +| Cleanup — basic | Remove resource from Nix config, rebuild, activate: `deletionRequestedAt` is set; final store transaction writes the `Deleted` revision and removes row/index atomically; `ResourceDeleted` audit append follows the committed revision with dedup | +| Cleanup — no false delete | Controller-managed dynamic child is NOT deleted when its configuration-managed parent is updated but not removed | +| Cleanup — owner-child safety | Configuration-managed parent with controller-managed children: parent has `deletionRequestedAt` set and stays `phase: Pending` or `phase: Degraded` until owner controller deletes all children; row removed atomically only after children clear | +| Cleanup — non-blocking | New generation activates and routes requests while prior-generation cleanup is in progress; `PendingCleanup: True` condition visible in `d2b op inspect` | +| Cleanup — prior retention | Prior bundle files present on disk up to `retainedGenerations` count; oldest pruned after cleanup complete once count exceeded | +| Rollback | Emergency rollback re-activates any retained prior generation; surviving resources re-reconcile without reprovisioning | +| Audit hygiene | All `ResourceDeletionRequested` and `ResourceDeleted` events present in audit segment; `ResourceDeleted` is the final event for each resource; message fields contain no paths, identifiers, or argv | + +--- + +## §0.3 Provider Crate Layout Requirement + +Every future `packages/d2b-provider--/` crate created for a work item +**must** contain the following four paths. Absence of any path is a workspace/package policy +failure enforced by the existing workspace policy tests. + +``` +packages/d2b-provider--/ +├── src/ # implementation, binaries, and colocated unit tests +├── tests/ # hermetic Cargo integration tests +├── integration/ # heavier test fixtures and scenarios +└── README.md # dossier document +``` + +### `src/` + +Owns the full provider implementation: controller logic, service/worker/binary entry points, +and colocated unit tests (`#[cfg(test)]` modules). All Rust source that belongs exclusively +to this Provider lives here. Sub-modules follow the crate's natural decomposition; no external +crate may reach into `src/` modules that are not part of the crate's declared public API. + +### `tests/` + +Owns **hermetic Cargo integration tests** (files under `tests/*.rs` invoked by `cargo test`). +Per D094 these are fast, in-process, deterministic, and parallel-safe: an individual normal +test has p95 ≤50 ms with no wall-clock sleep, and the crate's whole +`cargo test -p d2b-provider-- --lib --tests` execution (units + `tests/`) +completes in ≤2 s warm-cache execution time (compilation excluded). They use a deterministic +fake clock/RNG and the toolkit fakes only; no process spawn, container, network, DBus, systemd, +broker daemon, Nix eval/build, KVM, hardware, or live cloud, and no filesystem tree beyond tiny +temp fixtures. A test needing any of those is a placement violation and MUST move to +`integration/` — never gain a sleep, larger timeout, or `#[ignore]`. Bounded crypto/property +tests are the only classified exception and declare a capped case count and a higher per-test +budget by name. + +- ResourceType controller conformance tests — assert every declared ResourceType is reconciled + correctly from `spec` to `status` phase transitions. +- Service/worker lifecycle tests — assert each service and worker Process starts, reports + Ready, handles termination signals, and emits correct audit events. +- Provider conformance kit — run `check_provider_conformance` from `d2b-provider-toolkit` + against every `ProviderType` axis the Provider implements; assert zero `ConformanceError`. +- Fault injection — assert correct `phase: Degraded` / `phase: Failed` and condition reporting + when a controller-spawned Process exits unexpectedly or a required resource is unavailable. +- No container daemon, real Host, or real Guest required; mocks and `FakeProvider` fixtures + from `d2b-provider-toolkit` are the hermetic boundary. + +### `integration/` + +Owns **heavier test scenarios** invoked by the existing test orchestration +(`make test-integration` / `make test-host-integration`), not by bare `cargo test`. +Per D094 these may be slower but still carry a lane timeout/budget, run with parallel +isolation, and use fake external services by default; live/hardware runs remain a separate +manual tier. + +- Container-level scenarios: Provider controller running against a real Zone runtime in a + container; resource lifecycle under real broker calls. +- Host/Guest cross-process scenarios: Provider Process spawned as a real child process; + inter-process ComponentSession handshake verified. +- Provider–system interaction fixtures: real `Provider/system-core` or `Provider/system-minijail` + involvement where the Provider depends on it. +- Cleanup contract scenarios: Nix generation change triggers correct async Delete and + audit events for this Provider's ResourceTypes (§0.2 cleanup contract, integration tier). +- Files in `integration/` must not be reachable via `cargo test` alone; they are fixtures + or shell/Nix scripts discovered by the test orchestrator, not `#[test]` functions. + +### `README.md` + +Documents the Provider dossier. Must include all of: + +| Section | Required Content | +|---------|-----------------| +| Provider identity | `Provider/` canonical ResourceRef; `ProviderType` axis (D043–D049); crate path | +| Nix config schema | `d2b.zones..resources.` snippet with this Provider's `spec.*` fields; rendered canonical JSON; Credential ref fields identified | +| ResourceTypes | Table: ResourceType name, lifecycle phases, `owner` value, finalizer contract | +| Controllers/services/workers/binaries | Table: name, binary or library entry point, executionRef (Host or Guest), cgroup placement | +| Placement | Valid Host/Guest combinations; `allowedDomains`; required `providerRef` chain | +| Dependencies and RBAC | Required other Providers; `RoleBinding` resources needed; broker operations used | +| Security and state | Capabilities declared; secrets accessed only via Credential refs; persistent state paths; OFD lock contract | +| Telemetry | Metric labels (`provider`, `component`, `operation`, `outcome`); span attribute hygiene (no secrets/paths/argv); audit event names and fields | +| Build/test/integration commands | Exact `cargo test -p d2b-provider--`, `make test-integration` filter, and `make test-host-integration` filter invocations | +| Standalone-repo future usage | Note that the crate is designed to be extracted to its own repository; cross-crate dependencies must only be on published crates or on the d2b provider SDK subset | + +--- + +## §1 Workspace Crates + +### 1.1 `d2b-core` + +| Symbol / File | Evidence | Current Definition / Callers | Disposition | Target | Work Item | +|--------------|----------|------------------------------|-------------|--------|-----------| +| `host.rs` — `HostJson` top-level fields (hostname, site_policy, ch_config) | production-reachable | Physical host config struct; callers: `bundle_resolver.rs`, `d2b-priv-broker/src/runtime.rs` | ADAPT | `Host` ResourceSpec fields in `d2b-contracts/src/v3/resources/host.rs` | `ADR046-primitives-002` | +| `host.rs` — `HostJson.vm_runtimes: Vec` | production-reachable | One `VmRuntimeRow` per declared VM (vm name, tap, bridge, static_ip, state_dir, env, net_vm); callers: broker TAP ops, bundle_resolver | ADAPT | `Guest` ResourceSpec per VM in Zone resource store; TAP/bridge → Network resource attachment | `ADR046-primitives-002` | +| `host.rs` — `VmRuntimeRow` struct | production-reachable | Per-VM TAP/bridge/IP/state assignment; `d2b-priv-broker/src/ops/tap.rs:1006` | ADAPT | `Guest` ResourceSpec (vm name, state_dir) + `Network` attachment (tap, bridge, static_ip) | `ADR046-primitives-002` | +| `host.rs` — `HostJson.environments: Vec` | production-reachable | Per-env network fabric list; callers: `bundle_resolver.rs`, `net_route_preflight.rs` | ADAPT | `Network` resource per env (bridge, host_uplink_ip, net_uplink_ip, mtu, mss_clamp, LanPolicy) + Host/Guest `networkAttachments` | `ADR046-primitives-002` | +| `host.rs` — `NetEnv` struct | production-reachable | Per-env bridge name, LAN policy, MTU, MSS, USBIP firewall IPs; `d2b-core/src/host.rs:293` | ADAPT | `Network` ResourceSpec (bridge, policy, MTU/MSS clamp) | `ADR046-primitives-002` | +| `host.rs` — `HostQemuMedia` / `QemuMediaSourceIntent` | production-reachable | QEMU media registry/slot config; callers: broker QEMU media ops | ADAPT | Provider-specific config in `Guest.spec` under `runtime-qemu-media` Provider | `ADR046-primitives-002` | +| `bundle_resolver.rs` | production-reachable | Resolves all bundle artifacts from `/etc/d2b/`; primary callers: broker, d2bd init | ADAPT | Zone bootstrap bundle resolution; paths change to Zone-scoped layout | `ADR046-primitives-002` | +| `privileges.rs` | production-reachable | `BrokerOperation` / `BrokerRequest` typed enums (§3 below); callers: broker dispatch | ADAPT | Provider operation catalog; broker-to-provider RPC shape | `ADR046-provider-003` | +| `workload_identity.rs` — `WorkloadIdentity` | production-reachable | `workload_id: WorkloadId`, `realm_id: RealmId`, `workload_name`; used in public wire types | ADAPT | `ResourceRef` in `d2b-contracts/src/v3/identity.rs`; `WorkloadId` → label-shaped `ResourceName` within Zone | `ADR046-identities-001` | +| `workload_identity.rs` — `WorkloadTarget` (`= RealmTarget`) | production-reachable | DNS-label address `..d2b`; callers: CLI, daemon dispatch | ADAPT | `ResourceRef` with Zone path + resource name; canonical address format changes | `ADR046-identities-001` | +| `workload_identity.rs` — `WorkloadBackend` / `LocalVmBackendConfig` / `LocalQemuMediaBackendConfig` / `WorkloadRuntimeIntent` | production-reachable | Per-workload runtime config (backend kind, VM path, QEMU media slot); callers: bundle_resolver | ADAPT | `Guest.spec.providerRef` + Provider-specific runtime config in private bundle | `ADR046-primitives-002` | +| `realm_controller_config.rs` | production-reachable | `RealmControllerConfig` read by realm controller init; allocator socket ref | ADAPT | Zone runtime bootstrap config; realm controller config → Zone runtime config | `ADR046-core-001` | +| `realm_workloads_launcher.rs` | production-reachable | `LauncherWorkloadSummary`, launcher item catalog; callers: `d2b-clipd`, launcher verbs | ADAPT | `Guest` resource launcher items in Provider catalog | `ADR046-provider-001` | +| `host_json.nix` (emitter) | nix-emitted | Emits `/etc/d2b/host.json`; read by broker and d2bd | ADAPT | Zone resource store bootstrap inputs; rename/split per Host/Guest/Network split | `ADR046-primitives-002` | +| All other `d2b-core` modules | production-reachable | Error types, audit, tracing, security checks | RETAIN / ADAPT | Keep in `d2b-core`; update import paths | — | + +### 1.2 `d2bd` + +| Symbol / File | Evidence | Current Definition / Callers | Disposition | Target | Work Item | +|--------------|----------|------------------------------|-------------|--------|-----------| +| `lib.rs` — daemon entry, `AdminRequest` dispatch | production-reachable | `SO_PEERCRED`-gated admin socket; dispatches `ExecOp`, `ShellOp`, `ConsoleOp`, `KeysOp`, `UserdOp` | ADAPT | Zone runtime public socket; add `ResourceOp` dispatch path per resource-API spec | `ADR046-api-001` | +| `realm_stubs.rs` | production-reachable | Thin stubs for `WorkloadProvider` and `ProtocolCodec` (codec for vsock); callers: realm connection path | REPLACE | Provider process worker; codec moves to `d2b-bus` transport | `ADR046-session-001` | +| `workload_target_index.rs` | production-reachable | Index of `WorkloadTarget` → provider route; callers: admin dispatch for workload ops | ADAPT | Zone-local resource index; target becomes `ResourceRef`-keyed | `ADR046-core-001` | +| `realm_access_resolver.rs` | production-reachable | Resolves realm access tokens for workload connection; callers: `d2bd` exec/shell paths | ADAPT | Zone resource authz resolver; token format changes to ComponentSession bootstrap | `ADR046-session-001` | +| `supervisor/` module set | production-reachable | Pidfd supervision of VM/runner processes; `RunnerRole` from `d2b-contracts`; callers: VM lifecycle | ADAPT | `Process`/`EphemeralProcess` lifecycle supervisor in Zone runtime | `ADR046-core-001` | +| `exec_session*.rs` | production-reachable | Detached exec session table; guest-control vsock exec; `SO_PEERCRED` admin gate | RETAIN/ADAPT | Keep semantics; replace vsock transport with ComponentSession bootstrap | `ADR046-session-001` | +| `console_session.rs` | production-reachable | Console stream; callers: `ConsoleOp` handler | ADAPT | Replace with ComponentSession `Console` service | `ADR046-session-001` | +| `shell_handler.rs` (if present) | production-reachable | Shell op handler; delegates to unsafe-local helper via `d2bd/src/unsafe_local_helper.rs`; `DaemonToUnsafeLocalHelper`/`UnsafeLocalHelperToDaemon` protocol; metric label `provider = "unsafe-local"` in current tracing | ADAPT | Route `ShellOp` to user-only `Host` resource under `Provider/system-core`; preserve no-isolation posture in authoritative audit only; remove/redact `provider="unsafe-local"` from OTEL metrics, spans, and logs | `ADR046-primitives-003` | +| All test fixtures (realm JSON stubs) | test-only | Hardcoded `/run/d2b/allocator.sock`, controller configs | UPDATE | Update paths to Zone-scoped layout | — | + +### 1.3 `d2b-realm-core` + +| Symbol / File | Evidence | Current Definition / Callers | Disposition | Target | Work Item | +|--------------|----------|------------------------------|-------------|--------|-----------| +| `ids.rs` — `RealmId` | production-reachable | Label-shaped `^[a-z][a-z0-9-]*$` realm identifier; callers: realm routing, registry, config | EXTRACT/ADAPT | `ZoneId` in `d2b-contracts/src/v3/identity.rs`; rename only | `ADR046-identities-001` | +| `ids.rs` — `WorkloadId` | production-reachable | Label-shaped workload identifier within a realm; callers: `WorkloadIdentity`, `WorkloadTarget` | EXTRACT/ADAPT | `ResourceName` (label-shaped) within Zone-scoped `ResourceRef`; same validation shape | `ADR046-identities-001` | +| `ids.rs` — `NodeId` | production-reachable | Node/host identifier; callers: `NodeSummary`, constellation routing | EXTRACT/ADAPT | Node identifier in `d2b-contracts/src/v3/identity.rs` | `ADR046-identities-001` | +| `ids.rs` — `ProviderId` | production-reachable | Provider identifier; callers: `ProviderRegistryEntry` | EXTRACT/ADAPT | `ResourceName` for Provider resource | `ADR046-identities-001` | +| `ids.rs` — `GatewayId`, `ExecutionId`, `StreamId`, `PrincipalId` | production-reachable | Opaque session/stream identifiers; callers: session routing | EXTRACT/ADAPT | Corresponding types in `d2b-contracts/src/v3/identity.rs` | `ADR046-identities-001` | +| `ids.rs` — `AllocatorLeaseId`, `HostResourceId` | schema-only | Used only in xtask schema generation; no d2bd caller | EXTRACT/ADAPT | Zone resource provisioning lease identifiers; move to `d2b-contracts/src/v3/` | `ADR046-identities-001` | +| `realm.rs` — `RealmPath` | production-reachable | `Vec` path labels for nested realms; callers: realm routing, config | EXTRACT/ADAPT | Zone path in `ZoneRef`; same structural shape, rename | `ADR046-identities-001` | +| `realm.rs` — `EntrypointMode` | production-reachable | Realm controller entrypoint variant; callers: realm config | ADAPT | Zone runtime entrypoint config | `ADR046-core-001` | +| `realm.rs` — `RealmControllerPlacement` | production-reachable | `HostLocal` / remote placement; callers: realm controller config | ADAPT | Zone placement in Zone ResourceSpec; `HostLocal` → Zone runtime on `Host` | `ADR046-core-001` | +| `workload.rs` — `WorkloadProviderKind` | production-reachable | Enum `LocalVm \| QemuMedia \| ProviderManaged \| UnsafeLocal`; callers: `d2b-clipd`, `realm_workloads_launcher.rs`, `d2b-contracts/public_wire.rs` | ADAPT | **`UnsafeLocal` variant → `Host` ResourceSpec** (not Guest) with `defaultDomain=user`, `allowedDomains=[user]`, `defaultUserRef=User/`, reconciled by `Provider/system-core`; `LocalVm`/`QemuMedia`/`ProviderManaged` → `Guest.spec.providerRef` per D050 | `ADR046-primitives-002` | +| `workload.rs` — `IsolationPosture` | production-reachable | Enum `VirtualMachine \| ProviderManaged \| UnsafeLocal`; callers: `WorkloadExecutionPosture`, public wire; test fixture `workload-execution-posture-v1.json`; `IsolationPosture::UnsafeLocal` serializes as `"isolation":"unsafe-local"` | ADAPT | `VirtualMachine`/`ProviderManaged` → Provider-specific isolation field in `Guest.spec`; **`UnsafeLocal` → preserved as explicit no-isolation posture in `Host.status`, `Host` resource conditions, CLI/UI warnings, and authoritative audit events (`isolation:"unsafe-local"`) — must NOT be silently dropped or generalized; must NOT appear as an OTEL metric label, span attribute, or log field** | `ADR046-primitives-002` | +| `workload.rs` — `WorkloadExecutionPosture` | production-reachable | Five posture fields (isolation, environment, display, identity, session_persistence); callers: `WorkloadPublicSummary`, clipd; `unsafe_local_posture_round_trips_as_closed_typed_fields` test (`workload.rs:205`) validates `isolation:"unsafe-local"` round-trip | ADAPT | `ExecutionPolicy` fields shared by `Host` and `Guest` ResourceSpec; **`IsolationPosture::UnsafeLocal` in posture is the primary no-isolation signal: preserved verbatim in `Host` status surface, CLI warning display, and authoritative audit events; callers must not remap it to a generic "unmanaged" label in those surfaces; OTEL metrics, span attributes, and logs must not carry `isolation` or `provider="unsafe-local"` labels** | `ADR046-primitives-002` | +| `workload.rs` — `WorkloadState` | production-reachable | Enum (`Running`, `Stopped`, `Starting`, etc.) used in `WorkloadPublicSummary` / `d2b-contracts/src/public_wire.rs:266` | ADAPT | Maps to `Guest.status.runtimeState` (running/stopped/starting/etc.) and associated `Guest.status.detail` / `Guest.status.conditions`; current enum values become `runtimeState` values, NEVER `status.phase` values; common Resource `status.phase` uses standard Ready/Pending/Degraded/Failed phases only | `ADR046-reconcile-001` | +| `workload.rs` — `WorkloadSummary` | dead-reachable | Used in `WorkloadProvider.list()` trait method and mock; no live production path in d2bd | ADAPT | `Guest.status` (runtimeState + detail + conditions); common `status.phase` is standard Ready/Pending/Degraded/Failed — not workload-specific running/stopped values; handler wiring in Zone runtime required | `ADR046-api-001` | +| `workload.rs` — `WorkloadSelector` | dead-reachable | `All \| ByTarget` selector; callers: mock and conformance only | ADAPT | `ResourceListFilter` in resource-API; no current production caller | `ADR046-api-001` | +| `workload.rs` — `LauncherItemKind` / `LauncherIcon` / `LauncherItemSummary` | production-reachable | Per-workload launcher item metadata; callers: public_wire, clipd, launcher | ADAPT | Provider catalog item in Zone resource store | `ADR046-provider-001` | +| `workload.rs` — `WorkloadPlacement` / `WorkloadPlacementSummary` | schema-only | Used only in xtask schema; no production caller outside xtask | DELETE | Fold into `Guest.spec.providerRef` / provider-specific `Guest.spec.*` fields / `Guest.status` and `ZoneLink` routing; delete old schema after migration to Guest resource model | `ADR046-primitives-002` | +| `registry.rs` — `ProviderRegistryEntry` | schema-only | Used only in xtask schema generation | ADAPT | Provider resource registry entry in Zone store | `ADR046-provider-001` | +| `allocator.rs` — `HostResourceKind` | schema-only | Enum: Bridge, Tap, VethPair, NftablesTable, NftablesPartition, CgroupSubtree, HostFilePartition, NamespaceBoundary; callers: xtask schema only | ADAPT | Zone bootstrap resource provisioning kinds; maps to `Network`/`Device`/`Volume` resource pre-requisites | `ADR046-primitives-002` | +| `allocator_engine.rs` — `AllocatorEngine` | schema-only | Lease decision engine with `HostResourceKind` leases; callers: xtask only; socket at `/run/d2b/allocator.sock` is config-referenced but engine not live in d2bd at `b5ddbed6` | ADAPT | Functionality integrates into fixed core controllers and Zone runtime resource contracts; no separate allocator service or socket; Network/Volume/Device provisioning handled by respective Providers (`Provider/network-local`, `Provider/volume-local`, `Provider/device-*`) | `ADR046-core-001` | +| `allocator.rs` — `AllocatorLease` / `LeaseAllocationRequest` / `LeaseAllocationResult` | schema-only | Lease allocation request/response types; callers: xtask schema only | ADAPT | Zone resource lease protocol types | `ADR046-core-001` | +| `capability.rs` — `Capability` / `CapabilitySet` / `CapabilityNegotiation` | production-reachable | Protocol capability negotiation between realm router and workload; callers: session handshake | ADAPT | ComponentSession capability negotiation; reuse shape from main `a1cc0b2d` | `ADR046-session-001` | +| `route_engine.rs` — `RouteEngine` | production-reachable | Realm router routing decisions; callers: `d2b-realm-router` | REPLACE | Zone-local resource routing in Zone runtime | `ADR046-core-001` | +| `routing.rs` | production-reachable | Route types (`RouteEntry`, `RouteTarget`); callers: route_engine, realm-router | REPLACE | Resource routing in Zone runtime | `ADR046-core-001` | +| `execution.rs` | production-reachable | Execution context types for running workloads | ADAPT | `EphemeralProcess` execution context | `ADR046-primitives-002` | +| `shell.rs` | dead-reachable | Shell session types (persistent shell); callers: mock only for `PersistentShellProvider` | REPLACE | Persistent shell under `Provider/system-core` user-only Host per D042/D051 | `ADR046-primitives-003` | +| `node.rs` — `NodeKind` / `NodeSummary` | production-reachable | Constellation node kinds; callers: router, constellation | ADAPT | Host/Zone resource node summary | `ADR046-identities-001` | +| `frame.rs` / `payload.rs` / `mux.rs` / `stream.rs` | production-reachable | Transport framing types; callers: realm-router, session | REPLACE | ComponentSession transport framing from main `a1cc0b2d` | `ADR046-session-001` | +| `access.rs` | production-reachable | Access token types; callers: realm-router, daemon | REPLACE | ComponentSession bootstrap credentials | `ADR046-session-001` | +| `audit.rs` | production-reachable | Audit event types; callers: broker, daemon | RETAIN/ADAPT | Keep audit types; update realm→Zone field names | `ADR046-identities-001` | +| `token.rs` | production-reachable | Token types; callers: session handshake | REPLACE | ComponentSession handshake tokens from main | `ADR046-session-001` | +| `trace_context.rs` | production-reachable | W3C TraceContext propagation | RETAIN | Keep as-is | — | +| `migration.rs` | production-reachable | Workload state migration helpers | ADAPT | Guest resource state migration | `ADR046-reconcile-001` | +| `enrollment.rs` | production-reachable | Realm enrollment / gateway credential types | ADAPT | Zone gateway enrollment; update realm→Zone | `ADR046-core-001` | +| `identity_store.rs` / `identity_config.rs` | production-reachable | Realm controller identity; callers: realm controller init | ADAPT | Zone runtime identity store | `ADR046-core-001` | + +### 1.4 `d2b-realm-provider` — Trait Definitions + +All trait definitions live in `d2b-realm-provider/src/provider.rs`. +Evidence class applies to **implementations** found outside the trait file and `mock.rs`. + +| Trait | Evidence | Implementations Found | Disposition | Target | Work Item | +|-------|----------|-----------------------|-------------|--------|-----------| +| `WorkloadProvider` | production-reachable | `d2b-provider-aca/src/lib.rs` (`AcaWorkloadProvider`); `d2bd/src/realm_stubs.rs` (stub) | REPLACE | `Provider` controller process behavior; replace stub with typed Resource operation | `ADR046-provider-001` | +| `GuestControlEndpointProvider` | production-reachable | `d2b-provider-aca/src/lib.rs:1310` | REPLACE | ComponentSession server-side bootstrap in Provider Process worker | `ADR046-session-001` | +| `HostSubstrateProvider` | production-reachable | `d2b-host-providers/src/lib.rs:16` | REPLACE | `Provider/system-core` Host substrate provisioning | `ADR046-primitives-003` | +| `RuntimeProvider` | production-reachable | `d2b-host-providers/src/lib.rs:16` | REPLACE | Guest execution lifecycle in `Provider/runtime-cloud-hypervisor` controller | `ADR046-provider-001` | +| `DisplayProvider` | production-reachable | `d2b-host-providers/src/lib.rs:16` | REPLACE | Wayland cross-domain display in `Provider/display-wayland`; GPU device resource in `Provider/device-gpu` | `ADR046-provider-001` | +| `TransportProvider` | production-reachable | `d2b-provider-relay/src/lib.rs:446` (`AzureRelayTransportProvider implements TransportProvider`) | REPLACE | Zone-link transport Provider; Azure Relay → `Provider/transport-azure-relay` | `ADR046-provider-001` | +| `TransportListener` | production-reachable | `d2b-realm-transport` implements it | REPLACE | Zone-link inbound listener in `Provider/transport-vsock` or `Provider/transport-unix` | `ADR046-provider-001` | +| `ProtocolCodec` | production-reachable | `d2b-realm-codec-protobuf` implements it; `d2bd/src/realm_stubs.rs` stub | REPLACE | ComponentSession codec; replace with d2b-bus framing from main | `ADR046-session-001` | +| `StreamMux` | production-reachable | `d2b-realm-router` session mux | REPLACE | ComponentSession stream multiplexing from main `a1cc0b2d` | `ADR046-session-001` | +| `DaemonAccessTransport` / `DaemonAccessApi` | production-reachable | `d2b-daemon-access/src/lib.rs:869` implements `DaemonAccessApi` | ADAPT | Zone runtime client access API; reuse shape, update to ResourceOp dispatch | `ADR046-api-001` | +| `PersistentShellProvider` | dead-reachable | **Mock only** in `d2b-realm-provider/src/mock.rs`; no other implementation | REPLACE | Persistent shell under `Provider/system-core` user-only Host per D042/D051 | `ADR046-primitives-003` | +| `DurableExecutionProvider` | dead-reachable | No live implementation outside mock | DELETE | Replaced by `EphemeralProcess` ResourceType + resource/controller API; no separate Provider family; delete trait file after `EphemeralProcess` controller is operational | `ADR046-provider-001` | +| `CredentialProvider` | dead-reachable | No live implementation outside mock | DELETE | Replaced by frozen Credential Provider families `Provider/credential-secret-service`, `Provider/credential-entra`, `Provider/credential-managed-identity` (D048); delete old trait after Provider/credential-* migration; not system-core | `ADR046-provider-001` | +| `ObservabilitySinkProvider` | dead-reachable | No live implementation outside mock | DELETE | Replaced by `Provider/observability-otel` (D049); not Zone runtime; delete trait after `Provider/observability-otel` is operational | `ADR046-provider-001` | +| `InfrastructureProvider` | dead-reachable | No live implementation outside mock | DELETE | Semantics map to Guest runtime Providers, principally `Provider/runtime-azure-virtual-machine`; delete trait after Guest runtime Provider mapping is complete | `ADR046-provider-001` | +| `NodeProvider` | dead-reachable | No live implementation outside mock | DELETE | Node discovery/status handled by `Host`/`Guest`/`ZoneLink` resource status and their controllers; no separate Provider family; delete trait | `ADR046-core-001` | +| `RelayProvider` (d2b-realm-provider trait) | dead-reachable | **No implementation found**; `AzureRelayTransportProvider` implements `TransportProvider`, not `RelayProvider` | DELETE | Dead trait; relay behavior covered by `TransportProvider` | — | + +### 1.5 Provider Crates + +| Crate / Symbol | Evidence | Current Definition / Callers | Disposition | Target | Work Item | +|---------------|----------|------------------------------|-------------|--------|-----------| +| `d2b-provider-aca` — `AcaWorkloadProvider` + `GuestControlEndpointProvider impl` | production-reachable | ACA-managed VM lifecycle; guest-control vsock connect; callers: realm connection path | REPLACE | `Provider/runtime-azure-container-apps` controller Process; ComponentSession replaces vsock direct; old vsock protocol inert at d2b 3.0 cutover, no compatibility window | `ADR046-session-001` | +| `d2b-provider-aca` — `AcaRelayTransportConfig` | production-reachable | ACA relay transport config | ADAPT | ZoneLink transport config under `Provider/runtime-azure-container-apps` | `ADR046-provider-001` | +| `d2b-provider-relay` — `AzureRelayTransportProvider` | production-reachable | Implements `TransportProvider` for Azure Relay; callers: realm relay path | REPLACE | `Provider/transport-azure-relay` Zone-link transport Process | `ADR046-provider-001` | +| `d2b-host-providers` | production-reachable | Thin adapter: `HostSubstrateProvider`, `RuntimeProvider`, `DisplayProvider`; caller: d2bd host-local setup | REPLACE | `Provider/system-core` (substrate), `Provider/runtime-cloud-hypervisor` (VM execution lifecycle), and `Provider/display-wayland` (Wayland cross-domain) controllers | `ADR046-primitives-003` | +| `d2b-daemon-access` | production-reachable | `DaemonAccessApi` implementation; used by d2b CLI and external clients to reach d2bd | ADAPT | Zone runtime access client; update to ResourceOp dispatch | `ADR046-api-001` | +| `d2b-realm-codec-protobuf` | production-reachable | Protobuf `ProtocolCodec` impl; callers: realm-router codec path | REPLACE | ComponentSession wire codec from main `a1cc0b2d` | `ADR046-session-001` | + +### 1.6 `d2b-realm-router` — Session Types (REPLACE all) + +All session types in `d2b-realm-router/src/` are the **current v3 session implementation**. +They are the replacement target, not a reuse source. ComponentSession copies from main `a1cc0b2d`. + +| File | Evidence | Disposition | Removal Proof | +|------|----------|-------------|---------------| +| `session.rs` | production-reachable | REPLACE | Remove after ComponentSession passes `ADR046-session-001` integration test | +| `secure_session.rs` | production-reachable | REPLACE | Same | +| `mux_session.rs` | production-reachable | REPLACE | Same | +| `session_lifecycle.rs` | production-reachable | REPLACE | Same | +| `router.rs` / `realm_router.rs` | production-reachable | REPLACE | Zone runtime replaces realm router; remove after `ADR046-core-001` | + +### 1.7 `d2b-realm-transport` + +| Symbol | Evidence | Disposition | Target | Work Item | +|--------|----------|-------------|--------|-----------| +| `TransportListener` impl | production-reachable | REPLACE | Zone-link inbound socket in `Provider/transport-vsock` or `Provider/transport-unix` | `ADR046-provider-001` | +| Other transport types | production-reachable | REPLACE | ComponentSession transport framing from main | `ADR046-session-001` | + +### 1.8 `d2b-contracts` + +| Symbol / File | Evidence | Current Definition / Callers | Disposition | Target | Work Item | +|--------------|----------|------------------------------|-------------|--------|-----------| +| `public_wire.rs` — `WorkloadOp` / `WorkloadOpResponse` / `WorkloadPublicSummary` / `WorkloadListResult` | dead-reachable | Wire types exist; **no `WorkloadOp` handler found in d2bd** at `b5ddbed6` | DELETE | Deleted at clean v3 cutover; no compatibility window; `ResourceOp{list,get,watch}` for `Guest` resource type replaces; removal proof: handler-wire integration test passes via `ResourceOp` path | `ADR046-api-001` | +| `public_wire.rs` — `ShellOp` / `ShellOpResponse` / `ShellListResult` / `ShellName` | production-reachable | Callers: `d2bd/src/wire.rs:720` | ADAPT | Shell operations under `Provider/system-core` user-only Host per D042 | `ADR046-primitives-003` | +| `public_wire.rs` — `ConsoleProviderKind` / `ConsoleReadOutputResult` | production-reachable | Callers: `d2bd/src/console_session.rs:30` | ADAPT | Console service under Provider Process | `ADR046-provider-001` | +| `public_wire.rs` — `ExecOp` / `ExecOpResponse` | production-reachable | Guest-control exec; callers: d2bd exec_session | ADAPT | ComponentSession `Exec` service bootstrap | `ADR046-session-001` | +| `broker_wire.rs` — `RunnerRole` / `ChildExitStatus` / `ChildReapedNotification` | production-reachable | Callers: `d2bd/src/supervisor/` | ADAPT | Process lifecycle protocol in Zone runtime; `RunnerRole` → ProcessRole classification below | `ADR046-core-001` | +| `provider_registry_v2.rs` | production-reachable | Current provider registry (canonical IDs and opaque bundle intents); callers: Nix bundle emitter, broker | ADAPT | Zone-local Provider registry; extend for frozen Provider family per D043–D049 | `ADR046-provider-001` | + +### 1.9 `d2b-state` + +| Symbol | Evidence | Disposition | Target | Work Item | +|--------|----------|-------------|--------|-----------| +| Atomic state / OFD locks / leases | production-reachable | RETAIN/ADAPT | Zone resource store state backend; D006 embeds redb store | `ADR046-store-001` | +| Audit segment writer | production-reachable | RETAIN | Keep audit segment append; update Zone-scoped paths | `ADR046-core-001` | + +### 1.10 `d2b-guestd` / `d2b-userd` + +| Symbol / Binary | Evidence | Current Definition / Callers | Disposition | Target | Work Item | +|----------------|----------|------------------------------|-------------|--------|-----------| +| `d2b-guestd` — PAM login, workload user exec, `ExecOp` handler | production-reachable | Guest-control ttrpc/vsock server; callers: d2bd exec_session via guest-control | REPLACE | ComponentSession `Exec` service inside Guest; vsock transport → ComponentSession bootstrap | `ADR046-session-001` | +| `d2b-guestd` — detached runner / unit lifecycle | production-reachable | Detached exec jobs under transient user scope | ADAPT | `EphemeralProcess` worker inside Guest; keep PAM login semantics | `ADR046-primitives-002` | +| `d2b-userd` — `main.rs` exits 78 for service mode; `lib.rs`: `UserExecSession` trait (attach/resize), `UserSocketPolicy`/`UserdTransport`, `UserdConfig`, `UserSessionIdentity`, `UserAttachRequest`, `UserOutputCursor`, `UserdError`; `USERD_LISTENS_ON_VSOCK = false` | production-reachable | Main exits 78 (`"service mode is not implemented"`); lib defines user exec/session traits consumed by d2bd attach path; callers: d2bd user attach handler | REPLACE | Fixed user supervisor Process (`Provider/system-systemd` user domain) + `Provider/system-systemd` user Process effects; remove public `d2b userd` verb and stub after parity | `ADR046-primitives-003` | + +### 1.11 `d2b-clipd` + +| Symbol | Evidence | Current Callers | Disposition | Target | Work Item | +|--------|----------|-----------------|-------------|--------|-----------| +| `WorkloadProviderKind` consumer | production-reachable | `d2b-clipd` reads launcher catalog; uses `WorkloadProviderKind` to filter items | ADAPT | Filter by `Guest.spec.providerRef` value | `ADR046-provider-001` | +| Clipboard bridge (Wayland) | production-reachable | Per-workload clipboard mediation | ADAPT | `Provider/clipboard-wayland` Process worker; keep mediation logic | `ADR046-provider-001` | + +### 1.12 Out-of-Workspace Binaries + +These crates have separate lockfiles and are excluded from the main `packages/Cargo.toml` workspace. + +| Crate / Binary | Evidence | Current Function | Disposition | Target | Work Item | +|---------------|----------|-----------------|-------------|--------|-----------| +| `d2b-priv-broker` | production-reachable | Privileged broker: TAP, cgroup, nftables, swtpm, store_view_farm, virtiofsd, QEMU media, activation; callers: d2bd via `BrokerRequest` socket | ADAPT | Zone-local privileged broker; update `BrokerOperation` catalog per §3; TAP/bridge → Network provisioning | `ADR046-provider-003` | +| `d2b-unsafe-local-helper` | production-reachable | Unsafe-local helper binary; `ScopeRuntime`, `HelperSnapshot`, `HelperLaunchRequest`; protocol: `DaemonToUnsafeLocalHelper`/`UnsafeLocalHelperToDaemon` (`d2b-contracts/src/unsafe_local_wire.rs`); scope ledger at `~/.local/state/d2b/unsafe-local-scopes.json`; per-user socket; callers: `d2bd/src/unsafe_local_helper.rs` | ADAPT | **Remains the runtime agent for user-only `Host` resource child processes**; child process sessions become `Process` resources under normal Process Providers; no-isolation posture (`isolation:"unsafe-local"`) preserved in authoritative audit events and `Host.status` only; `provider="unsafe-local"` in current OTEL tracing is removed/redacted — must NOT appear in v3 OTEL metrics, span attributes, or logs; protocol wire types in `unsafe_local_wire.rs` adapt for ComponentSession handshake | `ADR046-primitives-003` | +| `d2b-guest-shell-runner` | production-reachable | Static guest helper for persistent shell feasibility; callers: unsafe-local shell path in d2bd | REPLACE | Persistent shell becomes a `Process` child of user-only `Host`; uses normal Process Providers; no-isolation warning required in CLI output and audit; no Guest involvement | `ADR046-primitives-003` | + +--- + +## §2 `ProcessRole` Dispatch Table + +Current definition: `d2b-contracts/src/broker_wire.rs` `RunnerRole` + +`d2b-core/src/privileges.rs` `ProcessRole` (two related enums; `ProcessRole` is the broker-side +catalog; `RunnerRole` is the supervisor-side enum). + +`ProcessRole` variants from `d2b-core/src/privileges.rs`: + +| ProcessRole Variant | Evidence | Current Callers | ADR 0046 Target | ResourceType / Provider | +|--------------------|----------|-----------------|-----------------|------------------------| +| `CloudHypervisor` | production-reachable | Broker `SpawnRunner`; d2bd VM lifecycle | `Process` (executionRef → `Guest`) | `Provider/runtime-cloud-hypervisor` controller spawns it | +| `Virtiofsd` (ro-store share) | production-reachable | Broker virtiofsd spawn; minijail profile `virtiofsdProfiles` | `Process` (executionRef → `Guest`) | `Provider/system-minijail` bootstrap per D051 | +| `Virtiofsd` (gctl share) | production-reachable | Broker virtiofsd spawn; `d2b--gctlfs` principal | `Process` (executionRef → `Guest`) | `Provider/system-minijail` bootstrap per D051 | +| `Swtpm` | production-reachable | Broker `SpawnRunner{role: Swtpm}`; swtpm_dir provisioning | `Process` (executionRef → `Guest`) | `Provider/runtime-cloud-hypervisor` | +| `CloudHypervisorGpu` | production-reachable | Graphics VM with GPU device attached; broker spawn | `Process` (executionRef → `Guest`) | `Provider/runtime-cloud-hypervisor` + GPU device resource in `Provider/device-gpu` | +| `VhostUserVideo` | production-reachable | Video sidecar; broker `SpawnRunner{role: Video}` | `Process` (executionRef → `Guest`) | `Provider/device-gpu` | +| `VhostUserSound` | production-reachable | Audio sidecar; broker `SpawnRunner{role: Sound}` | `Process` (executionRef → `Guest`) | `Provider/audio-pipewire` | +| `UsbipProxy` | production-reachable | USBIP attach/detach; broker `UsbipBindFirewallRule` + spawn | `EphemeralProcess` (executionRef → `Host`) | `Provider/device-usbip` | +| `NetVm` (sys-*) | production-reachable | Auto-declared net VM runner; broker spawn | `Process` (executionRef → `Guest`) | `Provider/runtime-cloud-hypervisor` | +| `NetRunner` | production-reachable | Per-env tap/bridge/nft setup runner | `EphemeralProcess` (executionRef → `Host`) | `Provider/system-core` | +| `StoreSync` | production-reachable | Per-VM `/nix/store` hardlink farm sync; broker `store_view_farm` op | `EphemeralProcess` (executionRef → `Host`) | `Provider/system-core` | +| `ActivationHelper` | production-reachable | Per-host activation; broker `HostPrepare` | `EphemeralProcess` (executionRef → `Host`) | `Provider/system-core` | +| `GuestControlForwarder` | production-reachable | Guest-control vsock bridge | REPLACE | ComponentSession guest-control bootstrap replaces this role; old vsock protocol inert at d2b 3.0 cutover, no compatibility window | `ADR046-session-001` | +| `RealmController` | production-reachable | Per-realm controller process; d2bd spawns via broker | `Process` (Zone runtime controller) | Zone runtime bootstrap | +| `RealmBroker` | production-reachable | Per-realm broker process; socket-activated PID1 unit at `b5ddbed6` → REPLACE with parent-spawned Zone process per ADR 0046 | `Process` (Zone broker) | Zone runtime bootstrap | +| `KeyRotation` | production-reachable | SSH key rotation; broker `KeyRotation` op | `EphemeralProcess` (executionRef → `Host`) | `Provider/system-core` | +| `QemuMediaRunner` | production-reachable | QEMU media attach/detach process | `EphemeralProcess` (executionRef → `Guest`) | `Provider/runtime-qemu-media` | + +--- + +## §3 Broker Operations Table + +Current definition: `d2b-core/src/privileges.rs` `BrokerOperation` / `BrokerRequest` enums. +Broker socket: `/run/d2b/broker.sock` (local root); per-realm: `/run/d2b/r-/broker.sock`. + +| BrokerOperation / BrokerRequest Variant | Evidence | Current Callers | ADR 0046 Target Operation | Target Provider | +|----------------------------------------|----------|-----------------|--------------------------|-----------------| +| `HostPrepare` (nftables, bridges, cgroup, NM unmanaged) | production-reachable | d2bd init, `make test-policy` | `Host` resource initialization in Zone bootstrap | `Provider/system-core` | +| `SpawnRunner{role: CloudHypervisor}` | production-reachable | d2bd VM start | Spawn `Process` with `executionRef: Guest/` | `Provider/runtime-cloud-hypervisor` | +| `SpawnRunner{role: Virtiofsd}` | production-reachable | d2bd VM start (ro-store, gctl shares) | Spawn `Process` via `Provider/system-minijail` bootstrap | `Provider/system-minijail` | +| `SpawnRunner{role: Swtpm}` | production-reachable | d2bd VM start | Spawn `Process` with `executionRef: Guest/` | `Provider/runtime-cloud-hypervisor` | +| `SpawnRunner{role: Video}` | production-reachable | d2bd graphics VM start | Spawn `Process` with `executionRef: Guest/` | `Provider/device-gpu` | +| `SpawnRunner{role: Sound}` | production-reachable | d2bd audio VM start | Spawn `Process` with `executionRef: Guest/` | `Provider/audio-pipewire` | +| `SpawnRunner{role: RealmController}` | production-reachable | d2bd realm start | Spawn Zone runtime controller `Process` | Zone bootstrap | +| `SpawnRunner{role: RealmBroker}` | production-reachable | d2bd realm start | Spawn Zone broker `Process` (parent-spawned per ADR 0046 target) | Zone bootstrap | +| `store_view_farm` / `StoreSync` | production-reachable | d2bd VM start; `VmRuntimeRow.state_dir` | Provision `Volume` (hardlink farm); sync on demand | `Provider/system-core` | +| `TapBridgeAllocate` | production-reachable | d2bd network setup; `VmRuntimeRow.tap` | Allocate `Network` resource (tap/bridge) for Guest | `Provider/system-core` | +| `TapBridgeFree` | production-reachable | d2bd network teardown | Release `Network` resource | `Provider/system-core` | +| `NftablesPartition` | production-reachable | Per-env nftables rules; `NetEnv` fields | `Network` resource policy; nftables managed zone | `Provider/system-core` | +| `CgroupSubtree` | production-reachable | Per-VM cgroup leaf; `d2b.slice///` | Zone cgroup delegation in `Host` resource | `Provider/system-core` | +| `SwtpmDir` | production-reachable | Per-VM swtpm state provisioning; marker at `/var/lib/d2b/swtpm-markers/` | `Volume` resource (swtpm state) per Guest | `Provider/runtime-cloud-hypervisor` | +| `KeyRotation` | production-reachable | `d2b keys rotate` CLI verb; broker SSH key gen | `EphemeralProcess` invoked by `Provider/system-core` | `Provider/system-core` | +| `UsbipBindFirewallRule` | production-reachable | USBIP attach; broker firewall carve-out | `Device` resource (USBIP attachment) + Network policy | `Provider/device-usbip` | +| `OpenPidfd` | production-reachable | Broker returns pidfd after spawn; d2bd adopts | Pidfd is mandatory Process Provider behavior (D022); no ResourceSpec field | Zone runtime | +| `QemuMediaRegistry` | production-reachable | QEMU media registry write; udev rule | `Device` resource (QEMU media slot) | `Provider/runtime-qemu-media` | +| `ActivationHelper` | production-reachable | d2bd activation; `host-activation.nix` | Activation as `EphemeralProcess` in `Provider/system-core` | `Provider/system-core` | +| `HostShutdown` | production-reachable | `ExecStop` shutdown hook (uid 0, `HostShutdown` role) | Shutdown `EphemeralProcess`; role-scoped narrow permission retained | Zone runtime | + +--- + +## §4 Nix Modules and Generated Artifacts + +> **§0.2 applies to every module listed here.** Each work item for a renamed or new option +> must include eval-time validation, build-time JSON schema validation against the committed +> ResourceTypeSchema and signed Provider schema, Credential refs for secrets, bundle/generation +> emission, and the full cleanup contract with required tests. + +### 4.1 Core NixOS Modules + +| Module / Option | Evidence | Current Function | Disposition | Target Option / Artifact | Work Item | +|----------------|----------|-----------------|-------------|--------------------------|-----------| +| `options-realms.nix` — `options.d2b.realms` | nix-emitted | Realm declaration schema; attr set of realm configs | ADAPT | **`d2b.zones`** option; direct rename + schema update; add `retainedGenerations` sub-option (default `3`, range `1..16`) for cleanup contract | `ADR046-identities-002` | +| `options-realms-workloads.nix` — `d2b.realms..workloads.` | nix-emitted | Realm-native workload declaration (runtime.kind: nixos/qemu-media); wraps legacy `d2b.vms.` | ADAPT | Flat `d2b.zones..resources. = { type = "Guest"; spec = { ... }; }` (or `type = "Process"`) declarations; guest NixOS system closure moves to `d2b.artifacts.` (`type = "nixos-system"`); Guest spec uses `spec.systemArtifactId = ""`; any inline `nixosSystem`/derivation field inside `spec` is removed | `ADR046-identities-002` | +| `options-realms-workloads.nix` — `vmsRef` link to `d2b.vms.` | nix-emitted | Compatibility bridge: realm workload wraps legacy VM entry | DELETE | Remove after `d2b.vms` entries migrate to Zone Guest resources | `ADR046-identities-002` | +| `options.nix` / `options-*.nix` — `d2b.vms.` | nix-emitted | Legacy NixOS VM declaration; many fields per-VM | ADAPT | Flat `d2b.zones..resources. = { type = "Guest"; spec = { ... }; }` under the target zone; direct rename migration; NixOS system closure moves to `d2b.artifacts.` (`type = "nixos-system"`); Guest spec uses `spec.systemArtifactId = ""`; any inline derivation field inside `spec` is removed | `ADR046-identities-002` | +| `options.nix` — `d2b.site.*` | nix-emitted | Site-wide policy options | RETAIN/ADAPT | `Host` ResourceSpec site policy | `ADR046-primitives-002` | +| `default.nix` — module aggregator | nix-emitted | Imports all nixos-modules; wires options to activation | ADAPT | Update imports for renamed modules and Zone layout | `ADR046-identities-002` | +| `assertions.nix` | nix-emitted | Eval-time invariants: CIDR overlap, platform gate, VM-name regex | RETAIN/ADAPT | Update realm→Zone references; add Zone ResourceSpec invariants | `ADR046-identities-002` | +| `index.nix` — `cfg._index.realms.*` | nix-emitted | Internal normalized realm/workload/env index; callers: many modules | ADAPT | `cfg._index.zones.*` — rename all internal index keys | `ADR046-identities-002` | +| `network.nix` / `net.nix` | nix-emitted | Per-env bridges + auto-declared net VM; `lib.mkForce` neutralization | RETAIN/ADAPT | Update `NetEnv` → `Network` resource wiring | `ADR046-primitives-002` | +| `store.nix` | nix-emitted | Per-VM `/nix/store` hardlink farm; `virtiofsd --shared-dir` | RETAIN/ADAPT | `Volume` resource (store-view farm) per Guest; keep security semantics | `ADR046-primitives-002` | +| `manifest.nix` | nix-emitted | `manifestVersion`-pinned JSON manifest; emits `/etc/d2b/manifest.json` | RETAIN | Keep manifest contract; bump `manifestVersion` on field changes | — | +| `realm-controller-config-json.nix` | nix-emitted | Emits `realm-controllers.json` for each enabled realm; includes allocator socket ref | ADAPT | Emit `zones.json` Zone resource entries; rename artifact | `ADR046-identities-002` | +| `allocator-json.nix` | nix-emitted | Emits `/etc/d2b/allocator.json` (`rootSocket`, `configPath`) | DELETE | No separate allocator service; remove artifact; provisioning integrates into fixed core controllers and Zone runtime resource contracts | `ADR046-core-001` | +| `host-broker.nix` | nix-emitted | Declares local-root broker socket+service units; per-realm socket units (`d2b-r--broker.socket`) as socket-activated PID1 units at `b5ddbed6` | ADAPT | Per-Zone socket units → parent-spawned Zone broker `Process`; per ADR 0046 D006 remove per-realm PID1 socket units; keep local-root `d2b-priv-broker.socket` only | `ADR046-core-001` | +| `host-controller.nix` | nix-emitted | Declares `d2bd.socket`, `d2bd.service` (local-root controller) | RETAIN/ADAPT | Keep local-root `d2bd.service`; update to Zone runtime entrypoint | `ADR046-core-001` | +| `host-activation.nix` | nix-emitted | Activation helper setup; key generation; polkit rules | RETAIN/ADAPT | Keep activation; update to Zone resource model | `ADR046-primitives-003` | +| `host-users.nix` | nix-emitted | `d2b` group; per-VM users (`d2b--*`); polkit surface | RETAIN/ADAPT | Keep `d2b` group authz surface; per-Guest principal generation | `ADR046-primitives-002` | +| `host-keys.nix` | nix-emitted | Framework-owned SSH keys `${keysDir}/_ed25519` | RETAIN/ADAPT | Per-Guest key in Zone key store | — | +| `bundle-artifacts.nix` | nix-emitted | Central bundle artifact install table (`root:d2bd` 0640) | RETAIN/ADAPT | Add Zone resource bundle artifacts; keep install semantics | `ADR046-primitives-002` | +| `processes-json.nix` | nix-emitted | Emits `processes.json` (per-VM `ProcessRole` spawn config) | ADAPT | Zone Process resource spawn config | `ADR046-primitives-002` | +| `storage.json` Nix emitter | nix-emitted | Per-VM storage lifecycle contract (`ADR 0034`) | ADAPT | `Volume` resource lifecycle config per Guest | `ADR046-primitives-002` | +| `unsafe-local-helper.nix` / `unsafe-local-workloads-json.nix` | nix-emitted | `unsafe-local-workloads-json.nix` filters `workload.kind == "unsafe-local"` and emits `unsafe-local-workloads.json` with `runtimeKind="unsafe-local"`, `providerId="unsafe-local"`, private argv and shell policy; `unsafe-local-helper.nix` builds the helper binary | ADAPT | **Emit user-only `Host` ResourceSpec** with `defaultDomain=user`, `allowedDomains=[user]`, `defaultUserRef=User/`; private argv/shell policy stays in integrity-pinned private bundle; keep no-isolation posture field explicit in emitted spec | `ADR046-primitives-003` | +| `components/graphics.nix` | nix-emitted | Graphics VM virtio-gpu + Wayland cross-domain | RETAIN/ADAPT | Graphics Provider config in Guest ResourceSpec | `ADR046-provider-001` | +| `components/tpm.nix` | nix-emitted | Per-VM swtpm 2.0 | RETAIN/ADAPT | TPM Device resource per Guest | `ADR046-primitives-002` | +| `components/usbip.nix` | nix-emitted | USBIP passthrough eval-time gating | RETAIN/ADAPT | `Device` resource (USBIP) per Guest/Host | `ADR046-primitives-002` | +| `components/audio/{guest,host}.nix` | nix-emitted | vhost-user-sound + PipeWire mediation | RETAIN/ADAPT | Audio Device/Provider resource | `ADR046-provider-001` | +| `components/home-manager.nix` | nix-emitted | HM-as-NixOS-module inside guest | RETAIN | Guest NixOS config; no framework changes | — | +| `minijail-profiles.nix` | nix-emitted | virtiofsd minijail profiles; `requiresStartRoot = false`; user-NS mapping | RETAIN/ADAPT | `Provider/system-minijail` bootstrap profiles per D051; keep zero-cap semantics | `ADR046-primitives-003` | + +### 4.2 Generated JSON Artifacts (at `/etc/d2b/`) + +| Artifact | Emitter | ADR 0046 Disposition | Notes | +|----------|---------|---------------------|-------| +| `host.json` | `host-json.nix` | ADAPT → split into `host.json` (Host ResourceSpec) + `zones//zone.json` | VmRuntimeRow → Guest entry per Zone | +| `realm-controllers.json` | `realm-controller-config-json.nix` | RENAME/ADAPT → `zones.json` | RealmPath → ZoneId in all entries | +| `allocator.json` | `allocator-json.nix` | DELETE — no separate allocator service | No allocator socket; provisioning integrates into core controllers | +| `processes.json` | `processes-json.nix` | ADAPT → Zone-scoped Process resource specs | ProcessRole → ResourceSpec field | +| `storage.json` | storage Nix emitter | ADAPT → Volume resource specs per Guest | Keep ADR 0034 lifecycle semantics | +| `manifest.json` | `manifest.nix` | RETAIN; bump `manifestVersion` | Schema + emitter move together | +| `provider-registry-v2.json` | `provider-registry-v2-json.nix` | ADAPT → extend for frozen Provider family | D043–D049 frozen catalog | +| `privileges.json` | privileges emitter | ADAPT → BrokerOperation catalog update | Per §3 target operations | +| `ui-colors.json` / `ui-colors.css` | `ui-colors.nix` / `niri-vm-borders.nix` | RETAIN | Not affected by Zone rename | +| `unsafe-local-workloads.json` | `unsafe-local-workloads-json.nix` | ADAPT → user-only `Host` ResourceSpec; `defaultDomain=user`, `allowedDomains=[user]`, `defaultUserRef=User/`; no-isolation posture field explicit | D042 | +| `closures.json` | `closures-json.nix` | RETAIN | Per-Guest Nix closure inventory | +| `minijail-profiles.json` | `minijail-profiles.nix` | ADAPT → Provider/system-minijail profiles | D051 | +| `zones//bundle/generation-.json` | new Zone bundle emitter | NEW — integrity-pinned Zone resource bundle; canonical sorted ResourceSpec JSON (type, spec, metadata.name, metadata.zone, apiVersion only); generation ID = SHA-256 of sorted canonical JSON; `metadata.managedBy` and `metadata.configurationGeneration` are NOT in the bundle — core sets them on persisted records at activation | §0.2 bundle/generation emission contract | +| `zones//bundle/current-generation` | new Zone bundle emitter | NEW — active generation pointer (symlink or ID file) | §0.2 cleanup contract; prior bundles retained up to `retainedGenerations` count (default 3, range 1..16) | +| `zones//providers//settings-.json` | new Provider settings emitter | NEW — signed provider-specific settings artifact per Provider per generation; covers `providerRef + provider-specific spec fields + generation`; validated by Zone runtime before activation | §0.2 signed Provider schema validation | +| `/etc/d2b/artifact-catalog.json` | new artifact catalog emitter | NEW — private integrity-pinned artifact catalog; maps each `d2b.artifacts.` to `type`, SHA-256 content digest, `storePath`, and closure metadata; installed `root:d2bd` 0640 by Nix activation; `storePath` is private catalog data — must NOT appear in any public ResourceSpec field, status field, audit event, log line, metric label, or span attribute | §0.2 artifact catalog | + +--- + +## §5 Systemd Units + +### 5.1 Fixed Local-Root Units (PID1-owned) + +These four units remain PID1-owned through ADR 0046. + +| Unit | Current Function | Disposition | +|------|-----------------|-------------| +| `d2bd.socket` | Local-root public socket activation | RETAIN (rename if Zone runtime renames daemon binary) | +| `d2bd.service` | Local-root Zone runtime controller | RETAIN/ADAPT | +| `d2b-priv-broker.socket` | Local-root broker socket activation | RETAIN | +| `d2b-priv-broker.service` | Local-root privileged broker | RETAIN/ADAPT | + +### 5.2 Per-Realm Broker and Controller Units (REPLACE) + +At `b5ddbed6`, per-realm broker units are **socket-activated PID1 units** declared in `host-broker.nix`. +ADR 0046 target: parent-spawned Zone broker and controller processes (not PID1 units). + +| Current Unit Pattern | Evidence | Disposition | Notes | +|---------------------|----------|-------------|-------| +| `d2b-r--broker.socket` | nix-emitted; PID1 socket activation | DELETE | Allocator pre-binds and parent-spawns Zone broker per ADR 0046 D006 | +| `d2b-r--broker.service` | nix-emitted; PID1 service | DELETE | Replaced by parent-spawned `d2bbr-r-` process | +| `d2b-r--controller.service` | nix-emitted; PID1 service | DELETE | Replaced by parent-spawned `d2bd-r-` process | +| Removal proof: no `d2b-r-*` units in `systemctl list-units` on updated host | — | Required | — | + +### 5.3 Per-VM Sidecar Units (ADAPT to Zone-scoped Processes) + +| Current Unit Pattern | Current Function | Disposition | Target | +|---------------------|-----------------|-------------|--------| +| `d2b--vm.service` | CloudHypervisor process; KillMode=process | ADAPT | `Process` resource (executionRef: Guest) in Zone runtime | +| `d2b--virtiofsd-*.service` | virtiofsd shares (ro-store, gctl) | ADAPT | `Process` resource via `Provider/system-minijail` bootstrap | +| `d2b--swtpm.service` | swtpm TPM 2.0 | ADAPT | `Process` resource (executionRef: Guest) | +| `d2b--audio-*.service` | vhost-user-sound | ADAPT | `Process` resource | +| `d2b--video.service` | vhost-user-video sidecar | ADAPT | `Process` resource | +| `d2b-runtime-systemd-user.socket` | Per-user runtime socket (unsafe-local) | ADAPT | user-only `Host` runtime socket; ComponentSession replaces helper protocol for shell process attachment per D042/D051 | + +### 5.4 Host-Level Service Units + +| Unit | Disposition | +|------|-------------| +| `d2b-activation-helper.service` | RETAIN/ADAPT — activation `EphemeralProcess` | +| `d2b-sysctl.service` | RETAIN | +| `d2b-network@.service` | ADAPT — Network resource provisioning | +| `d2b-keys-init.service` | RETAIN | + +--- + +## §6 CLI Commands + +Source: `packages/d2b/src/` `NativeCommand` enum + subcommand handlers. + +| Current CLI Verb | Evidence | Current Handler | Disposition | Target ResourceOp / Notes | +|-----------------|----------|-----------------|-------------|--------------------------| +| `d2b vm list` | production-reachable | `WorkloadOp::List` in wire; **no d2bd handler** at `b5ddbed6` | ADAPT | `resource list Guest` — wire handler wiring required | `ADR046-api-001` | +| `d2b vm status ..d2b` | production-reachable | `WorkloadOp::Status`; **no d2bd handler** | ADAPT | `resource get Guest/` in Zone scope | `ADR046-api-001` | +| `d2b vm up/down/switch` | production-reachable | VM lifecycle; d2bd admin handler | ADAPT | `resource apply/delete Guest/` | `ADR046-api-001` | +| `d2b vm exec [-d] -- ` | production-reachable | `ExecOp`; `exec_session.rs` | ADAPT | ComponentSession `Exec` service; keep admin gate | `ADR046-session-001` | +| `d2b shell ` | production-reachable | `ShellOp`; routes to unsafe-local helper via `d2bd/src/unsafe_local_helper.rs` | ADAPT | Shell `Process` child of user-only `Host`; no-isolation warning preserved in CLI output; `isolation:"unsafe-local"` in audit event | `ADR046-primitives-003` | +| `d2b console ` | production-reachable | `ConsoleOp`; `console_session.rs` | ADAPT | Console ComponentSession service | `ADR046-session-001` | +| `d2b launch ` | production-reachable | Launcher item from private bundle; admin gate | ADAPT | `EphemeralProcess` launch via Provider catalog | `ADR046-provider-001` | +| `d2b keys rotate []` | production-reachable | `KeysOp`; broker `KeyRotation` | RETAIN/ADAPT | Keep; route through `Provider/system-core` key resource | `ADR046-primitives-003` | +| `d2b host prepare [--apply]` | production-reachable | `HostPrepare` broker op; nftables/bridge/cgroup setup | ADAPT | `Host` resource initialize; Zone bootstrap | `ADR046-primitives-002` | +| `d2b op inspect` | production-reachable | Operation trace/status | RETAIN | Keep bounded inspect per observability spec | — | +| `d2b realm list` / `d2b realm inspect` | production-reachable | Lists realm controller status | ADAPT | `resource list Zone` / `resource get Zone/` | `ADR046-api-001` | +| `d2b userd *` | production-reachable | Stub exits 78; lib has `UserExecSession`/`UserSocketPolicy` traits | DELETE | Remove public `d2b userd` verb and service stub after parity with fixed user supervisor Process under `Provider/system-systemd` user domain | `ADR046-primitives-003` | +| `d2b wayland-proxy *` | production-reachable | Wayland cross-domain proxy verbs | ADAPT | Provider process worker in graphics Provider | `ADR046-provider-001` | + +--- + +## §7 Persistent Paths and State + +| Path | Current Owner | Disposition | Target ResourceType | Notes | +|------|--------------|-------------|--------------------|----| +| `/etc/d2b/` | Nix activation | RETAIN | Zone bootstrap bundle read root | Paths rename per §4.2 | +| `/var/lib/d2b/vms//store/` | `d2b-priv-broker` (hardlink farm) | ADAPT | `Volume` resource (store-view) per Guest | Keep same-filesystem requirement | +| `/var/lib/d2b/vms//swtpm/` | `d2b-priv-broker` swtpm_dir | ADAPT | `Volume` resource (swtpm state) per Guest | Keep marker + fail-closed semantics | +| `/var/lib/d2b/swtpm-markers/` | broker provisioning | ADAPT | Per-Guest Volume marker | Keep previously-provisioned fail-closed | +| `/run/d2b/d2bd.sock` | `d2bd.socket` | RETAIN | Zone runtime public socket | | +| `/run/d2b/broker.sock` | `d2b-priv-broker.socket` | RETAIN | Local-root broker socket | | +| `/run/d2b/allocator.sock` | config ref; engine not live at `b5ddbed6` | DELETE | No separate allocator socket; provisioning integrates into fixed core controllers and Zone runtime resource contracts | `ADR046-core-001` | +| `/run/d2b/r-/` | per-realm runtime directory | ADAPT | `/run/d2b/z-/` — rename | | +| `/run/d2b/r-/broker.sock` | per-realm broker socket | ADAPT | Pre-bound by Zone runtime allocator per ADR 0046; move to Zone runtime directory | `ADR046-core-001` | +| `/sys/fs/cgroup/d2b.slice/` | broker cgroup delegation | RETAIN | Zone-scoped cgroup leaves; no top-level rename needed | | +| `${XDG_RUNTIME_DIR}/d2b-runtime-systemd-user.sock` | `d2b-unsafe-local-helper` (user-owned) | ADAPT | user-only `Host` runtime socket; ComponentSession replaces helper protocol per D042/D051; no-isolation posture label preserved in all socket-level audit | | +| `/tmp/d2b-heavy-gates-$UID/` | `cargo xtask heavy-gate` semaphore | RETAIN | Keep semaphore; not a Zone resource | | +| OFD lock files under `/run/d2b/` | d2b-state | RETAIN | OFD locks are mechanisms; not ResourceSpec fields per D034 | | +| `/var/lib/d2b/zones//bundle/generation-.json` | new Zone bundle emitter (NEW) | NEW | Immutable integrity-pinned Zone resource bundle per generation; `root:d2bd` 0640 | §0.2 bundle/generation emission | +| `/var/lib/d2b/zones//bundle/current-generation` | Zone runtime | NEW | Active generation pointer; updated atomically on activation | §0.2 cleanup contract; prior bundles retained up to `retainedGenerations` count (default 3, range 1..16) | +| `/var/lib/d2b/zones//providers//settings-.json` | new Provider settings emitter (NEW) | NEW | Signed provider-specific settings artifact; `root:d2bd` 0640 | §0.2 signed Provider schema validation | +| `/etc/d2b/artifact-catalog.json` | Nix activation (new artifact catalog emitter) | NEW | (private catalog; contains `storePath` per entry, validated by core at activation) | §0.2 artifact catalog; `root:d2bd` 0640; `storePath` never surfaced in ResourceSpec/status/audit/logs/metrics/spans | + +--- + +## §8 Tests and Docs + +### 8.1 Existing Test Coverage Mapping + +| Test Location | Current Coverage | Disposition | Notes | +|--------------|-----------------|-------------|-------| +| `tests/unit/nix/cases/` (nix-unit) | Nix option eval cases; includes `niri-vm-borders.nix`, assertions | ADAPT | Update realm→zone in all cases; `make nix-unit-pin` after changes | +| `tests/unit/smoke/` (smoke evals) | Flake-check smoke evals including `smoke-eval-tpm.nix` | ADAPT | Update option names | +| `packages/d2b-realm-core/src/**` (`#[cfg(test)]`) | `RealmId`, `WorkloadId`, `AllocatorLease`, `HostResourceKind` unit tests | ADAPT | Update names to `ZoneId`, `ResourceName` | +| `packages/d2b-core/src/workload_identity.rs` (tests) | `WorkloadTarget::parse`, `WorkloadIdentity` serde | ADAPT | Update to `ResourceRef` format | +| `packages/d2bd/src/` (hermetic exec tests) | `exec_session` / `exec_client` matrices | RETAIN | Keep; update transport to ComponentSession | +| `packages/d2b-contracts/` (tests) | Wire type serde | ADAPT | Update `WorkloadOp` → `ResourceOp`; add handler wire test | +| `packages/d2b-contract-tests/tests/` | Rendered-artifact contract tests; drift checks | ADAPT | Update for renamed/split artifacts per §4.2 | +| `tests/unit/gates/drift-check.sh` | Schema drift gate: `xtask gen-schemas` + `git diff --exit-code` | RETAIN | Must pass after schema updates | +| `tests/assertions-eval.sh` | Eval-time assertion cases | ADAPT | Add Zone-scoped assertion cases | +| `tests/net-vm-network-eval.sh` | Net VM network eval | RETAIN | Keep; update `NetEnv` → `Network` option paths if needed | +| `tests/integration/containers/` | Container integration tests (Layer 2) | ADAPT | Update realm→zone fixture data | +| `tests/host-integration/*.nix` | runNixOSTest VM checks | ADAPT | Update `d2b.realms` → `d2b.zones` in all test configs | +| `tests/host-integration/unsafe-local-helper.nix` | unsafe-local shell integration | ADAPT | Update to user-only `Host` resource model per D042; verify no-isolation posture in `Host.status`, CLI warnings, and audit events | + +### 8.2 Tests Required for Removal Proof + +| Removal Target | Required Test / Proof | +|---------------|-----------------------| +| Per-realm PID1 socket units (`d2b-r--broker.socket` etc.) | `systemctl list-units 'd2b-r-*'` returns empty on updated host | +| `d2b-realm-router` session types | Integration test: ComponentSession handshake passes end-to-end | +| `unsafe-local` as separate Provider | user-only `Host` resource (not Provider, not Guest) created with `defaultDomain=user`, `allowedDomains=[user]`, `defaultUserRef=User/`; no-isolation posture visible in `d2b host status` output (or generic Host resource status), shell session CLI warnings, and audit events (`isolation:"unsafe-local"`); `d2b vm status` is v2 evidence only and is removed at v3 cutover; child process sessions are normal `Process` resources | +| `WorkloadOp` / `WorkloadListResult` / `WorkloadPublicSummary` wire types | `resource list Guest` integration test passes via `ResourceOp` path; `grep -r WorkloadOp packages/ --include='*.rs'` returns zero results | +| `RelayProvider` trait (dead trait deletion) | `grep -r RelayProvider packages/ --include='*.rs'` returns zero results | + +### 8.3 Cleanup Contract Tests (§0.2 — required for every spec work item) + +Each spec work item must add tests at the locations below. These are the minimum; specs may +add additional layer for their ResourceType-specific cleanup semantics. + +| Test Location | Test Kind | Required Assertion | +|--------------|-----------|-------------------| +| `tests/unit/nix/cases/-cleanup.nix` | nix-unit eval | Removing a resource from the Nix config produces a new bundle that does not contain that resource; the persisted prior record carries core-set `metadata.managedBy: configuration` (set by core, not by Nix authoring); generation ID of the new bundle changes | +| `tests/unit/nix/cases/-schema-validation.nix` | nix-unit eval | Invalid `providerRef`, out-of-bounds value, or inline secret byte produces exact expected eval error | +| `packages/d2b-contracts/tests/` | Rust unit | Generation ID is deterministic and order-independent; signed artifact covers `providerRef + provider-specific spec fields + generation` | +| `packages/d2b-core-controller/tests/` or `tests/host-integration/-cleanup.nix` | integration | Resource removed from Nix config → `deletionRequestedAt` set → atomic `Deleted` revision + row/index removal → deduplicated `ResourceDeleted` audit append; `PendingCleanup: True` condition present in Zone status during cleanup | +| `tests/host-integration/-cleanup.nix` | integration | Controller-managed dynamic child NOT deleted when configuration-managed parent is updated but not removed | +| `tests/host-integration/-cleanup.nix` | integration | Configuration-managed parent with controller-managed children: `deletionRequestedAt` is set on parent; parent stays `phase: Pending`/`Degraded` until owner controller deletes all children; row removed atomically only after children clear | +| `tests/host-integration/-cleanup.nix` | integration | New generation activates and routes requests while prior cleanup is in progress; `PendingCleanup: True` is visible in `d2b op inspect` | +| `tests/host-integration/-cleanup.nix` | integration | Prior generation bundles present on disk up to `retainedGenerations` count; oldest pruned after cleanup complete | +| `tests/host-integration/-cleanup.nix` | integration | Emergency rollback re-activates prior generation; surviving resources re-reconcile without reprovisioning | +| `tests/unit/nix/cases/artifact-catalog.nix` | nix-unit eval | Duplicate `d2b.artifacts` ID fails eval with exact error; undeclared `type` fails eval; `artifactId` referencing missing catalog entry fails build; `systemArtifactId` with wrong-type catalog entry fails build; derivation placed directly in `spec.*` fails type check; catalog renders deterministic digest for a fixed derivation; rendered ResourceSpec JSON contains only the bounded ID string, not any store path | +| `packages/d2b-contracts/tests/` or `packages/d2b-core-controller/tests/` | Rust unit | Artifact catalog type lookup is fail-closed for missing/wrong-type entries; `artifact-catalog.json` contains a `storePath` field per entry (present and validated by core privately); that `storePath` value is absent from all public ResourceSpec fields, status, audit events, logs, metric labels, and span attributes | +| `tests/host-integration/artifact-catalog-hygiene.nix` | integration | At runtime, store paths from the artifact catalog are absent from all public ResourceSpec fields, status, audit events, log lines, metric labels, and span attributes | + +--- + +## §8.4 Fast-execution placement and legacy retirement (D094) + +Every disposition in §8.1 and every removal target in §8.2 is bound to the +D094 execution model: + +- Each migration/replacement row names the **exact old test selector/file** it + covers and its keep/adapt/move/delete disposition. `RETAIN`/`ADAPT` targets + that are hermetic MUST meet the §10.16 budgets of + `ADR-046-validation-and-delivery` (individual normal test p95 ≤50 ms, no + wall-clock sleep; per-crate `--lib --tests` ≤2 s; Layer-1 hermetic shard + ≤60 s). An adapted test that can only pass by spawning a process, hitting the + network, or sleeping is re-placed into `integration/`, not slowed down in a + hermetic tier. +- When ADR 0046 replaces a behavior, the minimum reusable semantic assertions + migrate into the new hermetic suite and the old duplicate tests, shell gates, + fixtures, static artifacts, CI jobs, and manifest entries are **deleted** once + successor coverage and the §8.2 removal proof pass. Old and new suites never + run indefinitely. +- Each such deletion updates `tests/layer1-jobs.json`, the closed gate + manifests, the flake/matrix/Nix-unit pins, the generated ledgers, and the CI + workflow shards, and adds an absence assertion (the retired selector no longer + appears). This is enforced by the legacy-test retirement generator + (`ADR046-streamline-023`) and the test-runtime ledger/timing gate + (`ADR046-streamline-022`/`ADR046-delivery-007`). No new top-level + `tests/*.sh` gate is introduced; Rust unit/property and the existing + generated drift/policy gates are preferred. + +--- + +## §9 Main Reuse Inventory + +Per D001 and D041, code from main commit `a1cc0b2da4a08ca3240a770a972fe4da6f912bef` may be +copied/adapted as v3 implementation source. None of the packages listed below exist on the +pre-ADR45 v3 baseline (`b5ddbed6`). Every row records: exact main file path, exact symbols +selected, selected behavior, exact v3 destination path and integration note, and which +ADR 0045 assumptions must NOT be carried over. + +--- + +### 9.1 `d2b-session` — ComponentSession v2 Runtime + +Main crate: `packages/d2b-session/` — crate-level doc: "Portable ComponentSession v2 runtime. +Transport-specific socket and descriptor handling is intentionally outside this crate." +Not present on v3 baseline. + +| File (a1cc0b2d) | Symbols to Copy/Adapt | Selected Behavior | v3 Destination | Excluded ADR 0045 Assumptions | +|----------------|----------------------|------------------|----------------|-------------------------------| +| `d2b-session/src/handshake.rs` | `NoiseHandshake`, `EstablishedHandshake`, `NegotiatedOffer`, `HandshakeRole`, `HandshakeCredentials`, `encode_offer`, `negotiate_offer`, generation-discovery functions (`encode_generation_discovery_request`, `accept_generation_discovery_request`, `encode_generation_discovery_response`, `decode_generation_discovery_response`) | Noise-NK handshake state machine; generation-pinned transcript hash; discovery sub-protocol for reconnect | `packages/d2b-session/src/handshake.rs` | `RealmId` / `ZoneId` field names in `HandshakeOffer.channel_binding`; adapt to v3 naming during copy | +| `d2b-session/src/bootstrap.rs` | `Secret32`, `BootstrapPsk`, `AdmittedBootstrapPsk`, `BootstrapAdmission` | Operation-bound single-use bootstrap PSK; admission consume/is_consumed semantics | `packages/d2b-session/src/bootstrap.rs` | No domain-specific assumptions; clean copy | +| `d2b-session/src/record.rs` | `ProtectedRecord`, `RecordProtector` | Noise-NK post-handshake record encryption/decryption; directional sequencing; replay safety | `packages/d2b-session/src/record.rs` | No domain assumptions; depends only on `EstablishedHandshake`; clean copy | +| `d2b-session/src/engine.rs` | `SessionEngine`, `SessionEvent`, `outstanding_attachment_credits`, `complete_call`, `register_inbound_call`, `complete_inbound_call`, `open_named_stream` | Main session state machine: drives send/receive loop, attachment credit accounting, stream and cancellation bookkeeping | `packages/d2b-session/src/engine.rs` | Must exclude any reference to `d2b-bus` framing types that don't exist on v3; review import list during copy | +| `d2b-session/src/scheduler.rs` | `FairScheduler`, `OutboundFrame`, `QueueClass`, `LimitProfile` (re-exported from `v2_component_session`) | Fair per-stream credit-based outbound scheduler; control vs named stream priority; backpressure enforcement | `packages/d2b-session/src/scheduler.rs` | No domain assumptions; `LimitProfile` comes from `d2b-contracts/v2_component_session`; copy that too | +| `d2b-session/src/streams.rs` | `NamedStreamMux`, `StreamId`, `StreamPhase`, `StreamEvent`, all `NamedStreamMux` methods | Named stream state machine: open/send_credit/receive_data/release_receive_credit/close_local; independent from control channel | `packages/d2b-session/src/streams.rs` | No domain assumptions; clean copy | +| `d2b-session/src/lifecycle.rs` | `SessionLifecycle`, `SessionPhase`, `KeepaliveAction`, all lifecycle methods (new, phase, on_activity, poll_keepalive, receive_pong, disconnect, begin_reconnect, reconnect_established, close) | Keepalive ping/pong FSM; reconnect window with generation bump; close with reason/remediation | `packages/d2b-session/src/lifecycle.rs` | No domain assumptions; depends only on `LimitProfile` and `v2_component_session` types | +| `d2b-session/src/attachment.rs` | `AttachmentPayload` trait, `OwnedAttachment`, `AttachmentValidationError` | Typed FD attachment ownership and lifecycle; `unbound` for pre-validation attachments; `close` drops fd | `packages/d2b-session/src/attachment.rs` | No domain assumptions; clean copy | +| `d2b-session/src/transport.rs` | `OwnedTransport` trait, `TransportDescriptor`, `TransportPacket`, `TransportError` | Transport abstraction: async receive/send/close; packet carries bytes + attachments; distinct from session framing | `packages/d2b-session/src/transport.rs` | No domain assumptions; clean copy | +| `d2b-session/src/cancellation.rs` | `Cancellation`, `RequestRegistry`, all `RequestRegistry` methods (register, mark_dispatched, cancel, cancel_generated, complete, remove, signal, cancel_all, active) | Per-request cancellation tokens; generation-bound cancel propagation; `cancel_all` on disconnect | `packages/d2b-session/src/cancellation.rs` | No domain assumptions; clean copy | +| `d2b-session/src/driver.rs` | `ComponentSessionDriver` trait, `SessionDriverHandle` | Async session driver interface; `start_ttrpc`, `complete_ttrpc`, `cancel`, `send_ttrpc`, `receive_ttrpc`, `register_inbound_call` | `packages/d2b-session/src/driver.rs` | Trait boundary only; implementations on v3 side | +| `d2b-session/src/server.rs` | `SessionServerError`, `serve_ttrpc_services` | Serve a map of ttrpc service names from a `SessionDriverHandle`; main entry point for provider-agent server | `packages/d2b-session/src/server.rs` | Assumes ttrpc service map from `d2b-contracts/v2_services`; copy both together | +| `d2b-session/src/fragmentation.rs` | `Fragment`, `Fragmenter`, `Reassembler` | Message fragmentation over bounded records | `packages/d2b-session/src/fragmentation.rs` | No domain assumptions; clean copy | +| `d2b-session/src/deadline.rs` | `DeadlineBudget` | Wall + monotonic deadline intersection; ttrpc budget translation | `packages/d2b-session/src/deadline.rs` | No domain assumptions; clean copy | +| `d2b-session/src/metrics.rs` | `MetricEvent`, `MetricsSink` trait, `NoopMetrics` | Session metric hook; implementors attach to OTel or test sinks | `packages/d2b-session/src/metrics.rs` | No domain assumptions; clean copy | +| `d2b-session/src/error.rs` | `Result`, `SessionError` | Session error taxonomy | `packages/d2b-session/src/error.rs` | Clean copy | + +**Tests** (carry over to v3, adapt identities): + +| Test File (a1cc0b2d) | Test Functions | What They Validate | v3 Destination | +|---------------------|---------------|--------------------|----------------| +| `d2b-session/tests/component_session.rs` | `fixed_negotiation_and_all_noise_profiles_are_strict` | Handshake offer encoding and all Noise profile strictness | `packages/d2b-session/tests/component_session.rs` | +| | `public_daemon_handshake_rejects_a_guest_only_schema_peer` | Peer schema gating | same | +| | `direct_guest_handshake_rejects_a_guest_service_only_schema_peer` | Schema gating from guest side | same | +| | `protected_records_are_directional_sequenced_and_replay_safe` | Record protector invariants | same | +| | `protected_record_boundaries_and_tampering_fail_closed` | Record tampering detection | same | +| | `fragmentation_is_bounded_and_rejects_reordering` | Fragmenter/reassembler bounds | same | +| | `deadline_intersects_wall_monotonic_and_ttrpc_budgets` | `DeadlineBudget` correctness | same | +| | `cancellation_is_generation_bound_and_shared` | `RequestRegistry` generation isolation | same | +| | `lifecycle_keepalive_close_and_reconnect_change_generation` | `SessionLifecycle` FSM | same | +| | `named_stream_state_and_scheduler_have_independent_credit_and_fairness` | `NamedStreamMux` + `FairScheduler` credit | same | +| | `bootstrap_is_operation_bound_expiring_single_use_and_redacted` | `BootstrapAdmission` consume semantics | same | + +--- + +### 9.2 `d2b-session-unix` — Unix/vsock Transport Adapters + +Main crate: `packages/d2b-session-unix/`. Not present on v3 baseline. + +| File (a1cc0b2d) | Symbols to Copy/Adapt | Selected Behavior | v3 Destination | Excluded ADR 0045 Assumptions | +|----------------|----------------------|------------------|----------------|-------------------------------| +| `d2b-session-unix/src/adapter.rs` | `UnixSeqpacketTransport`, `PeerIdentityPolicy` (accepted/pathname/inherited_socketpair variants), `UnixAttachmentPayload`, `OwnedUnixAttachment` | AF_UNIX SOCK_SEQPACKET transport implementing `OwnedTransport`; `SO_PEERCRED` identity policy; FD ancillary data ownership | `packages/d2b-session-unix/src/adapter.rs` | `PeerIdentityPolicy::inherited_socketpair` for testing: retain. Do not assume main's specific socket path constants. | +| `d2b-session-unix/src/socket.rs` | `AncillaryCapacity`, `OutboundPacket`, `PacketBurst`, `SentPacket`, all send/receive helpers | Ancillary FD capacity calculation from `AttachmentPolicy`; packet batching; `cmsg` ancillary layout | `packages/d2b-session-unix/src/socket.rs` | No domain assumptions; depends on `v2_component_session` `AttachmentPolicy`; copy together | +| `d2b-session-unix/src/vsock.rs` | `FramedVsockTransport`, `NativeVsockTransport`, `NativeVsockListener` | Framed vsock implementing `OwnedTransport`; used for guest-control transport to guestd | `packages/d2b-session-unix/src/vsock.rs` | Replace vsock port constants (use v3 bundle-configured ports, not main hardcoded values) | +| `d2b-session-unix/src/descriptor.rs` | `PeerCredentials`, `FirstPacketCredentials`, `ObjectIdentity`, `PidfdIdentityPolicy`, `DescriptorPolicy`, `AcceptedAttachment` | Received FD classification; pidfd identity binding; first-packet directional credential verification | `packages/d2b-session-unix/src/descriptor.rs` | No domain assumptions; clean copy | +| `d2b-session-unix/src/credit.rs` | `CreditPool`, `CreditScopeSet`, `CreditBundle`, `CreditError`, `CreditScope` | Multi-scope attachment credit reservations; atomic rollback on partial failure; `MAX_PROCESS_ATTACHMENT_CREDITS` / `MAX_HOST_ATTACHMENT_CREDITS` from wire contract | `packages/d2b-session-unix/src/credit.rs` | Limits reference `v2_component_session` constants; copy both | +| `d2b-session-unix/src/pidfd.rs` | `PidfdEvidence`, `PidfdIdentityVerifier` trait, `PidfdInfoSource` trait, `ProcPidfdIdentityVerifier`, `parse_pidfd_fdinfo`, `DigestEvidenceCallback` | Pidfd identity proof via `/proc/self/fd/` fdinfo parsing; fail-closed on parse error | `packages/d2b-session-unix/src/pidfd.rs` | No domain assumptions; clean copy | +| `d2b-session-unix/src/systemd.rs` | `ActivatedSeqpacketListener`, `ActivatedSeqpacketListeners`, `from_systemd` methods, `SystemdActivationError` | Systemd socket activation for named `SOCK_SEQPACKET` sockets | `packages/d2b-session-unix/src/systemd.rs` | No domain assumptions; clean copy | +| `d2b-session-unix/src/error.rs` | `UnixSessionError` | Unix session error taxonomy | `packages/d2b-session-unix/src/error.rs` | Clean copy | + +**Tests** (carry over): + +| Test File (a1cc0b2d) | Test Functions | What They Validate | v3 Destination | +|---------------------|---------------|--------------------|----------------| +| `d2b-session-unix/tests/unix_session.rs` | `ancillary_capacity_is_derived_from_closed_hard_bounds` | `AncillaryCapacity` bound correctness | `packages/d2b-session-unix/tests/unix_session.rs` | +| | `process_limit_preserves_emergency_headroom` | Credit headroom semantics | same | +| | `failed_multiscope_reservation_rolls_back_every_prior_scope` | Atomic rollback invariant | same | +| | `staged_credit_reservations_release_once_at_each_scope` | Credit release idempotency | same | +| | `inherited_passcred_is_verified_but_never_repaired` | Credential verification path | same | +| | `first_packet_has_exact_directional_credentials` | First-packet credential direction | same | +| | `seqpacket_transfer_is_atomic_cloexec_and_object_exact` | FD transfer atomicity/CLOEXEC | same | +| | `duplicate_kernel_objects_are_rejected_and_cleaned_up` | Duplicate FD rejection | same | + +--- + +### 9.3 `d2b-contracts` — Wire Contract Modules + +**`v2_component_session.rs`** — All constants and binary contract types. + +| Symbol Group (a1cc0b2d) | Symbols | v3 Destination | Excluded Assumptions | +|------------------------|---------|----------------|----------------------| +| Protocol constants | `PREFACE_MAGIC`, `COMPONENT_SESSION_MAJOR/MINOR`, `MAX_HANDSHAKE_OFFER_BYTES`, `HANDSHAKE_OFFER_CANONICAL_LEN`, `ENDPOINT_POLICY_IDENTITY_CANONICAL_LEN`, `MAX_PROTECTED_CIPHERTEXT_BYTES`, `NOISE_TAG_BYTES`, `RECORD_LENGTH_BYTES`, `MAX_PROTECTED_PLAINTEXT_BYTES`, `MAX_LOGICAL_MESSAGE_BYTES` | `packages/d2b-contracts/src/v2_component_session.rs` (copy entire file) | None — these are wire protocol constants | +| Stream/attachment limits | `MAX_ACTIVE_NAMED_STREAMS`, `MAX_PACKET_ATTACHMENTS`, `MAX_REQUEST_ATTACHMENTS`, `MAX_OPERATION_ATTACHMENTS`, `MAX_SESSION_ATTACHMENTS`, `MAX_PROCESS_ATTACHMENT_CREDITS`, `MAX_HOST_ATTACHMENT_CREDITS`, `RESERVED_CONTROL_FDS` | same | `MAX_HOST_ATTACHMENT_CREDITS` = 8192 — do not lower for v3 | +| Queue limits | `MAX_NAMED_STREAM_QUEUE_BYTES`, `MAX_AGGREGATE_NAMED_STREAM_QUEUE_BYTES`, `MAX_TTRPC_CONTROL_QUEUE_BYTES`, `MAX_SESSION_CONTROL_QUEUE_BYTES` | same | None | +| Timing constants | `MAX_CLOCK_SKEW_MS`, `MAX_REQUEST_LIFETIME_MS`, `LOCAL_HANDSHAKE_DEADLINE_MS`, `REMOTE_HANDSHAKE_DEADLINE_MS`, `LOCAL_RECONNECT_DEADLINE_MS`, `REMOTE_RECONNECT_DEADLINE_MS`, `MAX_RECONNECT_ATTEMPTS`, `MAX_RECONNECT_WINDOW_MS`, `MAX_KEEPALIVE_INTERVAL_MS`, `MAX_KEEPALIVE_TIMEOUT_MS` | same | None | +| Guest credential constants | `GUEST_SESSION_CREDENTIAL_MAGIC`, `GUEST_SESSION_CREDENTIAL_SCHEMA_VERSION`, all `GUEST_SESSION_CREDENTIAL_*_BYTES`, `MAX_GUEST_BOOTSTRAP_CREDENTIAL_LIFETIME_MS`, `GUEST_SESSION_CREDENTIAL_FLAG_*` | same | None | +| Wire types | `ComponentSessionPreface`, `LimitProfile`, `AttachmentPolicy`, `TransportBinding`, `HandshakeOffer`, `EndpointPolicy`, `EndpointPolicyIdentity`, `HandshakeAccept`, `HandshakeReject`, `ChannelId`, `RecordHeader`, `FragmentHeader`, `CloseRecord`, `CloseReason`, `KeepaliveRecord`, `RequestEnvelope`, `AdmittedDeadline`, `CancelRequest`, `CancelAck`, `AttachmentDescriptor`, `BootstrapPskBinding`, `GuestBootstrapPsk`, `GuestBootstrapCredentialV1`, `GuestSessionCredentialV1`, `GuestIdentityBindingV1`, `BootstrapPskState`, `BoundedVec` | same | `HandshakeOffer.channel_binding` may contain `RealmId` / `ZoneId` encoded bytes; adapt field name in v3 | + +**`v2_provider.rs`** — Provider contract. + +| Symbol Group (a1cc0b2d) | Symbols | v3 Destination | Excluded Assumptions | +|------------------------|---------|----------------|----------------------| +| Constants | `PROVIDER_SCHEMA_VERSION`, `PROVIDER_API_MAJOR/MINOR`, `MAX_PROVIDER_CAPABILITIES`, `MAX_PROVIDER_REGISTRY_ENTRIES`, `MAX_PROVIDER_PLAN_RESOURCES`, `MAX_CREDENTIAL_OPERATION_CLASSES`, `MAX_PROVIDER_REQUEST_LIFETIME_MS`, `MAX_PROVIDER_LEASE_LIFETIME_MS`, `MAX_PROVIDER_DRAIN_MS`, `MAX_OBSERVABILITY_*`, `MAX_SAFE_JSON_INTEGER`, `PROVIDER_CONTRACT_FINGERPRINT` | `packages/d2b-contracts/src/v2_provider.rs` | None | +| `ProviderType` enum | 11 variants: `Runtime, Infrastructure, Transport, Substrate, Credential, Display, Network, Storage, Device, Audio, Observability` | same | Do NOT add variants; frozen catalog per D043–D049 | +| `ProviderMethod` enum | All ~50 methods across all axes (Runtime*, Infrastructure*, Transport*, Substrate*, Credential*, Display*, Network*, Storage*, Device*, Audio*, Observability*) | same | Frozen; do NOT add methods | +| Core types | `ProviderDescriptor`, `ProviderCapabilitySet`, `ProviderCapability`, `ProviderFactoryKey`, `ProviderApiVersion`, `ProviderAuthority`, `NetworkPosture`, `DeviceMediationPosture`, `ProviderContractError` | same | `ProviderAuthority` may reference main-specific realm auth; review for v3 | + +**`v2_services.rs`** — Service inventory. + +| Symbol Group (a1cc0b2d) | Symbols | v3 Destination | Excluded Assumptions | +|------------------------|---------|----------------|----------------------| +| Service constants | `MAX_PROTOBUF_MESSAGE_BYTES`, `MAX_SERVICE_STRING_BYTES`, `MAX_PAGE_CURSOR_BYTES`, `MAX_PAGE_SIZE`, `MAX_OBSERVATIONS`, `DIGEST_BYTES`, `MAX_ALLOCATOR_*`, `MAX_REALM_CHILD_FDS`, `CONTROLLER/BROKER_PIDFD_ATTACHMENT_INDEX`, all `MAX_DAEMON_*` | `packages/d2b-contracts/src/v2_services.rs` | `MAX_DAEMON_REALMS`/`MAX_DAEMON_WORKLOADS` — update counts for Zone model | +| `ServiceSpec`, `SERVICE_INVENTORY` | Static service spec array with all ~20 services (daemon, guest, realm, provider_*, clipboard, shell, terminal, security_key, wayland, notify, activation, tty, user, runtime_systemd_user) | same | `SERVICE_INVENTORY` references service names by string; update service names that change with Zone rename | +| `StrictWireMessage` trait | Used by `serve_ttrpc_services` and generated clients | same | Clean copy | + +**`generated_v2_services/`** — Generated protobuf+ttrpc bindings. Copy all 26 files: + +| Files (a1cc0b2d) | v3 Destination | Excluded ADR 0045 Assumptions | +|-----------------|----------------|-------------------------------| +| `daemon.rs`, `daemon_ttrpc.rs` | `packages/d2b-contracts/src/generated_v2_services/` | `RealmId` / `ZoneId` in daemon service request fields; update field names | +| `guest.rs`, `guest_ttrpc.rs` | same | Guest exec/shell fields use v3 `WorkloadTarget`; adapt to new address format | +| `realm.rs`, `realm_ttrpc.rs` | same | `RealmId` in realm service fields → `ZoneId` eventually; keep current names for now | +| `provider_runtime.rs/.._ttrpc.rs` | same | `RuntimePlan` / `RuntimeEnsure` etc. message types; no domain assumptions | +| `provider_substrate.rs/.._ttrpc.rs` | same | None | +| `provider_transport.rs/.._ttrpc.rs` | same | None | +| `provider_credential.rs/.._ttrpc.rs` | same | None | +| `provider_display.rs/.._ttrpc.rs` | same | None | +| `provider_network.rs/.._ttrpc.rs` | same | None | +| `provider_storage.rs/.._ttrpc.rs` | same | None | +| `provider_device.rs/.._ttrpc.rs` | same | None | +| `provider_audio.rs/.._ttrpc.rs` | same | None | +| `provider_observability.rs/.._ttrpc.rs` | same | None | +| `provider_infrastructure.rs/.._ttrpc.rs` | same | None | +| `clipboard.rs/.._ttrpc.rs`, `clipboard_picker.rs/.._ttrpc.rs` | same | None | +| `shell.rs`, `shell_ttrpc.rs` | same | Shell service v2; update to user-only Host scope per D042 | +| `terminal.rs` | same | Terminal stream types; no domain assumptions | +| `tty.rs`, `tty_ttrpc.rs` | same | TTY service; no domain assumptions | +| `security_key.rs`, `security_key_ttrpc.rs` | same | None | +| `wayland.rs`, `wayland_ttrpc.rs` | same | None | +| `notify.rs`, `notify_ttrpc.rs` | same | None | +| `activation.rs`, `activation_ttrpc.rs` | same | Activation service v2; update paths | +| `user.rs`, `user_ttrpc.rs` | same | None | +| `runtime_systemd_user.rs/.._ttrpc.rs` | same | User-only scope; aligns with user-only Host per D042 | +| `broker.rs`, `broker_ttrpc.rs` | same | Broker service v2; adapt for Zone-local broker address | +| `common.rs` | same | Common types; no domain assumptions | + +--- + +### 9.4 `d2b-provider` — Provider Runtime + +Main crate: `packages/d2b-provider/`. Not present on v3 baseline. + +| File (a1cc0b2d) | Symbols to Copy/Adapt | Selected Behavior | v3 Destination | Excluded Assumptions | +|----------------|----------------------|------------------|----------------|----------------------| +| `d2b-provider/src/registry.rs` | `ProviderRegistry`, `ProviderRegistryBuilder` (with `limits`, `register_factory`, `register_instance`, `register_constructed`, `finish`), `ProviderRegistryManager`, `AdmittedProvider`, `InFlightPermit`, `RegistryLimits`, `AdmissionOptions`, `RegistryLifecycle`, `ProviderRegistrySnapshot` | Thread-safe provider registry with limits; atomic swap via `ProviderRegistryManager`; per-request admission permits; lifecycle snapshot | `packages/d2b-provider/src/registry.rs` | `ProviderRegistryV2` import — v3 uses `provider_registry_v2.json`; update registry entry type to v3 bundle format | +| `d2b-provider/src/rpc.rs` | `RpcProviderProxy`, `AuthenticatedProviderRpc` trait, `RpcOperation`, `RpcCall<'a>`, `RpcResponse`, `SessionIdentity`, `ProviderClock` trait, `SystemProviderClock` | Over-session provider RPC proxy: dispatches operations through a live ComponentSession to a remote provider-agent; `SessionIdentity` binds the session to a provider call | `packages/d2b-provider/src/rpc.rs` | `SessionIdentity` includes `RealmId` / `ZoneId` field; adapt field name | +| `d2b-provider/src/instance.rs` | `ProviderInstance` enum (local vs proxy), `ProviderFactory` trait | Unified in-process / over-session provider dispatch; `ProviderFactory` for registry-constructed instances | `packages/d2b-provider/src/instance.rs` | No domain assumptions | +| `d2b-provider/src/context.rs` | `OwnedOperationContext`, `CancellationToken`, all context methods (`operation`, `cancellation`, `is_cancelled`, `remaining`, `call_context`) | Deadline-aware operation context propagated to provider implementations | `packages/d2b-provider/src/context.rs` | No domain assumptions; clean copy | +| `d2b-provider/src/error.rs` | `FactoryError`, `ProviderRuntimeError`, `RegistryBuildError`, `RegistryShutdownReport` | Error taxonomy for registry and runtime failures | `packages/d2b-provider/src/error.rs` | Clean copy | + +**Tests**: + +| Test File (a1cc0b2d) | Test Functions | What They Validate | +|---------------------|---------------|--------------------| +| `d2b-provider/tests/runtime.rs` | `every_axis_passes_identical_in_process_and_rpc_conformance` | In-process and over-session provider give identical results | +| | `conformance_uses_the_exact_real_descriptor_placement_and_target` | Descriptor binding is invariant | +| | `provider_values_preserve_all_descriptor_and_operation_bindings` | Binding table completeness | +| | `exact_registration_supports_all_axes_and_shared_factories` | All 11 axes register correctly | +| | `adapter_rejects_authenticated_identity_mismatch` | Identity mismatch is fail-closed | +| | `rpc_proxy_fails_closed_on_cancellation_and_method_mismatch` | RPC error taxonomy | +| | `rpc_proxy_preserves_plan_handle_and_adoption_bindings` | Plan handle lifecycle | +| | `rpc_proxy_rejects_mismatched_observability_query_results` | Observability query gating | +| | `closed_error_context_is_actionable_without_identity_leaks` | No identity in error messages | +| | `health_uses_no_input_inspection_methods_for_every_axis` | Provider health is read-only | +| | `all_provider_traits_are_object_safe` | Trait object safety | + +--- + +### 9.5 `d2b-provider-toolkit` — Provider-Agent Adapter and Conformance + +Main crate: `packages/d2b-provider-toolkit/`. Not present on v3 baseline. + +| File (a1cc0b2d) | Symbols to Copy/Adapt | Selected Behavior | v3 Destination | Excluded Assumptions | +|----------------|----------------------|------------------|----------------|----------------------| +| `d2b-provider-toolkit/src/adapter.rs` | `ProviderAgentAdapter`, `descriptor()`, `published_capabilities()` | Main bridge between a `ProviderRegistry` and an incoming ComponentSession; routes ttrpc service requests to registered provider instances | `packages/d2b-provider-toolkit/src/adapter.rs` | `ProviderDescriptor` references `ProviderId` from `v2_identity`; keep as-is in v3 | +| `d2b-provider-toolkit/src/server.rs` | `GeneratedProviderServiceServer`, `from_session_handle`, `generated_services` | Spawns all generated protobuf/ttrpc service handlers from a `SessionDriverHandle`; used by provider-agent binary | `packages/d2b-provider-toolkit/src/server.rs` | `SERVICE_INVENTORY` from `v2_services` drives generated_services; copy both | +| `d2b-provider-toolkit/src/conformance.rs` | `ConformanceError`, `check_descriptor_conformance`, `check_provider_conformance` | Asserts every required `ProviderMethod` for a given `ProviderType` is implemented and returns correct types | `packages/d2b-provider-toolkit/src/conformance.rs` | No domain assumptions; clean copy | +| `d2b-provider-toolkit/src/registration.rs` | `register_exact_instances`, `ToolkitError` | Registers exactly one instance per `ProviderType` with no duplicates or extras | `packages/d2b-provider-toolkit/src/registration.rs` | No domain assumptions | +| `d2b-provider-toolkit/src/redaction.rs` | `Redacted`, `Secret` | Wrapper types that suppress `Debug` / `Display` for sensitive values | `packages/d2b-provider-toolkit/src/redaction.rs` | No domain assumptions; clean copy | +| `d2b-provider-toolkit/src/fixture.rs` | `FakeProvider`, `Fixture`, `DeterministicClock`, `sample_lease_request` | Test helpers: fake provider, deterministic clock, sample lease for conformance tests | `packages/d2b-provider-toolkit/src/fixture.rs` | No domain assumptions; clean copy for tests | +| `d2b-provider-toolkit/src/values.rs` | `ProviderValues` | Typed value store for provider operation responses | `packages/d2b-provider-toolkit/src/values.rs` | Clean copy | + +**Tests**: + +| Test File (a1cc0b2d) | Test Functions | What They Validate | +|---------------------|---------------|--------------------| +| `d2b-provider-toolkit/tests/conformance.rs` | (all test functions) | Conformance kit correctly validates all frozen provider method signatures; `FakeProvider` passes | + +--- + +### 9.6 `d2b-gateway-runtime` — Provider-Agent Process Binary + +Main crate: `packages/d2b-gateway-runtime/`. Not present on v3 baseline. + +| File (a1cc0b2d) | Symbols to Copy/Adapt | Selected Behavior | v3 Destination | Excluded Assumptions | +|----------------|----------------------|------------------|----------------|----------------------| +| `d2b-gateway-runtime/src/provider_agent.rs` | `ProviderAgentProcess`, `from_registry`, `from_registry_with`, `service_names`, `audit_snapshot`, `run_registered`, `run`, `ProviderAgentAuditEvent`, `ProviderAgentAuditOutcome`, `ProviderAgentError` | Top-level provider-agent binary lifecycle: builds `GeneratedProviderServiceServer` from `ProviderAgentAdapter`, accepts ComponentSession connections, drives ttrpc service dispatch, emits bounded audit events | `packages/d2b-gateway-runtime/src/provider_agent.rs` (or new `packages/d2b-provider-agent/src/`) | Do NOT copy `aca_workload.rs` — ACA-specific; do NOT copy `d2b-gateway/` (orchestrator uses main's Zone model); do NOT copy `production.rs` if it references main-specific registry V3 or gateway enrollment paths | +| `d2b-gateway-runtime/src/audit_jsonl.rs` | JSONL audit writer | Bounded audit event sink for provider-agent | same target | No domain assumptions | + +**Tests**: + +| Test File (a1cc0b2d) | Test Functions | What They Validate | +|---------------------|---------------|--------------------| +| `d2b-gateway-runtime/tests/provider_agent_v2.rs` | `exposes_every_frozen_provider_service_family` | All 11 ProviderType services registered | +| | `rejects_unregistered_and_invalid_session_adapters` | Admission fail-closed | +| | `malformed_requests_return_closed_errors_and_bounded_audit` | Error isolation and audit bound | +| | `provider_errors_do_not_expose_registration_identity` | Identity redaction in errors | + +--- + +### 9.7 `d2bd` Provider Routing (Main-only Additions) + +The following modules exist on main but NOT on v3 baseline. They implement ADR 0045 Provider +routing inside d2bd and are reuse sources for the Zone runtime's provider dispatch. + +| File (a1cc0b2d) | Symbols to Copy/Adapt | Selected Behavior | v3 Destination | Excluded Assumptions | +|----------------|----------------------|------------------|----------------|----------------------| +| `d2bd/src/provider_registry.rs` | `HostProviderComposition`, `compose_host_provider_registry`, `AgentProviderComposition`, `compose_agent_provider_registry`, `StartupProviderRegistry` (with `registry()`, `runtime_route()`), `HostProviderBinding`, `AgentProviderBinding`, `RuntimeLifecycleInvocation`, `ProviderLifecycleDeadline`, `invoke_runtime_lifecycle`, `probe_startup_registry`, `load_provider_registry_v2`, `compose_startup_registry_with_policy` | Load `provider_registry_v2.json` from bundle; compose `ProviderRegistry` from host-resident and agent-provider bindings; manage startup probe and lifecycle invocation; `runtime_route(vm)` resolves which provider serves a given workload | `packages/d2bd/src/provider_registry.rs` | `ProviderRegistryEntryV2` references `WorkloadId` / `RealmId` keys; adapt to Zone-scoped Guest reference; `vm: &str` arg → `ResourceRef` | +| `d2bd/src/provider_effects.rs` | `DaemonEffectAdapters`, `DaemonEffectAdaptersBuilder`, all per-axis effect/query adapters (`DeviceEffectAdapter`, `AudioEffectAdapter`, `ObservabilityEffectAdapter`, `ProviderLifecycleDispatch`, etc.) | Effect adapter layer: translates provider operation results into d2bd-internal side effects (device state, audio session, observability export); isolated from wire framing | `packages/d2bd/src/provider_effects.rs` | `DeviceEffectPort`, `AudioEffectPort`, etc. — these trait bounds may reference main-specific effect types; verify each port trait's dependency on main vs v3 symbols | +| `d2bd/src/control_services/provider.rs` | `DaemonServiceV2`, `serve_daemon_session`, `DaemonSeqpacketTransport`, `daemon_endpoint_policy`, `daemon_channel_binding`, `DaemonPeerRole`, `DaemonAdapter`, `DaemonMethod`, `DaemonServiceFailure`, `DaemonCallContext`, `DaemonOperationHandler` trait | ComponentSession-based daemon public socket handler: accept seqpacket, build `UnixSeqpacketTransport`, negotiate handshake, dispatch `DaemonAdapter` methods (workload list, exec, shell, console, realm, provider, etc.) | `packages/d2bd/src/control_services/provider.rs` (rename to `daemon.rs` in v3 or merge with existing `lib.rs` dispatcher) | `daemon_channel_binding(uid, gid)` — binding includes `RealmId`-derived component; review computation for v3 identity model | +| `d2bd/src/provider_shutdown.rs` | `ProviderKind`, `ProviderShutdownTarget`, `ProviderRequestOutcome`, `ProviderGuestState`, `ProviderVmmExitOutcome`, `GracefulVmShutdown` trait, `CloudHypervisorShutdown` | Graceful VM shutdown via Provider RPC; maps provider-level guest states to d2bd VMM lifecycle | `packages/d2bd/src/provider_shutdown.rs` | `ProviderGuestState` field names may reference `WorkloadId`; adapt | +| `d2bd/src/child_realm_controller_bootstrap.rs` | `ControllerBootstrap`, `ControllerBootstrapError` | Bootstrap a child realm controller process: pre-bind sockets, spawn, deliver FDs | `packages/d2bd/src/child_realm_controller_bootstrap.rs` | `RealmId` / `ZoneId` in controller bootstrap identity fields; adapt | + +--- + +### 9.8 `d2b-realm-router` at Main — Routing and Exec Table + +The main version of `d2b-realm-router` has been significantly rewritten from v3 baseline. +The following additions are reuse sources: + +| File (a1cc0b2d) | Symbols to Copy/Adapt | Selected Behavior | v3 Destination | Excluded Assumptions | +|----------------|----------------------|------------------|----------------|----------------------| +| `d2b-realm-router/src/service_v2.rs` | `RealmSessionAuthority`, `RealmServiceServer`, `RealmServiceLimits`, `RealmServiceProcess`, `RealmClock` trait, `RealmServiceError`, `RealmMethod`, `RealmAuditEvent`, `RealmAuditOutcome`, `CredentialCustody`, `REALM_SERVICE_NAME`, `DEFAULT_MAX_REALM_BINDINGS`, `DEFAULT_MAX_SHORTCUTS`, `DEFAULT_MAX_MUTATION_RECORDS`, `DEFAULT_AUDIT_CAPACITY` | ComponentSession-based realm service: accepts ComponentSession from clients, validates `EndpointPolicy` authority, maintains binding table, routes to workload target (local vs gateway), emits bounded audit | `packages/d2b-realm-router/src/service_v2.rs` | `RealmSessionAuthority` contains `RealmId` field → retain as-is (will rename to Zone later); `locality()` / `purpose()` methods reference main's `Locality`/`PurposeClass` types; verify dependency | +| `d2b-realm-router/src/target_resolver.rs` | `RealmEntrypointTable`, `RealmEntrypoint` (host_resident/gateway_backed), `DispatchTarget`, `ResolveError`, `DEFAULT_MAX_REMOTE_NODES` (from remote_node) | Maps realm identifiers to local-resident or gateway-backed dispatch targets; `RealmEntrypointTable::with_local_default()` for single-realm setups | `packages/d2b-realm-router/src/target_resolver.rs` | `gateway_backed(gateway: impl Into)` references `RealmTarget` from v3 baseline; retain v3 type | +| `d2b-realm-router/src/remote_node.rs` | `RemoteNodeAvailability`, `RemoteNodeError`, `RemoteNodeErrorKind`, `DEFAULT_MAX_REMOTE_NODES`, `DEFAULT_HEARTBEAT_TIMEOUT` | Remote node state tracking and error taxonomy for constellation routing | `packages/d2b-realm-router/src/remote_node.rs` | References `RealmId` for node identity; adapt | +| `d2b-realm-router/src/execution.rs` | `DurableExecTable`, `DEFAULT_MAX_EXECUTIONS`, `ExecStartRequest`, `ExecAttachRequest`, `ExecLogsRequest`, `ExecCancelRequest`, `ExecutionSummary` | Bounded detached execution table: start/attach/logs/cancel with slot limits; used for `d2b vm exec -d` | `packages/d2b-realm-router/src/execution.rs` | `ExecStartRequest` contains `WorkloadId`; adapt to `ResourceRef` | +| `d2b-realm-router/src/session_lifecycle.rs` (main version) | `SessionLifecycle`, `SessionPhase`, `LifecycleError` | Simple per-realm session lifecycle FSM (distinct from `d2b-session`'s `SessionLifecycle` which is per-transport) | `packages/d2b-realm-router/src/session_lifecycle.rs` | No significant domain assumptions; retain | + +--- + +### 9.9 `d2b-daemon-access` — ComponentSession Daemon Access + +Main additions to this crate (the v3 baseline `d2b-daemon-access` implemented `DaemonAccessApi` +with TLS/relay; main adds the ComponentSession path). + +| File (a1cc0b2d) | Symbols to Copy/Adapt | Selected Behavior | v3 Destination | Excluded Assumptions | +|----------------|----------------------|------------------|----------------|----------------------| +| `d2b-daemon-access/src/component_session.rs` | `LocalDaemonSession`, `daemon()`, `guest(workload)` | Authenticated ComponentSession construction for local Unix socket; wraps `HostSocketConnector` + session engine; exposes `DaemonClient` and `GuestClient` | `packages/d2b-daemon-access/src/component_session.rs` | `guest(workload: &str)` — `workload` is a `WorkloadName` string; adapt to `ResourceRef` address format | +| `d2b-daemon-access/src/lib.rs` additions | `LocalUnixAdmissionSource`, `RemoteDaemonAccessAdmissionSource`, `RelayDaemonAccessAdmissionSource`, `LOCAL_UNIX_DAEMON_ACCESS_TRANSPORT_ID`, admission credential types | Admission source abstraction: local unix vs relay vs TLS; `LocalUnixAdmissionSource` is the primary v3 path | `packages/d2b-daemon-access/src/lib.rs` | Do NOT copy `direct_tls.rs` or `relay.rs` from main — those reference main's certificate infrastructure and relay credentials not present on v3 | + +--- + +### 9.10 `d2b-client` — Typed Async Client + +Main crate: `packages/d2b-client/`. Not present on v3 baseline. + +| File (a1cc0b2d) | Symbols to Copy/Adapt | Selected Behavior | v3 Destination | Excluded Assumptions | +|----------------|----------------------|------------------|----------------|----------------------| +| `d2b-client/src/client.rs` | `Client`, `ConnectedClient`, `MetadataInput`, `RetryPolicy`, `CallOptions`, `CancellationToken`, `Response`, `WallClock` trait, `SystemClock` | Typed async client: resolver-agnostic, connector-agnostic; retry policy; correlation/trace/idempotency metadata injection | `packages/d2b-client/src/client.rs` | `R: TargetResolver` — resolver type depends on v3 address format; `WorkloadName` in `DaemonLifecycleRequest::guest_proxy` → adapt to `ResourceRef` | +| `d2b-client/src/session.rs` | `ConnectedSession`, `ComponentSessionConnector` trait, `NamedStream`, `SessionFailure`, `SessionCall`, `SessionReply`, `SharedDriver` | Session-level client: drives `ComponentSessionDriver` for typed RPC; `NamedStream` for streaming services; `ComponentSessionConnector` for connection factory injection | `packages/d2b-client/src/session.rs` | No domain assumptions | +| `d2b-client/src/service.rs` | `ServiceHandle`, `GeneratedClient` enum, `MethodHandle`, `ServiceKind` | Per-service typed handle; `GeneratedClient` dispatches to the right generated protobuf client for each service kind | `packages/d2b-client/src/service.rs` | `ServiceKind` and `GeneratedClient` reference `SERVICE_INVENTORY` and all generated service clients from `v2_services`; copy together | +| `d2b-client/src/target.rs` | `TargetResolver` trait, `ResolvedTarget`, `ServiceOwner`, `TargetInput`, `RouteRecord`, `TransportKind`, `TransportSelection` | Target resolution: maps `TargetInput` to `ResolvedTarget` with a specific transport; extensible for local/relay/TLS | `packages/d2b-client/src/target.rs` | `ServiceOwner` enum may reference `RealmId` for realm-owned services; adapt | +| `d2b-client/src/host_socket.rs` | `HostSocketConnector`, `local_daemon_endpoint_identity` | `ComponentSessionConnector` impl that connects to local `d2bd.sock`; computes endpoint identity from `SO_PEERCRED` | `packages/d2b-client/src/host_socket.rs` | Socket path is `PUBLIC_SOCKET_PATH` constant — matches v3 `/run/d2b/d2bd.sock`; no change needed | +| `d2b-client/src/daemon_service.rs` | `DaemonClient`, `DaemonLifecycleRequest`, `DaemonMethod`, `DaemonTerminal`, `GuestClient`, `GuestOperation`, `GuestInspectCall`, `GuestCancelCall`, `GuestRetainedLogCall` | Typed daemon and guest service clients built on `ConnectedSession`; exec start/attach/logs/cancel; daemon lifecycle ops | `packages/d2b-client/src/daemon_service.rs` | `DaemonLifecycleRequest::guest_proxy(workload: &WorkloadName)` — adapt `WorkloadName` to `ResourceRef` address; `GuestClient` field names may reference ADR 0045 guest types | +| `d2b-client/src/error.rs` | `ClientError`, `RemoteErrorKind`, `RetryClass` | Client error taxonomy with retry classification | `packages/d2b-client/src/error.rs` | Clean copy | + +**Tests**: + +| Test File (a1cc0b2d) | What They Validate | +|---------------------|--------------------| +| `d2b-client/tests/client.rs` | Client driver/session integration; transport-agnostic end-to-end call | + +--- + +### 9.11 Noise-NK Key Material + +ComponentSession uses **Noise-NK** (static host key, ephemeral initiator key). +Every Zone runtime endpoint must have a static Noise keypair. Key generation: +- Must NOT be stored in Nix config or bundle (private key is never in a Nix store path) +- Provisioned through `d2b keys rotate` equivalent for Zone runtime identity +- Main uses `d2b-realm-core/src/identity_store.rs` + `identity_config.rs` for this path — copy/adapt those files alongside the session crates +- Exact key storage path and rotation mechanism → `ADR046-session-001` + +--- + +### 9.12 Symbols Excluded from Main Reuse + +The following symbols appear in main `a1cc0b2d` and MUST NOT be copied without explicit +per-symbol sign-off. Each exclusion states the exact reason. + +| Main Symbol / File | Reason Excluded | +|-------------------|----------------| +| `d2b-gateway/src/orchestrator.rs` — `GatewayOrchestrator` | References main's Zone resource model and `d2b.zones` Nix inputs; v3 has no Zone runtime | +| `d2b-gateway-runtime/src/aca_workload.rs` | ACA-specific workload lifecycle; depends on main's ACA Provider V2 registration path | +| `d2b-gateway-runtime/src/production.rs` | May reference main's `provider_registry_v3.rs` or Zone-scoped enrollment; verify before copy | +| `d2b-daemon-access/src/direct_tls.rs` | Direct TLS certificate infrastructure not present on v3 | +| `d2b-daemon-access/src/relay.rs` | Relay credential format changed on main; v3 uses `d2b-provider-relay` format | +| `d2b-contracts/src/v2_identity.rs` `ZoneId` field | v3 has `RealmId`; adapt field names during copy — do NOT change v3's `RealmId` type | +| `d2b-contracts/src/v2_guest_services.rs` | References main's ADR 0045 Guest service model; v3 guest service ops use `WorkloadOp` | +| `d2b-contracts/src/v2_guest_configured_launches.rs` | References main's configured launch model; adapt after `ADR046-provider-001` resolves | +| `d2b-contracts/src/v2_state.rs` | Main's resource-store state types; redb schema may differ from v3 D006 embedding | +| Any `resource-store.redb` integration in main | D006 embeds store in Zone runtime; validate redb schema version compatibility before copying | +| `d2b-provider-registry-v3.rs` (if present on main) | v3 uses `provider_registry_v2.json`; DO NOT replace v2 with v3 format silently | +| `DaemonClient::guest_proxy(workload: &WorkloadName)` | `WorkloadName` type on main may differ from v3 `WorkloadTarget`; requires explicit adaptation | +| Any reference to `d2b.zones` in main Nix modules | v3 Nix still uses `d2b.realms`; Zone rename is future v3 work (`ADR046-identities-002`) | diff --git a/docs/specs/ADR-046-decision-register.md b/docs/specs/ADR-046-decision-register.md new file mode 100644 index 000000000..02b416e55 --- /dev/null +++ b/docs/specs/ADR-046-decision-register.md @@ -0,0 +1,146 @@ +# ADR 0046 decision register + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-decision-register` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | ADR 0046 integrator | +| Depends on | None | +| Supersedes | None | + +This register records resolved architecture choices for the ADR 0046 normative +set. It contains design decisions and rationale, not conversation transcripts, +review metadata, or implementation status. + +## Resolved decisions + +| ID | Decision | Rationale and consequences | Affected specs | +| --- | --- | --- | --- | +| D001 | ADR 0046 is authored and implemented from protected `v3` at `b5ddbed6`, the final pre-ADR-0045 commit. | Main is not ancestry or current-state evidence, but any useful main-branch implementation may be copied/adapted through explicit reuse work items and independently integrated/validated against v3 contracts. | All | +| D002 | Keep number 0046 for cross-branch historical clarity. | The v3 branch has no ADR 0045, but the number distinguishes this replacement design from main-branch ADR 0045. | Parent, all specs | +| D003 | Reject kcp as the runtime substrate. | The spike proved desired object semantics but measured about 490 MiB RSS and a 176 MiB executable. v3 retains semantics without Kubernetes wire compatibility. | Resource store/API/reconciliation | +| D004 | Use one redb database per Zone. | redb supplies embedded ACID transactions, one writer with concurrent MVCC readers, crash safety, and a pure-Rust deployment shape. d2b owns schemas, indexes, watches, RBAC, operations, and routing. | Resource store | +| D005 | Use a single writer with optimistic conflicts. | Concurrent clients are accepted, but commits serialize. Every mutation has an expected revision; a stale client receives conflict and may re-read/retry. | Resource object/store/API/reconciliation | +| D006 | Embed the redb store and resource service in the Zone runtime. | This minimizes memory/process overhead and gives one clear owner. Provider/controller processes never access redb directly. | Resource store, process model | +| D007 | One fixed core-controller process per Zone hosts all trusted core controller handlers. | Separate handlers keep contracts explicit while one process avoids per-controller memory/process overhead. It uses d2b-bus like every other controller. | Core controllers, process model | +| D008 | Resource-plane acceptance is aggregate idle RSS <=64 MiB, readiness <=500 ms, p95 local reads <=2 ms, and p95 crash-safe writes <=10 ms on the pinned fixture. | The aggregate includes Zone resource service/store plus mandatory system-core and system-minijail controller processes. Optional Provider processes have separate budgets. | Resource store, validation | +| D009 | Controllers own their watch/coalescing queue and retry decisions; core minimizes unnecessary runs. | Core validates watch plans, maintains reverse indexes, suppresses irrelevant/converged/self-status events, coalesces revisions, and emits bounded hints. Provider logic remains independent. | Reconciliation | +| D010 | Every primitive is a complete low-level ResourceSpec. | Helpers, broker ops, syscalls, and sandbox fragments are implementation mechanisms, not primitives. Provider controllers compose behavior by creating primitive resources. | Resource object, primitive composition | +| D011 | d2b-bus over ComponentSession and a d2b transport is the only controller control-plane channel. | All get/list/watch/mutate/status/hint/checkpoint traffic uses the same authenticated route. There is no direct store, HTTP, or ambient IPC shortcut. | Bus/session/transport, resource API | +| D012 | Every Provider is one independently buildable crate/package; a Provider crate may build multiple process binaries. | The boundary supports separate controller/service/worker processes now and later extraction to one GitHub repository per Provider. | Provider model, every Provider dossier | +| D013 | ADR 0046 is a concise parent plus a manifest-bound `docs/specs/ADR-046-*` set. | The design is too large for one reviewable ADR. All specs are normative and accepted together. | Parent, all specs | +| D014 | Use one ADR/spec PR with user review before the final panel and again after it. | The panel runs only after the first approval. Any content change invalidates validation/panel evidence and repeats the final review. | Validation/delivery | +| D015 | Every field ending in `Ref` names another same-Zone resource using `/`. | Plain enums use plain names. Standard ResourceTypes are Zone-unique; vendor types are qualified; API binding collisions fail. | Resource object/API, all resource specs | +| D016 | Every resource belongs to exactly one Zone. | Refs never resolve across Zones. A Provider must be installed as a Zone-local `Provider/` resource before use. Cross-Zone resource relationships require separate review. | Zone, Provider, routing, resource API | +| D017 | Every Zone store has exactly one authoritative `Zone/` self resource; each non-root child stores one child-local `ZoneLink/` uplink, while compiler-only `parentZone` selects the parent allocator. | Parent access uses the child Zone API; a parent does not mount or mirror the child store and stores no reciprocal ZoneLink. | Zone resources/routing | +| D018 | Physical/local host execution contexts are `Host/` resources; VM, sandbox, cloud, and remote execution contexts are `Guest/` resources. | A Zone may have multiple Hosts reconciled by Provider/system-core and multiple Guests reconciled by runtime Providers. Process/EphemeralProcess uses one executionRef to either type. | Host/Guest, Provider model, process placement | +| D019 | Host and Guest share one ExecutionPolicy with plain `system|user` domains. | `defaultDomain`, `allowedDomains`, `defaultUserRef`, budgets, and attachment defaults are common. Process uses executionRef, optional domain, and conditional userRef; user-only Hosts/Guests reject system processes. | Host/Guest/Process specs | +| D020 | Resource ownership is singular. | Every resource has zero or one ownerRef. Any child mutation triggers owner reconciliation; owner cycles fail; deletion is child-first; UID binding prevents name-reuse confusion. | Resource object/store/reconciliation | +| D021 | `Provider/system-core`, `Provider/system-systemd`, and `Provider/system-minijail` are distinct. | `system-core` implements Host plus local User discovery/status only. systemd and minijail are interchangeable Process/EphemeralProcess implementations for Host or Guest execution. Volume, Network, Device, and Credential use separate Providers. | Resource catalog, Provider dossiers | +| D022 | Pidfd is mandatory local Process Provider behavior, not a ResourceSpec. | Minijail receives pidfd from clone3 and owns wait/reap. systemd binds InvocationID/cgroup/MainPID/start-time, opens pidfd, while systemd owns wait/reap. Pidfds never persist or cross the bus. | Process spec, Process Providers | +| D023 | Provider/controller descriptors declare supported Host/Guest Provider capabilities and Process domains. | ResourceTypes are declared once. Controller instances may run under several executionRef targets without host/guest-specific Process schemas. | Provider model, reconciliation, primitive composition | +| D024 | The initial task output is documentation only. | ADR/specs, manifests, indexes, changelog/instructions, PR, validation, and panel evidence are delivered. Future W0-W10 implementation requires a separate request. | Validation/delivery | +| D025 | Spec writing is parallel only after the shared foundation is stable. | File-disjoint Resource, core-controller, cross-cutting, and Provider dossier agents launch concurrently. Agents return decision-required rather than inventing shared contracts. | Validation/delivery | +| D026 | Standardize on ResourceType/ResourceSpec terminology and top-level `type`; do not use Kubernetes-style ResourceKind/kind terminology. | The vocabulary matches the native d2b model and canonical `/` refs. | All resource/controller specs | +| D027 | Every resource has `metadata`, `spec`, and `status`; common identity/Zone/revision/timestamps live in metadata. | Even resources with no desired fields carry `spec: {}`. Status is a separately authorized controller-owned subresource. | Resource object/store/API, every ResourceType | +| D028 | Status stores the latest bounded observation, not an embedded history. | Common status has numeric observedGeneration, phase, conditions, RFC 3339 timestamps, stable outcome code, optional exitCode, detailed bounded/redacted message, retryability, and ResourceType-specific typed fields. Earlier values remain in revision_log until compaction. | Resource object/store/API/reconciliation | +| D029 | The parent title is `ADR 0046: d2b 3.0 Provider control plane`. | It names the release and central architecture without exposing implementation-specific storage terminology. | Parent ADR/index | +| D030 | Reconciliation APIs and controller loops are asynchronous with hard local reaction targets. | Durable commit-to-controller-handler p95 is <=5 ms and ready Process commit-to-launch-attempt p95 is <=20 ms. Watch reception never waits for a running effect; independent resources reconcile/start concurrently within budgets; no polling/debounce delay. | Resource store/API/reconciliation, Process Providers, validation | +| D031 | Use `Process` for long-lived processes and `EphemeralProcess` for one-shot asynchronous processes. | They share a common execution spec and pidfd contract. EphemeralProcess reports terminal status directly and never references/creates a Process child merely to run. | Process resources/Providers, reconciliation | +| D032 | Replace File/Directory/ACL/FilesystemView ResourceTypes with one Volume ResourceType. | Volume has anchored relative layout entries, fine-grained owner/mode/access/default ACLs and lifecycle rules, named views, and same-Zone Host/Guest attachments. Process mounts select a volumeRef/view. | Volume, Process, storage migration | +| D033 | Volume supports Host-to-Guest transports such as virtiofs. | The Volume source may use an explicitly authorized Host path under its Provider policy; attachments name target executionRef, transport, mount path, view/access, and settings. The controller may own a Host virtiofsd Process and reports per-attachment export/mount status. | Volume/Host/Guest/Process specs and Providers | +| D034 | EphemeralProcess has outcome-specific terminal retention. | `successfulTtl` defaults to `1h`; `failedTtl` defaults to `24h`. TTL begins at status.completedAt and cleanup remains revision/finalizer/incident-hold safe. | EphemeralProcess, core cleanup controller, validation | +| D035 | Freeze the minimal standard ResourceType catalog. | Standard types are Zone, ZoneLink, Provider, Role, RoleBinding, Quota, EmergencyPolicy, Host, Guest, Process, EphemeralProcess, Volume, Network, Device, User, Credential, Endpoint, ResourceExport, and ResourceImport (19 standard types; Endpoint added by D092; ResourceExport and ResourceImport added by D096). Other behavior is inline or Provider-specific. | All resource/provider specs | +| D036 | The fixed core-controller process is also the `Provider/system-core` controller. | It creates/reconciles the first Host and Users and is one of two fixed Provider bootstrap exceptions. | Core controllers, system-core Provider, process/bootstrap | +| D037 | The universal status phase enum is `Pending`, `Ready`, `Succeeded`, `Degraded`, `Failed`, `Deleted`, or `Unknown`. | Conditions carry starting/deleting/retrying and other detail without multiplying common phases. | Resource object/API/store, all controllers | +| D038 | Final deletion has no retained resource tombstone. | After finalizers complete, core emits a Deleted status revision and removes the resource immediately. revision_log is the only deletion history; GET returns not found. | Resource store/API/reconciliation | +| D039 | ComponentSession uses Noise-based authenticated/record-protected profiles. | Copy/adapt the proven `d2b-session` and `d2b-session-unix` implementation from main, then reversion, integrate, and validate it against v3 Zone/resource/RBAC contracts. | ComponentSession/bus/security | +| D040 | ComponentSession and resource control use one authorization system. | Session authentication maps to a canonical resource subject; native Role/RoleBinding authorizes the closed resource verbs `get`, `list`, `watch`, `create`, `update-spec`, `update-status`, `update-metadata`, `update-finalizers`, `delete`, `use-credential`, and `admin-credential`, and the closed session verbs `connect`, `invoke`, `open-stream`, `relay`, `attach`, `cancel`, `observe`, `audit-export`, and `support-bundle`. `use-credential` uses only exact Credential operation subresources; `admin-credential` uses only exact lifecycle subresources and supplements ordinary CRUD. `audit-export` and `support-bundle` bind only their exact admin diagnostic service methods and grant no resource authority. `relay` permits only an authenticated ZoneLink/transport subject to forward an already-admitted invocation/stream one authorized hop; every forwarding hop independently requires relay plus the target verb, preserves named targets or nameless List/Watch filters exactly, and relay grants no CRUD, identity mapping, capability widening, or local lifecycle authority. Relay grants are core-generated/ZoneLink-scoped unless explicit durable local-admin policy permits an exact bounded grant; wildcard/self-asserted grants fail closed. Handshakes cannot self-assert roles; revision-bound leases revoke on policy change. | ComponentSession/bus/resource API/RBAC | +| D041 | Main is an unrestricted implementation reuse source, not the v3 baseline. | Work items may copy/adapt any useful main code. Each names the exact main commit/file/symbol/tests, proves the selected behavior, maps it to exact v3 destinations, and excludes unrelated ADR 0045 architecture. | Every implementation work item | +| D042 | Unsafe-local is not a separate v3 Provider. | It becomes a user-only Host using Provider/system-core, defaultDomain=user, allowedDomains=[user], and defaultUserRef. Its explicit no-isolation posture/warnings remain in Host status/UI; Processes use normal Process Providers. | Host/system-core/Nix/UI/reset | +| D043 | Freeze four Guest Provider families. | runtime-cloud-hypervisor, runtime-qemu-media, runtime-azure-container-apps, and runtime-azure-virtual-machine implement Guest. | Guest/Provider dossiers | +| D044 | Freeze two Volume Provider families. | volume-local owns anchored local storage/layout/ACL/views; volume-virtiofs owns same-Zone host-source→Guest virtiofs attachments and virtiofsd Processes. | Volume/Provider dossiers | +| D045 | Freeze one initial Network Provider. | network-local owns shared local fabrics; Azure/ACA networking remains in their Guest Providers until independently shared networking is required. | Network/Provider dossier | +| D046 | Freeze four Device Provider families. | device-tpm, device-usbip, device-security-key, and combined device-gpu (GPU/video) implement Device. Security-key owns unprivileged relay/frontend Processes; fixed broker only opens/passes hidraw. | Device/Provider dossiers | +| D047 | Freeze five interaction Provider families. | display-wayland, audio-pipewire, clipboard-wayland, notification-desktop, and shell-terminal own semantic resources/processes. Exec uses EphemeralProcess directly. | Interaction/Provider dossiers | +| D048 | Freeze three Credential Provider families. | credential-secret-service, credential-entra, and credential-managed-identity acquire/retain credentials and may deliver authorized token/signature bytes only through D055/D056 sensitive sessions. | Credential/Provider dossiers | +| D049 | Freeze transport/observability/activation Providers and omit an orchestrator. | transport-unix, transport-vsock, transport-azure-relay, observability-otel, and activation-nixos are initial Providers. Ordinary controllers perform composition; there is no orchestrator-standard Provider. | Provider dossiers, routing/telemetry/activation | +| D050 | Rename the non-host execution parent from Workload to Guest and add Host as a separate ResourceType. | Host excludes guests and is reconciled by system-core. Guest covers VM/sandbox/cloud/remote execution. Process/EphemeralProcess uses canonical executionRef to either Host or Guest; both share ExecutionPolicy. | All resource/provider/process/routing/Nix specs | +| D051 | Provider/system-minijail is the second fixed non-resource Provider/controller bootstrap process. | The first Process controller cannot itself depend on a Process controller. The fixed minijail controller launches/reconciles later Process resources, including the system-systemd controller. | Process/bootstrap/system Providers | +| D052 | Use compiled narrow bootstrap authorization before stored RBAC exists. | Only exact Provider/system-core and Provider/system-minijail subjects receive the closed initial recovery/config publication verbs. It is non-configurable and cannot be widened; normal stored Role/RoleBinding governs later work. | Resource API/authz/core startup/session | +| D053 | Use bounded group commit in the single writer. | Independently validated non-conflicting queued mutations may share one redb transaction/fsync and Zone revision with ordered ordinals; each caller receives its own result. Conflicting/dependent writes remain ordered/separate. | Resource store/watches/performance | +| D054 | Terminology/identity owns one shared AuthenticatedSubjectContext. | ComponentSession maps trusted transport/Noise/bootstrap evidence into it; d2b-bus/resource API consume it for Role/RoleBinding evaluation. No spec defines a parallel identity context. | Identity/session/bus/resource API | +| D055 | Credential Providers may deliver raw tokens or SignChallenge signatures to an authorized consumer Provider only over a dedicated end-to-end ComponentSession. | Sensitive bytes remain absent from resources/store/status/audit/telemetry. d2b-bus/Zone/relay intermediaries authorize then forward opaque protected records without decryption. Provider-level consumerRef is sufficient; its signed descriptor/RBAC selects the receiving component. | Credential/session/bus/security | +| D056 | Raw token delivery requires fully enrolled Noise_KK peers. | NN local and IKpsk2 bootstrap sessions cannot carry raw tokens. The session binds Credential/consumer Provider/component generations, audience/operation, deadline, route, schema, limits, and transcript. | Credential/session/enrollment | +| D057 | Every ResourceType/Provider spec includes Nix authoring, canonical rendering, and NixOS eval/build schema/reference validation. | Nix emits an integrity-pinned Zone generation. Removing a configuration-managed resource activates the new generation and deletes it asynchronously through owner/finalizers with visible Degraded cleanup status; controller-managed resources are not broadly swept. | All resource/provider/Nix/configuration specs | +| D058 | Nix authoring mirrors ResourceSpec directly. | Users set type, optional metadata.ownerRef/presentation metadata, and exact canonical spec. metadata.name/zone derive from the Nix attr path, apiVersion defaults, and status/UID/generation/revision/timestamps/finalizers/managedBy/configurationGeneration are omitted/read-only. | Every ResourceType/Provider/Nix spec | +| D059 | Every Provider crate has `src/`, `tests/`, `integration/`, and `README.md`. | src owns implementation/unit tests; tests owns hermetic Cargo/conformance tests; integration owns heavier Host/Guest/container scenarios; README documents complete Provider usage/contracts. Missing paths fail policy. | Provider model, every Provider dossier, validation | +| D060 | User resource name and OS username are separate. | metadata.name is the canonical Zone-local key; spec.osUsername is the actual bounded OS username resolved by system-core. | User/Process/Volume ACL/Nix specs | +| D061 | network-local reconciles runtime networks dynamically. | Closed broker operations create/delete bridges and apply state; mDNS is an owned Process; USBIP Processes remain device-usbip-owned. | Network/broker/device/Nix specs | +| D062 | Freeze Volume source/sharing/security defaults. | volume-local supports block-image and bounded tmpfs; requested quotas are hard/fail-if-unsupported; bounds are 1024 layout/64 views/64 attachments; virtiofs is single-writer by default with explicit supported shared-write; symlinks are relative/in-Volume only; typed User ACL refs and continuous repair apply. | Volume/Guest/Process specs | +| D063 | Freeze Device arbitration/status defaults. | First probe failure => Unknown, absent after 3; render-node may share, full/VFIO exclusive; Device attachment is desired state; security-key/frontend and other workers are Processes; conservative broker/FD limits apply. | Device/Process/broker specs | +| D064 | Freeze CLI context/loading/output/cutover. | Global --zone with host-command exemptions; local standard ResourceType validation and live vendor validation; lazy per-invocation Provider CLI; remove v2 aliases at cutover; non-TTY defaults JSON. | CLI/docs/tests | +| D065 | Full OTEL SDK/exporter runs only in optional Provider/observability-otel Process. | Mandatory Zone/core processes use lightweight bounded emitters; telemetry outage degrades/drops telemetry without blocking startup; audit remains authoritative. | Telemetry/Provider/process/performance | +| D066 | Freeze Zone/Nix trust and generation defaults. | ZoneLink transport providerRef is explicit/no default; built-in official signing root plus additional per-Zone roots; retain 3 prior generations by default, configurable 1–16. | ZoneLink/Provider trust/Nix/configuration | +| D067 | Unsafe-local successor isolation posture is not an OTEL dimension. | It remains explicit in Host status, CLI/UI, and authoritative audit only; no metric label/span attribute/log field. | Host/CLI/audit/telemetry | +| D068 | SignChallenge signatures use the same sensitive KK delivery channel as tokens. | Outer response carries only outcome metadata; signature bytes are end-to-end protected and opaque to intermediaries. | Credential/session/bus/security | +| D069 | Core-managed metadata.managedBy is `configuration`, `controller`, or `api`. | Configuration cleanup deletes only omitted configuration-managed resources; controller manages owned children; API-managed resources persist until explicit API deletion. | Resource object/API/Nix/cleanup | +| D070 | Nix derivations live in a separate named `d2b.artifacts` catalog. | Provider/Guest ResourceSpecs use plain artifactId/systemArtifactId fields. Nix builds/hashes closures and emits a private ID→type/digest/closure catalog; store paths never enter public resources. | Nix/Provider/Guest/package/trust specs | +| D071 | Zone.spec is empty; Zone-wide quota and emergency control are separate ResourceTypes. | Quota owns aggregate ceilings/usage and may be referenced by Host/Guest/Process; EmergencyPolicy owns disable scopes/actions/status. | Zone/Quota/EmergencyPolicy/core/Nix specs | +| D072 | ZoneLink uses explicit transportProviderRef plus signed settings/Credential refs. | It has no default/inferred transport. | ZoneLink/routing/Nix/transport Providers | +| D073 | Freeze Provider/RBAC bounds. | Provider: 8 controllers, 8 services, 32 worker templates, 16 ResourceTypes/controller. Role: 32 rules; rule max 16 types, 16 verbs, 64 names, 32 executionRefs. RoleBinding: 128 subjects. | Zone-control/Provider/RBAC schemas/tests | +| D074 | RoleBinding has no expiry field. | Revocation/lifecycle uses normal spec update or deletion, avoiding time-derived authorization state. | RoleBinding/authz/Nix/API | +| D075 | Canonical Provider ResourceSpec is exactly `{ artifactId; config }`. | Manifest/package/component/resource/status/generated properties (digests, trust/provenance, exported ResourceTypes/schemas, controller/service descriptors, worker templates, dependency aliases, permission claims, CLI projection, telemetry/state contracts, placement templates, upgrade/drain/restart policy) are resolved from the signed manifest/catalog entry the `artifactId` selects; none are authored spec fields or independently duplicated in the resource row. Core ProviderDeployment reads the signed manifest/catalog and creates the static component graph. | Provider model/packaging, all Provider dossiers | +| D076 | ProviderStateSet is the OPTIONAL, logical, query-time grouping of the Provider-owned Volume resources a Provider has explicitly declared (`ProviderStateSet(zone, provider-name)`); it is never a ResourceType or a stored row, and it is empty for a Provider that declares no state Volume. Core ProviderDeployment creates only the declared optional state Volumes from the signed state declarations, before launching the owning component's Process. It never creates an empty identity-only Volume, and a component that keeps its bounded non-secret operational state in resource `status`/the core Operation ledger declares no state Volume. Superseded in part by D087. | A component declares a state Volume only when a specific payload passes the D087 storage-need test (secret/sensitive private recovery data; large/binary/file payload; private data unsafe for status readers; or bounded but revision-unsuitable data with a demonstrated recovery need). Every declared state Volume still uses the canonical full Volume schema (extended per `ADR-046-provider-state` with `stateSchema`/`persistenceClass`/`sensitivityClass`/quota/sealing fields) and `User/` layout principals drawn from bounded, Nix-preprovisioned pools; each component mounts only its own declared view via `mounts`; there is no cross-component or cross-Provider sharing, and no separate non-Volume "compartment" concept. Resource rows, resource `status`, and the core Operation ledger remain the sole authority for resource refs, generation, backoff, idempotency, and session state; Provider state Volume payloads never duplicate this data. | Provider model/packaging, components/processes/sandbox, Volume resource, provider-state | +| D077 | No Provider process imports/calls the broker, receives a broker socket/DTO, or directly opens host paths/devices/systemd sockets/performs privileged mutation, including primitive Process/Volume/Network/Device Providers. | Providers validate/decide semantics and call injected async typed EffectPort traits using opaque resource/intent/template/policy IDs. Core/ProviderSupervisor (and the equivalent per-domain effect adapters for Volume/Network/Device) privately map effect calls to broker/allocator/systemd/user/guest/kernel operations. The broker remains the sole privileged executor and independent audit owner. | Components/processes/sandbox, primitive resource composition, Host/Guest/Process/User resources, Volume resource | +| D078 | Core ProviderDeployment creates static controller/service Processes and any *declared* optional per-component state Volumes (under the D087 storage-need test); a Provider controller never bootstraps its own Process. | Controllers may create authorized dynamic child Process/EphemeralProcess/primitive/vendor resources. Services needing worker help send typed internal requests to their controller. Workers have no ResourceClient, d2b-bus/dependency portal, Credential access, CLI, broker, or child-spawn authority; all resources/FDs/config are inherited via LaunchTicket. A narrowly declared worker may own its exact workload child only when that child is the worker's manifest-fixed data-plane purpose (e.g. a shell supervisor owning its shell), under explicit descriptor policy. | Provider model/packaging, components/processes/sandbox | +| D079 | UserNamespaceSpec drops numeric host UID/GID from the public Process SandboxSpec in favor of a frozen semantic `mappingClass` field. | `mappingClass: process-principal-root` is the sole frozen value: it maps in-namespace UID/GID 0 to the host UID/GID of the Process's resolved principal (a `User/` resource identified by the owning template/ownerRef). Core resolves the exact host UID/GID and writes uid_map/gid_map only into the private LaunchTicket/effect-adapter state; the `user` NamespaceClass and ADR 0021 mapped-root requirements remain unchanged and enforceable. | Host/Guest/Process/User resources, components/processes/sandbox, Volume resource | +| D080 | Provider-specific ResourceTypes use the single frozen qualification grammar `.d2bus.org.` on the project's public domain `d2bus.org`; standard ResourceTypes (the D035 frozen catalog) stay unqualified. | This freezes the qualification grammar used by Provider dossiers (e.g. `display-wayland.d2bus.org.WaylandSession`) as the one normative form referenced by D015. Provider-defined qualified ResourceType names, and every Provider spec/status extension schema ID, use the same `.d2bus.org` subdomain (e.g. `display-wayland.d2bus.org/WaylandSession/spec`). d2b 3.0 is a clean reset: the earlier placeholder `*.d2bus.org` / reverse-DNS `d2bus.org.*` forms are removed entirely with no aliases. Any Provider-specific type name in a spec/table that is not part of the D035 catalog must carry this qualification. | Provider model/packaging, primitive resource composition, all resource specs and Provider dossiers | +| D081 | Transport Providers are carriage services only; they never own ZoneLink. | The core ZoneLink handler alone reads/writes/finalizes ZoneLink and owns Noise/session/reconnect/route/idempotency/intent state, calling typed `OpenTransport`/`CloseTransport`/`ObserveTransport` on the installed Transport Provider. Transport Providers return only opaque `OwnedTransport`/byte-stream handles and observations; they hold no ZoneLink state. | Provider model/packaging, ZoneLink/routing | +| D082 | Volume `source.settings` never carries a raw host path in the authored/public spec; `local-path` and `block-image` sources use an opaque `sourcePolicyId` bound to a Provider-declared, Nix-authored allowlist policy entry. | The volume-local Provider's private root `config.allowedHostPaths` catalog (each entry carrying its own `id`) is private Nix/bundle/effect authority; the Volume spec references an entry by `sourcePolicyId` only. The Provider and its controller see the opaque ID and semantic settings only; raw path resolution happens inside the private effect-adapter/broker path, never in the Provider process. `tmpfs` continues to have no host path and requires hard `maxBytes`/`maxInodes`. | Volume resource | +| D083 | volume-local remains the single owner of the Volume ResourceType (layout/spec/ownership); volume-virtiofs is a separate attachment-implementation Provider that never writes Volume layout/spec/ownership fields. | volume-virtiofs writes only its own owned virtiofsd Process children and the per-attachment status entries it is authorized for; two controllers never write the same Volume resource row. This preserves DRVOL-010's private-socket-path contract while generalizing to any future non-virtiofs attachment transport. | Volume resource | +| D084 | Finalizer/deletion ordering is fixed and never force-clears an undrained finalizer. | The owning Provider controller drains/deletes its effects/children and clears its own finalizer; the core resource-store transaction then writes an event-only `Deleted` revision and removes the row/index atomically; the audit subsystem appends the deletion record afterward using a dedup/exactly-once recovery key. Generation-retention pruning removes only the historical bundle record, never a live resource's finalizer state; a resource whose finalizers have not drained remains Degraded/blocked until it finishes normally. | Volume resource, Host/Guest/Process/User resources, resource store/reconciliation | +| D085 | Provider status is derived, not self-declared: core computes aggregate Provider status from component/dependency/process health. | Provider controllers write status only for the ResourceTypes they own and for children/status fields they are explicitly authorized to write; a Provider never authors its own aggregate Provider-resource status. | Provider model/packaging | +| D086 | No mandatory per-component state Volume exists, so no bootstrap state-Volume cycle and no bootstrap-storage exception are required. Superseded by D087. | The fixed bootstrap components (`system-core`, `system-minijail`, and the first `volume-local` controller instance on each execution target) keep their bounded non-secret operational state in resource `status` and the core Operation ledger and declare no Provider state Volume. Because no component requires a state Volume before a `volume-local` instance is Ready, the previously specified closed bootstrap cycle and its per-execution-target local bootstrap storage mechanism are removed entirely. If future bootstrap code needs secret or large private recovery state that cannot enter status, it must be introduced through a new reviewed design; there is no hidden bootstrap store and no implicit framework-created bootstrap Volume. | Components/processes/sandbox, Volume resource, provider-state, decision register (D087) | +| D087 | Bounded non-secret controller/Provider operational state belongs in the owning resource's `status` subresource whenever possible; separate durable state storage (a Provider-owned Volume) is justified only for secrets or sensitive private data, large/binary/file content, or data unsuitable for the revisioned status API. | Resource `status` is the default durable observation and recovery surface for bounded non-secret operational state (reconcile stage, opaque non-authorizing external handles/IDs/digests, adoption observations, counters, closed-enum details, dependency readiness, last successful checkpoints). It is revisioned, optimistic-status-writer controlled, RBAC-readable, redacted, and reverified against external reality after restart. The spec remains the desired-state authority; status is observation only and is never a host-mutation or repair authority. A component declares a state Volume only when a payload passes an explicit storage-need test: secret/sensitive private recovery data; large/binary/file payload; private data unsafe for authorized status readers; or bounded-but-revision-unsuitable data with a demonstrated recovery need. The core Operation ledger owns in-flight idempotency/retry/transaction progress; resource status owns the latest bounded result/checkpoint; authoritative audit owns security history; OTEL owns metrics/traces; content streams stay in owning-process memory; the Nix/private artifact catalog owns paths and artifact integrity. Status must never carry secrets, raw tokens/keys/PSKs, authority-conferring credential handles, private endpoint/path/argv/env/PID/unit data, terminal/clipboard/CTAP bytes, raw cloud error bodies, large binary blobs, unbounded collections, or churn-heavy content. | All | +| D088 | Resource `status` has a frozen three-layer shape: (1) a universal `ResourceStatus` base present on every resource; (2) a ResourceType-common `status.resource` object that extends the base with provider-neutral fields required across ALL implementations of that ResourceType and its cross-resource/provider consumers; and (3) an optional bounded Provider-specific `status.provider` extension for implementation-only observation. | The universal base carries `observedGeneration`, common `phase`, `conditions`, `lastReconciledAt`/`startedAt`/`completedAt`, and a bounded `outcome` (D028/D037). `status.resource` is the frozen provider-neutral typed object for the ResourceType (e.g. Guest runtime readiness/capabilities, Device claim base, Credential lease metadata, Volume attachment base). `status.provider` is `{ providerRef, schemaId, schemaVersion, observedProviderGeneration, details }`: the Provider-specific extension builds on — never replaces, overrides, or duplicates — the universal/ResourceType-common fields. Generic API/CLI/controllers depend only on the universal base plus `status.resource`; any field shared across implementations is promoted to `status.resource`, never copied across provider extensions. The `status.provider.details` schema is signed/registered with the Provider, strict unknown-field-denied, size/cardinality bounded, redacted/non-secret, and versioned; the owning controller writes all present layers atomically in one status mutation. Status-first state mapping: shared state → `status.resource`; implementation-specific bounded non-secret state → `status.provider.details`; secret/large/private state → an optional Volume (D087). | Resource object/API/store/reconcile, Nix/security/validation/feasibility, Provider model/packaging, all Provider dossiers | +| D089 | Desired `spec` mirrors the D088 three-layer shape with symmetric frozen names: (1) universal Resource envelope/metadata rules; (2) a ResourceType base Spec schema carrying every provider-neutral required/optional field and semantic shared across implementations (including `spec.providerRef`, which stays base); and (3) a single canonical selected-Provider extension envelope `spec.provider = { schemaId, schemaVersion, settings }`. | The canonical `spec.provider` envelope replaces the inconsistent Host/Guest/Credential `providerSettings`, Device `settings`, and all ad hoc extensions; names are frozen symmetric to status (`spec.provider.settings` mirrors `status.provider.details`; `spec.provider` omits `providerRef`/`observedProviderGeneration` because `spec.providerRef` is base and spec is desired, not observed). Every Provider `ResourceApiBinding` declares and MUST implement the exact ResourceType base schema version/fingerprint and pass base lifecycle/status/error/finalizer/conformance; it MUST accept the canonical minimal valid base Spec; it MAY reject an optional base capability only via its signed standard capability matrix and a typed provider-neutral `unsupported-capability`, and MUST NOT ignore, reinterpret, rename, duplicate, or weaken the bounds of any base field, or require extension data for base-required behavior. The `spec.provider` extension is optional unless the selected Provider's signed schema requires it; it is strict deny-unknown, bounded, versioned/digested, validated against `spec.providerRef` at Nix build and API admission, carries implementation-only desired settings, and cannot shadow a base field. Shared fields/semantics are promoted to the ResourceType base. Generic CLI/controllers operate on base spec and base status; the selected `spec.provider` schema and the `status.provider` schema align for the same Provider. Qualified Provider-defined ResourceTypes carry the universal envelope, their own type-base spec, and an optional implementation extension. The `Provider` resource's `{ artifactId, config }` self-description remains the one documented exception, because a Provider cannot carry a non-circular `spec.providerRef`. | Resource object/API/store/reconcile, Nix/security/validation/feasibility, Provider model/packaging, all resource specs and Provider dossiers | +| D090 | Resource `Create`/`UpdateSpec`/`Delete` support an authorized expedited (`waitForReconcile`) mode: under one mutation ticket/`operationId`/deadline, Core admission plus a reserved-revision redb transaction run in parallel with the owning controller's preflight/plan, but the controller performs NO external effect, finalizer release, or status mutation until Core supplies a typed `CommittedRevisionProof { resourceUid, generation, revision, operationId }`; a DB failure sends Abort and no effect occurs, and a durable commit is never rolled back because reconcile later fails or times out. | The API returns success only if the initial resource mutation durably committed. Commit also enqueues the ordinary reconcile hint; the expedited request enters a bounded priority lane into the SAME per-resource single-flight executor, and any normally queued reconcile stays queued and runs after the expedited pass. All effect IDs/idempotency keys derive from `(UID, generation, revision, operationId)`; a normal re-entry observes converged/progressing state and no-ops/rejoins, never duplicating. The API waits in parallel for durable commit plus one expedited reconcile pass and returns the committed resource object, the post-pass projected layered status, `statusPersistence: pending|committed`, and the last persisted status revision; status persistence is asynchronous and the response need not wait for it, so no uncommitted status is represented as durable. Delete returns an event-only Deleted projection / not-found outcome. Expedited completion means one pass reaches a `Converged|Progressing|Blocked|UpgradeRequired|Failed` disposition, not long-running external Ready; timeout/cancel after commit returns a typed committed-but-reconcile-pending response while the ordinary queue continues. Priority quotas/fairness prevent starvation/DoS; only authorized UX mutations/core may request expedited mode, and the admin `resource reconcile` CLI action may reuse it. | Resource API/store/reconcile, core controllers, CLI, security, validation/feasibility, controller toolkit | +| D091 | The universal status base gains a provider-neutral bounded `status.update` currency object on every resource, and controllers expose `assess_update`/`plan_upgrade`/`execute_upgrade`; a controller MUST return `UpgradeRequired` rather than apply a disruptive in-place change, while non-disruptive changes reconcile normally. Upgrades preserve Resource UID/spec identity and durable/state/secret Volumes and TPM identity where possible, recycling only realization/owned ephemeral Processes/endpoints; `Replace` of the resource-row identity is used only when explicitly required and planned with ownership/state transfer, and full factory reset remains separate and destructive. | `status.update` carries `state` (`Current|UpdateAvailable|UpgradeRequired|Upgrading|Blocked|Unknown`), a closed `reasons` enum (`CoreGenerationChanged, ProviderGenerationChanged, ArtifactChanged, ImageOrSystemGenerationChanged, SpecChanged, DependencyChanged, SecurityPolicyChanged`), bounded non-secret observed/target generation/digest IDs, `disruption` (`None|Reload|Restart|Recycle|Replace`), `preserveState`, optional `operationId`, `lastAssessedAt`, and bounded/truncated owned/dependency aggregate counts and refs. These shared currency fields are universal/ResourceType base per D088 and are never duplicated in a provider extension. Core invokes dependency/owner triggers and aggregates self/owned/dependency currency for list/get; a dependency-aware planner topologically drains/recycles/restarts affected owned/dependent resources (e.g. a GPU marks `UpgradeRequired`/`Blocked` while applications depend on it, then drains dependent Processes/Guests, recycles the GPU realization, and restarts dependents — no surprise disruption). Triggers include d2b/core rollout, Provider package generation, selected artifact/image/NixOS generation, an immutable spec field, and dependency updates. Spec adds a provider-neutral update policy in the universal/ResourceType base per D089 (manual disruptive default; auto non-disruptive permitted); a `spec.provider` extension may add implementation knobs but cannot bypass the disruption policy. The core Operation ledger persists upgrade operation/idempotency/progress and resumes after crash/restart; per-resource single-flight serializes reconcile/upgrade; status carries only the latest bounded plan/result, not a second ledger. CLI list/get always show self/owned/dependency update state and disruption; `resource upgrade ` plans by default and `--recursive --apply` executes. | All resource specs, resource API/store/reconcile, core controllers, CLI, Nix, security, reset, telemetry, validation/feasibility, all Provider dossiers | +| D092 | Stable managed endpoint identities (TPM `endpointId` and similar) MUST be proper resources, not opaque IDs, unless demonstrably controller-internal or high-churn. A general **promotion test** governs every entity: it MUST be a ResourceType when any of these holds — stable across a reconcile pass/restart; independently lifecycle/readiness/finalizer/lease managed; referenced by another resource/controller/Provider; independently subject to authorization/policy/audit; visible in resource status/CLI; or required by the ownership/dependency graph. An opaque ID/handle is permitted only when it is confined to one controller/process, not API/status-visible as a locator, has no independent lifecycle/authz, and/or is per-connection/high-churn (pidfd, fd index, named stream id, transport byte-stream handle, reconcile operation token). Every retained public `*Id`/`Handle` documents its rationale. | A new standard ResourceType `Endpoint` (the 17th type at D092; D096 later raises the frozen catalog to 19) has the D089 layered spec and D088 layered status. Endpoint base spec: `providerRef`, `producerRef` (`Process`/`Device`/`Guest`/`Host` or qualified resource), `endpointClass` (`service|device|transport|control|data`), `transport` (closed `unix|vsock|tcp|fd-attachment|opaque-carriage`), `purpose`, service/schema fingerprint or capability class, locality/visibility, attachment support/policy, consumer policy/ref bounds, lifecycle/recycle policy — and NO raw path/address/CID/port/fd/credential. Endpoint base status: readiness, observed producer/resource generation, connection/lease availability, endpoint generation, closed capability/locality/transport observations, `status.update` currency, conditions, optional implementation extension, and NO raw locator. Core/ProviderSupervisor resolves an `Endpoint` resource to a private transport/FD only through EffectPort/LaunchTicket. Endpoints are controller-created by the signed component/semantic owner (static Endpoints may be Nix/API-authored only where the schema permits); `ownerRef` gives child-first deletion; consumers use `Endpoint/` ResourceRefs. Stable endpoints are migrated to `Endpoint` resources: TPM `tpmEndpointId`, GPU `crossDomainEndpointId`/sidecar sockets, Wayland cross-domain/proxy endpoints, vhost-user sound/video, QMP/CH control, guest-control vsock, notification/clipboard/shell persistent service endpoints, stable Provider service endpoints, and stable transport listener bindings. `ProcessSpec` inline `endpoints` is removed; a stable endpoint a Process produces is an owned `Endpoint` resource with `producerRef: Process/` (no duplicate inline field). The virtiofs `Export` resource remains the lifecycle owner and references its `Endpoint` where independently consumed. Per-session named streams/`OwnedTransport` handles remain internal/high-churn (not Endpoints); an allocator stable listener may be an Endpoint if its lifecycle is visible. No compatibility alias for prior fields (clean reset). | All resource specs, resource API/store/index/reconcile, core controllers, CLI, Nix, security, reset, validation/feasibility, parent ADR, all Provider dossiers | +| D093 | `Provider/credential-entra` login and token acquisition are grounded in an Entrablau-enabled identity `Guest`, never an abstract/in-process Host login. The controller is secret-free; each Entra `Credential` binds an exact same-Zone `identityGuestRef: Guest/` and a stable `Endpoint/` implementing a standard typed Entra login/token service inside that Guest. There is no Host login/token chain, `DefaultAzureCredential`, env, DBus, path, or browser fallback. | The identity Guest's NixOS system is consumer-composed with the sibling `vicondoa/entrablau.nix` (Himmelblau/Entrablau) module; d2b core never imports the sibling flake. The Guest package supplies the manifest-declared login-service Process + `Endpoint` contract and owns its secret/token/machine-credential storage, TPM binding, enrollment, refresh-token state, and interactive login implementation inside the Guest (guest-local, secret/large/private — outside resource status). The Credential ResourceType-common base adds shared interactive-login operations `BeginLogin`/`ObserveLogin`/`CancelLogin`, a closed `interactionState` (`NotRequired|Required|Starting|AwaitingUser|Authenticated|Failed|Unknown`), bounded non-secret challenge/progress metadata, and login-session generation/deadline; the provider extension carries only Entra-specific non-secret modes/reasons. No device code that confers authority, URL, cookie, token, or user PII appears in status/audit/OTEL; the interactive byte/UI stream is a named ComponentSession stream direct to the Guest login service. `BeginLogin` opens an authenticated end-to-end session to the Entrablau service, which conducts the browser/device/desktop login inside the Guest; on success the Entra agent requests an access-token lease on demand and the raw access token is delivered end-to-end (Noise_KK) only to the exact authenticated consumer process permitted by the Credential `consumerRef`/scope/RBAC — Host/bus/intermediate controllers see ciphertext only. Refresh tokens and private login state never leave the Entrablau Guest; there is no cross-Zone ResourceRef. Expedited reconcile (D090) may return `InteractionRequired`/projected status after create/update but never waits for human login past the deadline. The token API client stays injected/typed but its production implementation is the Entrablau Guest `Endpoint`; the credential-entra controller/agent performs no direct Entra network egress (Entrablau owns the Entra network/TLS flows). Reset preserves or explicitly destroys the identity Guest TPM/login state, never silently wiping it. ACA and Azure VM consumers obtain their access tokens over end-to-end KK from the identity Guest. Entrablau is a sibling/external component; target integration is ADR-only, validated by a feasibility spike with a fake Entrablau service (and manual real-Guest login), with no live Entra in CI. | Credential resource, credential-entra dossier, runtime-azure-container-apps/runtime-azure-virtual-machine consumers, resource API/reconcile, CLI, Nix, security, reset, validation/feasibility, provider catalog/work items | +| D094 | Every `src/` unit test and crate `tests/` hermetic suite MUST execute fast so the hermetic suite is the default inner loop on every change; all slower coverage belongs only in `integration/`. Execution-time budgets are measured after build (warm cache, compilation excluded) against a recorded reference runner: an individual normal hermetic test has p95 ≤50 ms with no wall-clock sleep; each Provider crate `cargo test -p d2b-provider-- --lib --tests` executes in ≤2 s; all 27 Provider hermetic suites execute in ≤30 s aggregate wall when sharded and ≤60 s single-host; each Layer-1 hermetic shard executes in ≤60 s. Bounded cryptographic/property tests are the only classified exception: each is named individually, declares a capped case count, and declares its own higher per-test budget; nothing else may sleep, retry on wall clock, spawn a process, use containers/network/DBus/systemd/broker daemon/Nix eval or build/KVM/USB/GPU/TPM hardware/live cloud, or build filesystem trees beyond tiny temp fixtures. | Hermetic tests are in-process only: deterministic fake clock/RNG; fake ResourceClient/EffectPort/broker/transport/credential/systemd; in-memory or tiny-temp bounded redb fixtures; parallel-safe with no global mutable or shared ports/paths; exact bounded property case counts. `integration/` owns any real process, socket rendezvous, container, Nix eval/build, guest VM/KVM/TCG, broker/systemd/DBus, real filesystem quota/mount/namespace, hardware, or live cloud/Entrablau; it may be slower but still has a lane timeout/budget, parallel isolation, and fake external services by default, with live/hardware kept a separate manual tier. A test violating placement is a policy failure and MUST move to the correct tier — never gain a sleep, larger timeout, or `#[ignore]`. Cold compile time is tracked and optimized separately via shared cache and dependency discipline; correctness never depends on any cache. A machine-readable test-runtime ledger plus a timing gate built on the existing tooling (no new test framework) measure execution-only time and enforce per-test/crate/shard budgets, top-slow-test reporting, and a historical regression threshold as a CI artifact. Legacy overhead is removed, not accumulated: when ADR 0046 replaces a behavior, the minimum reusable semantic assertions migrate into the new hermetic suite and the old duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest entries are deleted once successor coverage and removal proof pass; old and new suites never run indefinitely. Every current-code migration/replacement work item names the exact old test selectors/files with a keep/adapt/move/delete disposition and a removal gate, and updates `tests/layer1-jobs.json`, closed gate manifests, flake/matrix/Nix-unit pins, generated ledgers, and CI workflow shards. No new top-level `tests/*.sh` gate is added; Rust unit/property and existing generated drift/policy gates are preferred. The eight-wave entry/exit criteria require the fast hermetic suite as the default inner loop on every change, with integration running after focused green and concurrently in later lanes. | Validation/delivery, feasibility/spikes, streamline, current-code migration map, provider model/packaging, all Provider dossiers, test layout | +| D095 | The ADR 0046 set MUST ship a generated, machine-readable implementation DAG (`docs/specs/ADR-046-implementation-graph.json` plus the human view `docs/specs/ADR-046-implementation-graph.md`) that maps every one of the 55 member specs and every work item exactly once to a dependency-ordered launch wave (`W0`–`W7`) and a file-disjoint parallel group, so no author has to re-derive launch order or parallelism from prose. The graph derives its source spec DAG from `ADR-046-spec-set.json.members[].dependsOn` and its 8-wave implementation topology from `ADR-046-validation-and-delivery.md` §3; it adds nodes for specs and work items, typed edges (`spec-depends-on`, `work-item-depends-on`, `implements-spec`, `shared-contract`, `file-overlap-order`), exact wave, parallel group, owner/destinations, entry contracts, prerequisites, blockers, exit gate, and topological rank. | The graph is a generated non-member artifact: it is NOT one of the 55 `ADR-046-spec-set.json` members and does not change that count, but it is committed, deterministic (no timestamps/host paths), regenerated after any spec/work-item edit, and drift-gated. It MUST embody the anti-serialization invariant: every ready, file-disjoint parallel group launches concurrently; a same-wave dependency is a prep-barrier edge (`shared-contract`/`file-overlap-order`), never a reason to serialize a whole wave. Dependencies of any node MUST resolve to an earlier wave or an explicit same-wave prep barrier that precedes its consumers. `ADR-046-validation-and-delivery` owns the artifact contract, generation command, validation rules, and the ready-wave query; `ADR-046-streamline`'s spec-registry generator (`ADR046-streamline-001`) emits the files; the graph carries no normative authority beyond the specs/work items it indexes. | Validation/delivery, streamline, spec-set/work-items manifests, parent ADR index, all specs and Provider dossiers | + +| D096 | Scarce physical or singleton capabilities are shared across Zones only through `ResourceExport` and `ResourceImport` (19 standard ResourceTypes total) and a qualified semantic owner Service. The backing stays authoritative in one owner Zone; consumers never use a cross-Zone ResourceRef or open the backing directly. | `ResourceExport.resourceRef` names only the local owner Service. The signed semantic projection factory binds that Service type to its matching Binding type, allowed backing/target refs, and exact base-schema/factory fingerprints. Core owns routing and import lifecycle and creates exactly one same-type non-authoritative projection Service with `ownerRef: ResourceImport/`; it never projects Device, Endpoint, or Binding and never authors consumer intent. Nix/operators author same-Zone Bindings separately; Provider controllers realize Binding-owned Process/Endpoint children. Payloads use bounded encrypted named streams; leases, sessions, ceremonies, transfers, and streams are non-resource records. Revocation degrades the projection and its Bindings, and finalization waits for Bindings to be removed or retargeted without cascading them. Credential/token resources remain non-exportable absent a future reviewed semantic family. | Zone control, zone routing, resource object/API/store/index/reconcile/status, ComponentSession/bus, core controllers, Nix, CLI, RBAC/security/audit/telemetry, reset, upgrade, feasibility, validation/waves, implementation graph, provider model/state, audio-pipewire/device-security-key/observability-otel/device-usbip dossiers, all resource specs | +| D097 | Every scarce or singleton backing — a physical device, a singleton external service, a per-Zone runtime service, a per-Host/user/seat service, a fixed listener/store, or a globally-unique policy — is governed by a signed **`AuthorityDescriptor`** so exactly one authority owns/opens each backing and duplicate opens/effects are rejected before they happen. The descriptor is a provider-neutral declaration attached to the owning typed Resource and its owning Provider; it does **not** introduce a new opaque public ID and does **not** add a redundant ResourceType unless an audit proves no existing type (`Device`/`Network`/`Provider`/`Endpoint`/`Host`/`Guest`/`User`/`Zone`/…) can own the lifecycle. Preserves D096 (ResourceExport/ResourceImport remain the only cross-Zone sharing mechanism; a shared/multiplexed backing still has exactly one authority owner). | `AuthorityDescriptor` fields: `authorityScope` (`global\|zone\|host\|guest\|user\|seat\|physical-device\|external-service`); a canonical **opaque** `authorityKey` class (no raw path/serial/address); `cardinality` (`exactly-one\|zero-or-one\|bounded-many` + bound); `arbitration` (`exclusive\|shared\|multiplexed\|partitioned`); the single authority `resourceRef`/owner service; a typed `duplicateConflict` error; an adoption/restart **owner proof** (process/resource identity); an update/recycle strategy; `exportability` (`forbidden\|explicit-export`); and a quota/fairness policy. Core maintains an **authority index** keyed by `(Zone/scope, authorityClass, opaqueKeyDigest)` and rejects a conflicting authority Resource/Process before any external effect. `authorityKey` is internal and non-authorizing (never a locator, never an authz principal); the stable authority itself is the existing typed Resource. One authority service Process/resource opens/owns the physical/singleton backing; consumers get local refs/projections/leases/streams (cross-Zone via D096) with no duplicate direct opens. The ResourceType **status base** exposes bounded provider-neutral authority state (`available`, current holder count, queue depth, arbitration, update currency) with no raw identity; provider extension carries implementation detail only. The **spec base**/provider capability declares the requested share mode and cannot bypass the descriptor. Duplicate config/API requests produce a deterministic conflict with no second effect; config activation goes `Degraded` naming the exact owner digest. Restart adopts the exact authority by process/resource identity; ambiguity quarantines. D091 upgrade drains consumers then recycles the authority; reset preserves or destroys the underlying state per an explicit per-authority disposition. An audit matrix classifies every physical/scarce, per-user/session, and core/singleton authority across the 27 Providers and 19 ResourceTypes as shareable/exportable, singleton-but-not-cross-Zone-exportable (e.g. audit chain, broker, resource store), or scoped bounded-many; the provider-neutral generic contract is frozen here while per-Provider classification is refined by evidence with conservative existing-behavior defaults and no open placeholders. CLI exposes `resource authorities` (list authorities/holders and any conflict/owner digest). | Resource object/API/store/index/reconcile/status, provider model, Zone control/routing, ComponentSession/bus, Nix, CLI, RBAC/security/telemetry/audit, reset, upgrade, feasibility, validation/waves, implementation graph, all resource specs and all Provider dossiers | +| D098 | Freeze four semantic, provider-neutral qualified Service/Binding pairs with no aliases: `audio.d2bus.org.AudioService` + `audio.d2bus.org.AudioBinding`, `security-key.d2bus.org.SecurityKeyService` + `security-key.d2bus.org.SecurityKeyBinding`, `telemetry.d2bus.org.TelemetryService` + `telemetry.d2bus.org.TelemetryBinding`, and `usb.d2bus.org.UsbService` + `usb.d2bus.org.UsbBinding`. | Each family has one discoverable common base spec/status contract and fingerprint shared by every implementation. `providerRef` remains the base implementation selector; only authored owner Services/Bindings may carry a strict `spec.provider`, and every implementation accepts the canonical minimal base without it. Core-generated projection Services carry `providerRef`, semantic base/import fields, and `ownerRef: ResourceImport/`, but never `spec.provider`; routing derives from the signed local Provider descriptor, `providerRef`, and ResourceImport record, while implementation observations may appear only in `status.provider`. The authority-mode Service is the D097 backing authority and the only export target. ResourceImport invokes Core's signed factory to create exactly one same-type projection Service; Endpoint, Device, and Binding are never projections. The semantic factory fingerprint binds factory fields plus the projection-protocol version, never Provider or adapter identity; the signed Provider descriptor authenticates those identities separately. A Binding is Nix/operator desired consumer intent with same-Zone `serviceRef` and target; its Provider controller creates Binding-owned Process/Endpoint children, and observations live only in status. Sessions/streams remain non-resource records. `Provider/audio-pipewire`, `Provider/device-security-key`, `Provider/observability-otel`, and `Provider/device-usbip` are initial implementations, not type namespaces; future Providers may implement the identical bases. USB remains policy-gated. Any USB or security-key implementation backed by a physical USB device must first claim the identical Core-derived Host-global tuple `(Host, physical-usb-backing, opaqueKeyDigest)`, where Core computes the digest only after trusted physical-USB identity resolution; service-specific authorities may exist in addition, but cannot replace this shared claim. PipeWire, CTAPHID/hidraw, OTLP/OTEL/SigNoz, and USBIP details are forbidden from the base schemas, type names, conditions, errors, and aliases. | Parent ADR/index, decision register, provider model/catalog, resource object/API/reconcile/Zone control/routing, Nix, CLI, security, reset, validation, feasibility, streamline, and the four Provider dossiers | + +## Open decisions + +No unresolved foundation decision is currently recorded. New ambiguity +must be added here before an author selects a normative answer. + +## Implementation work items + +This decision register has no future production implementation owner. Its +authoring work item is: + +### ADR046-decisions-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-decisions-001` | +| Dependency/owner | ADR 0046 integrator | +| Current source | v3 evidence baseline plus parent/spec contradictions | +| Reuse action | adapt | +| Destination | `docs/specs/ADR-046-decision-register.md` | +| Detailed design | Record every evidence-underdetermined choice before dependent spec work proceeds | +| Integration | Parent decision summary and all affected specs link the decision ID | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Zero unresolved entries before pre-panel review; manifest/link consistency | +| Removal proof | Not applicable | diff --git a/docs/specs/ADR-046-feasibility-and-spikes.md b/docs/specs/ADR-046-feasibility-and-spikes.md new file mode 100644 index 000000000..6e9d6a548 --- /dev/null +++ b/docs/specs/ADR-046-feasibility-and-spikes.md @@ -0,0 +1,866 @@ +# ADR 0046 feasibility and spikes + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-feasibility-and-spikes` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | ADR 0046 integrator; validation/delivery; spike/proof authors | +| Depends on | `ADR-046-decision-register`, `ADR-046-terminology-and-identities`, `ADR-046-resource-store-redb`, `ADR-046-resource-reconciliation`, `ADR-046-componentsession-and-bus`, `ADR-046-components-processes-and-sandbox`, `ADR-046-provider-model-and-packaging`, `ADR-046-resources-credential`, `ADR-046-resources-volume`, `ADR-046-resources-host-guest-process-user`, `ADR-046-provider-state`, `ADR-046-zone-routing`, `ADR-046-nix-configuration`, `ADR-046-cli-and-operations`, `ADR-046-current-code-migration-map` | +| Supersedes | None | + +## Purpose + +ADR 0046 is documentation-only (D024): the parent and every `ADR-046-*` spec +describe a Proposed design, not a running system. This spec is the single +place that says, for every hard numeric target and every cross-process +contract in that design, exactly which claims already have executable proof, +which claims require a new disposable proof before any implementation work +item may begin, and which claims can only be closed by the production +implementation's own conformance/bench suite once that work item exists. + +It does three things and nothing else: + +1. Separates **completed evidence** (work already executed, with numbers) from + **mandatory disposable spikes** (work item authors must run these, or an + equivalent superseding proof, before starting the named production work + items) from **implementation validation** (the permanent test/bench + coverage that eventually replaces the disposable spike inside the real + crate). +2. Defines every mandatory spike as an exact, reproducible artifact: fixed + inputs, an exact command/harness, fixed metrics, a numeric pass/fail + threshold, an expected resource budget, a stated failure interpretation, + the exact decisions/work items the result binds, and a cleanup rule. +3. Fixes the evidence-classification vocabulary and anti-claim rules that + every other ADR 0046 spec's "Feasibility proof" row must honor, so a green + spike can never be cited as if it were a green production test. + +This spec creates no crate, dependency, Nix module, service, controller, or +Provider. It defines proof artifacts that live outside the production tree +(`proofs//`, matching the existing `proofs/w0-ch-connect-proof` +and `proofs/chunked-stdio-conformance` convention validated by +`make test-proofs` / `tests/test-proofs.sh`) until the owning work item below +promotes the proven behavior into a permanent in-tree test. No such crate is +created by this spec; authoring them is future implementation work, exactly +like every other `ADR046-*` work item in the set. + +## Evidence classification and anti-claim rules + +Every claim in this spec and in every spec it depends on carries exactly one +of the six classes defined in `docs/specs/README.md`: + +| Class | Meaning here | +| --- | --- | +| `implemented-and-reachable` | A live call path exists in a production binary today, with committed tests exercising it. | +| `implemented-but-unwired` | Code exists (in v3 or in main) and its own tests pass, but no v3 production caller invokes it yet. | +| `generated-or-eval-contract` | Produced by Nix/xtask codegen; validated by an eval/build/drift gate, not by a running process. | +| `test-only-or-preview` | Exercised only by test code or an exploratory branch; no production caller. | +| `ADR-only` | Described by an `ADR-046-*` spec; no code exists anywhere at any commit. | +| `unknown-requires-spike` | The claim is plausible but has no executable evidence at any commit; a spike is mandatory before any owning work item starts. | + +### Anti-claim rules (binding on this spec and every spec that cites it) + +1. **A spike changes only this spec's own spike-status field.** A passing + spike proves the named hypothesis under the named harness. It never + changes the evidence class recorded for the corresponding v3 production + artifact in any other spec's "Current-code fit" table. That table's + `ADR-only` / `unknown-requires-spike` entries move to + `implemented-and-reachable` only when the exact named work item lands in + `packages/` with its own committed tests passing in this repository's CI. +2. **Main reuse evidence never upgrades v3 reachability.** Per + `docs/specs/README.md` and decision D041, a passing main-commit test suite + (see §"Already-proven main evidence" below) proves the *borrowed + behavior's* correctness at the cited main commit. It is + `implemented-but-unwired` evidence for the *copy/adapt* work item, not + `implemented-and-reachable` evidence for v3. No claim in this document + states or implies that v3 already has a running ComponentSession, bus, + resource store, or Provider process. +3. **A failing spike changes the Proposed design, never the target.** + Mirroring `ADR-046-resource-store-redb`'s performance-contract rule, + generalized to every spike in this document: if a spike's measured value + misses its threshold, the affected spec's Proposed design changes (schema, + algorithm, batching, concurrency limits, or component boundary). No spike + result may be used to relax a durability, authorization, audit, redaction, + or security invariant defined by any `ADR-046-*` spec in order to pass. +4. **Rejected-alternative evidence is retained narratively, not + re-executed.** The completed kcp spike (§"Completed evidence") is retained + as historical numeric evidence supporting decision D003. No work item in + this set depends on kcp; nothing in the set requires re-running or + re-verifying that measurement, and doing so would validate a rejected + alternative rather than a required capability. +5. **A spike never substitutes for the panel or for implementation + validation.** Per D024 this task's initial output is documentation only, + and per the parent ADR's review process a ten-role panel runs only against + an integrated implementation candidate. Nothing in this document runs, or + claims to have run, that panel. Every spike below is additionally + scheduled to be **subsumed and then deleted**: once the named production + work item exists with its own in-tree conformance/bench test achieving + equal or stricter coverage, the disposable `proofs//` crate is + deleted and `tests/test-proofs.sh` drops its entry. Until that day, the + spike's continued presence under `proofs/` is not evidence that the + production behavior exists. +6. **Every spike is independently reproducible.** Each spike below pins its + Rust/redb/snow/tokio dependency versions (or points at the existing + `packages/rust-toolchain.toml` pin), states its exact command, and avoids + wall-clock nondeterminism in pass/fail logic (latency spikes use + monotonic clocks and a fixed percentile estimator over a fixed, stated + sample count; no spike's threshold depends on unpinned background load). +7. **No unresolved entries.** Every spike below has a concrete status. Status + `not yet executed` is a factual statement about sequencing (D024: this PR + is documentation only; execution is separate future implementation work), + not an unresolved decision, missing threshold, or placeholder. Every + numeric threshold, budget, and command in this document is final. + +## Completed evidence + +These items already have executable results. They are evidence, not spikes: +nothing further needs to run before the affected decisions can be treated as +settled for planning purposes. Re-running them is not a prerequisite for any +work item below. + +### E1 — kcp runtime-substrate rejection (decision D003) + +| Field | Value | +| --- | --- | +| Claim | A kcp-based resource plane (typed spec/status, revisions, watches, optimistic conflicts, owner/finalizer behavior, hierarchy, controller clients) is unsuitable for recursive Host/Guest/Zone embedding at d2b's scale. | +| Measured result | Approximately 490 MiB RSS and a 176 MiB executable for a minimal kcp-based control plane instance. | +| Source of the number | Parent ADR (`docs/adr/0046-d2b-3-provider-control-plane.md`, "A kcp feasibility spike proved ... measured approximately 490 MiB RSS and a 176 MiB executable") and `ADR-046-decision-register` entry D003 ("The spike proved desired object semantics but measured about 490 MiB RSS and a 176 MiB executable."). | +| Original artifact location | Not present in this repository at any inspected commit (`b5ddbed6`, `a1cc0b2d`, or main `HEAD`); the measurement is retained only as narrative evidence in the two citations above. | +| Semantics proven useful and retained | Typed spec/status, revisions, watches, optimistic conflicts, owner/finalizer behavior, resource hierarchy, and controller-client ergonomics — all carried into the native `ADR-046-resource-object-model` / `ADR-046-resource-store-redb` / `ADR-046-resource-reconciliation` design without the Kubernetes-API/etcd/workqueue machinery. | +| Consequence | D003: kcp is rejected as the v3 runtime substrate. D004-D006: one embedded redb database per Zone, one writer with concurrent MVCC readers, embedded in the Zone runtime process, replaces it. | +| Reproducibility disposition | Not reproduced by this spec (anti-claim rule 4). Any future dispute about the number must re-derive it against a stated kcp version/build profile as a new, separately reviewed spike; this spec does not schedule that work because no ADR 0046 work item depends on it. | + +### E2 — redb selection rationale (decisions D004, D005, D006) + +redb is selected as the per-Zone embedded store on **design-level**, not +**workload-level**, grounds: + +- **Embedded, pure Rust, no separate server process** — avoids the + kcp-class footprint measured in E1 (no separate apiserver/etcd binary, no + gRPC/HTTP control-plane surface to run per Zone). +- **ACID transactions with one writer and concurrent MVCC readers** — matches + D005 (single-writer optimistic-conflict model) without an external lock + manager. +- **Crash-safe B-tree storage with an `FileBackend::new(File)`-shaped API** — + matches `ADR-046-resource-store-redb`'s "Ownership and process boundary" + requirement that the store never resolves a caller-controlled path; the + storage owner passes one already-open `File`. +- **No wire protocol of its own** — every client access is mediated by + `d2b-bus`/ComponentSession per D011; redb is never reachable except through + the one embedding Zone runtime process (D006). + +This rationale is a completed **design** decision (D004-D006 are Resolved, +not `decision-required`). It is explicitly **not** a completed **workload** +proof: no committed evidence in this repository demonstrates that a redb +database built against the exact `ADR-046-resource-store-redb` schema (§ +"Physical tables") meets the aggregate ≤64 MiB idle-RSS, ≤500 ms readiness, +≤2 ms p95 read, ≤10 ms p95 write, ≤5 ms p95 commit-to-handler, or ≤20 ms p95 +ready-Process-to-launch-attempt targets in that spec's "Performance contract" +table. Those targets are `unknown-requires-spike` and are covered by +SPIKE-01 and SPIKE-02 below. Pinned version for every redb-touching spike in +this document: + +```toml +redb = { version = "=4.1.0", default-features = false } +# crates.io checksum (sha256, as published): +# 8e925444704b5f17d32bf42f5b6e2df050bceebc3dcd6e71cc73dafe8092e839 +# upstream: https://github.com/cberner/redb ; rust_version 1.89 (satisfied by +# the pinned packages/rust-toolchain.toml channel 1.94.1); edition 2024. +``` + +### E3 — main a1cc0b2d ComponentSession/Noise/Unix/vsock transport re-verification + +`ADR-046-componentsession-and-bus` cites main commit `a1cc0b2d` (`ADR-0045 +W9: coordinate toolkit and sibling cutover (#314)`) as the exact reuse source +for `d2b-session` and `d2b-session-unix`. That commit's own test suites were +re-executed during the authoring of this spec, in a disposable detached +worktree outside any tracked branch, to confirm the cited behavior is +presently green and not merely narratively true: + +```bash +# Reproduction (main a1cc0b2d only; never run against this v3 branch, which +# does not contain these crates): +git worktree add --detach /path/scratch a1cc0b2d +cd /path/scratch/packages +cargo test -p d2b-session-unix -p d2b-session --locked +cargo test -p d2b-session-unix --test unix_session --features host-socket --locked +cargo test -p d2b-session-unix --lib --features host-socket,native-vsock --locked +git worktree remove /path/scratch --force # cleanup; no artifact retained +``` + +| Suite | File | Feature flags | Result | +| --- | --- | --- | --- | +| `d2b-session` unit tests | `packages/d2b-session/src/{driver,server}.rs` inline `#[cfg(test)]` | default | 2 passed | +| `d2b-session` integration | `packages/d2b-session/tests/component_session.rs` | default | 27 passed | +| `d2b-session` Noise vectors | `packages/d2b-session/tests/noise_vectors.rs` | default | 1 passed (`every_canonical_w2_vector_verifies_exactly_with_snow_0_10`) | +| `d2b-session-unix` integration | `packages/d2b-session-unix/tests/unix_session.rs` | `host-socket` | 25 passed | +| `d2b-session-unix` unit tests (incl. vsock) | inline `#[cfg(test)]` in `adapter.rs`, `socket.rs`, `systemd.rs`, `vsock.rs` | `host-socket,native-vsock` | 15 passed | +| **Total** | | | **70 passed, 0 failed** | + +Toolchain used: `cargo 1.95.0` / `rustc 1.95.0` (nix-profile), against the +repository's pinned channel `1.94.1` (`packages/rust-toolchain.toml`) — a +newer patch/minor toolchain than pinned, disclosed here for transparency. +Anyone re-running this reproduction with the exact pinned `1.94.1` toolchain +is expected to reproduce the same pass count; a mismatch is itself a finding +to be filed against `ADR046-session-001`/`ADR046-session-002`, not silently +reconciled. + +**Evidence class implication:** this makes the *cited main behavior itself* +(Noise NN/KK/IKpsk2 handshake, directional record protection, replay/sequence +rejection, fair scheduling, named-stream credit, Unix seqpacket/stream +transport, pidfd/SO_PEERCRED identity evidence, in-memory vsock framing) a +demonstrated, currently-green implementation shape — this is what the +`Reuse action: copy and adapt` in `ADR046-session-001`/`ADR046-session-002` +inherits. Per anti-claim rule 2, it remains `implemented-but-unwired` for v3: +no ComponentSession exists in the `b5ddbed6` v3 baseline, and copying these +70 tests into `packages/d2b-session*` at their new v3 destination — then +adding the subject/RBAC/revocation/resource-watch/latency integration tests +those work items require — is exactly the validation column of +`ADR046-session-001` and `ADR046-session-002` in `ADR-046-componentsession-and-bus`, +not a step this spec performs. + +### E4 — current v3 reachable-evidence inventory + +These v3 (`b5ddbed6`) files exist at the exact paths cited by their owning +specs and were confirmed present during authoring of this spec. They are +`implemented-and-reachable` or `production-reachable` (per +`ADR-046-current-code-migration-map` §0 notation) evidence for the +*current-role* behavior being migrated, not for any ADR 0046 resource, +controller, or Provider (all of which remain `ADR-only` until their own work +item lands): + +| Path | Cited by | Confirmed | +| --- | --- | --- | +| `packages/d2b-core/src/storage.rs` (440 lines) | `ADR046-store-001` | present, contains `#[cfg(test)]` coverage | +| `packages/d2b-core/src/sync.rs` (232 lines) | `ADR046-store-001` | present, contains `#[cfg(test)]` coverage | +| `packages/d2bd/src/supervisor/dag.rs` | `ADR046-reconcile-001`, `ADR046-reconcile-003` | present | +| `packages/d2bd/src/supervisor/pidfd.rs` | `ADR046-reconcile-003` | present | +| `packages/d2b-priv-broker/src/ops/spawn_runner.rs` | `ADR046-process-001` | present | +| `packages/d2b-priv-broker/tests/pidfd_real_spawner.rs` | `ADR046-process-001` | present | +| `packages/d2b-realm-router/src/{session,secure_session,mux_session,session_lifecycle,target_resolver,execution,remote_node,display_transport}.rs` | `ADR-046-componentsession-and-bus`, `ADR-046-zone-routing` | present | + +Per `ADR-046-resource-reconciliation`'s own current-code-fit row, the DAG/ +pidfd/route logic in these files is tested for its **current** role-specific +behavior; the **generic** async controller loop, store watches, owner +triggers, and cross-resource concurrency described by ADR 0046 do not exist +in any of them and remain `ADR-only`. + +## Evidence classification matrix + +| Subsystem / claim | Evidence class today | Spike required before its work item starts | +| --- | --- | --- | +| kcp unsuitable at recursive Host/Guest/Zone scale | completed evidence (E1) | No — see anti-claim rule 4 | +| redb is the right embedding shape (single-writer/MVCC/pure-Rust/fd-backed) | design rationale, `ADR-only` for workload fit (E2) | Yes — SPIKE-01 | +| redb meets the 10k-resource functional/index/revision/watch/group-commit/crash-recovery/RSS targets | `unknown-requires-spike` | Yes — SPIKE-01 | +| p95 durable commit → controller handler start ≤5 ms | `unknown-requires-spike` | Yes — SPIKE-02 | +| p95 ready-Process commit → launch-attempt start ≤20 ms under concurrent dispatch | `unknown-requires-spike` | Yes — SPIKE-03 | +| Async EffectPort adapters never block the executor | `unknown-requires-spike` | Yes — SPIKE-04 | +| Noise NN/KK/IKpsk2 handshake, records, fair scheduling, Unix/vsock transport (main shape) | `implemented-but-unwired` for v3, proven at main `a1cc0b2d` (E3) | Only the v3 integration delta — SPIKE-06/07/08 | +| Independent Provider crate → multiple binaries → manifest/config-schema/component registration | `ADR-only` | Yes — SPIKE-05 | +| d2b-bus exact-addressed routing with per-recipient distinct Noise sessions | `ADR-only` | Yes — SPIKE-06 | +| Unix/vsock/Azure-Relay transports carry only opaque Noise record bytes | `ADR-only` for v3 wiring; per-transport performance gates already specified in each dossier | Yes — SPIKE-07 | +| Credential Provider → consumer Provider raw token/signature delivery over dedicated Noise_KK session | `ADR-only` | Yes — SPIKE-08 | +| Optional declared state-Volume creation order, guest-local vs. host-backed-guest, virtiofs `Export` child | `ADR-only` | Yes — SPIKE-09 | +| Volume ACL/`sourcePolicyId`/quota/marker/adoption policies | `ADR-only` | Yes — SPIKE-10 | +| systemd/minijail Process Providers: pidfd acquire/verify/reap/adopt/quarantine conformance | `ADR-only` | Yes — SPIKE-11 | +| Nix direct ResourceSpec authoring → codegen → build validation → removed-resource cleanup | `generated-or-eval-contract` pattern proven by existing `nixos-modules/assertions.nix` + `xtask gen-schemas` + drift gate machinery; ADR 0046 schema set is `ADR-only` | Yes — SPIKE-12 | +| CLI dynamic Provider-projection discovery with bounded deadline/size | `ADR-only` | Yes — SPIKE-13 | +| Clean v3 reset/cutover (no v2 alias dispatch, fresh Zone bootstrap) | `ADR-only` | Yes — SPIKE-14 | +| Representative local/cloud/interaction Provider end-to-end composition | `ADR-only` | Yes — SPIKE-15 | +| Three-layer spec + status shape: base-schema parity across implementations, minimal-base acceptance, capability-declared rejection, base-only projection, extension versioning/unknown-field/shadow | `ADR-only` | Yes — SPIKE-16 | +| Expedited reconcile commit-proof gating/idempotency/priority-lane and currency/disruptive-upgrade dependency planning | `ADR-only` | Yes — SPIKE-17 | +| Endpoint resource promotion, no-locator resolution, producer generation, consumer dependency trigger, promotion-test lint | `ADR-only` | Yes — SPIKE-18 | +| Entra identity-Guest login/token grounding via fake Entrablau Guest service (no live Entra) | `ADR-only` | Yes — SPIKE-19 | +| D096 signed Service/Binding projection factory, Service-only export, deterministic same-type projection, authored Binding graph/finalizers, and real cross-Zone streams | `ADR-only` | Yes — SPIKE-20 | +| Current v3 storage/DAG/pidfd/spawn_runner/router files exist and pass their own tests | `implemented-and-reachable`/`production-reachable` for current role (E4) | No — already evidenced | + +## Mandatory disposable spike catalog + +Every spike below lives at `proofs//`, is a standalone +`[workspace]`-rooted crate (matching `proofs/w0-ch-connect-proof/Cargo.toml` +and `proofs/chunked-stdio-conformance/Cargo.toml`), is **not** a member of +`packages/Cargo.toml`, and is run with +`cargo test --manifest-path proofs//Cargo.toml` (plus +`cargo bench --manifest-path proofs//Cargo.toml` where the metric is a +latency/throughput percentile) using the pinned `packages/rust-toolchain.toml` +channel. None of these crates exist yet; authoring one is the first step of +its listed work item in "Implementation work items" below, not a step +performed by this documentation-only spec. + +### SPIKE-01 — redb functional scale, indexes, revisions, watch, group commit, crash recovery, RSS + +| Field | Value | +| --- | --- | +| Hypothesis | An embedded redb 4.1.0 database, built against the `ADR-046-resource-store-redb` physical schema (`store_meta`, `api_schemas`, `resources`, `type_index`, `owner_index`, `controller_index`, `revision_log`, `operations`, `zone_link_cursors`), sustains 10,000 resources with 100 live watches, correct revision/index maintenance, bounded group commit, and forced-crash recovery, inside the aggregate ≤64 MiB idle-RSS budget shared with the fixed system-core/system-minijail controllers. | +| Minimal disposable artifact | `proofs/redb-resource-store-spike/` — a standalone crate implementing exactly the eight tables above over `redb::Database` with a `FileBackend::new(File)`-backed open, an async bounded fair write queue feeding one blocking store-actor thread (via `tokio::task::spawn_blocking`), and a minimal watch registrar replaying `revision_log`. No d2b-bus, no ComponentSession, no broker — a fake in-process caller drives the API directly. | +| Inputs | (a) empty store; (b) 10,000 pre-seeded resources across 6 synthetic ResourceTypes with realistic key/value sizes (JSON spec/status ≤4 KiB each, matching `ADR-046-resource-object-model` bounded-message expectations); (c) 100 concurrently registered watches with mixed ResourceType filters; (d) an expected-revision conflict storm of 500 concurrent writers targeting 50 shared resources; (e) an owner-trigger fan-in tree 4 levels deep, 8 children per level; (f) `SIGKILL` injected at each of the 13 commit-transaction boundaries listed in the write-transaction algorithm. | +| Command/harness | `cargo test --manifest-path proofs/redb-resource-store-spike/Cargo.toml -- --test-threads=1` for functional/index/revision/watch/conflict/owner-trigger/compaction cases; `cargo run --manifest-path proofs/redb-resource-store-spike/Cargo.toml --bin crash-fixture -- --kill-at-txn ` (13 invocations, one per boundary, each in a fresh subprocess) for crash recovery; `/usr/bin/time -v cargo run --manifest-path proofs/redb-resource-store-spike/Cargo.toml --bin rss-fixture -- --resources 10000 --watches 100` for RSS, read three times and take the median "Maximum resident set size (kbytes)". | +| Metrics | (1) resource/index/revision correctness — exact match of 10k resources against a parallel `BTreeMap` oracle after every mutation; (2) watch replay/live no-gap — every one of 100 watchers receives every committed ChangeBatch entry after its `afterRevision`, verified by a monotonic per-watcher received-revision set with no gap; (3) group-commit batch size distribution under the conflict storm; (4) crash-recovery: process re-open succeeds or fails closed (never silently creates an empty replacement) at all 13 boundaries; (5) median RSS in KiB. | +| Pass/fail threshold | (1) zero divergence from oracle across 10k resources / 5 repeated runs; (2) zero missed/duplicated ChangeBatch deliveries across 100 watchers; (3) non-conflicting writes in the storm achieve group commit (batch size > 1) at least 50% of the time; (4) 13/13 boundaries either recover to the last fully-committed state or refuse to open (never a silent empty store); (5) median RSS for the store+actor alone ≤ 24 MiB (leaving ≥40 MiB of the 64 MiB aggregate budget in `ADR-046-resource-store-redb` for the fixed system-core/system-minijail controllers measured separately in SPIKE-11/SPIKE-15). | +| Expected resource budget | Single-threaded build+run ≤5 minutes on a 4-vCPU/8 GiB CI runner; peak build RSS ≤1 GiB; on-disk database file ≤200 MiB for the 10k-resource fixture. | +| Failure interpretation | RSS miss → the schema/serialization plan in `ADR-046-resource-store-redb` §"Physical tables" changes (e.g., narrower key encoding, smaller in-memory index shape) before `ADR046-store-001` starts; a correctness miss on watch/crash-recovery → the async storage adapter or write-transaction algorithm in that same spec is revised, never the tolerance; group-commit batch-size miss → the "bounded group commit" admission window is retuned, but per anti-claim rule 3 not by weakening per-mutation validation. | +| Affected decisions/work items | D003, D004, D005, D006, D008, D053; `ADR046-store-001`, `ADR046-store-002`, `ADR046-store-003`. | +| Cleanup | `proofs/redb-resource-store-spike/` is deleted, and its entry removed from `tests/test-proofs.sh`, once `packages/d2b-resource-store-redb` (the real `ADR046-store-001` destination) has an in-tree benchmark reproducing metrics (1)-(5) at equal or stricter thresholds. | +| Status | Specified — not yet executed (D024: documentation-only task; execution is separate future implementation work). | + +### SPIKE-02 — durable commit → controller handler start, p95 ≤5 ms + +| Field | Value | +| --- | --- | +| Hypothesis | The post-commit dispatcher inside the Zone runtime (redb write-transaction commit → in-memory index swap → matching watch/reconcile-hint push) delivers a hint to a waiting async consumer with p95 latency ≤5 ms, independent of concurrent unrelated commit traffic. | +| Minimal disposable artifact | `proofs/redb-commit-handler-latency-spike/` — extends SPIKE-01's store-actor with a minimal in-process "hint bus" (a bounded `tokio::sync::mpsc` per registered consumer, no real d2b-bus/ComponentSession) and a synthetic "controller" task that records `Instant::now()` on hint receipt. | +| Inputs | 1,000 sequential single-resource writes each immediately followed (same async task) by measuring elapsed time to the matching consumer's hint receipt; repeated under three concurrency profiles: (a) no background writers; (b) 10 background writer tasks issuing unrelated resource writes at a combined 500 writes/s; (c) 100 background writer tasks at a combined 2,000 writes/s. | +| Command/harness | `cargo bench --manifest-path proofs/redb-commit-handler-latency-spike/Cargo.toml -- commit_to_handler` using `criterion` with `--sample-size 1000`; percentile computed by criterion's built-in estimator over the full 1,000-sample run per profile. | +| Metrics | p50/p95/p99 elapsed time in microseconds from `write_transaction.commit()` return to consumer task waking and reading the hint, for each of the 3 concurrency profiles. | +| Pass/fail threshold | p95 ≤5,000 µs (5 ms) in all 3 profiles; p99 ≤10,000 µs recorded and reported (not gating, but any p99 >20 ms is a documented finding attached to the result). | +| Expected resource budget | ≤3 minutes wall time per profile; single CI runner core pinned via `taskset`/`cset` where available, otherwise best-effort with reported CPU count and load. | +| Failure interpretation | A miss under profile (a) is a dispatcher-design failure — the post-commit swap/push path in `ADR-046-resource-store-redb` §"Async storage adapter" changes. A miss only under profiles (b)/(c) is an admission-fairness failure — the "per-principal/controller fair admission" rule in the same section is retuned (e.g., smaller max group-commit batch, dedicated hint-delivery task priority) before `ADR046-store-002` starts. | +| Affected decisions/work items | D030; `ADR046-store-001`, `ADR046-store-002`, `ADR046-reconcile-002`. | +| Cleanup | Deleted once `packages/d2b-controller-toolkit/benches/reaction.rs` (the real `ADR046-reconcile-003` destination named in `ADR-046-resource-reconciliation`) reproduces the same 3-profile p95/p99 gate against the real store. | +| Status | Specified — not yet executed. | + +### SPIKE-03 — ready Process commit → launch-attempt start, p95 ≤20 ms, concurrent reading and dispatch + +| Field | Value | +| --- | --- | +| Hypothesis | When a Process resource is durably committed with all dependencies Ready, an independent async controller task reaches "launch-attempt start" with p95 ≤20 ms, and — critically — the watch receiver dispatches the *next* independent ready Process without waiting for any in-flight launch, readiness wait, or long-running effect to complete. | +| Minimal disposable artifact | `proofs/process-fastlaunch-spike/` — builds on SPIKE-02's hint bus, replaces the synthetic consumer with a minimal async "Process controller" loop matching `ADR-046-resource-reconciliation` §"Async loop" (steps 1-14: register, list, watch, per-resource single-flight dispatch, parallel independent resources under a semaphore) and a fake `ProcessLaunchEffectPort` whose `spawn()` sleeps a configurable 0-500 ms to model a slow real launch without touching any real process/broker/systemd/minijail code. | +| Inputs | 1, 10, and 100 concurrently-ready Process resources committed in the same synthetic Zone within a 50 ms window; fake effect-port launch latency fixed at 200 ms per resource (chosen to exceed the 20 ms gate by 10x, so a passing "commit-to-launch-attempt-start" measurement cannot be an artifact of the launch itself finishing fast). | +| Command/harness | `cargo bench --manifest-path proofs/process-fastlaunch-spike/Cargo.toml -- launch_attempt_start` with `criterion`, one benchmark group per concurrency level (1/10/100), `--sample-size 200` per group. | +| Metrics | (1) p50/p95/p99 elapsed time from commit to launch-attempt-start (fake effect-port `spawn()` call entry) per concurrency level; (2) "next-dispatch" latency: elapsed time from resource *N*'s commit to resource *N+1*'s handler-start when both are ready simultaneously, for *N* in 1..100; (3) total wall time for all 100 resources to reach launch-attempt-start. | +| Pass/fail threshold | (1) p95 ≤20,000 µs (20 ms) at all three concurrency levels; (2) no resource's next-dispatch latency exceeds 20 ms regardless of how many earlier resources are still inside their (200 ms) fake launch sleep — this is the concurrency-independence assertion; (3) total wall time for 100 resources ≤ (20 ms dispatch budget + configured semaphore width × 200 ms), proving effects run in parallel under budget rather than serially. | +| Expected resource budget | ≤5 minutes wall time for the 100-resource benchmark group; process peak RSS ≤128 MiB (in-memory synthetic Zone only, no real redb file). | +| Failure interpretation | A miss in metric (1) revises the controller dispatch loop or hint-bus design in `ADR-046-resource-reconciliation` §"Process fast path". A miss in metric (2) — one resource's launch blocking another's dispatch — is treated as a severity-blocking finding against the "independent resources run in parallel under semaphore/budget" invariant in the same section; the fix is structural (remove the blocking await), never a threshold relaxation. | +| Affected decisions/work items | D030; `ADR-046-resource-reconciliation` (Process fast path, async loop); `ADR-046-resources-host-guest-process-user` (Fast path contract); `ADR046-reconcile-001`, `ADR046-reconcile-003`, `ADR046-process-001`. | +| Cleanup | Deleted once `packages/d2b-controller-toolkit/benches/reaction.rs` and the Process-Provider integration tests named by `ADR046-reconcile-003` reproduce the same 1/10/100-concurrency p95/independence gates against real Process Provider controllers. | +| Status | Specified — not yet executed. | + +### SPIKE-04 — async EffectPort adapters never block the executor + +| Field | Value | +| --- | --- | +| Hypothesis | `ProcessLaunchEffectPort`, `VolumeLayoutEffectPort`/`VolumeSourceEffectPort`, `NetworkEffectPort`, and `DeviceEffectPort` calls are fully async from the calling controller's perspective; every blocking kernel/filesystem/broker-socket call inside their implementation runs on a bounded blocking-adapter (`tokio::task::spawn_blocking` or an equivalent dedicated thread pool), and no controller task holds a redb transaction or a blocking call across an `.await`. | +| Minimal disposable artifact | `proofs/effectport-async-spike/` — implements the four EffectPort trait signatures from `ADR-046-components-processes-and-sandbox` and `ADR-046-resources-volume` against fake backends that perform a real blocking syscall analogue (a `std::fs::File::sync_all()` on a temp file standing in for a broker filesystem op, and a `std::thread::sleep` standing in for a blocking `clone3`/`pidfd_open` call), instrumented with a tokio `LocalSet`-free single-threaded current-thread runtime so any accidental blocking call inside an `.await` stalls the whole runtime and is trivially detected. | +| Inputs | 200 concurrent EffectPort calls (50 per port) issued against a single-threaded `tokio` runtime (`#[tokio::main(flavor = "current_thread")]`) alongside a fixed 10 ms-interval heartbeat task; each fake backend's blocking primitive is deliberately slow (50 ms) to make any accidental synchronous execution on the async worker visibly stall the heartbeat. | +| Command/harness | `cargo test --manifest-path proofs/effectport-async-spike/Cargo.toml -- --test-threads=1 effectport_never_blocks`; the test asserts on the heartbeat task's own recorded tick-to-tick jitter, not on the EffectPort latency itself. | +| Metrics | Heartbeat tick-to-tick jitter (max observed gap between consecutive 10 ms heartbeat ticks) while 200 EffectPort calls with 50 ms blocking backends are in flight on the same current-thread runtime. | +| Pass/fail threshold | Max heartbeat gap ≤15 ms (50% tolerance over the 10 ms nominal interval) throughout the entire 200-call run. A gap ≥50 ms (matching the fake backend's blocking duration) is conclusive proof a blocking call executed directly on the async worker and is an automatic fail. | +| Expected resource budget | ≤1 minute wall time; single OS thread for the async runtime plus the bounded blocking-adapter pool (sized to 16 threads, matching a conservative real broker/effect-adapter pool budget). | +| Failure interpretation | Any observed stall traces directly to the offending EffectPort implementation call site; the fix is moving that call behind `spawn_blocking` (or the broker-side blocking adapter it dispatches to) — this is a structural fix, not a threshold change, per anti-claim rule 3 (no relaxing "no handler holds ... a blocking kernel/filesystem call across an await" from `ADR-046-resource-reconciliation`). | +| Affected decisions/work items | D077; `ADR-046-components-processes-and-sandbox` (ProviderSupervisor and EffectPort); `ADR-046-resource-reconciliation` (Async interface); `ADR046-process-001`, `ADR046-volume-001` (see `ADR-046-resources-volume` implementation work items). | +| Cleanup | Deleted once `packages/d2b-provider-supervisor` (the real `ADR046-process-001` destination) and the volume-domain effect adapter each carry an in-tree blocking-adapter regression test with an equal or stricter jitter gate. | +| Status | Specified — not yet executed. | + +### SPIKE-05 — independent Provider crate → multiple binaries → manifest/config-schema/component registration + +| Field | Value | +| --- | --- | +| Hypothesis | A single Provider crate can declare one Provider identity, build two independently sandboxed binaries (a controller and a service, per `ADR-046-provider-model-and-packaging` §"Provider components"), publish a signed manifest carrying component descriptors + a root JSON Schema, and have core `ProviderDeployment` parse that manifest and create the exact declared static Process graph — without the crate importing `d2bd`, broker, or Zone-store internals. | +| Minimal disposable artifact | `proofs/provider-packaging-spike/` — a two-binary Cargo crate (`src/bin/controller.rs`, `src/bin/service.rs`) plus a hand-authored `manifest.json` matching the field list in `ADR-046-provider-model-and-packaging` §"Provider resource" and §"Package catalog" (package/executable/manifest/component digests, exported ResourceTypes, controller/service component descriptors, root config JSON Schema); a fake `ProviderDeployment` reads this manifest and asserts it can enumerate exactly the declared components/binaries/digests with no additional discovery. | +| Inputs | One manifest declaring 1 controller + 1 service + 1 worker template, a root config schema with 3 required fields (2 typed, 1 with a `sourcePolicyId`-shaped opaque string), and a deliberately mismatched second manifest (wrong digest) to test the negative path. | +| Command/harness | `cargo test --manifest-path proofs/provider-packaging-spike/Cargo.toml -- --test-threads=1`; a workspace-policy check script (`proofs/provider-packaging-spike/check_layout.sh`) asserting the crate has its own `src/`, `tests/`, `integration/`, and `README.md` per D059, mirrored against `cargo metadata --manifest-path proofs/provider-packaging-spike/Cargo.toml --no-deps` to confirm zero dependency edges on any `d2bd`/broker/Zone-store crate name. | +| Metrics | (1) exact component/binary/digest enumeration match against the manifest; (2) config validation rejects unknown top-level fields (`additionalProperties: false`) and out-of-bounds values; (3) digest-mismatch manifest is rejected before any component is considered; (4) `cargo metadata` dependency graph contains zero edges to any forbidden crate name. | +| Pass/fail threshold | All four metrics must hold exactly (binary pass/fail, no partial credit) across 20 repeated manifest-load cycles with randomized field ordering in the JSON to rule out order-dependent parsing bugs. | +| Expected resource budget | ≤2 minutes wall time; no filesystem beyond the crate's own `target/`. | +| Failure interpretation | A component/digest enumeration mismatch or forbidden dependency edge blocks `ADR046-provider-001`/`ADR046-provider-002` (see `ADR-046-provider-model-and-packaging` implementation work items) from starting until the manifest schema or crate-layout policy check is corrected. | +| Affected decisions/work items | D012, D057, D059, D075, D078; `ADR046-provider-001`, `ADR046-provider-002`, `ADR046-provider-003`. | +| Cleanup | Deleted once the real Provider-toolkit crate (destination of `ADR046-provider-001`) ships its own manifest-parsing/component-enumeration/workspace-policy conformance tests with equal or stricter coverage. | +| Status | Specified — not yet executed. | + +### SPIKE-06 — d2b-bus exact-addressed routing with per-recipient distinct Noise protection + +| Field | Value | +| --- | --- | +| Hypothesis | `d2b-bus` resolves the exact `(Zone, service package, method/stream, target ResourceRef or Provider, schema fingerprint, generation)` route key to exactly one destination process, and two distinct recipient components enrolled with distinct static Noise keys each get their own independent `Noise_KK` session — no session key, record sequence, or transcript is shared or reusable across recipients. | +| Minimal disposable artifact | `proofs/bus-routing-noise-spike/` — reuses main a1cc0b2d's `d2b-session`/`d2b-session-unix` crates (copied verbatim into the spike's own `Cargo.toml` path dependency from a pinned local checkout of `a1cc0b2d`, per the reuse policy in `ADR-046-componentsession-and-bus`) plus a minimal in-process router implementing only the route-key resolution and RBAC-attribute check (a fake static-allow-list RBAC, not the real Role/RoleBinding engine). | +| Inputs | 3 synthetic recipient components (`recipient-a`, `recipient-b`, `recipient-c`), each with its own enrolled `Noise_KK` static keypair; 500 routed calls fanned out round-robin across the 3 recipients; one deliberate cross-wiring attempt where the router is fed `recipient-a`'s route key but `recipient-b`'s transport handle, which must fail closed. | +| Command/harness | `cargo test --manifest-path proofs/bus-routing-noise-spike/Cargo.toml -- --test-threads=1 bus_routing_and_per_recipient_noise`. | +| Metrics | (1) every one of the 500 calls is delivered to exactly the recipient named by its route key, verified by a per-recipient received-call counter; (2) each recipient's session transcript hash and record sequence counter are independent (no shared state, verified by asserting the three sessions' internal sequence counters never reference each other's session object); (3) the deliberate cross-wiring case is rejected before any record is exchanged. | +| Pass/fail threshold | 500/500 correct routing, 0 cross-recipient session-state leakage, 1/1 cross-wiring case rejected with a stable typed error (not a panic, not a silent no-op). | +| Expected resource budget | ≤2 minutes wall time; ≤64 MiB RSS (three lightweight Noise sessions plus the fake router). | +| Failure interpretation | A misrouted call or shared-session-state finding blocks `ADR046-bus-001` from starting until the route-key resolution or per-session isolation in `ADR-046-componentsession-and-bus` §"d2b-bus" is corrected; per anti-claim rule 3, the fix is never "widen the route key to fail open." | +| Affected decisions/work items | D011, D039, D040, D054; `ADR046-session-001`, `ADR046-session-002`, `ADR046-bus-001`. | +| Cleanup | Deleted once `packages/d2b-bus/src/router.rs` (the real `ADR046-bus-001` destination) carries an in-tree message-isolation/route-authorization/no-direct-store-path conformance test with equal or stricter coverage, per that work item's own Validation column. | +| Status | Specified — not yet executed. | + +### SPIKE-07 — Unix/vsock/Azure-Relay transports carry only opaque Noise record bytes + +| Field | Value | +| --- | --- | +| Hypothesis | All three initial Transport Providers (`transport-unix` seqpacket/stream, `transport-vsock` framed vsock, `transport-azure-relay` WebSocket-carried) expose the same `OpenTransport`/`CloseTransport`/`ObserveTransport` shape and carry only 2-byte length-prefixed opaque Noise record bytes end-to-end; none of the three can decrypt, interpret, or leak a credential/path/PID through its carriage layer. | +| Minimal disposable artifact | `proofs/transport-opaque-streams-spike/` — three minimal transport backends: (a) a real Unix seqpacket socketpair (reusing main a1cc0b2d's `d2b-session-unix::adapter`); (b) main a1cc0b2d's in-memory vsock adapter (`d2b-session-unix::vsock` test-only `InMemoryVsockAdapter`, already exercised in E3); (c) a fake WebSocket-shaped byte pipe (an in-process `tokio::io::duplex` standing in for the real Azure Relay WebSocket, since no live Azure Relay resource is provisioned by a disposable spike) — each wrapped by the same generic `TransportHandle` trait object and driven by one shared conformance test suite. | +| Inputs | A 64 KiB pseudorandom Noise-record-shaped payload (2-byte length prefix + ciphertext) sent across each of the three transports; a byte-level payload inspector sitting "in the middle" of each transport that must observe only opaque length-prefixed bytes (never a decrypted plaintext, never an `SCM_RIGHTS` control message on the vsock/relay legs). | +| Command/harness | `cargo test --manifest-path proofs/transport-opaque-streams-spike/Cargo.toml -- --test-threads=1 opaque_byte_stream_conformance` — one parametrized test instantiated three times, once per transport backend. | +| Metrics | (1) byte-exact delivery of the 64 KiB payload on each transport; (2) the middle-observer never decodes a valid Noise record (proving it never sees plaintext — it can only see the same ciphertext bytes the endpoints exchange); (3) `SCM_RIGHTS`/attachment attempts are accepted only on the Unix seqpacket backend and rejected (`attachment-not-permitted-over-zone-link`) on the vsock and relay-shaped backends; (4) each transport's own dossier-defined performance gate: Unix `OpenTransport` p95 ≤2 ms (seqpacket) / ≤1 ms (stream); vsock `OpenTransport` overhead ≤2 ms p99 (excluding connect) and bridge throughput ≥512 MiB/s on loopback; relay-shaped backend backpressure propagates (send blocks) when the fake WebSocket leg's buffer is deliberately capped at 4 KiB. | +| Pass/fail threshold | (1)-(3) must hold exactly (binary); (4) must meet the exact numeric gates already committed in `docs/specs/providers/ADR-046-provider-transport-unix.md` §"Performance targets" and `ADR-046-provider-transport-vsock.md` §"Performance gates" (reproduced here, not redefined): Unix seqpacket open p95 ≤2 ms, Unix stream open p95 ≤1 ms, vsock open overhead p99 ≤2 ms, vsock bridge throughput ≥512 MiB/s. | +| Expected resource budget | ≤3 minutes wall time; ≤256 KiB working set per active transport (matching the vsock dossier's own bridge-task memory gate), measured via the same `/usr/bin/time -v` methodology as SPIKE-01. | +| Failure interpretation | A plaintext leak or accepted attachment on the vsock/relay-shaped backend is a severity-blocking security finding against `ADR-046-zone-routing` §"No FD, credential, or host path forwarding" and blocks every `ADR046-transport-*-00x` work item until fixed; a latency/throughput miss revises the transport's own implementation (buffer sizing, syscall batching), never the already-committed dossier target. | +| Affected decisions/work items | D081; `ADR046-transport-unix-001..011`, `ADR046-transport-vsock` work items, `ADR046-transport-relay-001..007`. | +| Cleanup | Deleted once each real transport Provider crate (`d2b-provider-transport-unix`, `-vsock`, `-azure-relay`) reproduces its own share of these metrics in its `tests/`/`integration/` per its dossier's "Required tests" section, including a real (non-fake) Azure Relay integration test gated behind the existing live-credential opt-in convention used elsewhere in this repository for cloud-backed tests. | +| Status | Specified — not yet executed. | + +### SPIKE-08 — Credential Provider → consumer Provider raw delivery over dedicated Noise_KK + +| Field | Value | +| --- | --- | +| Hypothesis | A Credential Provider can deliver a bounded raw token (or `SignChallenge` signature) to one authorized consumer Provider/component over a dedicated end-to-end `Noise_KK` ComponentSession, with the delivery binding contract fields from `ADR-046-resources-credential` §"Binding contract" enforced, `d2b-bus` forwarding only opaque records without decrypting them, and the plaintext zeroized immediately after extraction with no logging/audit/metric surface ever seeing the byte value. | +| Minimal disposable artifact | `proofs/credential-kk-e2e-spike/` — reuses main a1cc0b2d's `d2b-session` Noise_KK handshake/record machinery (proven green in E3) plus a fake Credential Provider (`acquire-token`) and fake consumer Provider, with a fake `d2b-bus` relay in the middle that can only forward opaque records (it is deliberately given no decryption capability at all, not merely "policy forbidding" it, so any successful decryption at the relay is a code-level bug, not a policy violation). | +| Inputs | A 256-byte synthetic "token" value; the 11 binding-contract fields from `ADR-046-resources-credential` §"Binding contract" (`credentialRef`, `credentialUID`, `credentialGeneration`, `consumerProviderRef`, `consumerComponentGeneration`, `audience`, `operationClass`, `expiryUnixMs`, `deadlineUnixMs`, `routeDigest`, `schemaVersion`, `maxTokenBytes`, `transcriptDigest`); one deliberate NN-profile bootstrap attempt (must be rejected per §"Security requirements", item 1); one deliberate oversize (larger than `maxTokenBytes`) delivery attempt (must be rejected per item 3); one deliberate replay of a prior sequence number (must be rejected per item 2). | +| Command/harness | `cargo test --manifest-path proofs/credential-kk-e2e-spike/Cargo.toml -- --test-threads=1 credential_kk_delivery`. | +| Metrics | (1) successful delivery: consumer receives the exact 256-byte token, binding fields verified by both endpoints before accepting records; (2) relay-observability: the fake relay's log of forwarded bytes never contains the plaintext token (checked by a substring search over everything the relay ever touches); (3) NN-bootstrap-for-delivery rejection; (4) oversize-payload rejection with channel close+zeroize; (5) sequence-replay rejection; (6) post-ACK channel closes and a canary zeroizing wrapper confirms the buffer is zeroed (test-only introspection into the zeroizing type, not available in production code paths). | +| Pass/fail threshold | All six metrics binary pass; zero occurrences of the plaintext token anywhere the relay can observe, across 100 repeated deliveries with freshly generated random tokens each run. | +| Expected resource budget | ≤2 minutes wall time; ≤32 MiB RSS. | +| Failure interpretation | Any plaintext-at-relay occurrence is an automatic, severity-blocking security finding against `ADR-046-resources-credential` §"Credential-delivery endpoint contract" and blocks every `ADR046-credential-00x` work item; a binding-field check bypass blocks the same work items pending a fix to the offer/prologue construction, never a relaxed binding contract. | +| Affected decisions/work items | D055, D056, D068; `ADR046-credential-001` through `ADR046-credential-008`. | +| Cleanup | Deleted once the real `d2b-provider-credential-secret-service` (or any of the three frozen Credential Provider crates) carries its own end-to-end KK delivery conformance test with equal or stricter coverage, per `ADR-046-resources-credential` §"Runtime tests". | +| Status | Specified — not yet executed. | + +### SPIKE-09 — Optional state-Volume creation order, guest-local vs. host-backed-guest, virtiofs Export child + +| Field | Value | +| --- | --- | +| Hypothesis | Core `ProviderDeployment` creates a component's *declared* optional state Volume (a stateless component declares none and gets none — its bounded non-secret state lives in resource `status`/the core Operation ledger per D087) before launching that component's Process, requires no bootstrap state-Volume mechanism (the first `volume-local` instance per execution target declares no state Volume and reaches Ready from its own status without crossing a Host/Guest boundary), and correctly creates exactly one `virtiofs.d2bus.org.Export` child (owned by the source Volume, not by the Provider) per attachment for `placementMode: host-backed-guest`, while `placementMode: guest-local` creates zero Export children and the Host volume-local controller never touches its bytes/dirfd/path. | +| Minimal disposable artifact | `proofs/provider-state-export-spike/` — an in-process fake resource store (a plain `HashMap`-backed oracle, not real redb) modeling just enough of `ADR-046-provider-state` §"State placement under Host/Guest/user execution" to construct the ordering/ownership graph and assert it; two fake execution targets ("Host/h1", "Guest/g1") each with their own fake `volume-local` instance that reaches Ready from status alone. | +| Inputs | (a) a stateless worker component descriptor (no declared `stateNamespaces`); (b) a stateful controller component descriptor with `placementMode: guest-local` targeting `Guest/g1`; (c) a stateful controller component descriptor with `placementMode: host-backed-guest` and 2 declared `attachments[]` targeting `Guest/g1`; (d) an attempted `placementMode: host-backed-guest` for a component descriptor schema-flagged as carrying "gateway credentials" (must be rejected `guest-local-required`). | +| Command/harness | `cargo test --manifest-path proofs/provider-state-export-spike/Cargo.toml -- --test-threads=1 provider_state_set_and_export`. | +| Metrics | (1) creation order: the stateless worker (a) has zero state Volumes; each *declared* state Volume exists and is Ready before its component's Process resource is created, in cases (b)/(c); (2) no bootstrap state Volume: the first `volume-local` instance on each target reaches Ready without any state Volume, and no cross-target bootstrap reference exists (asserted by tagging every provisioning call with its execution-target ID and checking zero cross-target references and zero bootstrap-storage calls); (3) Export-child count: exactly 0 for case (b), exactly 2 for case (c), each owned (`ownerRef`) by the source Volume, not by `Provider/`; (4) case (d) is rejected with `guest-local-required` before any Volume is created. | +| Pass/fail threshold | All four metrics binary pass across all 4 input cases; zero cross-target references and zero bootstrap-storage calls (metric 2) is a hard zero-tolerance gate, not a threshold. | +| Expected resource budget | ≤2 minutes wall time; ≤32 MiB RSS (pure in-memory graph construction, no real filesystem/virtiofs). | +| Failure interpretation | A creation-order violation (Process created before its declared state Volume is Ready) blocks every `ADR-046-provider-state` work item and the `ADR046-provider-00x` work items that depend on it; any bootstrap state-Volume mechanism or cross-target reference is a severity-blocking finding against the "No bootstrap state Volume" invariant in `ADR-046-components-processes-and-sandbox` and must be fixed structurally, never suppressed. | +| Affected decisions/work items | D076, D078, D086, D087; provider-state and volume-virtiofs implementation work items named in `ADR-046-provider-state` and `ADR-046-resources-volume`. | +| Cleanup | Deleted once core `ProviderDeployment`'s real implementation (destination named by the `ADR-046-provider-state` work items) carries an equivalent in-tree ordering/ownership/Export-count conformance test. | +| Status | Specified — not yet executed. | + +### SPIKE-10 — Volume ACL, `sourcePolicyId`, quota, and lifecycle-marker policies + +| Field | Value | +| --- | --- | +| Hypothesis | A Volume-local implementation can enforce `AclGrant` principals typed strictly as `User/` (no numeric UID/GID form accepted), resolve `source.settings.sourcePolicyId` to a real path only inside the private effect-adapter boundary (never surfacing the path in spec/status/audit), enforce `hard` quota by failing Volume creation immediately when the backing filesystem cannot guarantee the limit, and drive `CreatePolicy`/`RepairPolicy`/`CleanupPolicy`/`AdoptionPolicy`/`RestartPolicy` through their full state tables without silently reinterpreting any value. | +| Minimal disposable artifact | `proofs/volume-policy-spike/` — a fake `volume-local` controller operating against a real temporary directory (via `tempfile::TempDir`, cleaned up automatically) standing in for one allowlisted root, with a fake `VolumeSourceEffectPort`/`VolumeLayoutEffectPort` implementing exactly the layout-entry/ACL/quota logic described in `ADR-046-resources-volume`, never a broker call. | +| Inputs | (a) 6 layout entries covering every `CreatePolicy` value; (b) an ACL grant with a numeric-UID-shaped string (must be rejected at schema validation, not at runtime); (c) a `tmpfs`-kind Volume with `quota.maxBytes = 8 MiB`/`maxInodes = 1024` and an attempt to write 16 MiB (must fail at or before the 8 MiB boundary via the kernel `size=`/`nr_inodes=` mount options); (d) a `block-image`-kind Volume with `enforcement: hard` on a backing filesystem/loop device deliberately unable to guarantee the quota (a sparse file smaller than the requested quota) — must fail closed to `Failed` status with zero layout operations attempted; (e) one `sourcePolicyId` resolution, asserting the resolved absolute path never appears in the returned "public" Volume status/spec echo. | +| Command/harness | `cargo test --manifest-path proofs/volume-policy-spike/Cargo.toml -- --test-threads=1 volume_acl_quota_policy`. | +| Metrics | (1) every `CreatePolicy` value produces exactly its documented `StorageLifecycle`-analog behavior (matching the table in `ADR-046-resources-volume` §"CreatePolicy"); (2) numeric-UID ACL is rejected before any filesystem mutation; (3) tmpfs write beyond quota fails at the kernel boundary (`ENOSPC`/`EDQUOT`-class error), not silently truncated; (4) hard-enforcement Volume on an unenforceable backend reaches `Failed` with zero layout side effects (verified by asserting the target directory remains exactly as it was before the attempt); (5) resolved host path is absent from every field of the returned status/spec/audit-record structs (checked via a recursive string-search helper over the serialized DTOs). | +| Pass/fail threshold | All five metrics binary pass across all listed inputs; metric (5) is zero-tolerance (a single path leak anywhere in a public DTO is an automatic fail). | +| Expected resource budget | ≤2 minutes wall time; ≤64 MiB RSS; ≤32 MiB scratch disk (bounded by the 8 MiB tmpfs fixture plus the undersized sparse-file fixture). | +| Failure interpretation | A path leak is a severity-blocking security finding against D082 and blocks `ADR046-volume-001` through `ADR046-volume-006`; a quota-enforcement miss blocks the same work items pending a fix to the pre-creation filesystem-capability probe, never a relaxed `hard` semantics. | +| Affected decisions/work items | D032, D044, D062, D082, D083, D084; `ADR046-volume-001` through `ADR046-volume-006`. | +| Cleanup | Deleted once `d2b-provider-volume-local`'s own `tests/`/`integration/` suite (per its dossier) reproduces these five metrics with equal or stricter coverage against the real broker-mediated layout effect adapter. | +| Status | Specified — not yet executed. | + +### SPIKE-11 — systemd/minijail Process Provider pidfd acquire/verify/reap/adopt/quarantine conformance + +| Field | Value | +| --- | --- | +| Hypothesis | Both Process Provider implementations satisfy the identical pidfd contract in `ADR-046-resources-host-guest-process-user` §"Pidfd rules": a verified pidfd is acquired only after stable-identity verification, is never persisted/serialized/bus-exposed, is closed and reopened with full re-verification after every ProviderSupervisor restart, and ambiguous post-restart identity always quarantines rather than silently adopts or kills. | +| Minimal disposable artifact | `proofs/process-provider-conformance-spike/` — one shared conformance test suite (a single Rust `trait ProcessProviderHarness` with `launch`/`restart_supervisor`/`simulate_identity_drift` methods) instantiated twice: once against a real `clone3(CLONE_PIDFD)`-based minijail-shaped launcher (reusing the existing `packages/d2b-priv-broker/src/ops/spawn_runner.rs` real-spawn shape as a reference, invoked directly and unprivileged via a plain `fork`+`exec` substitute — no real broker/minijail sandbox compilation, since this spike proves the pidfd/identity state machine, not sandbox compilation), and once against a real transient systemd user-scope launcher (InvocationID+cgroup+MainPID+start-time) using `systemd-run --user --scope`. | +| Inputs | (a) 20 successful launches per implementation, each followed by a supervisor "restart" (drop and reacquire the pidfd) and re-verification; (b) 5 deliberate identity-drift cases per implementation (kill the original process and immediately start an unrelated process reusing the same PID before re-verification runs) which must quarantine, never silently adopt; (c) 1 case per implementation where the process exits cleanly before restart — must not attempt an invalid reap. | +| Command/harness | `cargo test --manifest-path proofs/process-provider-conformance-spike/Cargo.toml --features systemd-user -- --test-threads=1 process_provider_conformance` (the `systemd-user` feature gates the systemd half behind `systemd-run --user` availability, matching this repository's existing host-integration-only gating pattern for systemd-dependent tests; the minijail-shaped half has no such gate). | +| Metrics | (1) 20/20 successful launches per implementation acquire a pidfd only after all identity checks pass; (2) 20/20 restarts close and reopen the pidfd with full re-verification (never reusing the pre-restart pidfd); (3) 5/5 identity-drift cases per implementation quarantine (`adoptionState: quarantined`), never silently adopt the unrelated process nor blindly `SIGKILL` an unrelated PID; (4) 1/1 clean-exit case per implementation reports terminal status without an invalid reap attempt (no `ESRCH`/error swallowed silently). | +| Pass/fail threshold | All four metrics at 100% across both implementations (40 launches, 40 restarts, 10 drift cases, 2 clean-exit cases total); metric (3) is zero-tolerance — any single false adoption of an unrelated process is an automatic fail. | +| Expected resource budget | ≤5 minutes wall time (includes real process spawn/kill cycles); requires a Linux host with `clone3`/`pidfd_open` support and, for the systemd half, a running user `systemd --user` instance (skip with a clear diagnostic, not a silent pass, when unavailable — matching this repository's existing host-integration skip convention). | +| Failure interpretation | Any false adoption blocks `ADR046-process-002` (the systemd/minijail Provider work item in `ADR-046-components-processes-and-sandbox`) until the identity-verification tuple (executable hash + template generation + cgroup/scope placement + provider-specific attributes) is corrected; per anti-claim rule 3, the fix is never "trust the PID alone," which the spec already forbids. | +| Affected decisions/work items | D022, D051; `ADR046-process-001`, `ADR046-process-002`. | +| Cleanup | Deleted once `packages/d2b-provider-system-systemd` and `packages/d2b-provider-system-minijail` each carry this exact shared conformance suite in their own `tests/` per D059's required crate layout. | +| Status | Specified — not yet executed. | + +### SPIKE-12 — Nix direct ResourceSpec authoring → codegen → build validation → removed-resource cleanup + +| Field | Value | +| --- | --- | +| Hypothesis | A Nix expression authoring `d2b.zones..resources. = { type = "..."; spec = { ... }; }` directly mirrors the canonical ResourceTypeSchema (no second Nix vocabulary), a `cargo xtask gen-schemas`-shaped generator produces the same committed JSON Schema the Nix build validates against, an eval-time assertion rejects bound/ref/domain/cycle violations before any build, a build-time derivation rejects schema-violating or store-path-carrying `spec`/`config` values, and removing a configuration-managed resource from the next Nix generation triggers asynchronous finalizer-safe deletion (visible Degraded/pending-cleanup status) rather than an immediate destructive sweep. | +| Minimal disposable artifact | `proofs/nix-authoring-spike/` — a minimal flake (`flake.nix` + `resources.nix`) declaring 2 synthetic ResourceTypes (one with a `Ref` field, one with a nested object and a bound numeric field) with hand-written committed JSON Schemas under `proofs/nix-authoring-spike/schemas/`, a small standalone Rust `xtask`-shaped binary (`proofs/nix-authoring-spike/src/bin/gen-schemas.rs`) that emits the same schema from a hand-written DTO to prove the codegen/drift-gate shape (`git diff --exit-code`-style comparison) works end to end, and a Nix derivation that validates a rendered `resources.json` against the committed schema using the same offline/hermetic/no-network approach as `ADR-046-nix-configuration` §"Bundle integrity". | +| Inputs | (a) a valid two-resource configuration; (b) a configuration with a dangling `Ref` (must fail eval); (c) a configuration whose rendered `spec` violates the committed schema bound (must fail build); (d) a configuration whose `spec` contains a Nix-store-path-shaped string (must fail build per D070); (e) a second Nix generation that omits one of the two resources present in generation (a), with that resource's `managedBy: configuration`. | +| Command/harness | `nix build --no-link ./proofs/nix-authoring-spike#checks.$(nix eval --impure --raw --expr builtins.currentSystem).resources-valid` for case (a); `nix eval --impure ./proofs/nix-authoring-spike#checks... 2>&1` (expect eval failure) for case (b); `nix build ./proofs/nix-authoring-spike#checks....resources-schema-violation 2>&1` (expect build failure) for cases (c)/(d); `cargo run --manifest-path proofs/nix-authoring-spike/Cargo.toml --bin gen-schemas -- --check` (expect nonzero exit on intentional drift, matching `make test-drift`'s `xtask gen-schemas` + `git diff --exit-code` pattern) for the codegen check; a small harness script simulating generation (e) and asserting the omitted resource's status becomes `Degraded`/pending-cleanup rather than disappearing synchronously. | +| Metrics | (1) case (a) builds successfully and the rendered JSON is byte-identical on a second hermetic build (D-070-style reproducibility); (2) case (b) fails at eval, not build; (3) cases (c)/(d) fail at build with a structured error naming the exact offending Nix option path; (4) the codegen check exits nonzero exactly when the hand-written schema and generator-emitted schema diverge, and zero when they match; (5) the omitted resource in generation (e) is not present in the new generation's active resource set, is still observable (Degraded/pending-cleanup) until its simulated finalizer completes, and the resource present in both generations is untouched. | +| Pass/fail threshold | All five metrics binary pass; metric (1)'s reproducibility check must match byte-for-byte across 3 repeated hermetic builds. | +| Expected resource budget | ≤5 minutes wall time (Nix evaluation/build dominates); no network access during any `nix build` invocation (`--offline` or an equivalent sandboxed evaluation, matching this repository's existing hermetic-build convention). | +| Failure interpretation | A non-reproducible build or a schema-violating value that still builds blocks every `ADR-046-nix-configuration` work item and every ResourceType/Provider spec's own "Nix authoring and configuration cleanup" section; a synchronous (non-finalizer-safe) deletion on generation change is a severity-blocking finding against D057 and must be fixed structurally. | +| Affected decisions/work items | D057, D058, D069, D070; the Nix-authoring implementation work items in `ADR-046-nix-configuration` and in every ResourceType/Provider spec's own Nix section. | +| Cleanup | Deleted once the real `nixos-modules/resources.nix` + `packages/xtask` `gen-schemas` implementation (per `ADR-046-nix-configuration`'s own work items) reproduces these five metrics as part of `make test-drift`/`make test-flake`. | +| Status | Specified — not yet executed. | + +### SPIKE-13 — CLI dynamic Provider-projection discovery with bounded deadline and size + +| Field | Value | +| --- | --- | +| Hypothesis | The CLI discovers a Provider's `cliProjection` lazily via `InspectSchema` only when that Provider's subcommand is invoked or `--help` for `d2b provider` is requested, respects the exact bounds in `ADR-046-cli-and-operations` §"Dynamic descriptors — safety bounds" (64 KiB total, 32-byte name limits, 32 sub-verbs, 2 s per-Provider / 10 s total deadline), rejects a Provider-projected name colliding with a built-in verb at install/bind time (not at CLI runtime), and imposes zero added startup latency on any non-Provider command. | +| Minimal disposable artifact | `proofs/cli-discovery-spike/` — a fake `InspectSchema` server (a plain async TCP/Unix-socket stub, not real ComponentSession) returning configurable projection payloads, plus a minimal CLI-shaped harness implementing exactly the discovery/caching/rendering logic described in that section (lazy fetch, per-invocation-only cache, deadline enforcement, byte-escaping of completion strings). | +| Inputs | (a) a well-formed 8 KiB projection; (b) a 128 KiB projection (must be skipped for exceeding the 64 KiB bound, with the documented single stderr line, not a crash); (c) a projection whose top-level subcommand name is `list` (must be rejected at bind time with the built-in-collision rule); (d) a Provider whose `InspectSchema` never responds (must time out at exactly the 2 s per-Provider deadline and continue without blocking the CLI); (e) 6 Providers simultaneously slow, each taking 3 s, to verify the 10 s total-fetch deadline caps overall wait; (f) a projection containing a newline and an HTML-special character in a completion string, to verify escaping. | +| Command/harness | `cargo test --manifest-path proofs/cli-discovery-spike/Cargo.toml -- --test-threads=1 cli_dynamic_discovery`. | +| Metrics | (1) case (a) renders correctly; (2) case (b) is skipped with the exact documented single-line warning format; (3) case (c) is rejected before any command dispatch, with a stable error, not a runtime shadowing; (4) case (d)'s per-Provider wait is capped at 2 s (measured); (5) case (e)'s total wait is capped at 10 s, not 6×3=18 s; (6) case (f)'s rendered completion string contains no raw newline and no unescaped HTML/shell-special character; (7) a non-Provider command's measured startup time is statistically indistinguishable (within measurement noise, ≤5 ms difference) between a CLI build with zero installed Providers and one with 20 installed (slow) Providers, proving zero added startup cost. | +| Pass/fail threshold | All seven metrics binary pass except (7), which is a numeric gate: ≤5 ms measured difference in non-Provider command startup latency (median of 50 runs) between the 0-Provider and 20-Provider fixtures. | +| Expected resource budget | ≤3 minutes wall time (dominated by the deliberate 2 s/3 s timeout cases). | +| Failure interpretation | A collision that reaches runtime shadowing, or a startup-latency regression on non-Provider commands, blocks `ADR046-cli-001` through `ADR046-cli-011` (the exact set depends on which sub-item owns discovery/projection rendering) until the discovery/caching/bind-time-rejection logic is corrected; per anti-claim rule 3, the fix is never "widen the deadline" to make a slow-Provider case pass. | +| Affected decisions/work items | D064; the CLI implementation work items `ADR046-cli-001` through `ADR046-cli-011` in `ADR-046-cli-and-operations`. | +| Cleanup | Deleted once the real `d2b` CLI binary (destination of the owning `ADR046-cli-*` work item) carries an equivalent in-tree discovery/bound/collision/latency conformance test. | +| Status | Specified — not yet executed. | + +### SPIKE-14 — clean v3 reset and cutover (no v2 alias dispatch, fresh Zone bootstrap) + +| Field | Value | +| --- | --- | +| Hypothesis | A v3 CLI binary built from this spec's authoring baseline contains zero executable dispatch paths for any of the removed v2 commands listed in `ADR-046-cli-and-operations` §"v2 command surface removed at 3.0 clean break", exposes no migration-diagnostic command, and can bootstrap a fresh Zone from an empty state directory through Nix generation activation to a Ready `Zone/` self-resource with no v2/Realm state import of any kind. | +| Minimal disposable artifact | `proofs/clean-cutover-spike/` — a static-analysis check (`cargo metadata` + a symbol-table scan of the built CLI binary, e.g., via `nm`/`strings`-shaped inspection or, more robustly, a source-level `grep`-based assertion against the CLI crate's command-table source) confirming none of the 27 removed-command strings from that table dispatch to a handler function, plus a minimal "fresh Zone bootstrap" harness driving the fixed bootstrap sequence in `ADR-046-components-processes-and-sandbox` §"Bootstrap boundary" against fakes for the Zone runtime, broker, and fixed controllers (no real redb/broker — this spike proves *sequencing and state-freshness*, not the redb/process spikes already covered by SPIKE-01/SPIKE-03/SPIKE-11). | +| Inputs | (a) the 27-row removed-command table from `ADR-046-cli-and-operations`; (b) an empty state directory (no `/var/lib/d2b` content of any kind, no Realm artifacts); (c) one deliberately injected legacy Realm-shaped file dropped into the fresh state directory before bootstrap, which must be ignored (never imported, never migrated) by the v3 bootstrap sequence. | +| Command/harness | A source-scan test: `cargo test --manifest-path proofs/clean-cutover-spike/Cargo.toml -- --test-threads=1 no_v2_alias_dispatch` asserting, for every one of the 27 removed-command strings, that the CLI crate's command-table source (a copy of the relevant `packages/d2b/src/lib.rs`-shaped command enum used by this spike's fixture, not the real crate) contains no executable arm; a second test, `fresh_zone_bootstrap_ignores_legacy_state`, drives the fake bootstrap sequence against inputs (b) and (c). | +| Metrics | (1) 27/27 removed commands have zero executable dispatch arms and no migration-diagnostic branch exists; (2) fresh bootstrap from an empty directory reaches a Ready `Zone/` self-resource through the fixed sequence (Zone runtime → broker → core-controller → minimum Host/Guest supervisor → user supervisor → system-minijail) with no step skipped or reordered; (3) the injected legacy Realm-shaped file is never read, referenced, or copied by any step of the bootstrap sequence (verified by a file-access-tracing fake filesystem that fails the test on any open() of that path). | +| Pass/fail threshold | All three metrics binary pass; metric (3) is zero-tolerance — a single stat()/open() of the legacy file is an automatic fail, since ADR 0046 defines the cutover as destructive with the pre-ADR45 v3 tree as ancestry, not main, and with no v2 data-import path of any kind. | +| Expected resource budget | ≤2 minutes wall time; ≤32 MiB RSS. | +| Failure interpretation | Any surviving v2 dispatch arm or any legacy-file access blocks the entire CLI/cutover implementation wave; per D001/D041, the fix is deletion of the offending path, never a compatibility shim. | +| Affected decisions/work items | D001, D002, D041, D064; the v2-removal work items implied by `ADR-046-cli-and-operations` §"v2 command surface removed at 3.0 clean break" and the bootstrap work items in `ADR-046-components-processes-and-sandbox`. | +| Cleanup | Deleted once the real `packages/d2b` CLI crate and the real Zone-runtime bootstrap sequence are built; their own workspace-policy/lint gates (extending the existing `deny.toml`/policy-test convention already used for other closed-set invariants in this repository) enforce the same zero-v2-dispatch and no-legacy-import guarantees permanently, superseding this spike. | +| Status | Specified — not yet executed. | + +### SPIKE-15 — representative local / cloud / interaction Provider end-to-end composition + +| Field | Value | +| --- | --- | +| Hypothesis | Three representative Provider compositions each reconcile end-to-end through Zone bootstrap → Provider install → Process launch → Ready status inside the shared aggregate resource-plane budget, without any composition requiring a code path outside the ones already defined by the other 14 spikes: (a) **local** — `Guest/dev-vm` on `Provider/runtime-cloud-hypervisor` with a `Provider/volume-local`-backed state Volume and Process Providers `Provider/system-minijail` launching the VMM process; (b) **cloud** — a `Guest` on `Provider/runtime-azure-container-apps` (or `runtime-azure-virtual-machine`) acquiring its identity through `Provider/credential-managed-identity` over the SPIKE-08 KK delivery path; (c) **interaction** — a `Provider/shell-terminal` (or `display-wayland`) Process under a user-domain `Host`, mounting an operator-declared `Volume` via the SPIKE-10 ACL/quota path (shell-terminal/display-wayland declare no Provider state Volume of their own; their bounded non-secret operational state lives in resource `status`/the core Operation ledger per D087). | +| Minimal disposable artifact | `proofs/e2e-composition-spike/` — a single fake-but-integrated harness wiring together the fakes already built for SPIKE-01 (store), SPIKE-02/03 (reconcile/fast-path), SPIKE-04 (EffectPort), SPIKE-05 (Provider packaging), SPIKE-06/07 (bus/transport), SPIKE-08 (credential KK), SPIKE-09/10 (state/volume), and SPIKE-11 (process conformance) into one process, driving all three compositions through the same fixed bootstrap sequence as SPIKE-14, so this spike is explicitly an **integration** of the other 14 fakes rather than a sixteenth independent fake stack. | +| Inputs | Three Nix-authored (per SPIKE-12's harness) ResourceSpec sets, one per composition, each declaring exactly the resources named in the Hypothesis row; a shared Zone budget ceiling matching `ADR-046-resource-store-redb`'s aggregate ≤64 MiB idle-RSS target, measured with all three compositions' fixed/mandatory processes running simultaneously (system-core, system-minijail, and each composition's own controller/service processes — excluding the large Guest runtime processes themselves, which are out of the resource-plane budget by definition in that spec). | +| Command/harness | `cargo test --manifest-path proofs/e2e-composition-spike/Cargo.toml -- --test-threads=1 end_to_end_composition_{local,cloud,interaction}` (three named tests, one per composition) plus `/usr/bin/time -v` RSS measurement (methodology identical to SPIKE-01) taken once with all three compositions' fixed/mandatory control-plane processes running concurrently. | +| Metrics | (1) each composition reaches `phase: Ready` on its top-level resource (the `Guest` for (a)/(b), the `Process` for (c)) within a bounded wall-clock budget of 5 s from Nix-generation-activation-equivalent trigger to Ready, using only the fakes already validated by the other 14 spikes (no new unvalidated code path); (2) the cloud composition's credential acquisition reuses exactly the SPIKE-08 KK delivery path with no plaintext leak, re-asserted in this integrated context; (3) the interaction composition's mounted `Volume` passes the exact SPIKE-10 ACL/`sourcePolicyId`/quota checks, re-asserted in this integrated context; (4) combined idle RSS of the fixed/mandatory control-plane processes across all three compositions running simultaneously stays within the aggregate budget scaled for 3 Zones sharing one host (≤3× the single-Zone ≤64 MiB target, i.e., ≤192 MiB, since each Zone embeds its own store/core-controller/system-minijail per D006/D007). | +| Pass/fail threshold | Metrics (1)-(3) binary pass for all three compositions; metric (4) ≤192 MiB combined median RSS over 3 repeated runs. | +| Expected resource budget | ≤10 minutes wall time (dominated by the 3×5 s Ready-latency budget plus fixture setup); ≤256 MiB total build RSS. | +| Failure interpretation | A composition that only reaches Ready by adding a code path not already covered by SPIKE-01 through SPIKE-14 is itself a finding: it means the feasibility catalog is incomplete for that composition, and the missing capability must be added to this catalog (via a spec revision) before the corresponding production work item is scheduled, per anti-claim rule 7 ("no unresolved entries") applied prospectively. A metric-4 RSS miss revises the per-Zone footprint budget or the number of Zones assumed to co-reside on one host, never the individual Zone target already fixed by `ADR-046-resource-store-redb`. | +| Affected decisions/work items | D006, D007, D008, D043, D044, D047, D048, D076; every Guest/Volume/Credential/interaction Provider dossier's own implementation work items, and the core bootstrap work items in `ADR-046-components-processes-and-sandbox`. | +| Cleanup | Deleted once the real integration test suites named by the individual Provider dossiers (`integration/` per D059) collectively reproduce compositions (a), (b), and (c) against real (non-fake) Zone/store/bus/broker code, at which point this spike's role — proving the fakes compose without a missing capability — is fully subsumed. | +| Status | Specified — not yet executed. | + +### SPIKE-16 — three-layer spec + status shape: schema parity, base-only projection, extension versioning + +| Field | Value | +| --- | --- | +| Hypothesis | The frozen three-layer shape supports both status (D088) and desired spec (D089): universal base + ResourceType base (`status.resource` / `spec.*` incl. `spec.providerRef`) + optional canonical Provider extension (`status.provider.details` / `spec.provider.settings`). It supports (a) identical base shape across multiple implementations of one ResourceType (status.resource and base spec both); (b) a generic base-only consumer that reads/watches/authors on the base only while ignoring an absent, unknown, or version-mismatched extension; (c) strict per-layer bounds and signed/registered extension validation with unknown-field denial and shadow rejection; (d) atomic single-mutation status writes; (e) each `ResourceApiBinding` implements the exact base spec/status fingerprint, accepts the canonical minimal valid base Spec, and rejects an unsupported optional base capability only via its signed capability matrix and a provider-neutral `unsupported-capability`; and (f) `spec.provider` validation against `spec.providerRef` at build and admission. | +| Minimal disposable artifact | `proofs/layered-shape-spike/` — an in-process fake resource store (a `HashMap` oracle, not real redb) with registered spec/status extension-schema tables and a capability matrix; two fake `Guest` implementations (`runtime-cloud-hypervisor`, `runtime-azure-container-apps`) each accepting the same base spec and writing the same `status.resource`, with distinct `spec.provider.settings`/`status.provider.details`; a fake base-only consumer authoring base spec and reading universal + `status.resource`. | +| Inputs | (a) two implementations accepting an identical minimal base Spec and producing identical `status.resource`; (b) `spec.provider`/`status.provider` with an unregistered `schemaId`; (c) an unknown field in `settings`/`details`; (d) a `settings`/`details` restating a base field; (e) an oversize extension (>32 KiB); (f) an optional base capability one implementation's signed matrix marks unsupported; (g) a base-only consumer against each case. | +| Command/harness | `cargo test --manifest-path proofs/layered-shape-spike/Cargo.toml -- --test-threads=1 layered_three_layer_shape`. | +| Metrics | (1) both implementations accept the identical minimal base Spec and produce a byte-identical `status.resource` and base-spec shape; (2) base-only consumer authors/reads/watches all cases successfully and never parses `settings`/`details`; (3) unregistered schema → `spec-provider-schema-invalid`/`status-provider-schema-invalid`; unknown field → rejected; shadow of base → `spec-provider-shadow`/`status-provider-overlap`; oversize → bounds error; (4) all present status layers commit in exactly one mutation; a forced partial-layer write is rejected; (5) the capability-unsupported case returns provider-neutral `unsupported-capability` and never a silently-ignored base field; (6) `spec.provider` fails validation when `spec.providerRef` names a Provider that did not register that schema. | +| Pass/fail threshold | All metrics binary pass; metric (2) base-only compatibility across an unknown/newer extension is a hard zero-tolerance gate (a generic consumer must never fail because a Provider extension changed). | +| Expected resource budget | ≤2 minutes wall time; ≤32 MiB RSS (pure in-memory shape/validation checks). | +| Failure interpretation | A base-only consumer that fails on an unknown/newer extension, two implementations that diverge on the base spec or `status.resource`, or a base capability honored/degraded outside the capability matrix, is a severity-blocking finding against D088/D089: the shared field is either mis-placed in an extension (must be promoted to base) or the base contract is not truly provider-neutral. Fixed structurally, never suppressed. | +| Affected decisions/work items | D027, D028, D037, D075, D088, D089; resource object/API/store/reconcile work items and every Provider dossier's spec/status-schema work item. | +| Cleanup | Deleted once the real resource-contract crate (`packages/d2b-contracts`) and the provider conformance kit reach equal base-schema-parity, minimal-base-acceptance, capability-matrix, base-only-projection, and extension-version test coverage. | +| Status | Specified — not yet executed. | + +### SPIKE-17 — expedited reconcile commit-proof gating and currency/disruptive-upgrade planning + +| Field | Value | +| --- | --- | +| Hypothesis | (D090) An expedited (`waitForReconcile`) mutation can run controller preflight/plan in parallel with a reserved-revision commit while gating every external effect/finalizer/status write on a typed `CommittedRevisionProof`, enter a bounded priority lane in the same per-resource single-flight, and return committed-object + one-pass disposition + `statusPersistence` without waiting for the async status write; an `Abort` yields no effect; a durable commit is never rolled back on later reconcile timeout; and a normal re-entry no-ops via the `(UID,generation,revision,operationId)` idempotency key. (D091) A controller can assess currency into `status.update`, report `UpgradeRequired` for a disruptive change instead of applying it, and a dependency-aware planner can topologically drain→recycle→restart dependents (GPU example) while preserving UID/state/TPM. | +| Minimal disposable artifact | `proofs/expedited-upgrade-spike/` — an in-process fake resource store (reserved-revision + commit-proof/`Abort` emitter, not real redb) and a fake single-flight reconciler with a bounded priority lane; a fake GPU Device with dependent Processes for the drain/recycle/restart planner; a deterministic clock. | +| Inputs | (a) expedited create where commit succeeds then one pass reaches a disposition; (b) expedited create where commit `Abort`s (assert zero effects); (c) controller finishes preflight before proof (assert no effect until proof); (d) delayed status write (assert `statusPersistence: pending`); (e) a normally-queued reconcile after an expedited pass (assert no-op/rejoin, no duplicate); (f) expedited timeout after commit (assert committed-but-pending, queue continues); (g) restart mid-expedited (assert no duplicate effect); (h) each currency trigger → correct `state`/`reasons`; (i) disruptive change → `UpgradeRequired` not applied in place; (j) GPU with dependents → `Blocked`, planner drains/recycles/restarts, UID/state/TPM preserved. | +| Command/harness | `cargo test --manifest-path proofs/expedited-upgrade-spike/Cargo.toml -- --test-threads=1 expedited_and_upgrade`. | +| Metrics | (1) no effect/finalizer/status mutation occurs before `CommittedRevisionProof`; `Abort` → zero effects; (2) durable commit persists regardless of a later pass timeout; (3) idempotency key dedups the normal re-entry (exactly-once effect); (4) response returns committed object + one-pass disposition + `statusPersistence` without blocking on the status write; (5) priority lane is bounded/fair (ordinary reconciles not starved); (6) currency states/reasons/disruption computed correctly per trigger; (7) disruptive change never applied in place (always `UpgradeRequired`); (8) planner order is drain→recycle→restart and preserves UID/state/TPM. | +| Pass/fail threshold | All metrics binary pass; metrics (1)-(3) and (7)-(8) are hard zero-tolerance gates (no ungated effect, no lost commit, no duplicate, no surprise disruption, no silent state/TPM loss). | +| Expected resource budget | ≤3 minutes wall time; ≤48 MiB RSS (in-memory store/reconciler/planner). | +| Failure interpretation | An effect before proof, a rolled-back commit, a duplicated effect, an in-place disruptive change, or a planner that disrupts dependents before draining is a severity-blocking finding against D090/D091 and must be fixed structurally, never suppressed. | +| Affected decisions/work items | D005, D030, D084, D090, D091; resource API/store/reconcile/core-controller work items and every Provider dossier's currency/upgrade work item. | +| Cleanup | Deleted once the real resource-store/reconcile/core-controller crates and the provider conformance kit reach equal commit-proof-gating, idempotency, priority-lane, currency, and dependency-planner coverage. | +| Status | Specified — not yet executed. | + +### SPIKE-18 — Endpoint resource promotion, no-locator resolution, producer generation, consumer trigger + +| Field | Value | +| --- | --- | +| Hypothesis | (D092) A stable endpoint can be modeled as an `Endpoint` resource with `ownerRef` (lifecycle) and `producerRef` (Process/Device/Guest/Host) carrying only closed class/transport/locality/purpose + bounded fingerprints and NO raw locator; `spec.visibility` is exactly `owner\|provider\|zone`, while the strict `consumerPolicy` object is the only finer gate; a consumer references `Endpoint/` and resolves to a private transport/FD only through an EffectPort/LaunchTicket path under authorization; a producer restart bumps `endpointGeneration`/`status.update` and fires the consumer's dependency trigger; and the promotion test correctly keeps high-churn handles (pidfd, fd index, named stream, `OwnedTransport`, `operationId`) as non-resource opaque IDs. | +| Minimal disposable artifact | `proofs/endpoint-resource-spike/` — an in-process fake resource store with a `producer_index`, a fake TPM Device producing an `Endpoint`, a fake consumer Process referencing it, and a fake EffectPort that resolves an Endpoint to an opaque private handle only for an authorized consumer; a deterministic clock. | +| Inputs | (a) Device creates an owned `Endpoint` (producerRef=Device, no locator in spec/status); (b) authorized owner-, provider-, and Zone-scope consumers resolve via the EffectPort; (c) invalid visibility aliases and mismatches for each canonical `consumerPolicy` allowlist; (d) producer restart → `endpointGeneration` bump; (e) child-first deletion when the producer/owner is deleted; (f) a set of high-churn handles fed to the promotion-test lint; (g) all Markdown Endpoint examples fed to an Endpoint-only drift parser. | +| Command/harness | `cargo test --manifest-path proofs/endpoint-resource-spike/Cargo.toml -- --test-threads=1 endpoint_resource`. | +| Metrics | (1) Endpoint spec/status contain no path/address/CID/port/fd/credential and visibility accepts exactly `owner\|provider\|zone`; (2) authorized resolve returns a handle only through the EffectPort; (3) an out-of-scope subject or any present `allowedSubjects`/`allowedProviderComponents`/`allowedOperations` mismatch returns `endpoint-resolve-denied`, with no locator; (4) producer restart bumps `endpointGeneration` and fires exactly one consumer `dependency-changed` trigger; (5) deleting the producer/owner deletes the Endpoint child-first; (6) the promotion-test lint classifies pidfd/fd-index/named-stream/`OwnedTransport`/`operationId` as permitted opaque (non-resource) and any stable cross-boundary endpoint as requiring promotion; (7) every Endpoint example uses the exact visibility enum and the object-form `consumerPolicy` as its only finer gate, without inspecting ResourceExport visibility. | +| Pass/fail threshold | All metrics binary pass; metrics (1) and (3) are hard zero-tolerance gates (no locator in the resource surface; no unauthorized resolution). | +| Expected resource budget | ≤2 minutes wall time; ≤32 MiB RSS (in-memory store/EffectPort/lint). | +| Failure interpretation | A raw locator in an Endpoint's spec/status, an unauthorized resolution, or a high-churn handle wrongly promoted (or a stable cross-boundary endpoint left as an opaque ID) is a severity-blocking finding against D092 and must be fixed structurally, never suppressed. | +| Affected decisions/work items | D010, D022, D081, D084, D088, D089, D092; resource object/API/store/reconcile/core-controller work items and every Provider dossier's endpoint-migration work item. | +| Cleanup | Deleted once the real resource-contract/store/reconcile crates and the provider conformance kit reach equal Endpoint no-locator, resolution-authorization, producer-generation, consumer-trigger, and promotion-test-lint coverage. | +| Status | Specified — not yet executed. | + +### SPIKE-19 — Entra identity-Guest login/token grounding (fake Entrablau service) + +| Field | Value | +| --- | --- | +| Hypothesis | (D093) `credential-entra` can be secret-free by grounding login/token acquisition in an Entrablau-enabled identity `Guest`: the controller binds `identityGuestRef` + a login `Endpoint`, `BeginLogin` opens an authenticated end-to-end session to a Guest login service that conducts the interactive login inside the Guest, status carries only bounded non-secret interaction observations, and an on-demand access token is delivered end-to-end (Noise_KK) only to the exact `consumerRef` consumer while Host/bus see ciphertext — with no Host login/`DefaultAzureCredential`/env/DBus/path/browser fallback and no direct Entra egress from the controller. Entrablau is external/sibling, so target integration is ADR-only and proven here with a fake service; a manual real-Guest login is a separate non-CI check. | +| Minimal disposable artifact | `proofs/entra-guest-login-spike/` — an in-process fake resource store; a fake "Entrablau Guest login service" behind a fake login `Endpoint` that scripts success/required/cancel/timeout and issues opaque token leases over a fake KK channel; a fake consumer process; a fake Host observer asserting it only sees ciphertext. NO live Entra, NO network. | +| Inputs | (a) BeginLogin → scripted success → on-demand token lease to the authorized consumer; (b) login-required/AwaitingUser; (c) CancelLogin; (d) deadline timeout; (e) controller restart mid-login; (f) login Endpoint unavailable / generation mismatch; (g) Host placement of the login service; (h) a cross-Zone `identityGuestRef`. | +| Command/harness | `cargo test --manifest-path proofs/entra-guest-login-spike/Cargo.toml -- --test-threads=1 entra_guest_login`. | +| Metrics | (1) no token/URL/cookie/authority-conferring device code/user PII appears in Credential status/audit/OTEL (only closed `interactionState` + bounded metadata); (2) the raw token reaches only the exact `consumerRef` consumer over KK; the Host observer sees ciphertext only; (3) cancel and deadline-timeout leave the durable Credential unchanged and never block past the deadline; (4) restart mid-login resumes/re-derives without leaking secrets; (5) Endpoint-unavailable/generation-mismatch → typed error and no token; (6) Host placement → `host-placement-rejected`; cross-Zone `identityGuestRef` → rejected; (7) the controller performs no direct Entra network call (all Entra flow is inside the fake Guest service). | +| Pass/fail threshold | All metrics binary pass; metrics (1), (2), and (7) are hard zero-tolerance gates (no secret/PII in observable surfaces; token only to the exact consumer; no controller egress). | +| Expected resource budget | ≤3 minutes wall time; ≤48 MiB RSS (in-memory store + fake Guest service). | +| Failure interpretation | A token/URL/cookie/PII in an observable surface, a token reaching anyone but the exact consumer, a Host-grounded login path, or a direct Entra egress from the controller is a severity-blocking finding against D093 and must be fixed structurally, never suppressed. A manual real-Guest login with the sibling `vicondoa/entrablau.nix` composed validates the contract end-to-end outside CI. | +| Affected decisions/work items | D048, D055, D056, D088, D089, D090, D092, D093; credential-entra dossier work items and the ACA/Azure-VM consumer work items. | +| Cleanup | Deleted once `packages/d2b-provider-credential-entra` and the identity-Guest integration tests reach equal fake-service coverage and a documented manual real-Guest login procedure exists. | +| Status | Specified — not yet executed. | + +### SPIKE-20 — Cross-Zone singleton sharing via ResourceExport/ResourceImport (D096) + +| Field | Value | +| --- | --- | +| Hypothesis | (D096) Deterministic cross-Zone sharing requires a signed Provider projection factory binding a qualified semantic/provider-neutral `*Service` type, matching qualified semantic `*Binding` type, allowed Service backing refs, allowed Binding target refs, and projection schema/fingerprints. Export targets only the one owner Service; import preserves that semantic type in exactly one non-authoritative projection Service while local `providerRef` independently selects the implementation; operator-authored local Bindings express desired consumer intent, reference it, and own Process/Endpoint children. The canonical minimal base works without `spec.provider`; observed realization exists only in status. No Device/Endpoint/Binding projection, auto-Binding, or FD/secret/path/locator transfer is needed. | +| Minimal disposable artifact | `proofs/cross-zone-singleton-share-spike/` — an in-memory two-Zone resource store/catalog, signed/tampered factory fixtures, fake owner/import/Binding controllers, deterministic owner/dependency graph, fake ZoneLink/clock/stream, and a real encrypted named-stream feature. The fake tier exercises all metadata/admission/lifecycle rules without processes, sockets, sleeps, or network; the integration feature swaps in the production stream from SPIKE-06/SPIKE-07. | +| Inputs | (a) `audio.d2bus.org.AudioService` + `audio.d2bus.org.AudioBinding`, initially implemented by `audio-pipewire`: owner backed by local Device+Endpoint, export to work Zone, same-type projection Service, and two authored Bindings targeting a Guest/User; (b) `security-key.d2bus.org.SecurityKeyService` + `security-key.d2bus.org.SecurityKeyBinding` with `device-security-key` and exclusive fair queue; (c) `telemetry.d2bus.org.TelemetryService` + `telemetry.d2bus.org.TelemetryBinding` with `observability-otel` and producer backpressure; (d) `usb.d2bus.org.UsbService` + `usb.d2bus.org.UsbBinding` with `device-usbip` under allow/deny policy combinations; (e) every other Provider forbidden; (f) canonical minimal base without `spec.provider`, independently selected conformant owner/consumer implementations, Core projection attempts with `spec.provider`, Provider/adapter-identity-only descriptor mutations, and negatives for implementation-qualified type, implementation detail in base, absent/unsigned/wrong Service/Binding factory, disallowed backing/target, schema mismatch, and fingerprint tamper; (g) direct Device/Endpoint/Binding/Credential export, attempted Device/Endpoint/Binding projection, observed state smuggled into Binding spec, forged Binding authority status, stale generation, and FD/secret/path/socket/device-handle transfer; (h) security-key and USB Providers resolve one fake physical token through different labels to the same trusted USB identity and attempt to substitute Provider-private authority classes/digests. | +| Command/harness | Fast hermetic: `cargo test --manifest-path proofs/cross-zone-singleton-share-spike/Cargo.toml -- --test-threads=1 fake_stream_export_import`. Slower integration: `cargo test --manifest-path proofs/cross-zone-singleton-share-spike/Cargo.toml --features real-stream -- --test-threads=1 real_stream_export_import` after the real bounded encrypted stream crate is available. | +| Metrics | (1) factory signature/type/ref/schema/fingerprint validation is exact and fail-closed; Provider/adapter identity is authenticated by the signed descriptor but does not change the semantic fingerprint; (2) only owner Service accepted as export target; (3) exactly one same-semantic-type projection Service with import ownerRef, `providerRef`, semantic base/import fields, no `spec.provider`, and zero Device/Endpoint/Binding projections, including when local Provider implementations differ; (4) Binding exists only when authored, validates service/target refs, keeps desired intent in spec and observations only in status, cannot write remote authority, and common/provider observations remain under `status.resource`/`status.provider` respectively; (5) graph exactly import→Service→Binding→owned Process/Endpoint; (6) revoke/update propagates through that graph; import finalizer waits on Binding refs and never deletes them; (7) the four exact semantic pairs are admitted under their initial Provider mapping, USB policy is gated, and every other family is forbidden; security-key and USB implementations resolve one token to the same Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` tuple and the second receives `physical-usb-backing-conflict` before effects, regardless of Provider-private claims; (8) Nix lowering/resource names are stable across three evaluations and canonical minimal base works without `spec.provider`; (9) quota/fairness/deadline/cancel/reconnect behavior exact; (10) no FD/secret/Credential/backing/remote Ref/path/locator/bytes or PipeWire/CTAPHID/OTEL/USBIP base detail leaks, and intermediaries see ciphertext; high-churn sessions/streams never enter the store. | +| Pass/fail threshold | Every metric passes in the fast hermetic tier. The real-stream tier must reproduce stream, revoke, backpressure, cancellation, and redaction invariants for audio, security-key, observability, and policy-gated USBIP before `ADR046-zone-control-019`/`020` complete. Metrics (1)-(4), (7), and (10) are zero-tolerance security/admission gates. | +| Expected resource budget | Hermetic tier ≤3 minutes wall time and ≤64 MiB RSS. Real-stream integration tier ≤10 minutes wall time and ≤128 MiB RSS, allowed to run outside the fastest proof loop but required before implementation validation retires the spike. | +| Failure interpretation | A non-Service export, wrong projection type/cardinality, auto-Binding, observed state in Binding spec, Binding authority spoof, factory bypass, forbidden-Provider admission, plaintext/FD/secret/path leak, or stale reconnect is severity-blocking and requires structural contract/controller repair. Quota/fairness misses revise arbitration, never these invariants. | +| Affected decisions/work items | D096; `ADR046-zone-control-019`, `ADR046-zone-control-020`; Provider toolkit factory conformance; audio-pipewire, device-security-key, observability-otel, and policy-gated device-usbip adapter work. | +| Cleanup | Deleted only after the real controller/toolkit has equal hermetic factory/admission/graph/finalizer/Nix-stability coverage and production encrypted-stream integration covers all four classified families. | +| Status | Specified — not yet executed. | + +## Implementation validation — how a spike is retired + +Every spike above ends with a "Cleanup" row naming the exact future crate +whose own in-tree tests must reach equal or stricter coverage before the +disposable `proofs//` crate is deleted. This is the same disposition +already used for `proofs/w0-ch-connect-proof` and +`proofs/chunked-stdio-conformance` (both remain until their own owning +implementation absorbs their coverage) and it generalizes here as a fixed +three-stage lifecycle: + +1. **Spike stage.** The disposable crate under `proofs/` exists solely to + falsify or corroborate one hypothesis with a fixed harness. It is added to + `tests/test-proofs.sh`'s crate list (mirroring the two existing entries) + only when it is authored — this spec does not add it, since this spec + adds no code. +2. **Work-item stage.** The named `ADR046-*` work item (in the resource, + controller, or Provider spec that owns the destination) copies the + *proven shape* — not the disposable crate itself — into its real + destination crate, and extends its own `tests/`/`integration/` suite to + reach or exceed the spike's metrics. This is the "Validation" column of + that work item, already required by `docs/specs/README.md`; this spec + does not duplicate or relax it. +3. **Retirement stage.** Once the real crate's CI-gated test/bench suite + reproduces the spike's pass/fail thresholds (or stricter), the spike + author (or the owning work item's author) deletes the `proofs//` + crate and its `tests/test-proofs.sh` entry in the same change that lands + the real coverage, so there is never a window where a deleted disposable + proof is the only evidence for a shipped behavior. + +No spike in this catalog is retired by this spec; retirement is always +performed by the owning production work item, per anti-claim rule 5. + +## Hermetic test-runtime budget baseline (D094) + +The D094 execution budgets in `ADR-046-validation-and-delivery` §10.16 are +measurable and are recorded, not asserted by feel: + +- **Reference runner.** The pinned CI/reference runner class (fixed vCPU/RAM + shape) is recorded with the ledger; the same class is used for the + historical regression comparison so a budget change reflects test cost, not + hardware drift. +- **Repetitions and statistic.** Each measured test/crate/shard is executed a + fixed, recorded repetition count with a warm cache; the reported figure is + the p95 of execution-only time (build excluded). +- **Fail/regression policy.** A run fails when any budget in §10.16 is + exceeded or when a test/crate/shard regresses beyond the recorded historical + threshold versus the previous committed ledger. A classified crypto/property + exception is compared only against its own declared per-test budget and + capped case count. +- **Cold compile tracked separately.** Cold compilation time is recorded on a + separate line and optimized through shared cache and dependency discipline; + it is never mixed into the execution budgets, and correctness never depends + on any cache. + +This baseline is established by `ADR046-feasibility-011` and consumed by the +test-runtime ledger/timing gate (`ADR046-delivery-007`/`ADR046-streamline-022`). + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `docs/adr/0046-d2b-3-provider-control-plane.md` (kcp measurement, decision D003); `ADR-046-decision-register` D003-D086; `packages/d2b-core/src/{storage,sync}.rs`; `packages/d2bd/src/supervisor/{dag,pidfd}.rs`; `packages/d2b-priv-broker/src/ops/spawn_runner.rs` and `tests/pidfd_real_spawner.rs`; `packages/d2b-realm-router/src/*`; main `a1cc0b2d` `packages/d2b-session*` (re-executed in E3: 70/70 tests passing) | +| Evidence class | Mixed by claim; see §"Evidence classification matrix" above for the exact class of every subsystem this spec touches — no single class applies to the whole document | +| Behavior retained | The kcp measurement and its rejection rationale (E1); the redb design rationale (E2); the exact main reuse inventory and its currently-green test count (E3); the current v3 reachable-file inventory for storage/DAG/pidfd/router (E4) | +| Required delta | Every item in the "Evidence classification matrix" marked `unknown-requires-spike` needs its named spike executed (with a passing or revised-and-repassing result) before its owning `ADR046-*` work item may start, per anti-claim rules 1, 3, and 5 | +| Reuse path | SPIKE-06/07/08 explicitly reuse main `a1cc0b2d`'s `d2b-session`/`d2b-session-unix` crates as path dependencies from a pinned local checkout, per the same reuse policy as `ADR-046-componentsession-and-bus` | +| Replacement/deletion | No spike crate is ever a replacement for production code; each is deleted per its own Cleanup row once its owning production work item supplies equal-or-stricter in-tree coverage | +| Feasibility proof | This spec *is* the feasibility-proof registry; it has no further "proof of itself" beyond the completed evidence in E1-E4 and the reproducibility discipline in anti-claim rule 6 | +| Future owner | `ADR046-feasibility-001` through `ADR046-feasibility-010` below | + +## Implementation work items + +Each item below authors one or more of the 15 spikes above as a disposable +`proofs//` crate, runs it, and records its result. None of these items +touch `packages/`, `nixos-modules/`, or any other production path; their sole +production-adjacent output is the pass/fail evidence that unblocks the +downstream `ADR046-*` work items named in each spike's "Affected +decisions/work items" row. + +### ADR046-feasibility-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-feasibility-001` | +| Dependency/owner | W0 shared contract root; store/reconciliation integrator | +| Current source | `packages/d2b-core/src/{storage,sync}.rs` (atomic/idempotency reference shape, E4); no redb usage exists anywhere in this repository at any inspected commit | +| Reuse source | None (redb is a new external dependency; no main or v3 code implements it) | +| Reuse action | adapt | +| Destination | `proofs/redb-resource-store-spike/` | +| Detailed design | Implements SPIKE-01 and SPIKE-02: the eight-table schema, fair write queue, blocking store-actor, watch registrar, and hint bus described in those two spike entries Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (the atomic-write/idempotency discipline in `storage.rs`/`sync.rs` is adapted into the spike's write-transaction algorithm; redb itself is used unmodified). | +| Integration | None (standalone; no d2b-bus/ComponentSession/broker dependency) | +| Data migration | None (disposable fixture data only) | +| Validation | SPIKE-01 metrics (1)-(5) and SPIKE-02 metrics (1) across all 3 concurrency profiles, per those entries' exact pass/fail thresholds | +| Removal proof | Per SPIKE-01/SPIKE-02 Cleanup rows: deleted once `packages/d2b-resource-store-redb` and `packages/d2b-controller-toolkit/benches/reaction.rs` reproduce equal-or-stricter coverage | + +### ADR046-feasibility-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-feasibility-002` | +| Dependency/owner | `ADR046-feasibility-001`; reconciliation/process integrator | +| Current source | `packages/d2bd/src/supervisor/{dag,pidfd}.rs` (current DAG/pidfd reference shape, E4) | +| Reuse source | None (the generic async controller loop is ADR-only per `ADR-046-resource-reconciliation`'s own current-code-fit row) | +| Reuse action | adapt | +| Destination | `proofs/process-fastlaunch-spike/` | +| Detailed design | Implements SPIKE-03: the fake Process controller loop, fake `ProcessLaunchEffectPort`, and the 1/10/100-concurrency commit-to-launch-attempt and next-dispatch-independence benchmarks Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (current DAG ordering/readiness concepts are adapted into the spike's per-resource single-flight/parallel-semaphore loop). | +| Integration | Consumes `ADR046-feasibility-001`'s hint-bus shape as its watch-receiver input | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | SPIKE-03 metrics (1)-(3) and thresholds | +| Removal proof | Deleted once `packages/d2b-controller-toolkit/benches/reaction.rs` and the Process Provider integration tests named by `ADR046-reconcile-003` reproduce equal-or-stricter coverage | + +### ADR046-feasibility-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-feasibility-003` | +| Dependency/owner | Independent of `-001`/`-002`; EffectPort/ProviderSupervisor integrator | +| Current source | `packages/d2b-priv-broker/src/ops/spawn_runner.rs` and `tests/pidfd_real_spawner.rs` (current blocking-call reference shape, E4) | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `proofs/effectport-async-spike/` | +| Detailed design | Implements SPIKE-04: the four fake EffectPort traits, the deliberately slow blocking-primitive backends, and the current-thread-runtime heartbeat-jitter detector | +| Integration | None (standalone) | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | SPIKE-04 heartbeat-jitter metric and threshold | +| Removal proof | Deleted once `packages/d2b-provider-supervisor` and the volume-domain effect adapter each carry an equal-or-stricter in-tree blocking-adapter regression test | + +### ADR046-feasibility-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-feasibility-004` | +| Dependency/owner | Independent of `-001`/`-002`/`-003`; Provider packaging/toolkit integrator | +| Current source | None in v3 at `b5ddbed6` (no generic Provider registry exists; per parent ADR context, this is explicitly listed as missing) | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `proofs/provider-packaging-spike/` | +| Detailed design | Implements SPIKE-05: the two-binary crate, hand-authored manifest, fake `ProviderDeployment`, and the `cargo metadata` dependency-edge check Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (the crate-layout policy check reuses the same `src/`/`tests/`/`integration/`/`README.md` structure already enforced elsewhere in this repository's workspace policy tests). | +| Integration | None (standalone) | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | SPIKE-05 metrics (1)-(4) across 20 repeated randomized-order manifest loads | +| Removal proof | Deleted once the real Provider-toolkit crate (`ADR046-provider-001` destination) ships equal-or-stricter manifest-parsing/enumeration/workspace-policy coverage | + +### ADR046-feasibility-005 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-feasibility-005` | +| Dependency/owner | Independent of `-001` through `-004`; bus/session/transport/credential integrator | +| Current source | `packages/d2b-realm-router/src/{session,secure_session,mux_session}.rs` (current routing reference shape, E4); main `a1cc0b2d` `packages/d2b-session/**`, `packages/d2b-session-unix/**` (re-verified green in E3) | +| Reuse source | main `a1cc0b2d`: `packages/d2b-session/src/{handshake,bootstrap,record,engine,scheduler,streams,lifecycle,transport}.rs`, `packages/d2b-session-unix/src/{adapter,vsock,pidfd,socket,systemd,credit,descriptor}.rs`, and the exact test files listed in E3's table | +| Reuse action | adapt | +| Destination | `proofs/bus-routing-noise-spike/`, `proofs/transport-opaque-streams-spike/`, `proofs/credential-kk-e2e-spike/` | +| Detailed design | Implements SPIKE-06 (exact-addressed routing + per-recipient Noise isolation), SPIKE-07 (Unix/vsock/relay-shaped opaque byte-stream conformance across 3 backends), and SPIKE-08 (Credential Provider → consumer Provider KK delivery with the 13-field binding contract) Primary reuse disposition: `adapt`. Preserved source-plan detail: `copy-unchanged` for the Noise/record/transport machinery (path-dependency on a pinned local checkout of `a1cc0b2d`); `adapt` for the fake router/relay/credential-delivery wrapper code that SPIKE-06/07/08 add on top. | +| Integration | SPIKE-07's Unix backend and SPIKE-08's session machinery both depend on the same pinned `a1cc0b2d` path-dependency established for SPIKE-06 | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | SPIKE-06 metrics (1)-(3), SPIKE-07 metrics (1)-(4) against the exact numeric gates already committed in the transport-unix/vsock dossiers, SPIKE-08 metrics (1)-(6) | +| Removal proof | Deleted per each spike's own Cleanup row: `packages/d2b-bus/src/router.rs` for SPIKE-06; the three real transport Provider crates for SPIKE-07; the real Credential Provider crates for SPIKE-08 | + +### ADR046-feasibility-006 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-feasibility-006` | +| Dependency/owner | Independent of `-001` through `-005`; Provider-state/Volume integrator | +| Current source | None in v3 at `b5ddbed6` (ProviderStateSet and the generalized Volume ResourceType are both ADR-only) | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `proofs/provider-state-export-spike/`, `proofs/volume-policy-spike/` | +| Detailed design | Implements SPIKE-09 (optional declared state-Volume creation order, guest-local/host-backed-guest placement, virtiofs Export child ownership) and SPIKE-10 (Volume ACL/`sourcePolicyId`/quota/lifecycle-marker policy conformance) | +| Integration | None between the two spikes beyond sharing the same fake resource-store oracle shape | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | SPIKE-09 metrics (1)-(4); SPIKE-10 metrics (1)-(5), zero-tolerance on path leakage | +| Removal proof | Deleted per each spike's Cleanup row: the real `ADR-046-provider-state` work-item destination for SPIKE-09; `d2b-provider-volume-local`'s own `tests/`/`integration/` suite for SPIKE-10 | + +### ADR046-feasibility-007 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-feasibility-007` | +| Dependency/owner | Independent of `-001` through `-006`; Process Provider integrator | +| Current source | `packages/d2b-priv-broker/src/ops/spawn_runner.rs` (real-spawn reference shape, E4); current unsafe-local helper runtime/systemd invocation shape | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `proofs/process-provider-conformance-spike/` | +| Detailed design | Implements SPIKE-11: the shared `ProcessProviderHarness` trait, the minijail-shaped `clone3(CLONE_PIDFD)` launcher, and the systemd transient-user-scope launcher, plus the identity-drift/quarantine and clean-exit cases | +| Integration | None (standalone; requires a Linux host with `clone3`/`pidfd_open`, and optionally a running `systemd --user` instance behind the `systemd-user` feature) | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | SPIKE-11 metrics (1)-(4), zero-tolerance on false adoption | +| Removal proof | Deleted once `packages/d2b-provider-system-systemd` and `packages/d2b-provider-system-minijail` each carry this exact shared conformance suite in their own `tests/` | + +### ADR046-feasibility-008 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-feasibility-008` | +| Dependency/owner | Independent of `-001` through `-007`; Nix/xtask integrator | +| Current source | `nixos-modules/assertions.nix` pattern, `packages/xtask` `gen-schemas` pattern, `make test-drift` gate (existing generated-or-eval-contract precedent) | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `proofs/nix-authoring-spike/` | +| Detailed design | Implements SPIKE-12: the minimal flake, the two synthetic ResourceTypes, the hand-written committed schemas, the standalone `gen-schemas`-shaped drift check, and the two-generation removed-resource cleanup simulation | +| Integration | None (standalone flake; no dependency on the main `flake.nix`) | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | SPIKE-12 metrics (1)-(5), byte-for-byte reproducibility across 3 hermetic builds | +| Removal proof | Deleted once the real `nixos-modules/resources.nix` and `packages/xtask` `gen-schemas` implementation reproduce these metrics as part of `make test-drift`/`make test-flake` | + +### ADR046-feasibility-009 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-feasibility-009` | +| Dependency/owner | Independent of `-001` through `-008`; CLI integrator | +| Current source | `packages/d2b/src/lib.rs` `cmd_audio`/`cmd_clipboard_arm` and the current command-table shape (current CLI reference shape) | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `proofs/cli-discovery-spike/`, `proofs/clean-cutover-spike/` | +| Detailed design | Implements SPIKE-13 (dynamic Provider-projection discovery, bounds, latency isolation) and SPIKE-14 (zero v2 dispatch, fresh Zone bootstrap ignoring legacy state) | +| Integration | None between the two spikes beyond sharing the same fixture command-table shape | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | SPIKE-13 metrics (1)-(7); SPIKE-14 metrics (1)-(3), zero-tolerance on legacy-file access | +| Removal proof | Deleted per each spike's Cleanup row: the real `d2b` CLI crate's own discovery conformance test for SPIKE-13; the real CLI crate's workspace-policy/lint gate plus the real bootstrap sequence for SPIKE-14 | + +### ADR046-feasibility-010 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-feasibility-010` | +| Dependency/owner | `ADR046-feasibility-001` through `ADR046-feasibility-009` (integrates their fakes; must run last) | +| Current source | None (this is a pure integration of the other nine work items' fixtures) | +| Reuse source | None beyond what `-001` through `-009` already reuse | +| Reuse action | adapt | +| Destination | `proofs/e2e-composition-spike/` | +| Detailed design | Implements SPIKE-15: the three representative compositions (local/cloud-hypervisor, cloud/azure, interaction/shell-terminal-or-wayland), wired from the fakes built by `-001` through `-009`, plus the combined 3-Zone aggregate RSS measurement | +| Integration | Depends on and imports the fake shapes from `proofs/redb-resource-store-spike/`, `proofs/process-fastlaunch-spike/`, `proofs/effectport-async-spike/`, `proofs/provider-packaging-spike/`, `proofs/bus-routing-noise-spike/`, `proofs/transport-opaque-streams-spike/`, `proofs/credential-kk-e2e-spike/`, `proofs/provider-state-export-spike/`, `proofs/volume-policy-spike/`, and `proofs/process-provider-conformance-spike/` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | SPIKE-15 metrics (1)-(4) across all three compositions | +| Removal proof | Deleted once the real integration test suites named by the individual Provider dossiers (`integration/` per D059) collectively reproduce all three compositions against real, non-fake Zone/store/bus/broker code | + +### ADR046-feasibility-011 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-feasibility-011` | +| Dependency/owner | `ADR046-delivery-007`; delivery/test-tooling integrator | +| Current source | this codebase's ad hoc `tests/tools/` timing logs (`d2b-static-timing.$$/`), which are not a candidate-bound, reference-runner-recorded ledger | +| Reuse source | existing `libtest --format=json` timing output and `xtask` (no new test framework) | +| Reuse action | adapt | +| Destination | `proofs/test-runtime-budget-spike/`; the committed baseline ledger consumed by `ADR046-delivery-007` | +| Detailed design | Establishes the D094 measurement baseline: records the reference runner class, repetition count, and per-test/crate/shard p95 for a representative hermetic crate; proves the §10.16 budgets (individual normal test p95 ≤50 ms, per-crate `--lib --tests` ≤2 s, Layer-1 hermetic shard ≤60 s) are met on the reference runner and that an injected slow/sleeping test is detected as a regression | +| Integration | Output ledger shape is consumed by the runtime ledger/timing gate; establishes the historical threshold seed | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | The representative crate meets every budget; a synthetic slow/sleep/process/network test is flagged; cold compile time is recorded on a separate line and excluded from the execution budgets | +| Removal proof | Deleted once `ADR046-delivery-007`'s in-tree ledger/timing gate reproduces the baseline against the real crate set | diff --git a/docs/specs/ADR-046-implementation-graph.json b/docs/specs/ADR-046-implementation-graph.json new file mode 100644 index 000000000..4e7028585 --- /dev/null +++ b/docs/specs/ADR-046-implementation-graph.json @@ -0,0 +1,24707 @@ +{ + "adr" : "0046", + "artifactKind" : "d2b-adr-implementation-graph", + "counts" : { + "edges" : 1940, + "maxTopologicalRank" : 22, + "nodes" : 598, + "specNodes" : 55, + "waves" : 8, + "workItemNodes" : 543 + }, + "criticalPath" : [ + "ADR-046-decision-register", + "ADR-046-terminology-and-identities", + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-core-controllers", + "ADR-046-nix-configuration", + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-001", + "ADR046-qemu-media-003", + "ADR046-qemu-media-005", + "ADR046-qemu-media-006", + "ADR046-qemu-media-009", + "ADR046-qemu-media-010", + "ADR046-qemu-media-011", + "ADR046-qemu-media-013", + "ADR046-qemu-media-014", + "ADR046-qemu-media-015", + "ADR046-qemu-media-018", + "ADR046-qemu-media-019" + ], + "edgeTypes" : [ + "file-overlap-order", + "implements-spec", + "shared-contract", + "spec-depends-on", + "work-item-depends-on" + ], + "edges" : [ + { + "from" : "ADR046-cli-011", + "to" : "ADR046-nix-014", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-device-007", + "to" : "ADR046-core-001", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-exec-013", + "to" : "ADR046-core-001", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-exec-015", + "to" : "ADR046-core-001", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-network-008", + "to" : "ADR046-core-001", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-nix-014", + "to" : "ADR046-device-006", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-nix-019", + "to" : "ADR046-cli-011", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-nix-031", + "to" : "ADR046-nix-019", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-qemu-media-017", + "to" : "ADR046-transport-unix-009", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-telem-011", + "to" : "ADR046-core-001", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-transport-unix-009", + "to" : "ADR046-gpu-007", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-usbip-008", + "to" : "ADR046-qemu-media-017", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-zone-control-016", + "to" : "ADR046-core-001", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-zone-control-021", + "to" : "ADR046-core-001", + "type" : "file-overlap-order" + }, + { + "from" : "ADR046-aca-001", + "to" : "ADR-046-provider-runtime-azure-container-apps", + "type" : "implements-spec" + }, + { + "from" : "ADR046-aca-002", + "to" : "ADR-046-provider-runtime-azure-container-apps", + "type" : "implements-spec" + }, + { + "from" : "ADR046-aca-003", + "to" : "ADR-046-provider-runtime-azure-container-apps", + "type" : "implements-spec" + }, + { + "from" : "ADR046-aca-004", + "to" : "ADR-046-provider-runtime-azure-container-apps", + "type" : "implements-spec" + }, + { + "from" : "ADR046-aca-005", + "to" : "ADR-046-provider-runtime-azure-container-apps", + "type" : "implements-spec" + }, + { + "from" : "ADR046-aca-006", + "to" : "ADR-046-provider-runtime-azure-container-apps", + "type" : "implements-spec" + }, + { + "from" : "ADR046-aca-007", + "to" : "ADR-046-provider-runtime-azure-container-apps", + "type" : "implements-spec" + }, + { + "from" : "ADR046-activation-001", + "to" : "ADR-046-provider-activation-nixos", + "type" : "implements-spec" + }, + { + "from" : "ADR046-activation-002", + "to" : "ADR-046-provider-activation-nixos", + "type" : "implements-spec" + }, + { + "from" : "ADR046-activation-003", + "to" : "ADR-046-provider-activation-nixos", + "type" : "implements-spec" + }, + { + "from" : "ADR046-activation-004", + "to" : "ADR-046-provider-activation-nixos", + "type" : "implements-spec" + }, + { + "from" : "ADR046-activation-005", + "to" : "ADR-046-provider-activation-nixos", + "type" : "implements-spec" + }, + { + "from" : "ADR046-activation-006", + "to" : "ADR-046-provider-activation-nixos", + "type" : "implements-spec" + }, + { + "from" : "ADR046-activation-007", + "to" : "ADR-046-provider-activation-nixos", + "type" : "implements-spec" + }, + { + "from" : "ADR046-api-001", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "implements-spec" + }, + { + "from" : "ADR046-api-002", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-001", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-002", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-004", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-005", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-006", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-007", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-008", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-009", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-010", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-011", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-012", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-013", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audio-014", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audit-001", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audit-002", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audit-003", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-audit-004", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-azure-vm-001", + "to" : "ADR-046-provider-runtime-azure-virtual-machine", + "type" : "implements-spec" + }, + { + "from" : "ADR046-azure-vm-002", + "to" : "ADR-046-provider-runtime-azure-virtual-machine", + "type" : "implements-spec" + }, + { + "from" : "ADR046-azure-vm-003", + "to" : "ADR-046-provider-runtime-azure-virtual-machine", + "type" : "implements-spec" + }, + { + "from" : "ADR046-azure-vm-004", + "to" : "ADR-046-provider-runtime-azure-virtual-machine", + "type" : "implements-spec" + }, + { + "from" : "ADR046-azure-vm-005", + "to" : "ADR-046-provider-runtime-azure-virtual-machine", + "type" : "implements-spec" + }, + { + "from" : "ADR046-azure-vm-006", + "to" : "ADR-046-provider-runtime-azure-virtual-machine", + "type" : "implements-spec" + }, + { + "from" : "ADR046-azure-vm-007", + "to" : "ADR-046-provider-runtime-azure-virtual-machine", + "type" : "implements-spec" + }, + { + "from" : "ADR046-azure-vm-008", + "to" : "ADR-046-provider-runtime-azure-virtual-machine", + "type" : "implements-spec" + }, + { + "from" : "ADR046-azure-vm-009", + "to" : "ADR-046-provider-runtime-azure-virtual-machine", + "type" : "implements-spec" + }, + { + "from" : "ADR046-bus-001", + "to" : "ADR-046-componentsession-and-bus", + "type" : "implements-spec" + }, + { + "from" : "ADR046-ch-001", + "to" : "ADR-046-provider-runtime-cloud-hypervisor", + "type" : "implements-spec" + }, + { + "from" : "ADR046-ch-002", + "to" : "ADR-046-provider-runtime-cloud-hypervisor", + "type" : "implements-spec" + }, + { + "from" : "ADR046-ch-003", + "to" : "ADR-046-provider-runtime-cloud-hypervisor", + "type" : "implements-spec" + }, + { + "from" : "ADR046-ch-004", + "to" : "ADR-046-provider-runtime-cloud-hypervisor", + "type" : "implements-spec" + }, + { + "from" : "ADR046-ch-005", + "to" : "ADR-046-provider-runtime-cloud-hypervisor", + "type" : "implements-spec" + }, + { + "from" : "ADR046-ch-006", + "to" : "ADR-046-provider-runtime-cloud-hypervisor", + "type" : "implements-spec" + }, + { + "from" : "ADR046-ch-007", + "to" : "ADR-046-provider-runtime-cloud-hypervisor", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-001", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-002", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-003", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-004", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-005", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-006", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-007", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-008", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-009", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-010", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-011", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-012", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cli-013", + "to" : "ADR-046-cli-and-operations", + "type" : "implements-spec" + }, + { + "from" : "ADR046-client-001", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-001", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-002", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-003", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-004", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-005", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-006", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-007", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-008", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-009", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-010", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-011", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-clipboard-012", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-core-001", + "to" : "ADR-046-core-controllers", + "type" : "implements-spec" + }, + { + "from" : "ADR046-core-002", + "to" : "ADR-046-core-controllers", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cred-entra-001", + "to" : "ADR-046-provider-credential-entra", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cred-mi-001", + "to" : "ADR-046-provider-credential-managed-identity", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cred-mi-002", + "to" : "ADR-046-provider-credential-managed-identity", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cred-mi-003", + "to" : "ADR-046-provider-credential-managed-identity", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cred-mi-004", + "to" : "ADR-046-provider-credential-managed-identity", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cred-ss-001", + "to" : "ADR-046-provider-credential-secret-service", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cred-ss-002", + "to" : "ADR-046-provider-credential-secret-service", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cred-ss-003", + "to" : "ADR-046-provider-credential-secret-service", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cred-ss-004", + "to" : "ADR-046-provider-credential-secret-service", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cred-ss-005", + "to" : "ADR-046-provider-credential-secret-service", + "type" : "implements-spec" + }, + { + "from" : "ADR046-cred-ss-006", + "to" : "ADR-046-provider-credential-secret-service", + "type" : "implements-spec" + }, + { + "from" : "ADR046-credential-001", + "to" : "ADR-046-resources-credential", + "type" : "implements-spec" + }, + { + "from" : "ADR046-credential-002", + "to" : "ADR-046-resources-credential", + "type" : "implements-spec" + }, + { + "from" : "ADR046-credential-003", + "to" : "ADR-046-resources-credential", + "type" : "implements-spec" + }, + { + "from" : "ADR046-credential-004", + "to" : "ADR-046-resources-credential", + "type" : "implements-spec" + }, + { + "from" : "ADR046-credential-005", + "to" : "ADR-046-resources-credential", + "type" : "implements-spec" + }, + { + "from" : "ADR046-credential-006", + "to" : "ADR-046-resources-credential", + "type" : "implements-spec" + }, + { + "from" : "ADR046-credential-007", + "to" : "ADR-046-resources-credential", + "type" : "implements-spec" + }, + { + "from" : "ADR046-credential-008", + "to" : "ADR-046-resources-credential", + "type" : "implements-spec" + }, + { + "from" : "ADR046-decisions-001", + "to" : "ADR-046-decision-register", + "type" : "implements-spec" + }, + { + "from" : "ADR046-delivery-001", + "to" : "ADR-046-validation-and-delivery", + "type" : "implements-spec" + }, + { + "from" : "ADR046-delivery-002", + "to" : "ADR-046-validation-and-delivery", + "type" : "implements-spec" + }, + { + "from" : "ADR046-delivery-003", + "to" : "ADR-046-validation-and-delivery", + "type" : "implements-spec" + }, + { + "from" : "ADR046-delivery-004", + "to" : "ADR-046-validation-and-delivery", + "type" : "implements-spec" + }, + { + "from" : "ADR046-delivery-005", + "to" : "ADR-046-validation-and-delivery", + "type" : "implements-spec" + }, + { + "from" : "ADR046-delivery-006", + "to" : "ADR-046-validation-and-delivery", + "type" : "implements-spec" + }, + { + "from" : "ADR046-delivery-007", + "to" : "ADR-046-validation-and-delivery", + "type" : "implements-spec" + }, + { + "from" : "ADR046-delivery-008", + "to" : "ADR-046-validation-and-delivery", + "type" : "implements-spec" + }, + { + "from" : "ADR046-delivery-009", + "to" : "ADR-046-validation-and-delivery", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-001", + "to" : "ADR-046-resources-device", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-002", + "to" : "ADR-046-resources-device", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-003", + "to" : "ADR-046-resources-device", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-004", + "to" : "ADR-046-resources-device", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-005", + "to" : "ADR-046-resources-device", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-006", + "to" : "ADR-046-resources-device", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-007", + "to" : "ADR-046-resources-device", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-008", + "to" : "ADR-046-resources-device", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-001", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-002", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-003", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-004", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-005", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-006", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-007", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-008", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-009", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-010", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-011", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-012", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-device-tpm-013", + "to" : "ADR-046-provider-device-tpm", + "type" : "implements-spec" + }, + { + "from" : "ADR046-display-001", + "to" : "ADR-046-provider-display-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-display-002", + "to" : "ADR-046-provider-display-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-display-003", + "to" : "ADR-046-provider-display-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-display-004", + "to" : "ADR-046-provider-display-wayland", + "type" : "implements-spec" + }, + { + "from" : "ADR046-doctor-001", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-doctor-002", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-001", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-002", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-003", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-004", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-005", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-006", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-007", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-008", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-009", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-010", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-011", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-012", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-013", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-014", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-015", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-016", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-017", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-018", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-019", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-020", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-021", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-022", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-exec-023", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-feasibility-001", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "implements-spec" + }, + { + "from" : "ADR046-feasibility-002", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "implements-spec" + }, + { + "from" : "ADR046-feasibility-003", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "implements-spec" + }, + { + "from" : "ADR046-feasibility-004", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "implements-spec" + }, + { + "from" : "ADR046-feasibility-005", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "implements-spec" + }, + { + "from" : "ADR046-feasibility-006", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "implements-spec" + }, + { + "from" : "ADR046-feasibility-007", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "implements-spec" + }, + { + "from" : "ADR046-feasibility-008", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "implements-spec" + }, + { + "from" : "ADR046-feasibility-009", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "implements-spec" + }, + { + "from" : "ADR046-feasibility-010", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "implements-spec" + }, + { + "from" : "ADR046-feasibility-011", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "implements-spec" + }, + { + "from" : "ADR046-gpu-001", + "to" : "ADR-046-provider-device-gpu", + "type" : "implements-spec" + }, + { + "from" : "ADR046-gpu-002", + "to" : "ADR-046-provider-device-gpu", + "type" : "implements-spec" + }, + { + "from" : "ADR046-gpu-003", + "to" : "ADR-046-provider-device-gpu", + "type" : "implements-spec" + }, + { + "from" : "ADR046-gpu-004", + "to" : "ADR-046-provider-device-gpu", + "type" : "implements-spec" + }, + { + "from" : "ADR046-gpu-005", + "to" : "ADR-046-provider-device-gpu", + "type" : "implements-spec" + }, + { + "from" : "ADR046-gpu-006", + "to" : "ADR-046-provider-device-gpu", + "type" : "implements-spec" + }, + { + "from" : "ADR046-gpu-007", + "to" : "ADR-046-provider-device-gpu", + "type" : "implements-spec" + }, + { + "from" : "ADR046-gpu-008", + "to" : "ADR-046-provider-device-gpu", + "type" : "implements-spec" + }, + { + "from" : "ADR046-gpu-009", + "to" : "ADR-046-provider-device-gpu", + "type" : "implements-spec" + }, + { + "from" : "ADR046-host-posture-001", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-identities-001", + "to" : "ADR-046-terminology-and-identities", + "type" : "implements-spec" + }, + { + "from" : "ADR046-identities-002", + "to" : "ADR-046-terminology-and-identities", + "type" : "implements-spec" + }, + { + "from" : "ADR046-mi-topology-001", + "to" : "ADR-046-provider-credential-managed-identity", + "type" : "implements-spec" + }, + { + "from" : "ADR046-minijail-001", + "to" : "ADR-046-provider-system-minijail", + "type" : "implements-spec" + }, + { + "from" : "ADR046-minijail-002", + "to" : "ADR-046-provider-system-minijail", + "type" : "implements-spec" + }, + { + "from" : "ADR046-minijail-003", + "to" : "ADR-046-provider-system-minijail", + "type" : "implements-spec" + }, + { + "from" : "ADR046-minijail-004", + "to" : "ADR-046-provider-system-minijail", + "type" : "implements-spec" + }, + { + "from" : "ADR046-minijail-005", + "to" : "ADR-046-provider-system-minijail", + "type" : "implements-spec" + }, + { + "from" : "ADR046-minijail-006", + "to" : "ADR-046-provider-system-minijail", + "type" : "implements-spec" + }, + { + "from" : "ADR046-network-001", + "to" : "ADR-046-resources-network", + "type" : "implements-spec" + }, + { + "from" : "ADR046-network-002", + "to" : "ADR-046-resources-network", + "type" : "implements-spec" + }, + { + "from" : "ADR046-network-003", + "to" : "ADR-046-resources-network", + "type" : "implements-spec" + }, + { + "from" : "ADR046-network-004", + "to" : "ADR-046-resources-network", + "type" : "implements-spec" + }, + { + "from" : "ADR046-network-005", + "to" : "ADR-046-resources-network", + "type" : "implements-spec" + }, + { + "from" : "ADR046-network-006", + "to" : "ADR-046-resources-network", + "type" : "implements-spec" + }, + { + "from" : "ADR046-network-007", + "to" : "ADR-046-resources-network", + "type" : "implements-spec" + }, + { + "from" : "ADR046-network-008", + "to" : "ADR-046-resources-network", + "type" : "implements-spec" + }, + { + "from" : "ADR046-network-009", + "to" : "ADR-046-resources-network", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-001", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-002", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-003", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-004", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-005", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-006", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-007", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-008", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-009", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-010", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-011", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-012", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-013", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-014", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-015", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-016", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-017", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-018", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-019", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-020", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-021", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-022", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-023", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-024", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-025", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-026", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-027", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-028", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-029", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-030", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-031", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-032", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-033", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-034", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nix-035", + "to" : "ADR-046-nix-configuration", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-001", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-002", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-003", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-004", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-005", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-006", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-007", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-008", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-009", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-010", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-011", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-012", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-013", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-014", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-015", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-016", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-017", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-018", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-019", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-nl-020", + "to" : "ADR-046-provider-network-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-notify-001", + "to" : "ADR-046-provider-notification-desktop", + "type" : "implements-spec" + }, + { + "from" : "ADR046-notify-002", + "to" : "ADR-046-provider-notification-desktop", + "type" : "implements-spec" + }, + { + "from" : "ADR046-notify-003", + "to" : "ADR-046-provider-notification-desktop", + "type" : "implements-spec" + }, + { + "from" : "ADR046-notify-004", + "to" : "ADR-046-provider-notification-desktop", + "type" : "implements-spec" + }, + { + "from" : "ADR046-notify-005", + "to" : "ADR-046-provider-notification-desktop", + "type" : "implements-spec" + }, + { + "from" : "ADR046-notify-006", + "to" : "ADR-046-provider-notification-desktop", + "type" : "implements-spec" + }, + { + "from" : "ADR046-object-001", + "to" : "ADR-046-resource-object-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-object-002", + "to" : "ADR-046-resource-object-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-otel-001", + "to" : "ADR-046-provider-observability-otel", + "type" : "implements-spec" + }, + { + "from" : "ADR046-otel-002", + "to" : "ADR-046-provider-observability-otel", + "type" : "implements-spec" + }, + { + "from" : "ADR046-otel-003", + "to" : "ADR-046-provider-observability-otel", + "type" : "implements-spec" + }, + { + "from" : "ADR046-otel-004", + "to" : "ADR-046-provider-observability-otel", + "type" : "implements-spec" + }, + { + "from" : "ADR046-otel-005", + "to" : "ADR-046-provider-observability-otel", + "type" : "implements-spec" + }, + { + "from" : "ADR046-otel-006", + "to" : "ADR-046-provider-observability-otel", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pkg-001", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-primitives-001", + "to" : "ADR-046-primitive-resource-composition", + "type" : "implements-spec" + }, + { + "from" : "ADR046-primitives-002", + "to" : "ADR-046-primitive-resource-composition", + "type" : "implements-spec" + }, + { + "from" : "ADR046-primitives-003", + "to" : "ADR-046-primitive-resource-composition", + "type" : "implements-spec" + }, + { + "from" : "ADR046-process-001", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "implements-spec" + }, + { + "from" : "ADR046-process-002", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "implements-spec" + }, + { + "from" : "ADR046-provider-001", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "implements-spec" + }, + { + "from" : "ADR046-provider-002", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "implements-spec" + }, + { + "from" : "ADR046-provider-003", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "implements-spec" + }, + { + "from" : "ADR046-provider-004", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "implements-spec" + }, + { + "from" : "ADR046-provider-agent-001", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-001", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-002", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-003", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-004", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-005", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-006", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-007", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-008", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-009", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-010", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-011", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-pstate-012", + "to" : "ADR-046-provider-state", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-001", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-002", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-003", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-004", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-005", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-006", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-007", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-008", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-009", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-010", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-011", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-012", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-013", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-014", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-015", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-016", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-017", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-018", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reconcile-001", + "to" : "ADR-046-resource-reconciliation", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reconcile-002", + "to" : "ADR-046-resource-reconciliation", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reconcile-003", + "to" : "ADR-046-resource-reconciliation", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reset-001", + "to" : "ADR-046-reset-and-cutover", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reset-002", + "to" : "ADR-046-reset-and-cutover", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reset-003", + "to" : "ADR-046-reset-and-cutover", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reset-004", + "to" : "ADR-046-reset-and-cutover", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reset-005", + "to" : "ADR-046-reset-and-cutover", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reset-006", + "to" : "ADR-046-reset-and-cutover", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reset-007", + "to" : "ADR-046-reset-and-cutover", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reset-008", + "to" : "ADR-046-reset-and-cutover", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reset-009", + "to" : "ADR-046-reset-and-cutover", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reset-010", + "to" : "ADR-046-reset-and-cutover", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reset-011", + "to" : "ADR-046-reset-and-cutover", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reuse-001", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reuse-002", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reuse-003", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reuse-004", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reuse-005", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reuse-006", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reuse-007", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reuse-008", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-reuse-009", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-001", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-002", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-003", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-004", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-005", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-006", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-007", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-008", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-009", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-010", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-011", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-012", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-013", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-014", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-015", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-routing-016", + "to" : "ADR-046-zone-routing", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-001", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-002", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-003", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-004", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-005", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-006", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-007", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-008", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-009", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-010", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-011", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-012", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-013", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-014", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-015", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-016", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-017", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-018", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-019", + "to" : "ADR-046-security-and-threat-model", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-001", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-002", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-003", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-004", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-005", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-006", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-007", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-008", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-009", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-010", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-011", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-012", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-013", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-014", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-015", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-016", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-017", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-018", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-019", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-020", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-021", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-022", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-024", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-025", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-026", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-027", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-028", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-029", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-030", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-031", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-032", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-033", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-034", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-security-key-035", + "to" : "ADR-046-provider-device-security-key", + "type" : "implements-spec" + }, + { + "from" : "ADR046-session-001", + "to" : "ADR-046-componentsession-and-bus", + "type" : "implements-spec" + }, + { + "from" : "ADR046-session-002", + "to" : "ADR-046-componentsession-and-bus", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-001", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-002", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-003", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-004", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-005", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-006", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-007", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-008", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-009", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-010", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-011", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-012", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-sterm-013", + "to" : "ADR-046-provider-shell-terminal", + "type" : "implements-spec" + }, + { + "from" : "ADR046-store-001", + "to" : "ADR-046-resource-store-redb", + "type" : "implements-spec" + }, + { + "from" : "ADR046-store-002", + "to" : "ADR-046-resource-store-redb", + "type" : "implements-spec" + }, + { + "from" : "ADR046-store-003", + "to" : "ADR-046-resource-store-redb", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-001", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-002", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-003", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-004", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-005", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-006", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-007", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-008", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-009", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-010", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-011", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-012", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-013", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-014", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-015", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-016", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-017", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-018", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-019", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-020", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-021", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-022", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-023", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-streamline-024", + "to" : "ADR-046-streamline", + "type" : "implements-spec" + }, + { + "from" : "ADR046-system-core-001", + "to" : "ADR-046-provider-system-core", + "type" : "implements-spec" + }, + { + "from" : "ADR046-systemd-001", + "to" : "ADR-046-provider-system-systemd", + "type" : "implements-spec" + }, + { + "from" : "ADR046-systemd-002", + "to" : "ADR-046-provider-system-systemd", + "type" : "implements-spec" + }, + { + "from" : "ADR046-systemd-003", + "to" : "ADR-046-provider-system-systemd", + "type" : "implements-spec" + }, + { + "from" : "ADR046-telem-001", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-telem-002", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-telem-003", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-telem-004", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-telem-005", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-telem-006", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-telem-007", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-telem-008", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-telem-009", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-telem-010", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-telem-011", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-relay-001", + "to" : "ADR-046-provider-transport-azure-relay", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-relay-002", + "to" : "ADR-046-provider-transport-azure-relay", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-relay-003", + "to" : "ADR-046-provider-transport-azure-relay", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-relay-004", + "to" : "ADR-046-provider-transport-azure-relay", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-relay-005", + "to" : "ADR-046-provider-transport-azure-relay", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-relay-006", + "to" : "ADR-046-provider-transport-azure-relay", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-relay-007", + "to" : "ADR-046-provider-transport-azure-relay", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-unix-001", + "to" : "ADR-046-provider-transport-unix", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-unix-002", + "to" : "ADR-046-provider-transport-unix", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-unix-003", + "to" : "ADR-046-provider-transport-unix", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-unix-004", + "to" : "ADR-046-provider-transport-unix", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-unix-005", + "to" : "ADR-046-provider-transport-unix", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-unix-006", + "to" : "ADR-046-provider-transport-unix", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-unix-007", + "to" : "ADR-046-provider-transport-unix", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-unix-008", + "to" : "ADR-046-provider-transport-unix", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-unix-009", + "to" : "ADR-046-provider-transport-unix", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-unix-010", + "to" : "ADR-046-provider-transport-unix", + "type" : "implements-spec" + }, + { + "from" : "ADR046-transport-unix-011", + "to" : "ADR-046-provider-transport-unix", + "type" : "implements-spec" + }, + { + "from" : "ADR046-usbip-001", + "to" : "ADR-046-provider-device-usbip", + "type" : "implements-spec" + }, + { + "from" : "ADR046-usbip-002", + "to" : "ADR-046-provider-device-usbip", + "type" : "implements-spec" + }, + { + "from" : "ADR046-usbip-003", + "to" : "ADR-046-provider-device-usbip", + "type" : "implements-spec" + }, + { + "from" : "ADR046-usbip-004", + "to" : "ADR-046-provider-device-usbip", + "type" : "implements-spec" + }, + { + "from" : "ADR046-usbip-005", + "to" : "ADR-046-provider-device-usbip", + "type" : "implements-spec" + }, + { + "from" : "ADR046-usbip-006", + "to" : "ADR-046-provider-device-usbip", + "type" : "implements-spec" + }, + { + "from" : "ADR046-usbip-007", + "to" : "ADR-046-provider-device-usbip", + "type" : "implements-spec" + }, + { + "from" : "ADR046-usbip-008", + "to" : "ADR-046-provider-device-usbip", + "type" : "implements-spec" + }, + { + "from" : "ADR046-usbip-009", + "to" : "ADR-046-provider-device-usbip", + "type" : "implements-spec" + }, + { + "from" : "ADR046-user-session-001", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-001", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-002", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-003", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-004", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-005", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-006", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-007", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-008", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-009", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-010", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-011", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-012", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vl-013", + "to" : "ADR-046-provider-volume-local", + "type" : "implements-spec" + }, + { + "from" : "ADR046-volume-001", + "to" : "ADR-046-resources-volume", + "type" : "implements-spec" + }, + { + "from" : "ADR046-volume-002", + "to" : "ADR-046-resources-volume", + "type" : "implements-spec" + }, + { + "from" : "ADR046-volume-003", + "to" : "ADR-046-resources-volume", + "type" : "implements-spec" + }, + { + "from" : "ADR046-volume-004", + "to" : "ADR-046-resources-volume", + "type" : "implements-spec" + }, + { + "from" : "ADR046-volume-005", + "to" : "ADR-046-resources-volume", + "type" : "implements-spec" + }, + { + "from" : "ADR046-volume-006", + "to" : "ADR-046-resources-volume", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vsock-001", + "to" : "ADR-046-provider-transport-vsock", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vsock-002", + "to" : "ADR-046-provider-transport-vsock", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vsock-003", + "to" : "ADR-046-provider-transport-vsock", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vsock-004", + "to" : "ADR-046-provider-transport-vsock", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vsock-005", + "to" : "ADR-046-provider-transport-vsock", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vsock-006", + "to" : "ADR-046-provider-transport-vsock", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vsock-007", + "to" : "ADR-046-provider-transport-vsock", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vvfs-001", + "to" : "ADR-046-provider-volume-virtiofs", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vvfs-002", + "to" : "ADR-046-provider-volume-virtiofs", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vvfs-003", + "to" : "ADR-046-provider-volume-virtiofs", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vvfs-004", + "to" : "ADR-046-provider-volume-virtiofs", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vvfs-005", + "to" : "ADR-046-provider-volume-virtiofs", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vvfs-006", + "to" : "ADR-046-provider-volume-virtiofs", + "type" : "implements-spec" + }, + { + "from" : "ADR046-vvfs-export-001", + "to" : "ADR-046-provider-volume-virtiofs", + "type" : "implements-spec" + }, + { + "from" : "ADR046-wire-001", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-001", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-002", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-003", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-004", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-005", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-006", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-007", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-008", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-009", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-010", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-011", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-012", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-013", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-014", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-015", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-016", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-017", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-018", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-019", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-020", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-021", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-022", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-023", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR046-zone-control-024", + "to" : "ADR-046-resources-zone-control", + "type" : "implements-spec" + }, + { + "from" : "ADR-046-resource-api-and-authorization", + "to" : "ADR-046-resource-object-model", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-resource-api-and-authorization", + "to" : "ADR-046-resource-store-redb", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-resource-api-and-authorization", + "to" : "ADR-046-terminology-and-identities", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-resource-object-model", + "to" : "ADR-046-decision-register", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-resource-object-model", + "to" : "ADR-046-terminology-and-identities", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-resource-store-redb", + "to" : "ADR-046-resource-object-model", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-resource-store-redb", + "to" : "ADR-046-terminology-and-identities", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-streamline", + "to" : "ADR-046-reset-and-cutover", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-terminology-and-identities", + "to" : "ADR-046-decision-register", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-reset-and-cutover", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-security-and-threat-model", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-streamline", + "type" : "shared-contract" + }, + { + "from" : "ADR-046-cli-and-operations", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-cli-and-operations", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-cli-and-operations", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-cli-and-operations", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-cli-and-operations", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-cli-and-operations", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-components-processes-and-sandbox", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-components-processes-and-sandbox", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-components-processes-and-sandbox", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-componentsession-and-bus", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-componentsession-and-bus", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-core-controllers", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-core-controllers", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-core-controllers", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-core-controllers", + "to" : "ADR-046-resource-store-redb", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-cli-and-operations", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-current-code-migration-map", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-decision-register", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-resource-store-redb", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-feasibility-and-spikes", + "to" : "ADR-046-zone-routing", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-nix-configuration", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-nix-configuration", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-nix-configuration", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-nix-configuration", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-nix-configuration", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-primitive-resource-composition", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-primitive-resource-composition", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-activation-nixos", + "to" : "ADR-046-cli-and-operations", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-activation-nixos", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-activation-nixos", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-activation-nixos", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-activation-nixos", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-activation-nixos", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-activation-nixos", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-activation-nixos", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-activation-nixos", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-activation-nixos", + "to" : "ADR-046-resources-zone-control", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-activation-nixos", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-audio-pipewire", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-clipboard-wayland", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-clipboard-wayland", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-clipboard-wayland", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-clipboard-wayland", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-clipboard-wayland", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-clipboard-wayland", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-clipboard-wayland", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-clipboard-wayland", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-entra", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-entra", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-entra", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-entra", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-entra", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-entra", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-entra", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-managed-identity", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-managed-identity", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-managed-identity", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-managed-identity", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-managed-identity", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-managed-identity", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-secret-service", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-secret-service", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-secret-service", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-secret-service", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-secret-service", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-secret-service", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-secret-service", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-credential-secret-service", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-gpu", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-gpu", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-gpu", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-gpu", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-gpu", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-gpu", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-gpu", + "to" : "ADR-046-resources-device", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-gpu", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-security-key", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-security-key", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-security-key", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-security-key", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-security-key", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-security-key", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-security-key", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-security-key", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-security-key", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-security-key", + "to" : "ADR-046-resources-device", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-security-key", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-tpm", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-tpm", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-tpm", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-tpm", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-tpm", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-tpm", + "to" : "ADR-046-resources-device", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-tpm", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-tpm", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-resources-device", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-resources-network", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-resources-zone-control", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-device-usbip", + "to" : "ADR-046-zone-routing", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-display-wayland", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-display-wayland", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-display-wayland", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-display-wayland", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-display-wayland", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-display-wayland", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-display-wayland", + "to" : "ADR-046-resources-device", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-display-wayland", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-display-wayland", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-display-wayland", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-model-and-packaging", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-model-and-packaging", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-model-and-packaging", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-model-and-packaging", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-network-local", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-network-local", + "to" : "ADR-046-current-code-migration-map", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-network-local", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-network-local", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-network-local", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-network-local", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-network-local", + "to" : "ADR-046-resources-network", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-network-local", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-network-local", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-notification-desktop", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-notification-desktop", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-notification-desktop", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-notification-desktop", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-notification-desktop", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-notification-desktop", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-notification-desktop", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-observability-otel", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-container-apps", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-container-apps", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-container-apps", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-container-apps", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-container-apps", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-container-apps", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-container-apps", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-resources-network", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-azure-virtual-machine", + "to" : "ADR-046-zone-routing", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-resources-device", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-resources-network", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-cloud-hypervisor", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-qemu-media", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-qemu-media", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-qemu-media", + "to" : "ADR-046-current-code-migration-map", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-qemu-media", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-qemu-media", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-qemu-media", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-qemu-media", + "to" : "ADR-046-resources-device", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-qemu-media", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-qemu-media", + "to" : "ADR-046-resources-network", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-qemu-media", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-runtime-qemu-media", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-decision-register", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-shell-terminal", + "to" : "ADR-046-zone-routing", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-state", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-state", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-state", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-state", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-state", + "to" : "ADR-046-resource-store-redb", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-state", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-core", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-core", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-core", + "to" : "ADR-046-current-code-migration-map", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-core", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-core", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-core", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-core", + "to" : "ADR-046-resources-zone-control", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-core", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-minijail", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-systemd", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-systemd", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-systemd", + "to" : "ADR-046-current-code-migration-map", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-systemd", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-systemd", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-systemd", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-system-systemd", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-azure-relay", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-azure-relay", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-azure-relay", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-azure-relay", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-azure-relay", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-azure-relay", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-azure-relay", + "to" : "ADR-046-zone-routing", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-unix", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-unix", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-unix", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-unix", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-unix", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-unix", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-unix", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-unix", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-unix", + "to" : "ADR-046-resources-zone-control", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-unix", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-unix", + "to" : "ADR-046-zone-routing", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-vsock", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-vsock", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-vsock", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-vsock", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-vsock", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-vsock", + "to" : "ADR-046-resources-zone-control", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-vsock", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-transport-vsock", + "to" : "ADR-046-zone-routing", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-local", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-local", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-local", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-local", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-local", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-local", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-local", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-local", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-virtiofs", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-virtiofs", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-virtiofs", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-virtiofs", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-virtiofs", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-virtiofs", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-virtiofs", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-virtiofs", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-virtiofs", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-provider-volume-virtiofs", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-cli-and-operations", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-current-code-migration-map", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-decision-register", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-resource-store-redb", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-resources-device", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-resources-network", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-resources-zone-control", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-reset-and-cutover", + "to" : "ADR-046-zone-routing", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resource-api-and-authorization", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resource-api-and-authorization", + "to" : "ADR-046-resource-store-redb", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resource-api-and-authorization", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resource-object-model", + "to" : "ADR-046-decision-register", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resource-object-model", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resource-reconciliation", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resource-reconciliation", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resource-reconciliation", + "to" : "ADR-046-resource-store-redb", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resource-store-redb", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resource-store-redb", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-credential", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-credential", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-credential", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-credential", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-credential", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-credential", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-credential", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-device", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-device", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-device", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-device", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-device", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-device", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-host-guest-process-user", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-host-guest-process-user", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-host-guest-process-user", + "to" : "ADR-046-decision-register", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-host-guest-process-user", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-host-guest-process-user", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-host-guest-process-user", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-host-guest-process-user", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-host-guest-process-user", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-host-guest-process-user", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-network", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-network", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-network", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-network", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-network", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-volume", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-volume", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-volume", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-volume", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-volume", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-zone-control", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-zone-control", + "to" : "ADR-046-decision-register", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-zone-control", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-zone-control", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-zone-control", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-zone-control", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-zone-control", + "to" : "ADR-046-resource-store-redb", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-resources-zone-control", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-cli-and-operations", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-current-code-migration-map", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-decision-register", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-activation-nixos", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-credential-entra", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-credential-managed-identity", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-credential-secret-service", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-device-gpu", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-device-security-key", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-device-tpm", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-device-usbip", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-display-wayland", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-network-local", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-notification-desktop", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-observability-otel", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-runtime-azure-container-apps", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-runtime-azure-virtual-machine", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-runtime-cloud-hypervisor", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-shell-terminal", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-system-core", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-system-minijail", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-system-systemd", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-transport-azure-relay", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-transport-unix", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-transport-vsock", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-volume-local", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-provider-volume-virtiofs", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-resource-store-redb", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-resources-device", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-resources-network", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-resources-zone-control", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-security-and-threat-model", + "to" : "ADR-046-zone-routing", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-streamline", + "to" : "ADR-046-current-code-migration-map", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-streamline", + "to" : "ADR-046-decision-register", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-streamline", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-streamline", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-streamline", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-streamline", + "to" : "ADR-046-reset-and-cutover", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-streamline", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-streamline", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-streamline", + "to" : "ADR-046-zone-routing", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-telemetry-audit-and-support", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-telemetry-audit-and-support", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-telemetry-audit-and-support", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-telemetry-audit-and-support", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-telemetry-audit-and-support", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-telemetry-audit-and-support", + "to" : "ADR-046-resource-store-redb", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-telemetry-audit-and-support", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-terminology-and-identities", + "to" : "ADR-046-decision-register", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-cli-and-operations", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-components-processes-and-sandbox", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-core-controllers", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-current-code-migration-map", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-decision-register", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-feasibility-and-spikes", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-nix-configuration", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-primitive-resource-composition", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-activation-nixos", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-audio-pipewire", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-clipboard-wayland", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-credential-entra", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-credential-managed-identity", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-credential-secret-service", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-device-gpu", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-device-security-key", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-device-tpm", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-device-usbip", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-display-wayland", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-model-and-packaging", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-network-local", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-notification-desktop", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-observability-otel", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-runtime-azure-container-apps", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-runtime-azure-virtual-machine", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-runtime-cloud-hypervisor", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-runtime-qemu-media", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-shell-terminal", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-state", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-system-core", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-system-minijail", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-system-systemd", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-transport-azure-relay", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-transport-unix", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-transport-vsock", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-volume-local", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-provider-volume-virtiofs", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-reset-and-cutover", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-resource-store-redb", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-resources-credential", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-resources-device", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-resources-host-guest-process-user", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-resources-network", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-resources-volume", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-resources-zone-control", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-security-and-threat-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-streamline", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-telemetry-audit-and-support", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-validation-and-delivery", + "to" : "ADR-046-zone-routing", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-zone-routing", + "to" : "ADR-046-componentsession-and-bus", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-zone-routing", + "to" : "ADR-046-resource-api-and-authorization", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-zone-routing", + "to" : "ADR-046-resource-object-model", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-zone-routing", + "to" : "ADR-046-resource-reconciliation", + "type" : "spec-depends-on" + }, + { + "from" : "ADR-046-zone-routing", + "to" : "ADR-046-terminology-and-identities", + "type" : "spec-depends-on" + }, + { + "from" : "ADR046-aca-001", + "to" : "ADR046-provider-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-aca-002", + "to" : "ADR046-aca-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-aca-003", + "to" : "ADR046-aca-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-aca-004", + "to" : "ADR046-session-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-aca-005", + "to" : "ADR046-aca-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-aca-006", + "to" : "ADR046-aca-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-aca-007", + "to" : "ADR046-aca-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-activation-003", + "to" : "ADR046-activation-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-activation-004", + "to" : "ADR046-activation-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-activation-007", + "to" : "ADR046-activation-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-api-002", + "to" : "ADR046-api-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-002", + "to" : "ADR046-audio-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-004", + "to" : "ADR046-audio-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-005", + "to" : "ADR046-provider-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-006", + "to" : "ADR046-audio-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-006", + "to" : "ADR046-audio-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-006", + "to" : "ADR046-audio-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-006", + "to" : "ADR046-audio-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-007", + "to" : "ADR046-audio-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-008", + "to" : "ADR046-audio-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-009", + "to" : "ADR046-audio-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-010", + "to" : "ADR046-audio-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-010", + "to" : "ADR046-audio-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-010", + "to" : "ADR046-audio-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-011", + "to" : "ADR046-audio-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-012", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-012", + "to" : "ADR046-zone-control-020", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-013", + "to" : "ADR046-audio-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-013", + "to" : "ADR046-audio-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-013", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-014", + "to" : "ADR046-audio-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audio-014", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audit-002", + "to" : "ADR046-audit-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audit-002", + "to" : "ADR046-store-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audit-003", + "to" : "ADR046-audit-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audit-003", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audit-003", + "to" : "ADR046-session-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-audit-004", + "to" : "ADR046-audit-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-azure-vm-002", + "to" : "ADR046-azure-vm-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-azure-vm-003", + "to" : "ADR046-azure-vm-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-azure-vm-003", + "to" : "ADR046-azure-vm-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-azure-vm-004", + "to" : "ADR046-azure-vm-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-azure-vm-005", + "to" : "ADR046-azure-vm-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-azure-vm-006", + "to" : "ADR046-azure-vm-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-azure-vm-007", + "to" : "ADR046-azure-vm-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-azure-vm-008", + "to" : "ADR046-azure-vm-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-ch-002", + "to" : "ADR046-ch-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-ch-002", + "to" : "ADR046-network-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-ch-003", + "to" : "ADR046-ch-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-ch-004", + "to" : "ADR046-ch-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-ch-004", + "to" : "ADR046-identities-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-ch-005", + "to" : "ADR046-ch-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-ch-005", + "to" : "ADR046-session-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-ch-006", + "to" : "ADR046-ch-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-ch-006", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-ch-007", + "to" : "ADR046-ch-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-ch-007", + "to" : "ADR046-pstate-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-001", + "to" : "ADR046-api-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-001", + "to" : "ADR046-identities-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-002", + "to" : "ADR046-api-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-002", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-003", + "to" : "ADR046-api-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-003", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-004", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-005", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-006", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-006", + "to" : "ADR046-routing-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-006", + "to" : "ADR046-routing-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-007", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-008", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-009", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-010", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-011", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-011", + "to" : "ADR046-cli-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-011", + "to" : "ADR046-cli-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-011", + "to" : "ADR046-identities-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-012", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-012", + "to" : "ADR046-cli-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-013", + "to" : "ADR046-cli-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-013", + "to" : "ADR046-cli-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cli-013", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-client-001", + "to" : "ADR046-zone-control-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-client-001", + "to" : "ADR046-zone-control-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-client-001", + "to" : "ADR046-zone-control-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-client-001", + "to" : "ADR046-zone-control-018", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-002", + "to" : "ADR046-clipboard-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-003", + "to" : "ADR046-clipboard-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-004", + "to" : "ADR046-clipboard-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-004", + "to" : "ADR046-clipboard-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-005", + "to" : "ADR046-clipboard-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-007", + "to" : "ADR046-clipboard-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-008", + "to" : "ADR046-clipboard-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-009", + "to" : "ADR046-clipboard-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-009", + "to" : "ADR046-clipboard-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-009", + "to" : "ADR046-clipboard-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-009", + "to" : "ADR046-clipboard-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-009", + "to" : "ADR046-clipboard-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-009", + "to" : "ADR046-clipboard-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-009", + "to" : "ADR046-clipboard-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-009", + "to" : "ADR046-clipboard-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-010", + "to" : "ADR046-clipboard-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-011", + "to" : "ADR046-clipboard-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-clipboard-012", + "to" : "ADR046-clipboard-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-core-002", + "to" : "ADR046-core-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-core-002", + "to" : "ADR046-exec-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-core-002", + "to" : "ADR046-exec-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-core-002", + "to" : "ADR046-exec-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-core-002", + "to" : "ADR046-system-core-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-entra-001", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-entra-001", + "to" : "ADR046-credential-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-001", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-001", + "to" : "ADR046-credential-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-001", + "to" : "ADR046-mi-topology-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-001", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-002", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-002", + "to" : "ADR046-credential-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-002", + "to" : "ADR046-mi-topology-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-002", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-002", + "to" : "ADR046-reconcile-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-003", + "to" : "ADR046-cred-mi-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-003", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-003", + "to" : "ADR046-credential-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-004", + "to" : "ADR046-cred-mi-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-mi-004", + "to" : "ADR046-mi-topology-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-ss-003", + "to" : "ADR046-cred-ss-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-ss-003", + "to" : "ADR046-cred-ss-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-cred-ss-003", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-001", + "to" : "ADR046-identities-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-001", + "to" : "ADR046-object-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-002", + "to" : "ADR046-api-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-002", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-002", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-003", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-003", + "to" : "ADR046-credential-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-003", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-004", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-004", + "to" : "ADR046-credential-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-004", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-005", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-005", + "to" : "ADR046-credential-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-005", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-006", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-006", + "to" : "ADR046-credential-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-006", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-006", + "to" : "ADR046-reconcile-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-007", + "to" : "ADR046-api-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-007", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-007", + "to" : "ADR046-identities-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-007", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-008", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-credential-008", + "to" : "ADR046-credential-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-delivery-003", + "to" : "ADR046-delivery-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-delivery-004", + "to" : "ADR046-delivery-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-delivery-005", + "to" : "ADR046-delivery-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-delivery-005", + "to" : "ADR046-delivery-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-delivery-006", + "to" : "ADR046-delivery-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-delivery-006", + "to" : "ADR046-delivery-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-delivery-006", + "to" : "ADR046-delivery-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-delivery-008", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-delivery-009", + "to" : "ADR046-delivery-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-delivery-009", + "to" : "ADR046-delivery-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-002", + "to" : "ADR046-device-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-003", + "to" : "ADR046-device-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-004", + "to" : "ADR046-device-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-005", + "to" : "ADR046-device-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-006", + "to" : "ADR046-device-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-006", + "to" : "ADR046-device-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-006", + "to" : "ADR046-device-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-006", + "to" : "ADR046-device-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-006", + "to" : "ADR046-device-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-007", + "to" : "ADR046-device-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-008", + "to" : "ADR046-device-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-008", + "to" : "ADR046-device-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-008", + "to" : "ADR046-device-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-008", + "to" : "ADR046-device-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-002", + "to" : "ADR046-device-tpm-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-003", + "to" : "ADR046-device-tpm-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-004", + "to" : "ADR046-device-tpm-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-005", + "to" : "ADR046-device-tpm-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-006", + "to" : "ADR046-device-tpm-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-007", + "to" : "ADR046-device-tpm-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-008", + "to" : "ADR046-device-tpm-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-009", + "to" : "ADR046-device-tpm-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-010", + "to" : "ADR046-device-tpm-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-011", + "to" : "ADR046-device-tpm-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-012", + "to" : "ADR046-device-tpm-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-device-tpm-013", + "to" : "ADR046-device-tpm-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-display-001", + "to" : "ADR046-process-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-display-001", + "to" : "ADR046-provider-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-display-002", + "to" : "ADR046-display-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-display-003", + "to" : "ADR046-display-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-display-004", + "to" : "ADR046-display-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-doctor-001", + "to" : "ADR046-audit-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-doctor-001", + "to" : "ADR046-core-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-doctor-002", + "to" : "ADR046-doctor-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-002", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-003", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-004", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-005", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-005", + "to" : "ADR046-exec-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-006", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-006", + "to" : "ADR046-exec-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-007", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-007", + "to" : "ADR046-exec-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-008", + "to" : "ADR046-exec-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-008", + "to" : "ADR046-exec-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-009", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-010", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-010", + "to" : "ADR046-exec-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-011", + "to" : "ADR046-exec-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-011", + "to" : "ADR046-exec-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-012", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-013", + "to" : "ADR046-exec-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-014", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-014", + "to" : "ADR046-exec-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-015", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-015", + "to" : "ADR046-exec-014", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-015", + "to" : "ADR046-exec-022", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-016", + "to" : "ADR046-exec-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-017", + "to" : "ADR046-exec-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-018", + "to" : "ADR046-exec-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-019", + "to" : "ADR046-exec-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-019", + "to" : "ADR046-exec-018", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-020", + "to" : "ADR046-exec-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-021", + "to" : "ADR046-exec-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-021", + "to" : "ADR046-exec-018", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-022", + "to" : "ADR046-exec-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-022", + "to" : "ADR046-exec-021", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-023", + "to" : "ADR046-exec-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-exec-023", + "to" : "ADR046-exec-021", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-feasibility-002", + "to" : "ADR046-feasibility-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-feasibility-010", + "to" : "ADR046-feasibility-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-feasibility-010", + "to" : "ADR046-feasibility-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-feasibility-011", + "to" : "ADR046-delivery-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-gpu-002", + "to" : "ADR046-gpu-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-gpu-003", + "to" : "ADR046-gpu-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-gpu-004", + "to" : "ADR046-gpu-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-gpu-005", + "to" : "ADR046-gpu-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-gpu-006", + "to" : "ADR046-gpu-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-gpu-007", + "to" : "ADR046-gpu-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-gpu-008", + "to" : "ADR046-gpu-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-gpu-009", + "to" : "ADR046-gpu-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-host-posture-001", + "to" : "ADR046-audit-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-host-posture-001", + "to" : "ADR046-core-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-identities-002", + "to" : "ADR046-identities-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-mi-topology-001", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-mi-topology-001", + "to" : "ADR046-credential-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-001", + "to" : "ADR046-process-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-001", + "to" : "ADR046-provider-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-002", + "to" : "ADR046-minijail-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-003", + "to" : "ADR046-minijail-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-004", + "to" : "ADR046-minijail-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-005", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-005", + "to" : "ADR046-minijail-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-005", + "to" : "ADR046-minijail-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-005", + "to" : "ADR046-minijail-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-005", + "to" : "ADR046-minijail-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-005", + "to" : "ADR046-session-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-minijail-006", + "to" : "ADR046-minijail-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-002", + "to" : "ADR046-network-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-003", + "to" : "ADR046-network-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-003", + "to" : "ADR046-network-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-004", + "to" : "ADR046-network-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-004", + "to" : "ADR046-network-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-004", + "to" : "ADR046-network-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-005", + "to" : "ADR046-network-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-006", + "to" : "ADR046-network-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-006", + "to" : "ADR046-network-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-007", + "to" : "ADR046-network-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-008", + "to" : "ADR046-network-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-008", + "to" : "ADR046-network-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-009", + "to" : "ADR046-network-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-network-009", + "to" : "ADR046-network-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-001", + "to" : "ADR046-identities-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-001", + "to" : "ADR046-identities-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-002", + "to" : "ADR046-nix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-003", + "to" : "ADR046-nix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-004", + "to" : "ADR046-nix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-004", + "to" : "ADR046-nix-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-005", + "to" : "ADR046-nix-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-006", + "to" : "ADR046-nix-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-006", + "to" : "ADR046-primitives-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-007", + "to" : "ADR046-nix-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-007", + "to" : "ADR046-primitives-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-008", + "to" : "ADR046-nix-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-009", + "to" : "ADR046-nix-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-010", + "to" : "ADR046-nix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-012", + "to" : "ADR046-nix-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-012", + "to" : "ADR046-nix-022", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-013", + "to" : "ADR046-nix-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-014", + "to" : "ADR046-nix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-014", + "to" : "ADR046-nix-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-015", + "to" : "ADR046-nix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-016", + "to" : "ADR046-nix-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-017", + "to" : "ADR046-nix-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-018", + "to" : "ADR046-nix-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-019", + "to" : "ADR046-nix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-019", + "to" : "ADR046-nix-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-019", + "to" : "ADR046-nix-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-020", + "to" : "ADR046-nix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-020", + "to" : "ADR046-nix-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-021", + "to" : "ADR046-nix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-022", + "to" : "ADR046-nix-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-022", + "to" : "ADR046-nix-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-023", + "to" : "ADR046-nix-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-024", + "to" : "ADR046-nix-023", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-024", + "to" : "ADR046-nix-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-025", + "to" : "ADR046-nix-023", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-025", + "to" : "ADR046-nix-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-026", + "to" : "ADR046-nix-023", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-028", + "to" : "ADR046-nix-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-029", + "to" : "ADR046-nix-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-029", + "to" : "ADR046-nix-028", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-030", + "to" : "ADR046-nix-028", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-030", + "to" : "ADR046-nix-029", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-031", + "to" : "ADR046-provider-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-031", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-031", + "to" : "ADR046-zone-control-020", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-031", + "to" : "ADR046-zone-control-024", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-032", + "to" : "ADR046-nix-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-032", + "to" : "ADR046-nix-029", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-033", + "to" : "ADR046-nix-023", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-033", + "to" : "ADR046-nix-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-034", + "to" : "ADR046-nix-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-034", + "to" : "ADR046-nix-028", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-034", + "to" : "ADR046-nix-029", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-035", + "to" : "ADR046-nix-029", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nix-035", + "to" : "ADR046-nix-034", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nl-002", + "to" : "ADR046-nl-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nl-004", + "to" : "ADR046-nl-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nl-006", + "to" : "ADR046-nl-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nl-006", + "to" : "ADR046-nl-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nl-006", + "to" : "ADR046-nl-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nl-006", + "to" : "ADR046-nl-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nl-006", + "to" : "ADR046-nl-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-nl-014", + "to" : "ADR046-nl-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-notify-002", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-notify-002", + "to" : "ADR046-session-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-notify-003", + "to" : "ADR046-notify-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-notify-003", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-notify-004", + "to" : "ADR046-notify-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-notify-004", + "to" : "ADR046-notify-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-notify-005", + "to" : "ADR046-notify-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-notify-006", + "to" : "ADR046-notify-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-object-002", + "to" : "ADR046-object-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-001", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-002", + "to" : "ADR046-otel-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-002", + "to" : "ADR046-provider-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-002", + "to" : "ADR046-provider-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-002", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-003", + "to" : "ADR046-otel-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-003", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-003", + "to" : "ADR046-volume-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-004", + "to" : "ADR046-otel-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-005", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-005", + "to" : "ADR046-zone-control-020", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-006", + "to" : "ADR046-otel-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-006", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-otel-006", + "to" : "ADR046-zone-control-020", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pkg-001", + "to" : "ADR046-zone-control-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-process-002", + "to" : "ADR046-process-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-provider-004", + "to" : "ADR046-provider-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-provider-agent-001", + "to" : "ADR046-zone-control-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-provider-agent-001", + "to" : "ADR046-zone-control-017", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-provider-agent-001", + "to" : "ADR046-zone-control-018", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-001", + "to" : "ADR046-primitives-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-002", + "to" : "ADR046-pstate-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-003", + "to" : "ADR046-pstate-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-004", + "to" : "ADR046-pstate-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-004", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-005", + "to" : "ADR046-pstate-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-006", + "to" : "ADR046-pstate-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-006", + "to" : "ADR046-pstate-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-007", + "to" : "ADR046-pstate-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-008", + "to" : "ADR046-pstate-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-009", + "to" : "ADR046-pstate-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-009", + "to" : "ADR046-pstate-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-009", + "to" : "ADR046-pstate-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-009", + "to" : "ADR046-pstate-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-009", + "to" : "ADR046-pstate-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-009", + "to" : "ADR046-pstate-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-009", + "to" : "ADR046-pstate-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-009", + "to" : "ADR046-pstate-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-010", + "to" : "ADR046-pstate-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-011", + "to" : "ADR046-pstate-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-012", + "to" : "ADR046-pstate-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-pstate-012", + "to" : "ADR046-pstate-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-002", + "to" : "ADR046-qemu-media-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-003", + "to" : "ADR046-qemu-media-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-004", + "to" : "ADR046-qemu-media-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-004", + "to" : "ADR046-qemu-media-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-005", + "to" : "ADR046-qemu-media-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-005", + "to" : "ADR046-qemu-media-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-006", + "to" : "ADR046-qemu-media-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-006", + "to" : "ADR046-qemu-media-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-007", + "to" : "ADR046-qemu-media-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-008", + "to" : "ADR046-qemu-media-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-009", + "to" : "ADR046-qemu-media-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-009", + "to" : "ADR046-qemu-media-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-009", + "to" : "ADR046-qemu-media-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-009", + "to" : "ADR046-qemu-media-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-009", + "to" : "ADR046-qemu-media-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-009", + "to" : "ADR046-qemu-media-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-010", + "to" : "ADR046-qemu-media-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-011", + "to" : "ADR046-qemu-media-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-011", + "to" : "ADR046-qemu-media-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-012", + "to" : "ADR046-network-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-012", + "to" : "ADR046-qemu-media-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-013", + "to" : "ADR046-qemu-media-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-013", + "to" : "ADR046-qemu-media-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-013", + "to" : "ADR046-qemu-media-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-013", + "to" : "ADR046-qemu-media-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-013", + "to" : "ADR046-qemu-media-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-013", + "to" : "ADR046-qemu-media-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-013", + "to" : "ADR046-qemu-media-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-013", + "to" : "ADR046-qemu-media-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-014", + "to" : "ADR046-qemu-media-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-015", + "to" : "ADR046-qemu-media-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-015", + "to" : "ADR046-qemu-media-014", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-016", + "to" : "ADR046-qemu-media-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-016", + "to" : "ADR046-qemu-media-014", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-017", + "to" : "ADR046-qemu-media-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-017", + "to" : "ADR046-qemu-media-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-018", + "to" : "ADR046-qemu-media-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-018", + "to" : "ADR046-qemu-media-014", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-018", + "to" : "ADR046-qemu-media-015", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-018", + "to" : "ADR046-qemu-media-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-014", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-015", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-017", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-qemu-media-019", + "to" : "ADR046-qemu-media-018", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reconcile-002", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-002", + "to" : "ADR046-reset-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-003", + "to" : "ADR046-reset-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-003", + "to" : "ADR046-reset-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-004", + "to" : "ADR046-reset-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-005", + "to" : "ADR046-core-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-005", + "to" : "ADR046-reset-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-005", + "to" : "ADR046-store-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-006", + "to" : "ADR046-reset-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-007", + "to" : "ADR046-reset-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-008", + "to" : "ADR046-reset-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-009", + "to" : "ADR046-reset-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-009", + "to" : "ADR046-reset-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-009", + "to" : "ADR046-reset-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-009", + "to" : "ADR046-reset-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-010", + "to" : "ADR046-reset-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-010", + "to" : "ADR046-reset-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-010", + "to" : "ADR046-reset-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-011", + "to" : "ADR046-reset-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-011", + "to" : "ADR046-reset-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-011", + "to" : "ADR046-reset-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-011", + "to" : "ADR046-reset-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-011", + "to" : "ADR046-reset-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-011", + "to" : "ADR046-reset-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-011", + "to" : "ADR046-reset-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-011", + "to" : "ADR046-reset-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-011", + "to" : "ADR046-reset-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reset-011", + "to" : "ADR046-reset-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-001", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-002", + "to" : "ADR046-reuse-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-003", + "to" : "ADR046-reuse-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-004", + "to" : "ADR046-reuse-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-004", + "to" : "ADR046-reuse-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-005", + "to" : "ADR046-audit-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-005", + "to" : "ADR046-reuse-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-006", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-006", + "to" : "ADR046-reuse-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-007", + "to" : "ADR046-reuse-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-007", + "to" : "ADR046-reuse-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-008", + "to" : "ADR046-reuse-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-009", + "to" : "ADR046-reuse-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-009", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-reuse-009", + "to" : "ADR046-telem-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-002", + "to" : "ADR046-routing-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-003", + "to" : "ADR046-routing-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-003", + "to" : "ADR046-routing-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-004", + "to" : "ADR046-routing-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-005", + "to" : "ADR046-routing-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-005", + "to" : "ADR046-routing-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-006", + "to" : "ADR046-routing-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-008", + "to" : "ADR046-routing-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-009", + "to" : "ADR046-routing-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-010", + "to" : "ADR046-routing-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-010", + "to" : "ADR046-routing-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-011", + "to" : "ADR046-routing-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-012", + "to" : "ADR046-routing-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-012", + "to" : "ADR046-routing-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-013", + "to" : "ADR046-routing-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-013", + "to" : "ADR046-routing-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-014", + "to" : "ADR046-routing-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-015", + "to" : "ADR046-routing-014", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-016", + "to" : "ADR046-routing-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-016", + "to" : "ADR046-routing-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-016", + "to" : "ADR046-routing-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-routing-016", + "to" : "ADR046-routing-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-001", + "to" : "ADR046-telem-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-002", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-002", + "to" : "ADR046-session-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-003", + "to" : "ADR046-zone-control-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-003", + "to" : "ADR046-zone-control-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-004", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-004", + "to" : "ADR046-routing-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-006", + "to" : "ADR046-minijail-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-006", + "to" : "ADR046-minijail-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-007", + "to" : "ADR046-aca-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-007", + "to" : "ADR046-minijail-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-007", + "to" : "ADR046-systemd-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-007", + "to" : "ADR046-volume-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-008", + "to" : "ADR046-exec-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-009", + "to" : "ADR046-pstate-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-010", + "to" : "ADR046-credential-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-010", + "to" : "ADR046-credential-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-011", + "to" : "ADR046-notify-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-012", + "to" : "ADR046-audit-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-013", + "to" : "ADR046-session-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-013", + "to" : "ADR046-zone-control-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-013", + "to" : "ADR046-zone-control-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-014", + "to" : "ADR046-doctor-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-014", + "to" : "ADR046-doctor-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-015", + "to" : "ADR046-audit-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-017", + "to" : "ADR046-routing-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-019", + "to" : "ADR046-exec-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-019", + "to" : "ADR046-exec-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-019", + "to" : "ADR046-minijail-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-019", + "to" : "ADR046-minijail-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-001", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-002", + "to" : "ADR046-security-key-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-002", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-003", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-004", + "to" : "ADR046-security-key-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-005", + "to" : "ADR046-provider-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-005", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-006", + "to" : "ADR046-security-key-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-006", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-007", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-009", + "to" : "ADR046-security-key-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-009", + "to" : "ADR046-security-key-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-009", + "to" : "ADR046-security-key-025", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-009", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-009", + "to" : "ADR046-security-key-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-010", + "to" : "ADR046-security-key-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-010", + "to" : "ADR046-security-key-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-010", + "to" : "ADR046-security-key-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-010", + "to" : "ADR046-security-key-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-010", + "to" : "ADR046-security-key-014", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-010", + "to" : "ADR046-security-key-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-010", + "to" : "ADR046-security-key-018", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-011", + "to" : "ADR046-security-key-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-011", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-012", + "to" : "ADR046-security-key-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-012", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-013", + "to" : "ADR046-security-key-025", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-014", + "to" : "ADR046-security-key-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-014", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-015", + "to" : "ADR046-security-key-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-015", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-016", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-016", + "to" : "ADR046-security-key-015", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-016", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-017", + "to" : "ADR046-security-key-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-017", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-017", + "to" : "ADR046-security-key-014", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-017", + "to" : "ADR046-security-key-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-017", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-017", + "to" : "ADR046-security-key-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-018", + "to" : "ADR046-security-key-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-018", + "to" : "ADR046-security-key-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-018", + "to" : "ADR046-security-key-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-019", + "to" : "ADR046-security-key-017", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-019", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-019", + "to" : "ADR046-zone-control-024", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-020", + "to" : "ADR046-security-key-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-020", + "to" : "ADR046-security-key-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-020", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-021", + "to" : "ADR046-security-key-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-021", + "to" : "ADR046-security-key-018", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-022", + "to" : "ADR046-security-key-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-022", + "to" : "ADR046-security-key-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-014", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-015", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-017", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-018", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-020", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-021", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-022", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-024", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-025", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-027", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-028", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-023", + "to" : "ADR046-security-key-029", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-024", + "to" : "ADR046-security-key-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-024", + "to" : "ADR046-security-key-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-024", + "to" : "ADR046-security-key-014", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-024", + "to" : "ADR046-security-key-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-024", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-025", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-026", + "to" : "ADR046-provider-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-026", + "to" : "ADR046-security-key-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-026", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-027", + "to" : "ADR046-security-key-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-027", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-028", + "to" : "ADR046-security-key-024", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-028", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-028", + "to" : "ADR046-security-key-029", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-028", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-028", + "to" : "ADR046-zone-control-020", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-029", + "to" : "ADR046-security-key-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-029", + "to" : "ADR046-security-key-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-029", + "to" : "ADR046-security-key-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-029", + "to" : "ADR046-security-key-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-029", + "to" : "ADR046-security-key-018", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-029", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-029", + "to" : "ADR046-zone-control-024", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-030", + "to" : "ADR046-security-key-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-030", + "to" : "ADR046-security-key-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-030", + "to" : "ADR046-security-key-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-030", + "to" : "ADR046-security-key-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-030", + "to" : "ADR046-security-key-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-031", + "to" : "ADR046-security-key-030", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-032", + "to" : "ADR046-security-key-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-032", + "to" : "ADR046-security-key-020", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-032", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-033", + "to" : "ADR046-security-key-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-033", + "to" : "ADR046-security-key-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-034", + "to" : "ADR046-security-key-015", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-034", + "to" : "ADR046-security-key-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-035", + "to" : "ADR046-security-key-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-035", + "to" : "ADR046-security-key-018", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-035", + "to" : "ADR046-security-key-020", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-security-key-035", + "to" : "ADR046-security-key-026", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-session-002", + "to" : "ADR046-session-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-store-002", + "to" : "ADR046-store-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-store-003", + "to" : "ADR046-store-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-002", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-003", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-003", + "to" : "ADR046-streamline-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-004", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-004", + "to" : "ADR046-streamline-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-005", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-006", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-007", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-008", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-009", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-010", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-012", + "to" : "ADR046-streamline-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-013", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-013", + "to" : "ADR046-streamline-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-015", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-015", + "to" : "ADR046-streamline-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-015", + "to" : "ADR046-streamline-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-016", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-016", + "to" : "ADR046-streamline-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-018", + "to" : "ADR046-streamline-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-019", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-020", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-021", + "to" : "ADR046-streamline-020", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-022", + "to" : "ADR046-delivery-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-023", + "to" : "ADR046-streamline-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-023", + "to" : "ADR046-streamline-022", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-streamline-024", + "to" : "ADR046-streamline-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-system-core-001", + "to" : "ADR046-audit-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-system-core-001", + "to" : "ADR046-exec-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-system-core-001", + "to" : "ADR046-exec-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-system-core-001", + "to" : "ADR046-exec-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-system-core-001", + "to" : "ADR046-provider-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-system-core-001", + "to" : "ADR046-pstate-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-system-core-001", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-systemd-001", + "to" : "ADR046-process-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-systemd-002", + "to" : "ADR046-systemd-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-systemd-003", + "to" : "ADR046-systemd-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-002", + "to" : "ADR046-store-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-002", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-003", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-003", + "to" : "ADR046-session-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-003", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-004", + "to" : "ADR046-core-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-004", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-005", + "to" : "ADR046-process-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-005", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-006", + "to" : "ADR046-process-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-006", + "to" : "ADR046-provider-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-007", + "to" : "ADR046-telem-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-008", + "to" : "ADR046-telem-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-010", + "to" : "ADR046-store-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-010", + "to" : "ADR046-telem-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-010", + "to" : "ADR046-telem-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-011", + "to" : "ADR046-audit-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-011", + "to" : "ADR046-store-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-011", + "to" : "ADR046-telem-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-telem-011", + "to" : "ADR046-telem-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-relay-002", + "to" : "ADR046-transport-relay-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-relay-003", + "to" : "ADR046-transport-relay-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-relay-004", + "to" : "ADR046-transport-relay-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-relay-005", + "to" : "ADR046-transport-relay-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-relay-006", + "to" : "ADR046-transport-relay-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-relay-006", + "to" : "ADR046-transport-relay-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-relay-006", + "to" : "ADR046-transport-relay-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-relay-006", + "to" : "ADR046-transport-relay-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-relay-006", + "to" : "ADR046-transport-relay-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-001", + "to" : "ADR046-session-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-002", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-002", + "to" : "ADR046-transport-unix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-003", + "to" : "ADR046-transport-unix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-004", + "to" : "ADR046-transport-unix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-005", + "to" : "ADR046-transport-unix-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-006", + "to" : "ADR046-transport-unix-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-007", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-007", + "to" : "ADR046-session-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-007", + "to" : "ADR046-transport-unix-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-007", + "to" : "ADR046-transport-unix-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-007", + "to" : "ADR046-transport-unix-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-007", + "to" : "ADR046-transport-unix-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-007", + "to" : "ADR046-transport-unix-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-008", + "to" : "ADR046-provider-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-008", + "to" : "ADR046-transport-unix-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-009", + "to" : "ADR046-transport-unix-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-010", + "to" : "ADR046-transport-unix-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-011", + "to" : "ADR046-transport-unix-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-011", + "to" : "ADR046-transport-unix-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-011", + "to" : "ADR046-transport-unix-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-transport-unix-011", + "to" : "ADR046-transport-unix-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-002", + "to" : "ADR046-usbip-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-002", + "to" : "ADR046-zone-control-024", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-003", + "to" : "ADR046-provider-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-003", + "to" : "ADR046-usbip-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-004", + "to" : "ADR046-usbip-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-004", + "to" : "ADR046-usbip-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-004", + "to" : "ADR046-usbip-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-004", + "to" : "ADR046-zone-control-024", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-005", + "to" : "ADR046-usbip-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-006", + "to" : "ADR046-usbip-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-007", + "to" : "ADR046-usbip-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-007", + "to" : "ADR046-usbip-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-008", + "to" : "ADR046-usbip-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-008", + "to" : "ADR046-zone-control-024", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-009", + "to" : "ADR046-usbip-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-usbip-009", + "to" : "ADR046-usbip-008", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-user-session-001", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-001", + "to" : "ADR046-primitives-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-002", + "to" : "ADR046-vl-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-003", + "to" : "ADR046-reconcile-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-003", + "to" : "ADR046-vl-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-004", + "to" : "ADR046-vl-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-005", + "to" : "ADR046-vl-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-006", + "to" : "ADR046-vl-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-007", + "to" : "ADR046-pstate-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-007", + "to" : "ADR046-pstate-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-007", + "to" : "ADR046-pstate-006", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-007", + "to" : "ADR046-vl-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-007", + "to" : "ADR046-vl-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-008", + "to" : "ADR046-vl-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-008", + "to" : "ADR046-vl-007", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-009", + "to" : "ADR046-vl-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-010", + "to" : "ADR046-pstate-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-010", + "to" : "ADR046-vl-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-011", + "to" : "ADR046-pstate-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-011", + "to" : "ADR046-vl-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-012", + "to" : "ADR046-vl-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-012", + "to" : "ADR046-vl-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-013", + "to" : "ADR046-vl-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vl-013", + "to" : "ADR046-vl-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-volume-002", + "to" : "ADR046-volume-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-volume-003", + "to" : "ADR046-volume-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-volume-004", + "to" : "ADR046-volume-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-volume-005", + "to" : "ADR046-volume-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-volume-005", + "to" : "ADR046-volume-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-volume-006", + "to" : "ADR046-volume-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-volume-006", + "to" : "ADR046-volume-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vsock-001", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vsock-002", + "to" : "ADR046-vsock-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vsock-003", + "to" : "ADR046-bus-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vsock-003", + "to" : "ADR046-vsock-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vsock-004", + "to" : "ADR046-vsock-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vsock-006", + "to" : "ADR046-vsock-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vsock-006", + "to" : "ADR046-vsock-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-001", + "to" : "ADR046-volume-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-001", + "to" : "ADR046-vvfs-export-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-002", + "to" : "ADR046-volume-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-002", + "to" : "ADR046-vvfs-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-003", + "to" : "ADR046-volume-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-003", + "to" : "ADR046-vvfs-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-003", + "to" : "ADR046-vvfs-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-003", + "to" : "ADR046-vvfs-export-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-004", + "to" : "ADR046-vvfs-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-005", + "to" : "ADR046-volume-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-005", + "to" : "ADR046-vvfs-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-005", + "to" : "ADR046-vvfs-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-006", + "to" : "ADR046-volume-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-006", + "to" : "ADR046-vvfs-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-vvfs-export-001", + "to" : "ADR046-volume-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-wire-001", + "to" : "ADR046-zone-control-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-001", + "to" : "ADR046-identities-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-001", + "to" : "ADR046-object-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-001", + "to" : "ADR046-store-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-002", + "to" : "ADR046-zone-control-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-002", + "to" : "ADR046-zone-control-018", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-003", + "to" : "ADR046-api-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-003", + "to" : "ADR046-zone-control-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-003", + "to" : "ADR046-zone-control-017", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-004", + "to" : "ADR046-api-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-004", + "to" : "ADR046-zone-control-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-005", + "to" : "ADR046-zone-control-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-006", + "to" : "ADR046-zone-control-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-006", + "to" : "ADR046-zone-control-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-007", + "to" : "ADR046-identities-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-007", + "to" : "ADR046-zone-control-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-007", + "to" : "ADR046-zone-control-002", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-007", + "to" : "ADR046-zone-control-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-007", + "to" : "ADR046-zone-control-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-007", + "to" : "ADR046-zone-control-005", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-007", + "to" : "ADR046-zone-control-009", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-007", + "to" : "ADR046-zone-control-010", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-008", + "to" : "ADR046-zone-control-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-008", + "to" : "ADR046-zone-control-003", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-009", + "to" : "ADR046-store-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-009", + "to" : "ADR046-zone-control-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-010", + "to" : "ADR046-store-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-010", + "to" : "ADR046-zone-control-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-011", + "to" : "ADR046-identities-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-011", + "to" : "ADR046-store-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-012", + "to" : "ADR046-zone-control-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-013", + "to" : "ADR046-identities-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-014", + "to" : "ADR046-routing-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-014", + "to" : "ADR046-zone-control-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-015", + "to" : "ADR046-zone-control-014", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-016", + "to" : "ADR046-zone-control-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-016", + "to" : "ADR046-zone-control-015", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-017", + "to" : "ADR046-zone-control-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-017", + "to" : "ADR046-zone-control-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-018", + "to" : "ADR046-zone-control-011", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-018", + "to" : "ADR046-zone-control-012", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-018", + "to" : "ADR046-zone-control-013", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-019", + "to" : "ADR046-provider-004", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-019", + "to" : "ADR046-zone-control-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-020", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-021", + "to" : "ADR046-zone-control-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-021", + "to" : "ADR046-zone-control-016", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-022", + "to" : "ADR046-api-001", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-022", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-023", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-024", + "to" : "ADR046-zone-control-019", + "type" : "work-item-depends-on" + }, + { + "from" : "ADR046-zone-control-024", + "to" : "ADR046-zone-control-022", + "type" : "work-item-depends-on" + } + ], + "generatedFrom" : { + "specSet" : "docs/specs/ADR-046-spec-set.json", + "waveTopology" : "docs/specs/ADR-046-validation-and-delivery.md#3-delivery-wave-topology", + "workItems" : "docs/specs/ADR-046-work-items.json" + }, + "nodes" : [ + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-cli-and-operations.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-cli-and-operations", + "kind" : "spec", + "owner" : "ADR046-W5 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W5-parallel-specs", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 9, + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-components-processes-and-sandbox.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-components-processes-and-sandbox", + "kind" : "spec", + "owner" : "ADR046-W4 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W4-parallel-specs", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging" + ], + "specId" : "ADR-046-components-processes-and-sandbox", + "topologicalRank" : 8, + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-componentsession-and-bus.md" + ], + "entryContracts" : [ + "ADR-046-resource-api-and-authorization", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W1 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-componentsession-and-bus", + "kind" : "spec", + "owner" : "ADR046-W1 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W1-reconcile-and-bus", + "prerequisites" : [ + "ADR-046-resource-api-and-authorization", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-componentsession-and-bus", + "topologicalRank" : 5, + "wave" : "W1" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-core-controllers.md" + ], + "entryContracts" : [ + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-core-controllers", + "kind" : "spec", + "owner" : "ADR046-W4 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W4-parallel-specs", + "prerequisites" : [ + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb" + ], + "specId" : "ADR-046-core-controllers", + "topologicalRank" : 8, + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-current-code-migration-map.md" + ], + "entryContracts" : [], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-current-code-migration-map", + "kind" : "spec", + "owner" : "ADR046-W0 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W0-reference-docs", + "prerequisites" : [], + "specId" : "ADR-046-current-code-migration-map", + "topologicalRank" : 0, + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-decision-register.md" + ], + "entryContracts" : [], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-decision-register", + "kind" : "spec", + "owner" : "ADR046-W0 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W0-reference-docs", + "prerequisites" : [], + "specId" : "ADR-046-decision-register", + "topologicalRank" : 0, + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-feasibility-and-spikes.md" + ], + "entryContracts" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-feasibility-and-spikes", + "kind" : "spec", + "owner" : "ADR046-W7 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W7-closing", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 10, + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-nix-configuration.md" + ], + "entryContracts" : [ + "ADR-046-core-controllers", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-nix-configuration", + "kind" : "spec", + "owner" : "ADR046-W5 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W5-parallel-specs", + "prerequisites" : [ + "ADR-046-core-controllers", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 9, + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-primitive-resource-composition.md" + ], + "entryContracts" : [ + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-primitive-resource-composition", + "kind" : "spec", + "owner" : "ADR046-W2 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W2-composition-and-routing", + "prerequisites" : [ + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "specId" : "ADR-046-primitive-resource-composition", + "topologicalRank" : 6, + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-activation-nixos.md" + ], + "entryContracts" : [ + "ADR-046-cli-and-operations", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-activation-nixos", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-transport-observability-activation", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-activation-nixos", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-audio-pipewire.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-audio-pipewire", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-interaction", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-clipboard-wayland.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-clipboard-wayland", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-interaction", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-credential-entra.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-credential-entra", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-credentials", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-credential-entra", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-credential-managed-identity.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-credential-managed-identity", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-credentials", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-credential-managed-identity", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-credential-secret-service.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-credential-secret-service", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-credentials", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-credential-secret-service", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-device-gpu.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-device-gpu", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-storage-network-device", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-device-gpu", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-device-security-key.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-device-security-key", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-storage-network-device", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-device-tpm.md" + ], + "entryContracts" : [ + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-device-tpm", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-storage-network-device", + "prerequisites" : [ + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-device-usbip.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-network", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support", + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-device-usbip", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-storage-network-device", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-network", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support", + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-provider-device-usbip", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-display-wayland.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-display-wayland", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-interaction", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-display-wayland", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-provider-model-and-packaging.md" + ], + "entryContracts" : [ + "ADR-046-primitive-resource-composition", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "exitGate" : "ADR046-W3 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-model-and-packaging", + "kind" : "spec", + "owner" : "ADR046-W3 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W3-provider-contract", + "prerequisites" : [ + "ADR-046-primitive-resource-composition", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "specId" : "ADR-046-provider-model-and-packaging", + "topologicalRank" : 7, + "wave" : "W3" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-network-local.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-network-local", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-storage-network-device", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-notification-desktop.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-reconciliation", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-notification-desktop", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-interaction", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-reconciliation", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-notification-desktop", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-observability-otel.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-observability-otel", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-transport-observability-activation", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-provider-observability-otel", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-runtime-azure-container-apps.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-runtime-azure-container-apps", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-system-host-guest", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-runtime-azure-virtual-machine.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-runtime-azure-virtual-machine", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-system-host-guest", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-runtime-cloud-hypervisor.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-runtime-cloud-hypervisor", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-system-host-guest", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-runtime-qemu-media", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-system-host-guest", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-shell-terminal.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-shell-terminal", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-interaction", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-provider-state.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-state", + "kind" : "spec", + "owner" : "ADR046-W4 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W4-parallel-specs", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 8, + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-system-core.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-system-core", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-system-host-guest", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-system-core", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-system-minijail.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-system-minijail", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-system-host-guest", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-provider-system-minijail", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-system-systemd.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-system-systemd", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-system-host-guest", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-system-systemd", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-transport-azure-relay.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resources-credential", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-transport-azure-relay", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-transport-observability-activation", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resources-credential", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-provider-transport-azure-relay", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-transport-unix.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-transport-unix", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-transport-observability-activation", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-transport-vsock.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-transport-vsock", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-transport-observability-activation", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-provider-transport-vsock", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-volume-local.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-volume-local", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-storage-network-device", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/providers/ADR-046-provider-volume-virtiofs.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-provider-volume-virtiofs", + "kind" : "spec", + "owner" : "ADR046-W6 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W6-storage-network-device", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-provider-volume-virtiofs", + "topologicalRank" : 10, + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-reset-and-cutover.md" + ], + "entryContracts" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-reset-and-cutover", + "kind" : "spec", + "owner" : "ADR046-W7 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W7-closing", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 10, + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-resource-api-and-authorization.md" + ], + "entryContracts" : [ + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-resource-api-and-authorization", + "kind" : "spec", + "owner" : "ADR046-W0 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W0-foundation-chain", + "prerequisites" : [ + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-resource-api-and-authorization", + "topologicalRank" : 4, + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-resource-object-model.md" + ], + "entryContracts" : [ + "ADR-046-decision-register", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-resource-object-model", + "kind" : "spec", + "owner" : "ADR046-W0 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W0-foundation-chain", + "prerequisites" : [ + "ADR-046-decision-register", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-resource-object-model", + "topologicalRank" : 2, + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-resource-reconciliation.md" + ], + "entryContracts" : [ + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb" + ], + "exitGate" : "ADR046-W1 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-resource-reconciliation", + "kind" : "spec", + "owner" : "ADR046-W1 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W1-reconcile-and-bus", + "prerequisites" : [ + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb" + ], + "specId" : "ADR-046-resource-reconciliation", + "topologicalRank" : 5, + "wave" : "W1" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-resource-store-redb.md" + ], + "entryContracts" : [ + "ADR-046-resource-object-model", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-resource-store-redb", + "kind" : "spec", + "owner" : "ADR046-W0 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W0-foundation-chain", + "prerequisites" : [ + "ADR-046-resource-object-model", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-resource-store-redb", + "topologicalRank" : 3, + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-resources-credential.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-resources-credential", + "kind" : "spec", + "owner" : "ADR046-W4 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W4-parallel-specs", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-resources-credential", + "topologicalRank" : 8, + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-resources-device.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-resources-device", + "kind" : "spec", + "owner" : "ADR046-W5 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W5-parallel-specs", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "specId" : "ADR-046-resources-device", + "topologicalRank" : 9, + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-resources-host-guest-process-user.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-core-controllers", + "ADR-046-decision-register", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-resources-host-guest-process-user", + "kind" : "spec", + "owner" : "ADR046-W5 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W5-parallel-specs", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-core-controllers", + "ADR-046-decision-register", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 9, + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-resources-network.md" + ], + "entryContracts" : [ + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-resources-network", + "kind" : "spec", + "owner" : "ADR046-W4 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W4-parallel-specs", + "prerequisites" : [ + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-resources-network", + "topologicalRank" : 8, + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-resources-volume.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-resources-volume", + "kind" : "spec", + "owner" : "ADR046-W5 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W5-parallel-specs", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "specId" : "ADR-046-resources-volume", + "topologicalRank" : 9, + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-resources-zone-control.md" + ], + "entryContracts" : [ + "ADR-046-core-controllers", + "ADR-046-decision-register", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-resources-zone-control", + "kind" : "spec", + "owner" : "ADR046-W5 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W5-parallel-specs", + "prerequisites" : [ + "ADR-046-core-controllers", + "ADR-046-decision-register", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 9, + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-security-and-threat-model.md" + ], + "entryContracts" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-activation-nixos", + "ADR-046-provider-audio-pipewire", + "ADR-046-provider-clipboard-wayland", + "ADR-046-provider-credential-entra", + "ADR-046-provider-credential-managed-identity", + "ADR-046-provider-credential-secret-service", + "ADR-046-provider-device-gpu", + "ADR-046-provider-device-security-key", + "ADR-046-provider-device-tpm", + "ADR-046-provider-device-usbip", + "ADR-046-provider-display-wayland", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-network-local", + "ADR-046-provider-notification-desktop", + "ADR-046-provider-observability-otel", + "ADR-046-provider-runtime-azure-container-apps", + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR-046-provider-runtime-qemu-media", + "ADR-046-provider-shell-terminal", + "ADR-046-provider-state", + "ADR-046-provider-system-core", + "ADR-046-provider-system-minijail", + "ADR-046-provider-system-systemd", + "ADR-046-provider-transport-azure-relay", + "ADR-046-provider-transport-unix", + "ADR-046-provider-transport-vsock", + "ADR-046-provider-volume-local", + "ADR-046-provider-volume-virtiofs", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-security-and-threat-model", + "kind" : "spec", + "owner" : "ADR046-W7 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W7-closing", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-activation-nixos", + "ADR-046-provider-audio-pipewire", + "ADR-046-provider-clipboard-wayland", + "ADR-046-provider-credential-entra", + "ADR-046-provider-credential-managed-identity", + "ADR-046-provider-credential-secret-service", + "ADR-046-provider-device-gpu", + "ADR-046-provider-device-security-key", + "ADR-046-provider-device-tpm", + "ADR-046-provider-device-usbip", + "ADR-046-provider-display-wayland", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-network-local", + "ADR-046-provider-notification-desktop", + "ADR-046-provider-observability-otel", + "ADR-046-provider-runtime-azure-container-apps", + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR-046-provider-runtime-qemu-media", + "ADR-046-provider-shell-terminal", + "ADR-046-provider-state", + "ADR-046-provider-system-core", + "ADR-046-provider-system-minijail", + "ADR-046-provider-system-systemd", + "ADR-046-provider-transport-azure-relay", + "ADR-046-provider-transport-unix", + "ADR-046-provider-transport-vsock", + "ADR-046-provider-volume-local", + "ADR-046-provider-volume-virtiofs", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 11, + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-streamline.md" + ], + "entryContracts" : [ + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-reset-and-cutover", + "ADR-046-resources-volume", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-streamline", + "kind" : "spec", + "owner" : "ADR046-W7 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W7-closing", + "prerequisites" : [ + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-reset-and-cutover", + "ADR-046-resources-volume", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 11, + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-telemetry-audit-and-support.md" + ], + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-telemetry-audit-and-support", + "kind" : "spec", + "owner" : "ADR046-W5 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W5-parallel-specs", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 9, + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-terminology-and-identities.md" + ], + "entryContracts" : [ + "ADR-046-decision-register" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-terminology-and-identities", + "kind" : "spec", + "owner" : "ADR046-W0 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W0-foundation-chain", + "prerequisites" : [ + "ADR-046-decision-register" + ], + "specId" : "ADR-046-terminology-and-identities", + "topologicalRank" : 1, + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-validation-and-delivery.md" + ], + "entryContracts" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-feasibility-and-spikes", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-activation-nixos", + "ADR-046-provider-audio-pipewire", + "ADR-046-provider-clipboard-wayland", + "ADR-046-provider-credential-entra", + "ADR-046-provider-credential-managed-identity", + "ADR-046-provider-credential-secret-service", + "ADR-046-provider-device-gpu", + "ADR-046-provider-device-security-key", + "ADR-046-provider-device-tpm", + "ADR-046-provider-device-usbip", + "ADR-046-provider-display-wayland", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-network-local", + "ADR-046-provider-notification-desktop", + "ADR-046-provider-observability-otel", + "ADR-046-provider-runtime-azure-container-apps", + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR-046-provider-runtime-qemu-media", + "ADR-046-provider-shell-terminal", + "ADR-046-provider-state", + "ADR-046-provider-system-core", + "ADR-046-provider-system-minijail", + "ADR-046-provider-system-systemd", + "ADR-046-provider-transport-azure-relay", + "ADR-046-provider-transport-unix", + "ADR-046-provider-transport-vsock", + "ADR-046-provider-volume-local", + "ADR-046-provider-volume-virtiofs", + "ADR-046-reset-and-cutover", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-resources-zone-control", + "ADR-046-security-and-threat-model", + "ADR-046-streamline", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-validation-and-delivery", + "kind" : "spec", + "owner" : "ADR046-W7 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W7-closing", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-feasibility-and-spikes", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-activation-nixos", + "ADR-046-provider-audio-pipewire", + "ADR-046-provider-clipboard-wayland", + "ADR-046-provider-credential-entra", + "ADR-046-provider-credential-managed-identity", + "ADR-046-provider-credential-secret-service", + "ADR-046-provider-device-gpu", + "ADR-046-provider-device-security-key", + "ADR-046-provider-device-tpm", + "ADR-046-provider-device-usbip", + "ADR-046-provider-display-wayland", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-network-local", + "ADR-046-provider-notification-desktop", + "ADR-046-provider-observability-otel", + "ADR-046-provider-runtime-azure-container-apps", + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR-046-provider-runtime-qemu-media", + "ADR-046-provider-shell-terminal", + "ADR-046-provider-state", + "ADR-046-provider-system-core", + "ADR-046-provider-system-minijail", + "ADR-046-provider-system-systemd", + "ADR-046-provider-transport-azure-relay", + "ADR-046-provider-transport-unix", + "ADR-046-provider-transport-vsock", + "ADR-046-provider-volume-local", + "ADR-046-provider-volume-virtiofs", + "ADR-046-reset-and-cutover", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-resources-zone-control", + "ADR-046-security-and-threat-model", + "ADR-046-streamline", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-validation-and-delivery", + "topologicalRank" : 12, + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "docs/specs/ADR-046-zone-routing.md" + ], + "entryContracts" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR-046-zone-routing", + "kind" : "spec", + "owner" : "ADR046-W2 wave (ADR-046-validation-and-delivery §3.2)", + "parallelGroup" : "W2-composition-and-routing", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 6, + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-runtime-azure-container-apps/src/controller.rs`" + ], + "detailedDesign" : "Async `Guest` reconcile loop: `describe` → `validateSpec` → `plan` → `reconcile` → `observe` → `finalize`. Adoption before first `RuntimeEnsure`; operation/requeue truth remains in the core Operation ledger and no Provider state Volume is created. Credential lease acquire/revoke per call. The controller creates a same-Zone Provider-owned semantic sandbox-agent Endpoint (`ownerRef` remains the Guest lifecycle edge), resolves opaque transport carriage, and performs Noise KK enrollment for the ACA Provider service/session. `providerPhase` and `guestIdentityDigest` stay in `status.provider.details`; Endpoint readiness/generation/availability stay only in Endpoint status; no raw endpoint/path, cross-Zone ref, route cursor, transport handle, or authority appears in status. **ProviderDeployment creates both static Processes; ACA controller never instantiates its own Processes and never writes Provider status directly. All Processes run inside the gateway Guest. The managed sandbox remains a Guest in the owning Zone and is not a Zone. No Host Process, no Host Credential, no Host Azure HTTP socket. Long-running cloud ops return `progressing`/`requeue-at` immediately; never block watch loop.** Primary reuse disposition: `replace`. Preserved source-plan detail: REPLACE (old) + ADAPT (main types/traits).", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-container-apps" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-aca-001", + "kind" : "work-item", + "owner" : "ADR046-provider-001; runtime-aca owner", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-container-apps", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-container-apps", + "ADR046-provider-001" + ], + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "topologicalRank" : 11, + "validation" : "Controller conformance suite; adoption/ambiguity tests; Endpoint create/adopt/finalize and generation tests; deadline/cancellation/backpressure matrix; redaction coverage; **gateway Guest placement validation: assert no Process has `executionRef: Host/*`**; Process spec field schema tests (`spec.template`, canonical `sandbox`/`budget`/`networkUsage`/`endpoints`/`readiness`/`restartPolicy` fields, `mounts` with `required: true`, `providerRef: Provider/system-minijail`); ProviderDeployment creates both Processes (controller never self-spawns); no raw endpoint/path or ZoneLink status/cursor/authority in Guest or Endpoint status", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-runtime-azure-container-apps/src/deployment_service.rs`" + ], + "detailedDesign" : "`ProviderAgentProcess`-shaped binary; bounded dispatch (64 in-flight); bounded audit ring (1024 capacity); shutdown within 5 s; serves `d2b.aca.v3.deployment` service schema including `GuestHealth` (health probing folded in from former health worker). All ACA API calls go through the injected `AcaControl` port — no ambient network call, no SDK default chain. Long-running ops return `progressing`/`requeue-at` to the caller; no blocking on Azure API completion. Primary reuse disposition: `adapt`. Preserved source-plan detail: COPY/ADAPT (partial); exclude `aca_workload.rs`.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-container-apps" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-aca-002", + "kind" : "work-item", + "owner" : "ADR046-aca-001; deployment service owner", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-container-apps", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-container-apps", + "ADR046-aca-001" + ], + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "topologicalRank" : 12, + "validation" : "Service dispatch matrix; RBAC refusal tests; redaction tests; shutdown deadline tests", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/provider_effects/aca.rs` (shared `d2b-contracts` provider-effects module; no new crate; provider crate remains one package)" + ], + "detailedDesign" : "`AcaCredentialLeaseClient`, `AcaCredentialLease`, `AcaCredentialLeaseRequest`, and `AcaCredentialPurpose` live in the shared `d2b-contracts` provider-effects module. Adapt `CredentialLease` to v3 Credential resource opaque lease handle. `AcaCredentialPurpose` maps to `allowedOperations` check against `Credential.spec`. Lease expiry capped at call deadline. Cleanup job pattern retained verbatim. Primary reuse disposition: `adapt`. Preserved source-plan detail: RETAIN+ADAPT.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-container-apps" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-aca-003", + "kind" : "work-item", + "owner" : "ADR046-aca-001; credential integration owner", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-container-apps", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-container-apps", + "ADR046-aca-001" + ], + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "topologicalRank" : 12, + "validation" : "Mock credential client tests; lease cleanup timeout tests; token non-exposure assertion", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "ACA sandbox-agent Endpoint/session controller (§§7, 12); `Provider/transport-azure-relay` private configuration and carriage dossier (separate)" + ], + "detailedDesign" : "Split `AcaRelayTransportConfig`: relay-private namespace/connection/credential fields move behind `Provider/transport-azure-relay`; this Provider retains only `sandboxTransportAlias`. The ACA controller creates the Provider-owned semantic Endpoint (with the Guest lifecycle edge), resolves an opaque `OwnedTransport`, and owns KK enrollment, authenticated `d2b.aca.v3.sandbox-agent` service/session generation, deadlines, cancel, bounded queues, credits/backpressure, reconcile, adoption, and finalization. The ordinary managed sandbox is explicitly not a Zone; no cross-Zone ResourceRef or ZoneLink status/cursor/authority is accepted or emitted. Primary reuse disposition: `replace`. Preserved source-plan detail: REPLACE (both); ADAPT relay fields only into the carriage Provider's private config.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-container-apps" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-aca-004", + "kind" : "work-item", + "owner" : "ADR046-session-001; ACA Endpoint/session and transport-capability integration owner", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-container-apps", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-container-apps", + "ADR046-session-001" + ], + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "topologicalRank" : 11, + "validation" : "Endpoint ownership/resolution tests; relay unavailability tests; KK re-enrollment after sandbox/controller restart; deadline/cancel/credit-backpressure tests; schema/status tests reject ZoneLink refs, phase, cursors, and authority", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-runtime-azure-container-apps/src/types.rs`" + ], + "detailedDesign" : "Replace `RealmId`/`WorkloadId` with v3 owning-`Zone`/`Guest` UID types. `AcaResourceBinding` keys the adoption query but never names a child Zone. The provider declares **no** Provider state Volume: bounded, non-secret sandbox binding/adoption metadata lives in `Guest.status` (latest bounded observed handle digests) and in-flight operation/requeue truth lives in the core Operation ledger (D087). Neither Process mounts a state Volume; there is no `sandbox-state`/`service-state` Volume, no `User/d2b-aca-controller`/`User/d2b-aca-deployment-service` state-layout principal, and no empty identity-only Volume. On restart the controller re-derives observed binding from `Guest.status`, the core Operation ledger, and an external `find_sandboxes` query, ensures the Provider-owned Endpoint, resolves fresh transport carriage, and authenticates a fresh Provider session, treating all status as observation and never authority. Host never holds cloud binding, admission, PSK, operation, Endpoint, or session state. Primary reuse disposition: `adapt`. Preserved source-plan detail: RETAIN+ADAPT.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-container-apps" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-aca-005", + "kind" : "work-item", + "owner" : "ADR046-aca-001; state/migration owner", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-container-apps", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-container-apps", + "ADR046-aca-001" + ], + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "topologicalRank" : 12, + "validation" : "Controller/service declare empty `stateNamespaces`; no `sandbox-state`/`service-state` Volume created; neither Process mounts a state Volume; `Guest.status` binding/adoption fields and standard Endpoint status are bounded, non-secret, and carry no credential/endpoint/poll-URL/ZoneLink cursor or authority bytes; restart re-derivation from status/core ledger/external `find_sandboxes`, Endpoint ensure, and fresh KK session without a Volume; core Operation ledger adapter integration test", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/` (generated Guest resource options); `packages/d2b-provider-runtime-azure-container-apps/src/{audit,metrics}.rs`" + ], + "detailedDesign" : "Eval-time assertions for ACA-specific invariants (§15.7), including rejection of ZoneLink/child-Zone fields for an ordinary sandbox and exact same-Zone sandbox-agent Endpoint template ownership. Closed OTEL label set (§13.4). Audit event schema (§13.3). Tracing target constant `d2b_provider_runtime_azure_container_apps::credential_lease_cleanup` retained. Primary reuse disposition: `replace`. Preserved source-plan detail: REPLACE (Nix emitter) + ADAPT (metric/audit shapes).", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-container-apps" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-aca-006", + "kind" : "work-item", + "owner" : "ADR046-aca-001; Nix/telemetry owner", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-container-apps", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-container-apps", + "ADR046-aca-001" + ], + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "topologicalRank" : 12, + "validation" : "Label cardinality policy test; audit commit-before-complete test; Nix assertion eval tests; generated resource scan proves no ACA sandbox ZoneLink is emitted", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/` (gateway Guest declaration, Process template wiring, Credential scope assertion); eval-time validation module" + ], + "detailedDesign" : "Nix eval-time assertions for: (a) `gatewayExecutionRef` resolves to a `Guest` resource, not `Host/*`; (b) Credential `scope.executionRef` matches `gatewayExecutionRef`; (c) all Process templates emitted for this Provider have `executionRef` equal to `gatewayExecutionRef`; (d) `sandboxTransportAlias` resolves the signed carriage capability; and (e) an ordinary ACA sandbox cannot declare/reference ZoneLink, child-Zone, route-cursor, or ZoneLink-authority fields. The controller, not Nix, creates each same-Zone Provider-owned sandbox-agent Endpoint with the Guest as lifecycle `ownerRef`. No `User` resource or `users.users.*` declarations required — component principals are framework-assigned and not OS accounts. Gateway Guest NixOS closure includes only the two ACA component binaries (§15.6). Assertion error messages name the offending resource and the required `gatewayExecutionRef`; Zone-shaped ACA config fails `aca-sandbox-is-not-zone`.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-container-apps" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-aca-007", + "kind" : "work-item", + "owner" : "ADR046-aca-001; Nix/gateway wiring owner", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-container-apps", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-container-apps", + "ADR046-aca-001" + ], + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "topologicalRank" : 12, + "validation" : "Nix eval assertion tests (wrong `executionRef` → assertion fires; correct setup → passes; ACA ZoneLink/child-Zone fields → `aca-sandbox-is-not-zone`); Endpoint template ownership/shape and §15.7 assertion coverage tests", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-host/src/bin/d2b-activation-helper.rs" + ], + "detailedDesign" : "Replace the helper CLI flag interface with structured JSON input and JSON output, accept bounded systemArtifactId and activationMode, resolve store path internally, emit bounded outcome code, write no resource metadata, and preserve the no-bash-fallback invariant. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt in place.", + "entryContracts" : [ + "ADR-046-provider-activation-nixos" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-activation-001", + "kind" : "work-item", + "owner" : "Provider/activation-nixos runner owner; reused helper owner in d2b-host", + "parallelGroup" : "wi:ADR-046-provider-activation-nixos", + "prerequisites" : [ + "ADR-046-provider-activation-nixos" + ], + "specId" : "ADR-046-provider-activation-nixos", + "topologicalRank" : 11, + "validation" : "Unit tests for JSON protocol, bounded outcomes, no resource metadata writes, and no Command::new bash fallback.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "docs/reference/schemas/v3/activation-nixos.d2bus.org.NixosGeneration.json and packages/d2b-contracts/src/activation_nixos.rs" + ], + "detailedDesign" : "Define JSON schema and Rust DTOs for activation-nixos.d2bus.org.NixosGeneration, with systemArtifactId as the only artifact reference, no store path fields, activationDetail as a closed status.resource enum absent from spec, and phase constrained to the common framework enum values. Primary reuse disposition: `create`. Preserved source-plan detail: net-new schema and DTOs.", + "entryContracts" : [ + "ADR-046-provider-activation-nixos" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-activation-002", + "kind" : "work-item", + "owner" : "ADR-046-resource-object-model and ADR-046-core-controllers; d2b-contracts activation-nixos owner", + "parallelGroup" : "wi:ADR-046-provider-activation-nixos", + "prerequisites" : [ + "ADR-046-provider-activation-nixos" + ], + "specId" : "ADR-046-provider-activation-nixos", + "topologicalRank" : 11, + "validation" : "Schema golden vectors, serde unknown-field rejection, phase enum tests, activationDetail-not-in-spec test, and no-store-path-in-spec-or-status test.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-activation-nixos/src/controller/" + ], + "detailedDesign" : "Implement the reconcile loop for activation-nixos.d2bus.org.NixosGeneration: validate executionRef, systemArtifactId, and priorGenerationRef; dispatch one activation-runner EphemeralProcess with canonical startRoot=true shape; observe runner status; mark superseded generations; prune by retainedGenerations through the finalizer protocol; emit §12.3 metrics with fixed `mode`/`outcome` semantics and no Zone or resource-name-derived labels; never perform direct store-path operations, nix-collect-garbage, explicit VolumeGcRequest, raw argv composition, or store path writes to resources. Primary reuse disposition: `replace`. Preserved source-plan detail: replace top-level imperative activation flow with resource controller logic.", + "entryContracts" : [ + "ADR-046-provider-activation-nixos" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-activation-003", + "kind" : "work-item", + "owner" : "ADR046-activation-002; activation-nixos controller owner", + "parallelGroup" : "wi:ADR-046-provider-activation-nixos", + "prerequisites" : [ + "ADR-046-provider-activation-nixos", + "ADR046-activation-002" + ], + "specId" : "ADR-046-provider-activation-nixos", + "topologicalRank" : 12, + "validation" : "Controller tests for retention, finalizer sequence, no TTL retention, no direct store ops, no store path in status, deleted event-only removal, runner shape, and a structural metric descriptor assertion that `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys are absent and a generation/Zone-name canary never enters labels.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-activation-nixos/src/runner/" + ], + "detailedDesign" : "Implement target-local activation-runner worker that executes on NixosGeneration.spec.executionRef for Host and Guest targets using the same contract, reads private artifact-catalog.json through the integrity channel, resolves systemArtifactId to a store path in memory only, invokes d2b-activation-helper through structured JSON, executes target-local switch-to-configuration through typed helper dispatch with no raw exec or SSH, emits structured outcome JSON, and never outputs store paths. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new runner invoking adapted helper.", + "entryContracts" : [ + "ADR-046-provider-activation-nixos" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-activation-004", + "kind" : "work-item", + "owner" : "ADR046-activation-003; activation-runner owner", + "parallelGroup" : "wi:ADR-046-provider-activation-nixos", + "prerequisites" : [ + "ADR-046-provider-activation-nixos", + "ADR046-activation-003" + ], + "specId" : "ADR-046-provider-activation-nixos", + "topologicalRank" : 13, + "validation" : "Runner tests for artifact lookup, JSON helper invocation, Host and Guest target parity, no raw argv, no SSH, no store path in output, and terminal nonzero handling.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b/src/activation.rs" + ], + "detailedDesign" : "Implement d2b activation build, switch, boot, test, rollback, adopt, generations, gc, and migrate subcommands, projecting ADR-046 CLI and Operations behavior and ensuring no store path, digest, or artifact catalog field appears in JSON or human output. Primary reuse disposition: `replace`. Preserved source-plan detail: replace with grouped CLI projection.", + "entryContracts" : [ + "ADR-046-provider-activation-nixos" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-activation-005", + "kind" : "work-item", + "owner" : "ADR-046-cli-and-operations; activation CLI owner", + "parallelGroup" : "wi:ADR-046-provider-activation-nixos", + "prerequisites" : [ + "ADR-046-provider-activation-nixos" + ], + "specId" : "ADR-046-provider-activation-nixos", + "topologicalRank" : 11, + "validation" : "CLI integration tests for subcommand parsing, authorization, resource creation/listing, rollback priorGenerationRef, gc ownership release, and output redaction.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "nixos-modules/providers/activation-nixos.nix" + ], + "detailedDesign" : "Emit Provider spec and activation-nixos.d2bus.org.NixosGeneration resources per target, flow retainedGenerations only through Provider.spec.config.retainedGenerations, reference systems by systemArtifactId only, omit store paths from all emitted resources, and avoid dedicated state-layout User or ComponentPrincipal because ProviderStateSet is empty. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new resource emitter adapted from existing Nix activation inputs.", + "entryContracts" : [ + "ADR-046-provider-activation-nixos" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-activation-006", + "kind" : "work-item", + "owner" : "ADR-046-nix-configuration; activation-nixos Nix owner", + "parallelGroup" : "wi:ADR-046-provider-activation-nixos", + "prerequisites" : [ + "ADR-046-provider-activation-nixos" + ], + "specId" : "ADR-046-provider-activation-nixos", + "topologicalRank" : 11, + "validation" : "Nix eval tests for Provider config, NixosGeneration shape, retainedGenerations source, no systemStorePath in bundle, no state Volume or state-layout principal, and artifact ID resolution.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b/src/lib.rs" + ], + "detailedDesign" : "Remove the legacy top-level activation command functions and their dispatcher registrations after the grouped d2b activation namespace passes integration tests. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete legacy top-level commands.", + "entryContracts" : [ + "ADR-046-provider-activation-nixos" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-activation-007", + "kind" : "work-item", + "owner" : "ADR046-activation-005; d2b CLI dispatcher owner", + "parallelGroup" : "wi:ADR-046-provider-activation-nixos", + "prerequisites" : [ + "ADR-046-provider-activation-nixos", + "ADR046-activation-005" + ], + "specId" : "ADR-046-provider-activation-nixos", + "topologicalRank" : 12, + "validation" : "CLI integration matrix for d2b activation passes; grep or contract test confirms old cmd_* symbols and dispatcher registrations are absent.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/proto/d2b-resource-v3.proto`, `packages/d2b-resource-api/src/service.rs`, `client.rs`" + ], + "detailedDesign" : "Async methods, contexts, preconditions, limits, errors, status/finalizer separation, batch API Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resource-api-and-authorization" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-api-001", + "kind" : "work-item", + "owner" : "W0; resource API integrator", + "parallelGroup" : "wi:ADR-046-resource-api-and-authorization", + "prerequisites" : [ + "ADR-046-resource-api-and-authorization" + ], + "specId" : "ADR-046-resource-api-and-authorization", + "topologicalRank" : 5, + "validation" : "Protocol vectors; malformed/oversize/conflict/status-owner tests", + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-api/src/authz.rs`, `packages/d2b-core-controller/src/rbac.rs`" + ], + "detailedDesign" : "Role/RoleBinding schemas/evaluator/cache/revision invalidation, canonical resource/session verb enums including ZoneLink-scoped `relay`, ComponentSession subject mapping, parent Zone access, and independent per-hop relay plus target-verb admission", + "entryContracts" : [ + "ADR-046-resource-api-and-authorization" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-api-002", + "kind" : "work-item", + "owner" : "ADR046-api-001; authorization integrator", + "parallelGroup" : "wi:ADR-046-resource-api-and-authorization", + "prerequisites" : [ + "ADR-046-resource-api-and-authorization", + "ADR046-api-001" + ], + "specId" : "ADR-046-resource-api-and-authorization", + "topologicalRank" : 6, + "validation" : "Decision matrix/property tests; closed-enum and relay-origin/scope rejection; relay-missing and target-verb-missing fail-closed vectors; revocation/cache/outage/parent-child tests", + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/src/audio_policy.rs`; re-exported from crate root" + ], + "detailedDesign" : "`LevelPercent`, `AudioGrant`, `AudioPolicyState`, `parse_audio_state`, `to_v2_bytes`, `AudioPolicyError` copy unchanged. `AudioPolicyState` is the canonical in-memory representation of `AudioBinding.spec.grants`. `parse_audio_state`/`to_v2_bytes` are used only once during first-activation migration from a prior v1/v2 on-disk file; there is no ongoing state file in v3. Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: `copy-unchanged` (no daemon imports; pure DTO library).", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-001", + "kind" : "work-item", + "owner" : "No prerequisites; `d2b-provider-audio-pipewire` crate", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 11, + "validation" : "`tests/audio_policy.rs`: all existing tests from `d2b-core/tests/audio_policy.rs` plus AudioBinding spec serialization tests", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/src/argv.rs` (component template renderer)" + ], + "detailedDesign" : "`generate_audio_argv` remains the canonical argv builder for the `vhost-user-sound-worker` component template. The resulting argv/env/executableRef are sealed into the LaunchTicket. The per-Guest binary copy path enforcement remains via the LaunchTicket verifier. The live Process resource spec contains no argv or executableRef. The `--socket` argument is removed; the vhost-user service identity is `Endpoint/corp-vm-audio-vhost-user`, while the backing locator is resolved into the LaunchTicket under authorization. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — argv builder retained; becomes a signed component-template projection, not a live Process spec field.", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-002", + "kind" : "work-item", + "owner" : "Depends on `ADR046-audio-001`; Process Provider template schema", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-audio-001" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 12, + "validation" : "`tests/argv.rs`: rejection matrix (Nix store path, symlink, cross-guest copy, empty name); no-socket-in-argv assertion; no-argv-in-process-spec assertion", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/src/mediator/enforcement.rs`" + ], + "detailedDesign" : "Owner AudioService only: speaker `SetGrant` maps `\"off\"` to `pw_node_set_param(SPA_PARAM_Props, mute=true, target.object=-1)` on the worker's node and `\"on\"` to `mute=false`; `SetLevel` maps to a bounded volume. Microphone `\"on\"` is applied only after the owner authority grants its single capture slot; queued/blocked consumers remain muted, and release mutes/disconnects before handoff. `captureAlias` resolves privately through the registry. A projection AudioService routes the operation to the remote owner over its import stream and is denied any local mediator/PipeWire open. `FakeAudioMediator` is the hermetic test double. No state file, wpctl, EphemeralProcess, or node ID in any external surface. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — enforcement logic becomes a libpipewire API implementation behind the `SetGrant`/`SetLevel` ComponentSession service.", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-004", + "kind" : "work-item", + "owner" : "Depends on `ADR046-audio-001`; ComponentSession service contract; libpipewire 1.x API", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-audio-001" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 12, + "validation" : "`tests/mediator.rs` and `tests/enforcement.rs`: owner-Service SetGrant/SetLevel round-trip; speaker mixing; microphone queued consumers remain muted; release/lease-expiry mute-before-handoff and no-overlap proof; projection routing with fake streams; projection-PipeWire-open denial; no-node-id-in-bus-message; ProviderSessionUnavailable; captureAlias registry resolution", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/src/{resource_type,admission,provider_extension}.rs` (strict implementation extensions and binding only; common base lives under ADR046-provider-004)" + ], + "detailedDesign" : "Bind the shared D098 `audio.d2bus.org.AudioService` and `audio.d2bus.org.AudioBinding` base schema versions/fingerprints from ADR046-provider-004 and define only strict audio-pipewire Provider extensions/admission. AudioService validates immutable `serviceRole`, same-Zone local Endpoint refs, owner-only D097 AuthorityDescriptor, projection-only `ownerRef: ResourceImport/`, Core-only projection creation, and projection `spec.provider` rejection. Initial-v3 owner admission requires speaker `multiplexed` and microphone `exclusive` and rejects multiplexed capture plus every consent/approval/priority placeholder. Owner Service status carries only bounded aggregate microphone active/request/Zone counts; Binding status carries only its own arbitration state. AudioBinding validates Guest ownership, required immutable same-Zone `serviceRef`, grants/levels/users, and forbids authority/export/projection semantics. Semantic authority/import/attachment observations stay under `status.resource`; implementation observations stay under `status.provider`. PipeWire fields are rejected from base spec/status. Register no provider-qualified or AudioState identifier and no serde/schema alias. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` provider-neutral closed enums; remove `AudioProviderKind` from base status; `ADR-only` for schema/admission.", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-005", + "kind" : "work-item", + "owner" : "ADR046-provider-004 common audio Service/Binding base; Core resource-api foundation; `d2b-provider-audio-pipewire` crate", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-provider-004" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 11, + "validation" : "`tests/resource_type.rs`: consume the ADR046-provider-004 common fixtures/fingerprints; canonical minimal base without `spec.provider`; neutral qualified-name registration; both schema/status round-trips including bounded aggregate owner queue status and per-Binding arbitration state; clean-break rejection of provider-qualified names, every AudioState spelling, and all aliases; fake alternate-provider base conformance; strict base/provider unknown-field matrices; projection `spec.provider` rejection; D088 `status.resource`/`status.provider` placement; PipeWire fields only in strict provider envelopes/config; Service role/AuthorityDescriptor/ownerRef/Endpoint-locality rules; initial-v3 exclusive-mic/mixed-speaker schema and consent/approval/priority/concurrent-capture rejection; Core-only projection admission; AudioBinding required same-Zone serviceRef and Guest owner; immutable refs; out-of-range levels/users; explicit tests that AudioBinding cannot be exported or projected", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/src/controller/audio_service.rs`; `src/controller/audio_binding.rs`" + ], + "detailedDesign" : "One controller binary registers deterministic handlers for the two neutral ResourceTypes, constrained to immutable `spec.providerRef: Provider/audio-pipewire`. Service handler watches AudioService, its ResourceImport owner and local Endpoints; owner semantics claim/revalidate D097, local mediator, and aggregate exclusive-mic queue state, while projection semantics bind only encrypted import streams and deny PipeWire. It cannot create/delete projection Service. Binding handler watches AudioBinding, same-Zone serviceRef, Guest/User, owned Process, and private Endpoints; creates the vhost-user worker, GuestAudioAgents, and private Endpoints, then calls the resolved Service and guest agents. It maps `Applied|Queued|MicQueueFull` into the closed conditions/status/phase, and release/delete/revocation cancels queue state before child teardown. A Service event enqueues serviceRef-indexed Bindings. A resource selecting another conforming Provider is ignored and cannot be status/finalizer-mutated. Neither handler uses broker/pidfd/EphemeralProcess/Volume/User operations or direct filesystem access. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — dispatch logic is the reference for step ordering only.", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-006", + "kind" : "work-item", + "owner" : "Depends on `ADR046-audio-001` through `ADR046-audio-005`; core ResourceImport controller; system Process Provider; AudioMediator (`ADR046-audio-007`); GuestAudioAgent (`ADR046-audio-011`); no Provider state Volume under D087", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-audio-001", + "ADR046-audio-005", + "ADR046-audio-007", + "ADR046-audio-011" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 14, + "validation" : "Fast hermetic `tests/audio_service_controller.rs`: neutral type/provider selection, foreign-provider ignore/deny, owner authority, bounded aggregate microphone status, projection ownerRef/import chain, core-only create/delete, projection no-PipeWire-open, revocation queue cancellation, and D091 propagation. `tests/audio_binding_controller.rs`: neutral type/provider selection, required same-Zone serviceRef, owner/projection dispatch, child Process/private Endpoint state machine, `Applied|Queued|MicQueueFull` status mapping, off/delete/revocation cancellation, grant changes, absence/failures/deletion. Conformance asserts no AudioBinding export/projection, no broker/pidfd/EphemeralProcess/Volume/User ops. ProviderDeployment integration remains fake-only and validates empty ProviderStateSet.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/src/mediator/mod.rs`; `src/bin/audio_pipewire_mediator.rs`" + ], + "detailedDesign" : "Owner AudioService implementation only. Long-lived user-session Process maintains per-AudioBinding nodes under the single owner backing, receives the pre-opened local PipeWire portal FD, and exposes `SetGrant`/`SetLevel` through `Endpoint/audio-pipewire-authority`. It enforces the authority arbiter's single microphone slot and mute-before-handoff result while speaker nodes remain mixed. Projection Services never start/call a local mediator and cannot receive its FD. No EphemeralProcess, wpctl, remote Ref, or node identity in external surfaces.", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-007", + "kind" : "work-item", + "owner" : "Depends on `ADR046-audio-004`; ComponentSession service (ADR-046-componentsession-and-bus); libpipewire 1.x", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-audio-004" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 13, + "validation" : "`tests/mediator.rs`: owner-Service FD handoff and calls; captureAlias; node-id sealing; session-unavailable; concurrent speaker Guest isolation; exclusive microphone no-overlap and mute-before-handoff; teardown; projection Service cannot resolve mediator Endpoint or portal attachment", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/components/audio/v3-resource.nix`; `nixos-modules/components/audio/host-config.nix`; `nixos-modules/components/audio/guest-config.nix`" + ], + "detailedDesign" : "`v3-resource.nix` emits only `audio.d2bus.org.AudioService` and `audio.d2bus.org.AudioBinding`, selected by `Provider/audio-pipewire`, with required same-Zone serviceRef. For projection identity, ResourceExport emits `resourceRef` plus canonical `serviceType`, `projectionSchemaFingerprint`, and `factoryFingerprint`; its Service retains the Endpoint. ResourceImport emits the matching three `expected*` fields. Projection Services are never authored: core materializes them from ResourceImport. Eval rejects every obsolete Export/Import field in the explicit rejection matrix, provider-qualified ResourceTypes, every AudioState spelling, aliases, PipeWire fields in neutral base spec/status, AudioBinding export/projection, non-exclusive microphone or consent/approval/priority/concurrent-capture fields, Service role/ownerRef/authority mismatches, cross-Zone Endpoint/service refs, and duplicate owner authority. Existing captureAlias stays in Provider config; guestUsers/group injection, runtime-audio derivation, host stream rules, and guest stack remain.", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-008", + "kind" : "work-item", + "owner" : "Depends on `ADR046-audio-005`; Nix resource compilation framework; `ADR-046-nix-configuration`", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-audio-005" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 12, + "validation" : "`tests/unit/nix/cases/audio-v3-resource.nix`: exact neutral type names; provider-qualified/AudioState/alias rejection; strict provider-field placement; owner Service and Binding round-trip; same-Zone serviceRef; exact canonical Export/Import Service type and both fingerprints; obsolete Export/Import field rejection; Export Endpoint-field rejection; projection core-only ownerRef chain; AudioBinding export/projection rejection; exclusive-mic/mixed-speaker and consent/approval/priority/concurrent-capture rejection; authority uniqueness; Endpoint locality; plus existing grants/users/captureAlias/deprecation/no-wpctl/no-audioFrontend assertions", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/tests/minijail_contract.rs` (provider-local); retain cross-bundle source greps in `d2b-contract-tests`" + ], + "detailedDesign" : "Retain worker sandbox assertions and add role-sensitive attachment checks: an owner-Service worker may receive only the local AudioMediator attachment; a projection-Service worker receives only its same-Zone route Endpoint and can never receive a PipeWire FD. Binding-owned private Endpoint ownership/producerRef is explicit. All Service/Binding/Process/Endpoint serialized forms remain locator-free.", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-009", + "kind" : "work-item", + "owner" : "Depends on `ADR046-audio-005`; `d2b-contract-tests` crate", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-audio-005" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 12, + "validation" : "`cargo test -p d2b-provider-audio-pipewire -- minijail` must pass; existing cross-bundle tests must continue to pass", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/src/telemetry.rs`" + ], + "detailedDesign" : "Emit closed-label Service and Binding metrics plus post-commit audit. Service events distinguish only `owner|projection` and closed outcomes; microphone arbitration emits closed transition/outcome plus bounded aggregate counts only. Metrics expose active count `0|1`, aggregate queue depth, and closed arbitration state without Zone/Binding/handle/position labels. Events omit authority keys, import/export keys, remote identity, stream/session ids, endpoints, client timestamps, and queue entries. Enforcement metrics cover owner-local and projection-routed calls without exposing route identity. ProcessEffect audit remains Process Provider-owned.", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-010", + "kind" : "work-item", + "owner" : "Depends on `ADR046-audio-006`, `ADR046-audio-007`, `ADR046-audio-011`; `d2b-telemetry` lightweight emitter", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-audio-006", + "ADR046-audio-007", + "ADR046-audio-011" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 15, + "validation" : "`tests/audio_telemetry.rs`: Service/Binding event separation, microphone aggregate metrics and transition audit, redaction of Zone/Binding/handle/position/timestamp, post-commit ordering, label cardinality, forbidden authority/import/stream/path fields, no ProcessEffect duplication", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/src/guest_agent/mod.rs`; `src/guest_agent/enforcement.rs`; `src/bin/audio_pipewire_guest_agent.rs`" + ], + "detailedDesign" : "Long-lived user-domain Process running in the Guest under the guest workload user's UID. One Process resource per entry in `AudioBinding.spec.guestUsers`; each named by opaque UID digest (`ag-`) and carrying label `audio-pipewire.d2bus.org/role: guest-audio-agent`. `userRef` is the corresponding `User/` Zone resource. Opens a PipeWire connection in the Guest's compositor session (same-UID, natural access). Exposes a typed `AudioSet` ComponentSession service through an owned `Endpoint/ag--audio-set` (vsock transport, Guest→Zone d2b-bus). `AudioSet(mic, speaker, speakerLevel, micGain)` applies changes via libpipewire API (`pw_node_set_param` with `SPA_PARAM_Props`, `pw_stream_set_control`) on the guest virtio-snd PipeWire node. No wpctl binary, no command path, no EphemeralProcess. Controller calls ALL active GuestAudioAgent instances in parallel for each grant change and aggregates failures. `FakeGuestAudioAgent` is a test double behind `#[cfg(test)]`. Primary reuse disposition: `adapt`. Preserved source-plan detail: `ADR-only` (new component; supersedes guestd wpctl dispatch path).", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-011", + "kind" : "work-item", + "owner" : "Depends on `ADR046-audio-004`; ComponentSession service contract; libpipewire 1.x; system-systemd Process Provider for guest domain", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-audio-004" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 13, + "validation" : "`tests/guest_agent.rs`: AudioSet service call → libpipewire apply; mute/route/level; session-unavailable path; reconnect state restore; no wpctl binary; no command path; N-agent creation (one per guestUser); parallel call and aggregated failure", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/src/share_adapter.rs`" + ], + "detailedDesign" : "Implement signed `Provider/audio-pipewire` adapters only for canonical `serviceType: audio.d2bus.org.AudioService` when `spec.providerRef` selects this Provider. ResourceExport carries that `serviceType` plus the signed `projectionSchemaFingerprint` and semantic `factoryFingerprint`; ResourceImport carries the matching `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`. Export adapter admits only the owner Service. Its local authority Endpoint remains Service-owned and is never an Export field. Core creates/deletes the projection AudioService with `ownerRef: ResourceImport/`, `providerRef`, and semantic base/import fields but no `spec.provider`; routing derives from the signed local descriptor and ResourceImport record. The semantic factory fingerprint binds factory metadata plus projection-protocol version, never Provider/adapter identity, which the signed descriptor authenticates separately. The import adapter reconciles its semantic route and never creates AudioBinding or opens PipeWire. Per-Guest AudioBindings are ordinary consumer resources with same-Zone serviceRef. No provider-qualified type alias, FD/path/socket/remote Ref crosses a Zone. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (implement the signed audio export/import adapter).", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-012", + "kind" : "work-item", + "owner" : "ADR046-zone-control-019, ADR046-zone-control-020; audio Provider owner", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-zone-control-019", + "ADR046-zone-control-020" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 13, + "validation" : "Fast hermetic `tests/share_adapter.rs`: exact neutral AudioService `serviceType`; exact projection-schema/factory fingerprint match; explicit rejection of obsolete `endpointRef`, `exportedType`, `baseSchemaFingerprint`, `expectedType`, `expectedBaseSchemaFingerprint`, and `projectionType`; reject every Export Endpoint field, provider-qualified alias, and AudioBinding export/projection; accept owner AudioService export; Core-only projection creation/deletion; exact ResourceImport -> projection AudioService ownerRef chain with no `spec.provider`; semantic factory fingerprint unchanged by Provider/adapter identity mutation while signed identity authentication remains exact; projection never opens PipeWire; reconnect/revocation/D091 propagation with fake streams. Only `integration/real_stream.rs` exercises a real encrypted named stream.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/src/authority.rs` (speaker mixer + mic arbiter); `AuthorityDescriptor` on owner `AudioService`" + ], + "detailedDesign" : "Exactly one owner AudioService holds the real PipeWire connection and D097 AuthorityDescriptor. Projection Services and AudioBindings cannot carry it. Speaker streams remain multiplexed/mixed. Microphone capture has exactly one slot across owner and importing Zones. The arbiter keys requests by authenticated Zone plus route-scoped opaque consumer handle, permits one pending entry per handle, bounds pending entries to 16 per Zone and 64 total, uses owner-sequenced FIFO per Zone and round-robin across non-empty Zones, and ignores client timestamps/priority. Its 30-second active lease renews only while no other Zone waits; contended expiry mutes/disconnects, dequeues the next Zone, and atomically requeues a still-requesting old holder at its Zone FIFO tail. Off/delete/revoke/disconnect cancel idempotently. Queue entries are memory-only and restart rebuild fails closed with capture muted. Multiplexed capture and consent/approval/priority/concurrent-capture surfaces are rejected; a future spec must define a concrete consent authorization ResourceType and resource-API verb before concurrent capture exists. Core rejects duplicate owner Services before open and adopts by ownerProof. No new ProcessRole, broker path, or state file. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — the host controller becomes the single authority service; no daemon `Mutex`/state-file wrapping.", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-013", + "kind" : "work-item", + "owner" : "Depends on `ADR046-audio-001`, `ADR046-audio-004`, `ADR046-zone-control-019`; audio Provider owner", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-audio-001", + "ADR046-audio-004", + "ADR046-zone-control-019" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 13, + "validation" : "Fast hermetic `tests/authority.rs`: AuthorityDescriptor accepted only on owner AudioService; Binding/projection rejection; duplicate conflict; multiplexed speaker mix/quota; one active mic across local/imported Zones; per-Zone FIFO and cross-Zone round-robin; one-entry-per-handle, per-Zone/total bounds and `MicQueueFull`; idempotent cancellation; contended 30-second lease; mute-before-handoff/no overlap; restart-muted rebuild; multiplexed-capture and consent/approval/priority/concurrent-verb rejection; ownerProof adoption; D091 drain/recycle with fake clock/FakeHostController", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-audio-pipewire/src/streams.rs`" + ], + "detailedDesign" : "Per-import audio frames flow only over bounded encrypted named streams: one stream with two `StreamChannel`s for playback/capture split direction and a single `StreamAuthz` (a consumer never opens two authz contexts to split direction), credit-based backpressure (a sender spends only receiver-granted credit), per-import session generation, cancel, and deadline. `StreamKind::AudioPlayback`/`AudioCapture` require `Capability::AudioPlayback`/`AudioCapture`. Playback streams may run concurrently; the owner activates capture frames for only its single granted opaque consumer handle, and revocation/disconnect cancels that Zone's active/pending requests before route teardown. No PipeWire FD/socket crosses a Zone; intermediaries see ciphertext. Guest audio calls (`audio_set`/`audio_status`) are issued to all active guests concurrently and results aggregated. Volume/gain (`LevelPercent`), queue identity/position, and node identity are redacted from audit/OTEL/logs. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — audio frames ride the existing ComponentSession named-stream credit machinery.", + "entryContracts" : [ + "ADR-046-provider-audio-pipewire" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audio-014", + "kind" : "work-item", + "owner" : "Depends on `ADR046-audio-013`, `ADR046-zone-control-019`; audio Provider owner", + "parallelGroup" : "wi:ADR-046-provider-audio-pipewire", + "prerequisites" : [ + "ADR-046-provider-audio-pipewire", + "ADR046-audio-013", + "ADR046-zone-control-019" + ], + "specId" : "ADR-046-provider-audio-pipewire", + "topologicalRank" : 14, + "validation" : "Fast hermetic `tests/streams.rs`: projection-Service ownerRef/import binding, split-direction single-authz stream, credits, generation isolation, cancel/deadline, concurrent playback, one active capture across imports, route loss cancels that Zone's active/pending capture, ciphertext-only intermediary, redaction. Only `integration/real_stream.rs` runs the slower real encrypted stream.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-audit/src/{hash_chain.rs,segment.rs,rate_limit.rs,record_types.rs,sink.rs,export.rs}`" + ], + "detailedDesign" : "`d2b-audit` provides: typed record structs per class; canonical serialization with `zone` replacing `realm: RealmPath`; SHA-256 hash chain (extracted from `daemon_audit.rs`); segment writer (O_APPEND CLOEXEC, 64 MiB / UTC-midnight rotation); 30-day compaction (adapts `prune_old` from `JsonlGatewayAudit`); `AuditWriteClass::{Privileged,Standard,BestEffort}` (extends current `{Privileged,Unprivileged}`); rate-limit with privileged-never-dropped invariant; export iterator with inline hash-break reporting. `AuditStreamKind` re-versioned: `Daemon→Zone`, `Gateway→ZoneLink`, `RemoteNode→RemoteZone`. `AuditChainRecord` re-versioned: `{zone: String}` replaces `{realm: RealmPath, node: NodeId}`. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract unchanged: `AuditHash`, `AuditChainLink` from `d2b-realm-core/src/audit.rs`; copy hash-chain append algorithm from `daemon_audit.rs`; copy `AuditWriteClass`/rate-limit/rotation/prune from broker `audit.rs`; adapt `JsonlGatewayAudit` segment writer; adapt `OpAuditRecord` to `BrokerEffect` record class.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audit-001", + "kind" : "work-item", + "owner" : "W0/W1a; audit crate owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 10, + "validation" : "`audit_record_hash_chain`, `audit_record_schema` (no `realm`/`node` fields), `audit_segment_rotation`, `audit_rate_limit_privileged_never_dropped`, `audit_unavailable_blocks_privileged`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-store-redb/src/audit.rs`, `packages/d2b-core-controller/src/authz_audit.rs`" + ], + "detailedDesign" : "`ResourceMutation` records emitted by the store actor inside the write transaction before commit returns. The audit sink must durably fsync the audit record before returning the commit success (privileged durability class). `RBACChange` emitted by the authz handler in the same write transaction. `subject_digest` = SHA-256 of normalized canonical subject string from v3 `AuthenticatedSubjectContext` (ADR-046-componentsession-and-bus). Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `OpAuditRecord` structural pattern for `ResourceMutation` / `RBACChange` record classes; adapt `PrincipalId` → `subject_digest` derivation.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audit-002", + "kind" : "work-item", + "owner" : "ADR046-audit-001 + ADR046-store-001; store/authz owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-audit-001", + "ADR046-store-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "Integration test: 100 mutations → verify hash-chained audit records with `zone` field, no `realm` field", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-session/src/audit.rs`, `packages/d2b-bus/src/audit.rs`" + ], + "detailedDesign" : "`SessionConnect` records emitted at handshake completion. `GatewayAuditKind::DisplaySessionOpenAdmitted/Denied` → `event=\"connect\"`, `authz_decision=\"allowed/denied\"`. `GatewayAuditKind::DisplaySessionRunning` → informational `ProcessEffect`. `GatewayAuditKind::DisplaySessionClosed` → `event=\"close\"`. `transport_class=zone_link` covers what the current `AuditStreamKind::Gateway` stream recorded for gateway-backed realm sessions. `RouteAdmission` records emitted at bus route resolution for denied routes. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `GatewayAudit` trait pattern for `SessionConnect` and `RouteAdmission` record classes; `NoopGatewayAudit` pattern reused for test sinks.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audit-003", + "kind" : "work-item", + "owner" : "ADR046-audit-001 + ADR046-session-001 + ADR046-bus-001; session/bus owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-audit-001", + "ADR046-bus-001", + "ADR046-session-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "Session connect/close/auth-failure audit tests; `GatewayAuditKind` → `SessionConnect` mapping test", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/zone_audit.rs` (new `d2b zone audit export` subcommand); `packages/d2b/tests/zone_audit_contract.rs`" + ], + "detailedDesign" : "`d2b zone audit export` opens segments read-only (shared flock), streams NDJSON to stdout, validates hash chain inline, reports breaks as inline error records, and invokes only `d2b.audit.v3.AuditService/Export` under the admin-only `audit-export` session verb (same `SO_PEERCRED`/Role check as current `ExportBrokerAuditOk`). The session grant provides no Zone resource authority. Assert no `realm`, `node`, `workload_id` fields in exported records. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt audit CLI contract test (daemon-down/exit behavior); adapt broker export test to new `zone` field and v3 record schema.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-audit-004", + "kind" : "work-item", + "owner" : "ADR046-audit-001; CLI owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-audit-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "`export_audit.rs`: admin-only, hash break inline, no old field names (`realm`/`node`/`workload_id`), no path/argv in output, exit 0 on clean chain", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`src/{lib.rs,config.rs,schema.rs,error.rs,effect/mod.rs}`" + ], + "detailedDesign" : "Provider descriptor/manifest; `spec.config` schema; Guest spec.provider.settings schema; `AzureEffectPort` trait + `AzureOperationHandle`; `AzureVmError` enum; `SandboxSpec` with semantic classes; `BudgetSpec` with SI suffix memory fields; `restartPolicy` class/backoffBase/backoffMax; `networkUsage.allowEgress=false`; Endpoint ResourceType templates with name/transport/purpose Primary reuse disposition: `adapt`. Preserved source-plan detail: Extract and adapt; DELETE `InfrastructureProvider` after this Provider is operational.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-virtual-machine" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-azure-vm-001", + "kind" : "work-item", + "owner" : "Provider contract owner", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-virtual-machine", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-virtual-machine" + ], + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "topologicalRank" : 11, + "validation" : "Provider catalog; descriptor fingerprint; schema/conformance tests", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/effect/{mod.rs,real.rs,fake.rs,rate_limit.rs}`" + ], + "detailedDesign" : "`AzureEffectPort` async trait; opaque `AzureOperationHandle` (bounded bytes, no poll URL); real `azure_core`/`azure_mgmt_compute` impl; `FakeAzureEffectPort` for hermetic tests; ARM 429/503/409 handling Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy and adapt.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-virtual-machine" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-azure-vm-002", + "kind" : "work-item", + "owner" : "ADR046-azure-vm-001", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-virtual-machine", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR046-azure-vm-001" + ], + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "topologicalRank" : 12, + "validation" : "`tests/lifecycle_hermetic.rs`; all ARM paths via `FakeAzureEffectPort`; no ARM URL in test assertions", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/controller/{mod.rs,lifecycle.rs,idempotency.rs}`" + ], + "detailedDesign" : "Non-blocking reconcile: `start_*(...)` → persist `AzureOperationHandle` → `requeue-at`; `poll_lro` on subsequent ticks; controller as authorized `update-status` writer for Guest resources; finalizer held until ARM delete confirmed; top-level `phase`, `status.resource`, and Azure `status.provider.details.providerPhase` written atomically Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy/adapt main toolkit; adapt conformance shape.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-virtual-machine" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-azure-vm-003", + "kind" : "work-item", + "owner" : "ADR046-azure-vm-001; ADR046-azure-vm-002; Guest ResourceType controller contract", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-virtual-machine", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR046-azure-vm-001", + "ADR046-azure-vm-002" + ], + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "topologicalRank" : 13, + "validation" : "`tests/lifecycle_hermetic.rs`; `tests/conformance.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/controller/bootstrap.rs`; `src/bootstrap_svc/{mod.rs,admission.rs,enrollment.rs}`" + ], + "detailedDesign" : "PSK generation; sealed PSK/admission/enrollment recovery material (ciphertext) in the controller's single guest-local sealed recovery Volume; `GrantBootstrapAdmission` typed bus call; IKpsk2 in bootstrap-svc; enrollment record; enrolled KK; the bootstrap-svc declares **no** state Volume (session state in process memory; obtains sealed PSK/admission from the controller only); the controller's sealed recovery Volume is an ordinary Volume resource created by core ProviderDeployment (before component Process start) from the controller's single `stateNamespaces` declaration with a Nix-preprovisioned `User/azure-vm-controller` layout principal; ARM operation/idempotency records live in the core Operation ledger and non-secret observed cloud phase lives in `Guest.status` (D087); controller does not own, create, or add Volume to exported ResourceTypes; it consumes its view dirfd only Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy/adapt main `BootstrapPsk`/`BootstrapAdmission`.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-virtual-machine" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-azure-vm-004", + "kind" : "work-item", + "owner" : "ADR046-azure-vm-001; ComponentSession IKpsk2", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-virtual-machine", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR046-azure-vm-001" + ], + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "topologicalRank" : 12, + "validation" : "`tests/bootstrap_hermetic.rs`; `tests/error_redaction.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/credential.rs`" + ], + "detailedDesign" : "ARM credential via enrolled KK `AcquireToken`; zeroizing token handling; no ambient credential fallback; `credential-managed-identity` guest-agent placement Primary reuse disposition: `adapt`. Preserved source-plan detail: Retain `OpaqueAzureRef` directly; adapt credential acquisition to enrolled KK.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-virtual-machine" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-azure-vm-005", + "kind" : "work-item", + "owner" : "ADR046-azure-vm-003; Credential ResourceType; D055/D056", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-virtual-machine", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR046-azure-vm-003" + ], + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "topologicalRank" : 14, + "validation" : "`tests/credential_hermetic.rs`; `tests/error_redaction.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/controller/idempotency.rs`" + ], + "detailedDesign" : "Deterministic ARM request ID derivation; `AzureOperationHandle` opaque persistence (no poll URL in state); ARM 409 adoption; finalizer held through async deletion Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt to deterministic per-Guest keys.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-virtual-machine" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-azure-vm-006", + "kind" : "work-item", + "owner" : "ADR046-azure-vm-003", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-virtual-machine", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR046-azure-vm-003" + ], + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "topologicalRank" : 14, + "validation" : "`tests/idempotency.rs`; restart-recovery scenario", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/` (Provider/Guest resource emitters); crate Nix build" + ], + "detailedDesign" : "Nix `spec.config` shape; `controllerExecutionRef`/`networkRef` eval-time assertions; no Volume refs for data disks; `systemArtifactId=null` enforcement; the single controller sealed recovery Volume is an ordinary Volume resource created by core ProviderDeployment (not in Zone bundle; not operator-authored); the bootstrap-svc declares no state Volume; guest-local placement — reconciled by the Guest-local volume-local instance and expressed by `source.executionRef` = config gateway Guest; host MUST NOT hold ARM binding, admission, PSK, or operation state; ARM operation/idempotency records live in the core Operation ledger and non-secret observed cloud phase in `Guest.status` (D087); no virtiofs or host-to-guest attachment; manifest freezes guest-local with no fallback; controller does not create, own, or list Volume in exported ResourceTypes; `Provider/volume-local` is the sole Volume reconciler; controller consumes required view dirfd only; **the recovery Volume is `kind: state`, `persistenceClass: persistent`, `storageNeed: secret`, sealed via `sealingCredentialRef`, with nonzero `quotaBytes`, `quota.maxBytes`, `quota.maxInodes`, and `source.settings.sourcePolicyId`; `persistenceClass: ephemeral` and zero quotas are rejected**; it survives component/Provider restart and participates in upgrade/destroy/reset; full canonical Volume spec including `stateSchema`, `source`, `layout` with a Nix-preprovisioned `User/` principal (not ComponentPrincipal), `views`, `identityMarker`, `snapshotPolicy: null`, `retentionPolicy: null`; `sensitivityClass: private` and `volume-domain-mismatch` isolation enforced; canonical `SandboxSpec` fields with `namespaceClasses`/`capabilityClasses`/`seccompClass`/`noNewPrivileges`/`startRoot`/`environmentClass`/`readOnlyRoot`; `BudgetSpec` with SI suffix; `restartPolicy` class/backoffBase/backoffMax; Endpoint ResourceType templates with name/transport/purpose", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-virtual-machine" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-azure-vm-007", + "kind" : "work-item", + "owner" : "ADR046-azure-vm-001; ADR-046-provider-state; ADR-046-nix-configuration", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-virtual-machine", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR046-azure-vm-001" + ], + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "topologicalRank" : 12, + "validation" : "Nix eval tests; `make test-flake`; `make test-drift`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/{telemetry.rs,audit.rs}`" + ], + "detailedDesign" : "Closed metric labels; OTEL span attributes; audit durability classes; `azure-vm-deleted` appended post-commit; no ARM URI, ARM resource ID, or cloud endpoint in any telemetry surface Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt audit shape; replace Prometheus with d2b-telemetry emitter.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-virtual-machine" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-azure-vm-008", + "kind" : "work-item", + "owner" : "ADR046-azure-vm-003; ADR-046-telemetry-audit-and-support", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-virtual-machine", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR046-azure-vm-003" + ], + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "topologicalRank" : 14, + "validation" : "`tests/error_redaction.rs`; `d2b-contract-tests/tests/policy_observability.rs` updated", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`tests/`; `integration/`" + ], + "detailedDesign" : "See §Test requirements Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy/adapt fake toolkit; write new tests.", + "entryContracts" : [ + "ADR-046-provider-runtime-azure-virtual-machine" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-azure-vm-009", + "kind" : "work-item", + "owner" : "All ADR046-azure-vm-*", + "parallelGroup" : "wi:ADR-046-provider-runtime-azure-virtual-machine", + "prerequisites" : [ + "ADR-046-provider-runtime-azure-virtual-machine" + ], + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "topologicalRank" : 11, + "validation" : "All tests pass", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/{router,registry,authorization,streams,operations}.rs`" + ], + "detailedDesign" : "Exact service/resource routes, RBAC, pinned reverse route, cancellation, named stream bridge, no wildcard pub/sub Primary reuse disposition: `adapt`. Preserved source-plan detail: extract/adapt.", + "entryContracts" : [ + "ADR-046-componentsession-and-bus" + ], + "exitGate" : "ADR046-W1 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-bus-001", + "kind" : "work-item", + "owner" : "Sessions + resource API; bus owner", + "parallelGroup" : "wi:ADR-046-componentsession-and-bus", + "prerequisites" : [ + "ADR-046-componentsession-and-bus" + ], + "specId" : "ADR-046-componentsession-and-bus", + "topologicalRank" : 6, + "validation" : "Message isolation; closed session-verb enum including `relay`, `audit-export`, and `support-bundle`; exact diagnostic service/method binding with no implied resource grant; relay missing/target verb missing/provider self-assertion fail-closed vectors; named-target and nameless List/Watch selector/filter preservation at every hop; route/auth revocation; fairness; reconnect; no direct-store path", + "wave" : "W1" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-runtime-cloud-hypervisor/src/controller.rs`" + ], + "detailedDesign" : "End-to-end: single Guest reconcile → synchronous dependency-readiness check via ResourceClient → VMM Process creation → guest-control health check in observe handler → status write. Uses fake bus/store/supervisor stubs from toolkit. Proves fast-path latency gates (≤5 ms hint, ≤20 ms VMM Process creation when all deps ready). No EphemeralProcess resources at any step. Primary reuse disposition: `adapt`. Preserved source-plan detail: Extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-runtime-cloud-hypervisor" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-ch-001", + "kind" : "work-item", + "owner" : "Provider toolkit / system-minijail; W1 spike owner", + "parallelGroup" : "wi:ADR-046-provider-runtime-cloud-hypervisor", + "prerequisites" : [ + "ADR-046-provider-runtime-cloud-hypervisor" + ], + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "topologicalRank" : 11, + "validation" : "Unit: reconcile state machine, fast-path latency, adoption/ambiguity, finalize ordering. Integration: end-to-end VMM boot with real KVM and guest-control session (requires `make test-host-integration`)", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-runtime-cloud-hypervisor/src/bootstrap_graph.rs`" + ], + "detailedDesign" : "Single owned VMM Process resource; synchronous ResourceClient dependency check (Device/kvm + all declared Devices, Networks, virtiofs Volumes); immediate Process creation when all deps ready; no EphemeralProcess resources; conditional net-VM Guest creation; per-dependency readiness tracking in reconcile loop. For each Network attachment the launch resolution carries only an opaque ref; network-local declares the semantic effect, the Core-owned `NetworkEffectPort` adapter performs `CreatePersistentTap → SetBridgePortFlags`, and `ProviderSupervisor` receives the connected CLOEXEC `OwnedFd` directly for LaunchTicket inheritance. The runtime controller receives no fd or broker operation. Primary reuse disposition: `replace`. Preserved source-plan detail: EXTRACT and REPLACE.", + "entryContracts" : [ + "ADR-046-provider-runtime-cloud-hypervisor" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-ch-002", + "kind" : "work-item", + "owner" : "Depends on `ADR046-ch-001` and `ADR046-network-005`; owner: cloud-hypervisor bootstrap-graph integration", + "parallelGroup" : "wi:ADR-046-provider-runtime-cloud-hypervisor", + "prerequisites" : [ + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR046-ch-001", + "ADR046-network-005" + ], + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "topologicalRank" : 12, + "validation" : "Golden VMM Process spec vectors; dependency-ordering tests; parallel Guest tests (8 concurrent); net-VM creation tests; Device/kvm explicit-ref enforcement; tap launch routing proves `CreatePersistentTap → SetBridgePortFlags → ProviderSupervisor LaunchTicket`; fd-lifetime tests prove CLOEXEC, one inheritable child slot, and close-before-generation-fenced-delete on every failed launch; boundary tests prove the controller and bus payloads contain only opaque refs", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-runtime-cloud-hypervisor/src/vmm_argv.rs`; `tests/vmm_argv_golden_test.rs`" + ], + "detailedDesign" : "`VmmArgvInput` derived from validated `GuestSpec.spec.provider.settings`; kernel/initrd/rootfs paths resolved privately from artifact catalog at dispatch time; no path in spec/status; tap argv accepts only the declared child fd slot inherited from the sealed LaunchTicket, with no runtime-selected handoff mode or host tap name; golden tests for headless/q35/microvm/gpu/video/macvtap variants Primary reuse disposition: `adapt`. Preserved source-plan detail: COPY/ADAPT.", + "entryContracts" : [ + "ADR-046-provider-runtime-cloud-hypervisor" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-ch-003", + "kind" : "work-item", + "owner" : "ADR046-ch-002; artifact catalog foundation", + "parallelGroup" : "wi:ADR-046-provider-runtime-cloud-hypervisor", + "prerequisites" : [ + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR046-ch-002" + ], + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "topologicalRank" : 13, + "validation" : "Golden argv vectors matching `cloud-hypervisor-argv-*.txt` shapes with v3 adaptations; tap vector accepts only the declared LaunchTicket child fd slot; redaction test (no store path in Debug output)", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-runtime-cloud-hypervisor/nix/` (Nix emitter); `nixos-modules/` option extension for `runtime-cloud-hypervisor` Guest schema" + ], + "detailedDesign" : "`d2b.zones..resources.` with `type = \"Guest\"` and `spec.provider.settings` validated against signed Provider schema; `spec.systemArtifactId` top-level field; artifact catalog `type = \"nixos-system\"` enforced by rule 17; Guest-control `Endpoint` resource emitted without raw locator; `make test-drift` gate for schema/Nix drift Primary reuse disposition: `adapt`. Preserved source-plan detail: ADAPT and REPLACE.", + "entryContracts" : [ + "ADR-046-provider-runtime-cloud-hypervisor" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-ch-004", + "kind" : "work-item", + "owner" : "ADR046-ch-002; nix-configuration foundation (`ADR046-identities-002`)", + "parallelGroup" : "wi:ADR-046-provider-runtime-cloud-hypervisor", + "prerequisites" : [ + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR046-ch-002", + "ADR046-identities-002" + ], + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "topologicalRank" : 13, + "validation" : "nix-unit eval tests: rule CH-1 through CH-4 + rules 1–17; golden resource bundle JSON (no store path); type-mismatch eval errors; raw locator rejection; `spec.systemArtifactId` at top-level in JSON (not in `spec.provider.settings`)", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-runtime-cloud-hypervisor/src/health.rs`; `src/adoption.rs`" + ], + "detailedDesign" : "Authenticated KK ComponentSession health check over vsock; adoption verification (pid/cgroup/executable/generation) within `adoptionWindow`; ambiguity → Unknown/Degraded, never broad kill; graceful shutdown via guest-control session before SIGTERM", + "entryContracts" : [ + "ADR-046-provider-runtime-cloud-hypervisor" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-ch-005", + "kind" : "work-item", + "owner" : "ADR046-ch-001; ComponentSession/d2b-bus (`ADR046-session-001`)", + "parallelGroup" : "wi:ADR-046-provider-runtime-cloud-hypervisor", + "prerequisites" : [ + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR046-ch-001", + "ADR046-session-001" + ], + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "topologicalRank" : 12, + "validation" : "Fake guest-control server test; health check timeout/failure/retry; adoption property test (ambiguity, gone, stale pid); graceful shutdown ordering", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-runtime-cloud-hypervisor/src/metrics.rs`; `src/audit.rs`" + ], + "detailedDesign" : "`d2b_runtime_ch_*` metrics from §18.3 with closed semantic labels and no `vm`, `zone`, `zone_id`, `zone_uid`, or resource-name-derived key; bounded durable audit records from §17.3; no path/argv/socket in any field; identity retained in the closed OTEL resource-attribute allowlist extended per §18.4", + "entryContracts" : [ + "ADR-046-provider-runtime-cloud-hypervisor" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-ch-006", + "kind" : "work-item", + "owner" : "ADR046-ch-001; telemetry foundation (`ADR046-telem-001`)", + "parallelGroup" : "wi:ADR-046-provider-runtime-cloud-hypervisor", + "prerequisites" : [ + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR046-ch-001", + "ADR046-telem-001" + ], + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "topologicalRank" : 12, + "validation" : "`policy_observability.rs` updated with v3 allowlist; structural descriptor tests assert exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus Guest/Zone-name canary absence; bounded message/field tests; audit record schema golden vectors", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-runtime-cloud-hypervisor/src/state.rs`; `packages/d2b-provider-runtime-cloud-hypervisor/tests/state_status_test.rs`" + ], + "detailedDesign" : "`state.rs` owns the controller's bounded non-secret operational-state projection into the owning resource's `status` subresource (reconcile stage, per-Guest launch/adoption observations, bounded counters, closed-enum error detail) — the controller declares no Provider state Volume and mounts no `/state`; on restart it re-derives observed state from the Zone resource store, the core Operation ledger, and external observation (running VMM/virtiofsd re-adopted from cgroup leaves + fresh pidfds), treating `status` as observation, never authority (D087); status writes occur only on material change and stay within the status bounds. The superseded state-Volume integration, migration, validation, and removal rows are rejected: this Provider has no state Volume, state mount, or `StateEnvelope` startup path. Primary reuse disposition: `replace`. Preserved source-plan detail: REPLACE (storage.rs).", + "entryContracts" : [ + "ADR-046-provider-runtime-cloud-hypervisor" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-ch-007", + "kind" : "work-item", + "owner" : "ADR046-ch-001; `ADR046-pstate-001` (common status types)", + "parallelGroup" : "wi:ADR-046-provider-runtime-cloud-hypervisor", + "prerequisites" : [ + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR046-ch-001", + "ADR046-pstate-001" + ], + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "topologicalRank" : 12, + "validation" : "`state_status_test.rs` (hermetic): status projection round-trip and bound enforcement; restart re-derivation from store/ledger/external observation without a state Volume; no secret/path/argv/PID in status", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/lib.rs`, `packages/d2b/src/context.rs`, `packages/d2b/src/dispatch.rs`" + ], + "detailedDesign" : "Introduce `ZoneContext` (zone name, socket path, ComponentSession client); implement `--zone`/`D2B_ZONE`/nearest-socket discovery using adapted `LocalUnixDaemonAccess::connect_component_session()` chain; introduce `ResourceRef` argument parser; introduce unified `--json`/`--human`/`--deadline` flag infrastructure bounded by `MAX_REQUEST_LIFETIME_MS=900s`; freeze `--json` schema version 1; stabilize exit code table. Excluded ADR 0045 assumptions: `TargetInput::Realm`, `TargetInput::Workload`, `TargetInput::Provider` variants; `RealmPath::parse(\"local-root\")` / `RealmId::derive` pattern; `RealmPath`-based service owner types.", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-001", + "kind" : "work-item", + "owner" : "ADR046-identities-001, ADR046-api-001; CLI crate owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-api-001", + "ADR046-identities-001" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 10, + "validation" : "Zone-unavailable/fallback tests; ResourceRef parse/reject vectors; exit-code round-trip tests; TTY detection tests; adapt `client.rs:typed_routes_select_exact_transport_without_fallback` (line 1053), `connector_discovers_and_authenticates_the_driver_generation` (line 1254), `daemon_transport_rejects_ancillary_data_and_oversized_packets` (line 1312)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/guest.rs` (`d2b guest start/stop/restart/list/status`); unsafe-local workloads go to `packages/d2b/src/host.rs` (`d2b host list/status/get`), NOT guest.rs" + ], + "detailedDesign" : "Route Guest lifecycle (WorkloadProviderKind: LocalVm/QemuMedia/ProviderManaged) through `d2b.resource.v3` Get/UpdateSpec/Watch; map dry-run/apply to resource API precondition; `--no-wait-ready` exits on accepted; with-wait uses `d2b status --watch` loop. WorkloadProviderKind::UnsafeLocal entries MUST route to `d2b host` commands only; any code path that would return an unsafe-local entry from `d2b guest list` is a correctness violation. v2 commands (`d2b up/down/restart/list/status`, `d2b vm start/stop/restart/list/status`) are deleted at 3.0; replacement guidance remains documentation-only, with no migration-diagnostic command.", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-002", + "kind" : "work-item", + "owner" : "ADR046-cli-001, ADR046-api-001; CLI crate owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-api-001", + "ADR046-cli-001" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 11, + "validation" : "Dry-run/apply/wait/no-wait-ready tests; zone-unavailable degraded path; JSON output schema tests; confirm v2 command paths are absent (compilation failure if any cmd_vm_start/stop alias re-introduced)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/exec.rs` (`d2b exec run/attach/wait/status/list/logs/kill`)" + ], + "detailedDesign" : "Map EphemeralProcess resource lifecycle; `exec run` creates resource and returns ref; `exec attach` opens named stream via adapted `DaemonClient::open_terminal(DaemonMethod::Exec, ...)` → `DaemonTerminal`; retain full `exec_client.rs` FSM and TTY machinery from baseline; retain `--json` envelope fields `source`/`reason`/`guestExitCode`/`signal`/`transportExitCode`; retain reserved exit codes 42/69/70/75/76/77. v2 commands (`d2b vm exec *`) are deleted at 3.0; no dispatch wiring. Excluded ADR 0045: `GuestClient` vsock/guest-control proxy path; `TargetInput::Workload`; old `WorkloadName`-keyed exec management. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-then-adapt.", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-003", + "kind" : "work-item", + "owner" : "ADR046-cli-001, ADR046-api-001; CLI crate owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-api-001", + "ADR046-cli-001" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 11, + "validation" : "Full `exec_client.rs` test suite migrated; adapted tests from main `client.rs:terminal_*` and `guest_exec_*`; TTY/raw-mode/RAII/signal tests; `--json` envelope/disambiguation tests; capacity/transport/auth/protocol exit-code tests; confirm v2 `cmd_vm_exec` path is absent", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/shell.rs` (`d2b shell open/attach/list/detach/kill/status`)" + ], + "detailedDesign" : "Route ShellSession resource lifecycle through resource API using adapted `ShellService` generated types; `shell open` → `ShellCreate` → `DaemonClient::open_terminal(Shell)` → `DaemonTerminal`; retain FSM/TTY/signal/RAII behavior from `run_shell_fsm`; `--name` required for kill; SIGHUP detaches without kill. v2 commands (`d2b shell *`) are deleted at 3.0; no dispatch wiring. Excluded: gateway relay path (`VmTargetRoute::Gateway`); old `ShellOp`/`ShellOpResponse` seqpacket protocol; `TargetInput::Workload`-keyed realm routing. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-then-adapt.", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-004", + "kind" : "work-item", + "owner" : "ADR046-cli-001, `shell-terminal` Provider dossier; CLI crate owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-cli-001" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 11, + "validation" : "Shell list/detach/kill/attach unit tests (adapted from existing); adapted `client.rs:shell_management_*` and `named_stream_*` tests; TTY RAII/signal tests; confirm v2 `cmd_shell` path is absent", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/provider.rs` (`d2b provider list/get/status/inspect`; dynamic projection loading)" + ], + "detailedDesign" : "`d2b provider list/get/status/inspect`; InspectSchema call returns dynamic projection descriptor using `ConnectedClient::invoke` with generated provider service types; projection bounds enforcement (64 KiB, 32 sub-verbs, 2s deadline, shell-escape, newline strip); define the single canonical built-in command registry consumed by parser/help/completion and Provider projection binding; make bind-time rejection the sole authority for built-in and Provider/Provider top-level-name collisions, with no dispatch fallback; render projected commands from their declared top-level subcommand names; audio/clipboard/display are the first providers to migrate their projections", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-005", + "kind" : "work-item", + "owner" : "ADR046-cli-001, Provider model spec; CLI crate owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-cli-001" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 11, + "validation" : "Projection size/name/timeout bounds tests; registry-completeness test asserting exact equality with all built-in parser commands; bind-time built-in and Provider/Provider collision rejection tests; negative test proving no collision fallback dispatch exists; audio/clipboard/display projection conformance tests asserting `d2b audio ...`, `d2b clipboard ...`, and `d2b display ...` rendering; completion script safety tests; adapted `conformance.rs` tests", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/complete.rs` (`d2b complete bash/zsh/fish`)" + ], + "detailedDesign" : "`d2b complete ` emits completion script; uses clap `CommandFactory::command()` plus dynamic projection fetch (2s per-Provider, 10s total); result bounded at 256 KiB; shell-escaped; newlines stripped Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged (clap_complete).", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-006", + "kind" : "work-item", + "owner" : "ADR046-cli-001, ADR046-routing-012, ADR046-routing-016; CLI crate owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-cli-001", + "ADR046-routing-012", + "ADR046-routing-016" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 12, + "validation" : "Completion script tests (bash/zsh/fish syntax valid); projection injection safety tests; deadline/partial-Provider tests", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/activation.rs` (`d2b activation build/generations/switch/boot/test/rollback/gc/migrate/keys/trust/rotate-known-host/config`)" + ], + "detailedDesign" : "Route through `activation-nixos` Provider service via `ConnectedClient::invoke` using adapted `DaemonMethod::Apply`/lifecycle dispatch pattern; retain dry-run/apply; retain guest-control transport for config sync (no SSH). v2 top-level activation commands (`d2b build/switch/boot/test/rollback/gc/migrate/keys/trust/rotate-known-host/config`) are deleted at 3.0; no dispatch wiring.", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-007", + "kind" : "work-item", + "owner" : "ADR046-cli-001; CLI/activation Provider owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-cli-001" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 11, + "validation" : "All existing switch/boot/test/rollback/keys tests adapted; config sync/diff/approve/reject tests; confirm v2 top-level activation paths are absent; adapted `client.rs:daemon_typed_list_preserves_projection_and_truncation` apply pattern", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/host.rs` (all `d2b host` subcommands)" + ], + "detailedDesign" : "Route `host prepare/destroy` through Zone resource API Host reconcile operation via `ConnectedClient::invoke`; retain broker-mediated mutation and ownership-marker semantics; `host doctor` prefers Zone resource API status, falls back to local state files; `host check` retains exit-code 3; `host validate` retains wave/evidence-dir/scripts-dir/signature", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-008", + "kind" : "work-item", + "owner" : "ADR046-cli-001; CLI crate owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-cli-001" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 11, + "validation" : "All existing host-check/prepare/destroy/doctor/install/reconcile/validate tests; exit-code 3 regression; doctor Zone-fallback/local-state-fallback tests", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/zone.rs` (`d2b zone get/list/status`)" + ], + "detailedDesign" : "`d2b zone get []` fetches the current Zone self resource only for the local name; another declared name is inspected through the read-only Zone topology projection. `d2b zone list` lists sorted compiler-only `{ childZone, parentZone }` rows joined with authenticated route/projection status. `d2b zone status --watch` watches the corresponding projection revision. None of these commands lists or gets a parent-local ZoneLink: the sole ZoneLink resource and handler are child-local, while the parent owns only sealed allocator topology and authenticated route state. Generated DTO/golden tests reject ZoneLink names/spec/status/provider refs/fingerprints/transport settings in parent-side output. v2 commands (`d2b realm list/inspect/enter/run`) are deleted at 3.0; no dispatch wiring. Excluded ADR 0045: `RealmServiceServer`/`RealmServiceProcess` multi-realm service; `RemoteNodeRegistration` constellation routing; `TargetInput::Realm`; `RealmMethod::ResolveRoute`/`AuthorizeShortcut`/`RevokeShortcut`.", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-009", + "kind" : "work-item", + "owner" : "ADR046-cli-001; CLI crate owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-cli-001" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 11, + "validation" : "Zone self-get plus topology list/get/status/watch tests; disconnected and stale authenticated-route projections; golden output contains `{ childZone, parentZone }` and route/projection status but no ZoneLink fields; parent store has no ZoneLink row or watch; confirm v2 `cmd_realm_*` paths are absent", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/resource.rs` (standard `d2b get/list/watch/create/update-spec/delete/status` top-level verbs)" + ], + "detailedDesign" : "Generic typed dispatch to resource API Get/List/Watch/Create/UpdateSpec/Delete using `ConnectedClient::invoke` (Get/List/Create/UpdateSpec/Delete) and `ConnectedClient::open_server_stream` + `NamedStream` (Watch); ResourceRef argument parsing and validation; page token pagination; `--phase`/`--label-selector` filters; `--deadline` bounded by `MAX_REQUEST_LIFETIME_MS=900s` via `DeadlineBudget`; Watch output streams resource events as JSON lines; JSON schema version field; `CancellationToken` wired to process signal handlers. Excluded: `GuestClient` vsock exec/shell routing; `TargetInput` realm/workload/provider variants. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-then-adapt.", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-010", + "kind" : "work-item", + "owner" : "ADR046-cli-001; CLI crate owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-cli-001" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 11, + "validation" : "Get/list/watch/create/update-spec/delete tests per ResourceType; pagination/filter/watch-deadline tests; error-class/exit-code tests; adapted `client.rs:metadata_retries_*` and `mutating_retries_*` and `concurrent_named_streams_*` tests", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "Nix: `nixos-modules/options-zones.nix` (unified `d2b.zones..resources` attrset; per-type `spec` sub-options generated from ResourceTypeSchema/Provider schema), `nixos-modules/bundle-emit.nix` (canonical JSON emit + SHA256 pin), `nixos-modules/assertions.nix` (updated); core controller: `packages/d2b-core-controller/src/configuration.rs`, `packages/d2b-core-controller/src/cleanup.rs`; Contracts: `packages/d2b-contracts/src/zone_bundle.rs` (new)" + ], + "detailedDesign" : "**Nix shape:** `d2b.zones..resources` is `attrsOf (submodule { type; optional metadata { ownerRef; labels; annotations }; spec })`. `spec` sub-options per `type` are generated from ResourceTypeSchema and signed Provider schemas; field names remain identical. `metadata.name`/`metadata.zone`/`apiVersion` are derived; status and all core metadata are rejected in input. Vendor-qualified types are admitted only when their schema is installed. **Nix emit:** `bundle-emit.nix` emits `/etc/d2b/zones//resource-bundle.json` plus its integrity pin with canonical resource ordering and schema digests. **Core-controller apply:** `configuration.rs` verifies bundle/catalog integrity, applies Create/Update/no-op intents with bounded async concurrency, refreshes `configurationGeneration` for unchanged configuration-managed resources without waking their controller, handles controller/API name collisions per-item without seizing them, and asynchronously deletes only persisted `managedBy=configuration` resources absent from the new configured set. `cleanup.rs` consumes `Deleted` revision watches and maintains `PendingCleanup`; it never force-removes finalizers. **Prior generation retention:** `d2b.zones..retainedGenerations`, default 3 and range 1–16, is a compiler setting outside `Zone.spec`; no TTL. Rollback reapplies a retained bundle as a new higher generation.", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-011", + "kind" : "work-item", + "owner" : "ADR046-identities-002, ADR046-cli-001, ADR046-cli-002, ADR046-cli-007, ADR046-nix-014; Nix module owner + Zone runtime owner; `nixos-modules/assertions.nix` base migration precedes this item's unified-resource update", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-cli-001", + "ADR046-cli-002", + "ADR046-cli-007", + "ADR046-identities-002", + "ADR046-nix-014" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 14, + "validation" : "Runtime integration: all CLI-visible cleanup/status/rollback/gc/audit tests (§CLI-visible tests for activation and cleanup), including no force-finalizer path; Nix unit and build tests owned by ADR-046-nix-configuration spec; canonical-example fixture validates every authored resource against the authoritative Host, Guest, and Provider schemas, resolves every artifact/resource reference, rejects unsupported fields, and proves the compiled Guest `spec` is field-for-field identical to the adjacent JSON", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/endpoint.rs` (`d2b endpoint get/list/watch/status/resolve`)" + ], + "detailedDesign" : "Add `Endpoint` to the frozen standard ResourceType set (19 types; local ResourceRef parser accepts `Endpoint/` with no Zone round-trip). Implement `d2b endpoint get/list/watch/status` over the ADR046-cli-010 generic verbs and `d2b endpoint resolve ` as a provider-neutral resolution projection printing `producerRef`, `endpointClass`, transport class, readiness, and capability/locality observations. Endpoint output is the base envelope only (base spec + base status per D092); any `status.provider` projection is bounded, redacted, and deny-unknown. **No raw locator** (path/address/CID/port/fd/credential) appears in any CLI field or the `resolve` projection. `status.update` currency and `d2b upgrade Endpoint/` visibility follow the standard verbs and D091. `create`/`update-spec`/`delete` are rejected on the operator surface except for statically Nix/API-authored Endpoints whose schema permits it.", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-012", + "kind" : "work-item", + "owner" : "ADR046-cli-001, ADR046-cli-010; CLI crate owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-cli-001", + "ADR046-cli-010" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 12, + "validation" : "`Endpoint` accepted as a frozen standard type; `Endpoint/` parses locally without a Zone round-trip; `d2b endpoint get/list/watch/status/resolve` parse and route correctly; `get`/`resolve` output carries no raw path/address/CID/port/fd/credential; provider status projection is bounded/redacted; `d2b list Endpoint --updates` and `d2b upgrade Endpoint/` expose `status.update` currency and disruption; operator `create`/`update-spec`/`delete` rejected for controller-owned Endpoints", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/share.rs` (`d2b export …` and `d2b import …` nouns)" + ], + "detailedDesign" : "Add `ResourceExport` and `ResourceImport` to the frozen 19-type standard set while leaving qualified semantic/provider-neutral Service/Binding types dynamically cataloged. Implement export/import generic verbs, `d2b import projection`, and `d2b import graph`. Export accepts only a factory-bound owner Service. Import status exposes the same-semantic-type projection Service and verified factory/schema fingerprints; local `providerRef` selects the implementation. Graph renders import → projection Service → authored Bindings → controller-owned Process/Endpoint children with deterministic ownership/dependency edges. Binding spec is desired intent only and observations are status-only. Reject Device/Endpoint/Binding export targets, cross-Zone refs, absent/mismatched factory/type/fingerprints, implementation detail in the base, forbidden Providers, USB policy denial, unauthorized Zones, and capability-ceiling violations. No backing/remote refs, internal sessions/streams, FD, secret, path, locator, or bytes in output. Delete waits for authored Bindings to be removed/retargeted and never cascades them.", + "entryContracts" : [ + "ADR-046-cli-and-operations" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cli-013", + "kind" : "work-item", + "owner" : "ADR046-cli-001, ADR046-cli-010, ADR046-zone-control-019; CLI crate owner", + "parallelGroup" : "wi:ADR-046-cli-and-operations", + "prerequisites" : [ + "ADR-046-cli-and-operations", + "ADR046-cli-001", + "ADR046-cli-010", + "ADR046-zone-control-019" + ], + "specId" : "ADR-046-cli-and-operations", + "topologicalRank" : 12, + "validation" : "Both standard types parse locally; qualified semantic Service/Binding resolve dynamically; verbs route; Service-only export and factory/type/fingerprint/policy/capability rejections; projection preserves semantic Service type across local implementation selection; canonical minimal base works without `spec.provider`; graph shape exact and bounded; Binding never auto-created/deleted; Binding spec intent-only/status observations; no implementation detail/backing/remote-ref/session/stream/FD/secret/path/locator/bytes leakage", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-client/src/` (updated for v3 Zone API, replacing ADR45 daemon verbs with Zone resource operations)" + ], + "detailedDesign" : "**Selected**: `Client::connect()` target-resolve → ComponentSession-open → `ConnectedClient` lifecycle; `RetryPolicy` 1..8 bound + `retryable_failure()` safe-only retry detection; `NamedStream::send/receive/close_local/reset` lifecycle; `ComponentSessionConnector` trait as connector abstraction; `HostSocketConnector::from_seqpacket_fd` + `local_daemon_endpoint_identity` for Zone runtime socket connector; `RouteTable` ambiguous-route rejection; `ServiceHandle`/`GeneratedClient`/`MethodHandle` typed service client pattern; `map_ttrpc_error`/`validate_reply`/`map_retry` error-handling chain; `ClientError`/`RemoteErrorKind`/`RetryClass` error taxonomy; `DaemonClient` call-options and outcome helpers (infrastructure only). **Excluded ADR45 assumptions**: `DaemonMethod` enum (lines 29–56 of daemon_service.rs): ADR45 daemon verbs (`vm_start`, `vm_stop`, `list_realms`, etc.) — replaced with Zone API verbs. `GuestClient`/`guest_service.rs`: ADR45 guest operations; excluded until v3 Guest transport work item. Hardcoded socket path `PUBLIC_SOCKET_PATH = \"/run/d2b/public.sock\"` in `host_socket.rs`: replaced by Zone-resource-managed path. `TransportKind::LocalUnix` restriction in daemon-access: v3 allows multiple transport kinds per ZoneLink binding. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-client-001", + "kind" : "work-item", + "owner" : "ADR046-zone-control-011, ADR046-zone-control-012, ADR046-zone-control-013, ADR046-zone-control-018", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-011", + "ADR046-zone-control-012", + "ADR046-zone-control-013", + "ADR046-zone-control-018" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 13, + "validation" : "`client-retry-policy-max-8-attempts-enforced`, `client-named-stream-close-local-then-remote-close-transitions-closed`, `client-route-table-ambiguous-route-rejected`, `client-host-socket-peer-uid-verified-on-connect`, `client-retryable-failure-only-safe-mutations`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-clipboard-wayland/ with src, tests, integration, README.md, and binaries clipboard-controller, clipd-host, picker-session" + ], + "detailedDesign" : "Create the provider crate skeleton, required source layout, three binaries, and README covering purpose, component map, local build instructions, test commands, and display-wayland fake dependency for integration tests.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-001", + "kind" : "work-item", + "owner" : "ADR-046-provider-model-and-packaging; Provider/clipboard-wayland crate owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 11, + "validation" : "Workspace provider layout policy plus README content review and follow-on make test-rust -p d2b-provider-clipboard-wayland once implementation exists.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-clipboard-wayland/src/clipd_host/ service binary modules such as service, display_client, bridge, picker_coord, policy, fd, audit, history" + ], + "detailedDesign" : "Adapt d2b-clipd into clipd-host: call RequestPickerSession over d2b.clipboard.picker-coord.v3, consume d2b.display.host-clipboard.v3 instead of WAYLAND_DISPLAY or NIRI_SOCKET, serve d2b.clipboard.bridge.v3 on Endpoint/clipboard-bridge, remove filesystem bridge and peer group ACL logic, and preserve MIME allowlist, FD safety, fail-closed audit, loop suppression, and LRU history algorithms. Primary reuse disposition: `adapt`. Preserved source-plan detail: port and adapt algorithms; replace direct compositor, picker subprocess, Unix bridge, SO_PEERCRED, bridge directory, and group ACL paths.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-002", + "kind" : "work-item", + "owner" : "ADR046-clipboard-001; clipd-host service owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland", + "ADR046-clipboard-001" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 12, + "validation" : "make test-rust -p d2b-provider-clipboard-wayland plus unit coverage for MIME policy, FD safety, audit fail-closed queue, history bounds, lifecycle purge and suspension, no filesystem bridge, and no bytes in status or audit.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-clipboard-wayland/src/controller/ and clipboard-controller binary" + ], + "detailedDesign" : "Implement Process/clipboard-controller as a system-domain system-minijail Process serving d2b.clipboard.picker-coord.v3, creating picker EphemeralProcesses from the signed picker-session template, observing picker status, relaying Guest lifecycle messages to clipd-host, creating clipboard RBAC Roles and RoleBindings, writing only bounded redacted operational observations through the optimistic status writer, and never owning or mounting Provider state Volumes. Primary reuse disposition: `create`. Preserved source-plan detail: net-new controller using existing resource API and ComponentSession contracts.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-003", + "kind" : "work-item", + "owner" : "ADR046-clipboard-001; clipboard-controller owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland", + "ADR046-clipboard-001" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 12, + "validation" : "Controller unit tests for picker request validation, EphemeralProcess spec shape, terminal status callback, GuestStopped and GuestLocked handling, RBAC resources, status bounds, and empty ProviderStateSet.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-clipboard-wayland/src/picker_session/ and picker-session binary" + ], + "detailedDesign" : "Implement picker-session as a user-domain worker EphemeralProcess with minimal environment, metadata over inherited ComponentSession named stream, restricted WAYLAND_SOCKET FD from display-wayland presentation portal, GTK4 closure-contained runtime, one Select or Cancel response, no clipboard FDs, no clipboard bytes, no compositor credentials, no socket paths, and typed PickerStartFailed on install or start failure instead of bypass. Primary reuse disposition: `adapt`. Preserved source-plan detail: rewrite as EphemeralProcess worker.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-004", + "kind" : "work-item", + "owner" : "ADR046-clipboard-001 and ADR046-clipboard-003; picker worker owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland", + "ADR046-clipboard-001", + "ADR046-clipboard-003" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 13, + "validation" : "Contract test that picker cannot bind zwlr_data_control_manager_v1 plus unit tests for processClass worker, no FDs or payload in picker config, TTL defaults, response framing, and requirePickerForPaste false bypass semantics in clipd-host.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-clipboard-wayland service descriptors and generated Rust async ttrpc bindings, plus any shared contracts crate selected by ADR-046-componentsession-and-bus" + ], + "detailedDesign" : "Generate stubs for d2b.clipboard.bridge.v3, d2b.clipboard.picker-coord.v3, and d2b.clipboard.v3, consume display-wayland d2b.display.host-clipboard.v3 client stubs, reject service-name collisions, and declare attachment classes clipboard-transfer-fd, host-selection-transfer-fd, and host-selection-supply-fd in the signed descriptor for ComponentSession handshake validation. Primary reuse disposition: `create`. Preserved source-plan detail: net-new generation of service stubs and named-stream types.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-005", + "kind" : "work-item", + "owner" : "ADR046-clipboard-001; ADR-046-componentsession-and-bus; clipboard service contract owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland", + "ADR046-clipboard-001" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 12, + "validation" : "Contract tests for wire format, service-name collision rejection, attachment class matching, and descriptor handshake validation.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "nixos-modules/providers/clipboard-wayland.nix and d2b.artifacts.clipboard-wayland catalog entry" + ], + "detailedDesign" : "Implement Nix module emitting d2b.zones..resources. Provider resources with spec.artifactId and spec.config, validate hostExecutionRef, hostUserRef, displayWaylandRef, and pickerArtifactId, forbid spec.componentPlacements, spec.settings, and spec.status, and remove nixos-modules/clipboard.nix in the same landing sequence as the new module. Primary reuse disposition: `replace`. Preserved source-plan detail: replace option surface with Provider resource Nix module.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-006", + "kind" : "work-item", + "owner" : "ADR-046-nix-configuration; Provider/clipboard-wayland Nix owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 11, + "validation" : "Nix eval tests for resource shape, reference validation, null displayWaylandRef host-only mode, artifact catalog lookup, and absence of deprecated spec fields.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-clipboard-wayland/src/controller/rbac.rs or equivalent controller reconcile module" + ], + "detailedDesign" : "Controller creates Role/clipboard-admin, Role/clipboard-viewer, Role/clipboard-bridge-peer, Role/clipboard-picker-worker and RoleBindings display-wayland-bridge, host-admin-clipboard, picker-session-worker, all Zone-scoped, owned by Process/clipboard-controller, selector-bound for Process/picker-*, and cleaned up when Provider is deleted.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-007", + "kind" : "work-item", + "owner" : "ADR046-clipboard-003; clipboard RBAC owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland", + "ADR046-clipboard-003" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 13, + "validation" : "Controller RBAC reconcile tests for create, idempotent update, provider deletion cleanup, selector scoping, bridge peer authorization, and denied unauthorized management calls.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-clipboard-wayland/src/service/audit.rs and packages/d2b-provider-clipboard-wayland/src/service/metrics.rs" + ], + "detailedDesign" : "Implement ClipboardAuditEvent and fail-closed Zone audit queue by porting baseline audit code, renaming realm fields to source_zone_id and dest_zone_id, making ReasonCode a closed enum with unknown protobuf fields rejected, replacing exact byte counts with SizeBucket, emitting to d2b.audit.v3, and adding closed-semantic-label OTEL metrics and spans from the dossier tables. Metric descriptors carry no Zone/resource-name-derived identity; `d2b.zone` remains a resource attribute. Primary reuse disposition: `adapt`. Preserved source-plan detail: port and adapt audit plus resource-name-free metrics and redaction changes.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-008", + "kind" : "work-item", + "owner" : "ADR046-clipboard-002; ADR-046-telemetry-audit-and-support; clipboard observability owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland", + "ADR046-clipboard-002" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 13, + "validation" : "Audit tests for no bytes in events, closed ReasonCode deserialization, fail-closed queue rejection, SizeBucket discretization, excluded span attributes, and structural metric descriptor assertions for exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus clipboard/Zone-name canary absence while preserving `d2b.zone` resource attributes.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-clipboard-wayland/tests/" + ], + "detailedDesign" : "Create hermetic unit and Cargo integration tests covering closed MIME policy, secret-hint suppression, FD validation and bounds, LRU and TTL history, fail-closed audit, lifecycle purge and suspend, picker EphemeralProcess invariants, no filesystem bridge, core-created Processes, empty ProviderStateSet, no state mounts or state-layout principals, status-first observation, and no clipboard bytes in status, audit, metrics, Operations, or Volumes. Primary reuse disposition: `extract`. Preserved source-plan detail: extract semantic assertions into hermetic provider tests.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-009", + "kind" : "work-item", + "owner" : "ADR046-clipboard-001 through ADR046-clipboard-008; clipboard test owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland", + "ADR046-clipboard-001", + "ADR046-clipboard-002", + "ADR046-clipboard-003", + "ADR046-clipboard-004", + "ADR046-clipboard-005", + "ADR046-clipboard-006", + "ADR046-clipboard-007", + "ADR046-clipboard-008" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 14, + "validation" : "All tests listed in the Required test coverage table must pass under make test-rust -p d2b-provider-clipboard-wayland.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-clipboard-wayland/integration/" + ], + "detailedDesign" : "Implement e2e paste, host capture, bridge backpressure, rate limiting, echo suppression, dependency absent host-only mode, GuestDestroyed purge, audit fail-closed, picker start timeout, and cross-zone denied scenarios using fake d2b.display.host-clipboard.v3 server and fake wayland-proxy bridge client without requiring a live compositor. Primary reuse disposition: `create`. Preserved source-plan detail: net-new integration suite with fake display-wayland and fake wayland-proxy.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-010", + "kind" : "work-item", + "owner" : "ADR046-clipboard-009; display-wayland fake and clipboard integration owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland", + "ADR046-clipboard-009" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 15, + "validation" : "All integration scenarios in the dossier table pass and assert no live Wayland compositor dependency.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-contract-tests/tests/policy_clipboard.rs" + ], + "detailedDesign" : "Add contract tests for d2b.clipboard.bridge.v3 and d2b.clipboard.picker-coord.v3 wire formats, ReasonCode numeric stability, and attachment class descriptor names while removing tests that assume shared filesystem bridge paths or SO_PEERCRED config. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt contract tests and delete obsolete filesystem bridge assumptions.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-011", + "kind" : "work-item", + "owner" : "ADR046-clipboard-005; packages/d2b-contract-tests owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland", + "ADR046-clipboard-005" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 13, + "validation" : "packages/d2b-contract-tests policy_clipboard.rs passes with v3 wire format and attachment descriptor assertions.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "nixos-modules/default.nix, docs/how-to/ migration guide, tests/static.sh example iteration expectations, CHANGELOG.md" + ], + "detailedDesign" : "After the new Provider Nix module validates, delete nixos-modules/clipboard.nix, remove its default.nix import, update migration guide with the option mapping table, ensure tests/static.sh examples no longer rely on removed paths, and add an Unreleased changelog entry. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete superseded module and update migration docs.", + "entryContracts" : [ + "ADR-046-provider-clipboard-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-clipboard-012", + "kind" : "work-item", + "owner" : "ADR046-clipboard-006; Nix/module removal owner", + "parallelGroup" : "wi:ADR-046-provider-clipboard-wayland", + "prerequisites" : [ + "ADR-046-provider-clipboard-wayland", + "ADR046-clipboard-006" + ], + "specId" : "ADR-046-provider-clipboard-wayland", + "topologicalRank" : 12, + "validation" : "New module eval tests pass and tests/static.sh example iteration has no references to removed option paths.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/{main,configuration,api_catalog,authz,providers,controllers,ownership,watches,cleanup,zone_links,budgets,store}.rs`" + ], + "detailedDesign" : "One fixed process, isolated handlers, async ResourceClient, health/startup/restart Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-core-controllers" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-core-001", + "kind" : "work-item", + "owner" : "W0/W1a; core-controller owner", + "parallelGroup" : "wi:ADR-046-core-controllers", + "prerequisites" : [ + "ADR-046-core-controllers" + ], + "specId" : "ADR-046-core-controllers", + "topologicalRank" : 9, + "validation" : "Per-handler unit/property tests plus multi-process startup/restart", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/tests/system_core_coordination.rs`" + ], + "detailedDesign" : "Coordination-only acceptance: prove the fixed core controller loads the manifest/audit boundary from ADR046-system-core-001 and routes Host/User reconciliation to the canonical ADR046-exec-003/004 handlers after ADR046-exec-005 bootstrap ordering. This item defines no schema, handler, status, capability, or `packages/d2b-provider-system-core/src/{host,user}.rs` destination. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt only the integration acceptance.", + "entryContracts" : [ + "ADR-046-core-controllers" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-core-002", + "kind" : "work-item", + "owner" : "ADR046-core-001, ADR046-exec-003, ADR046-exec-004, ADR046-exec-005, ADR046-system-core-001; coordination-only owner", + "parallelGroup" : "wi:core-controller-coordination:w6", + "prerequisites" : [ + "ADR-046-core-controllers", + "ADR046-core-001", + "ADR046-exec-003", + "ADR046-exec-004", + "ADR046-exec-005", + "ADR046-system-core-001" + ], + "specId" : "ADR-046-core-controllers", + "topologicalRank" : 14, + "validation" : "Coordination test asserts one Host handler owner, one User handler owner, manifest load after bootstrap ordering, and no duplicate Host/User destination or reconcile path in core-controller", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-credential-entra/src/{lib.rs,controller.rs,service.rs,controller_main.rs,agent_main.rs,audit.rs,telemetry.rs}` and corresponding tests/integration docs" + ], + "detailedDesign" : "Implement secret-free controller/helper; require Credential base `identityGuestRef` and `loginEndpointRef`; resolve dependency alias `entra-login-token`; validate same-Zone Guest placement and exact `consumerRef`; implement typed `EntraTokenLeaseClient` whose production implementation is the Entrablau Endpoint; implement `BeginLogin`/`ObserveLogin`/`CancelLogin` projection to Credential base interaction status; admit service calls only when the same exact allowed operation (`acquire-token`, `refresh-token`, `revoke-token`, or `inspect-metadata`) is present in both `Credential.spec.allowedOperations` and the canonical `use-credential` Role `subresources`; require matching `create`, `update-spec`, or `delete` under `admin-credential` in addition to ordinary CRUD, with no Credential-specific Role fields or coarse aliases; emit the login Endpoint with canonical `visibility = provider` and an exact `consumerPolicy` for the orchestration Provider and configured consumer; route access-token leases end-to-end from Entrablau service to exact consumer over Noise_KK; keep all refresh/login/TPM state inside the Entrablau Guest; declare no Provider state Volume; reject Host placement and all ambient fallback chains; enforce Credential name/ResourceRef/UID/digest as audit-only observable identity, with only `resource_name_digest` retained in authorized bounded audit and no Credential identity in status/errors/logs/OTEL Resource or span attributes/metric labels. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt tests/types where non-secret; replace production token acquisition with Entrablau Endpoint client.", + "entryContracts" : [ + "ADR-046-provider-credential-entra" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cred-entra-001", + "kind" : "work-item", + "owner" : "`ADR046-credential-001` (Credential base fields and status), `ADR046-credential-002` (`d2b.credential.v3` service), D092 Endpoint, D093 Entrablau identity Guest decision, credential-entra owner", + "parallelGroup" : "wi:ADR-046-provider-credential-entra", + "prerequisites" : [ + "ADR-046-provider-credential-entra", + "ADR046-credential-001", + "ADR046-credential-002" + ], + "specId" : "ADR-046-provider-credential-entra", + "topologicalRank" : 11, + "validation" : "See §13", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-credential-managed-identity/src/{lib.rs, controller.rs, agent.rs, service.rs, audit.rs, telemetry.rs}`; `packages/d2b-provider-credential-managed-identity/{controller/main.rs, agent/main.rs}`; `packages/d2b-provider-credential-managed-identity/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs, topology.rs}`; `packages/d2b-provider-credential-managed-identity/integration/{container-service.sh, host-guest-placement.nix, aca-credential-ref.sh, cleanup-rollback.sh}`; `packages/d2b-provider-credential-managed-identity/README.md`" + ], + "detailedDesign" : "(1) Adapt `ManagedIdentityCredentialProvider` to `d2b.credential.v3` service interface; split controller and agent roles (see `ADR046-mi-topology-001`). (2) Enforce `ManagedIdentityCredentialOwner::ExactSdkConsumer` in agent via `AuthenticatedSubjectContext` from ComponentSession, independently of scope fields. (3) Reject `user-agent` placement: `scope.domainFilter=user` returns `credential-placement-mismatch` before agent spawn. (4) Validate `clientId` using `OpaqueAzureRef::parse` from v3 baseline; artifact IDs match `^[a-z][a-z0-9-]*$`. (5) Validate `imdsEndpointAlias` against closed enum `{azure-imds, azure-imds-aca}`; project into LaunchTicket at spawn time (never into Process spec config or env); co-located runtime Provider constructs `ManagedIdentityCredentialClient` from LaunchTicket projection and supplies via effect port; resolved URL never in any output surface. (6) Agent Process declares `networkUsage.allowEgress=false`; uses canonical Process template shape (see `ADR046-mi-topology-001` design item 6). (7) Reject `sign-challenge` with `credential-schema-invalid` immediately. (8) Map `ManagedIdentityClientState::Unavailable` to `credential-provider-unavailable`; no `InteractionRequired` state. (9) Implement `ManagedIdentityLeaseHandle` as opaque bounded newtype with redacted `Debug`. (10) All token bytes held by injected client in agent; delivered only via agent-terminated `Noise_KK` delivery session. (11) Integrate with Provider resource descriptor and controller toolkit. (12) Confirm `credential_canary` never appears in any service response, status field, delivery record outer header, or audit record. (13) Apply D087 status-first state: declare no Provider state Volume, keep ProviderStateSet empty, and write only bounded non-secret lease observation to `Credential.status` plus the Operation ledger. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-provider-credential-managed-identity" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cred-mi-001", + "kind" : "work-item", + "owner" : "`ADR046-credential-001` (contracts), `ADR046-credential-002` (service); `ADR046-reconcile-001`; `ADR046-mi-topology-001`; `credential-managed-identity` crate owner", + "parallelGroup" : "wi:ADR-046-provider-credential-managed-identity", + "prerequisites" : [ + "ADR-046-provider-credential-managed-identity", + "ADR046-credential-001", + "ADR046-credential-002", + "ADR046-mi-topology-001", + "ADR046-reconcile-001" + ], + "specId" : "ADR-046-provider-credential-managed-identity", + "topologicalRank" : 12, + "validation" : "See §Tests. Run `cargo test -p d2b-provider-credential-managed-identity --lib --test lifecycle --test conformance --test faults --test canary --test delivery --test placement --test topology`; run `integration/{container-service.sh,aca-credential-ref.sh,cleanup-rollback.sh}` through `make test-integration` and `integration/host-guest-placement.nix` through `make test-host-integration`.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-credential-managed-identity/src/controller.rs; packages/d2b-contracts/src/v3/credential_controller.rs" + ], + "detailedDesign" : "Managed-identity-specific controller design: implement async reconcile and agent spawn/teardown from §Async reconcile; enforce system-only domain; spawn agent on Credential admission plus dependency-ready, not on `phase=Ready`; implement `observeInterval=30s` health-check RPC to the agent, which calls `InspectMetadata` on the injected client; controller never calls IMDS; derive idempotency key as `SHA-256(UID || \":\" || rotationGeneration.to_le_bytes() || \":\" || operation_class_byte)`; enforce `MAX_LOCAL_LEASES=256` in the resource store; implement Deleted-phase closure by clearing `provider-revoke` only after agent Process deletion and revocation confirmation while core/audit own Deleted revision and deletion record. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt shared Credential controller lifecycle to managed-identity controller/agent spawn and teardown.", + "entryContracts" : [ + "ADR-046-provider-credential-managed-identity" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cred-mi-002", + "kind" : "work-item", + "owner" : "ADR046-credential-001, ADR046-credential-002; ADR046-reconcile-001, ADR046-reconcile-002; ADR046-mi-topology-001; owner: Credential controller toolkit and managed-identity controller", + "parallelGroup" : "wi:ADR-046-provider-credential-managed-identity", + "prerequisites" : [ + "ADR-046-provider-credential-managed-identity", + "ADR046-credential-001", + "ADR046-credential-002", + "ADR046-mi-topology-001", + "ADR046-reconcile-001", + "ADR046-reconcile-002" + ], + "specId" : "ADR-046-provider-credential-managed-identity", + "topologicalRank" : 12, + "validation" : "Managed-identity reconcile/controller tests; shared Credential reconciliation tests; topology tests validating agent spawn/teardown and Deleted-phase cleanup", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "nixos-modules/options-resources.nix; nixos-modules/activation-nixos-cleanup.nix; integration/aca-credential-ref.sh" + ], + "detailedDesign" : "Shared Nix and cleanup: implement Nix eval-time assertions 1–12 from §Eval-time assertions, closed enum schema for `imdsEndpointAlias`, `clientId` validation via `OpaqueAzureRef` charset, generation cleanup contract, and artifact catalog validation for `credential-managed-identity-bin`. Integration fixture asserts that the migrated ACA Provider config carries `credentialRef: \"Credential/aca-relay-mi\"` and the raw `managed_identity_client_id` string field is absent from rendered Provider config JSON and ACA runtime bundle. Primary reuse disposition: `replace`. Preserved source-plan detail: replace raw ACA managed identity client-id fields with Credential resource references and v3 Provider/Credential Nix emission.", + "entryContracts" : [ + "ADR-046-provider-credential-managed-identity" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cred-mi-003", + "kind" : "work-item", + "owner" : "Depends on ADR046-credential-001, ADR046-credential-002, and ADR046-cred-mi-001; owner: Nix resource compiler and activation cleanup", + "parallelGroup" : "wi:ADR-046-provider-credential-managed-identity", + "prerequisites" : [ + "ADR-046-provider-credential-managed-identity", + "ADR046-cred-mi-001", + "ADR046-credential-001", + "ADR046-credential-002" + ], + "specId" : "ADR-046-provider-credential-managed-identity", + "topologicalRank" : 13, + "validation" : "Nix eval assertion tests; artifact catalog validation tests; `integration/aca-credential-ref.sh`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-credential-managed-identity/src/{audit.rs,telemetry.rs}; packages/d2b-contract-tests/tests/credential_audit.rs" + ], + "detailedDesign" : "Shared audit/OTEL: emit audit records for all methods and controller events per §Audit, with Credential identity represented only by the authorized bounded `resource_name_digest`; emit OTEL spans and metrics per §OTEL and metrics with no Credential resource name, ResourceRef, UID, digest, derived identity token, Zone/Credential/resource-name-derived label, or non-allowlisted OTEL Resource attribute; retain applicable generic collector-allowlisted Resource attributes (`d2b.zone`, `d2b.provider`, `d2b.component`, and service fields); report expiry as the minimum for each provider/placement aggregate; add `d2b_credential_imds_calls_total` counter with bounded `alias` label; enforce `contains_sensitive_shape` on all string fields in audit records and metric labels; add canary tests for `managed-identity-canary`, `credential_canary`, `imds-endpoint-canary`, Credential name/ref/UID/digest, and Zone name in `canary.rs`. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing sensitive-shape guard and canary pattern to v3 audit, OTEL, and metric surfaces.", + "entryContracts" : [ + "ADR-046-provider-credential-managed-identity" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cred-mi-004", + "kind" : "work-item", + "owner" : "Depends on ADR046-cred-mi-001 and ADR046-mi-topology-001; owner: credential-managed-identity audit/telemetry implementation", + "parallelGroup" : "wi:ADR-046-provider-credential-managed-identity", + "prerequisites" : [ + "ADR-046-provider-credential-managed-identity", + "ADR046-cred-mi-001", + "ADR046-mi-topology-001" + ], + "specId" : "ADR-046-provider-credential-managed-identity", + "topologicalRank" : 13, + "validation" : "`packages/d2b-contract-tests/tests/credential_audit.rs` requires `resource_name_digest` in authorized audit records and rejects raw Credential name/ResourceRef/UID; managed-identity `canary.rs` and audit/OTEL unit tests structurally assert exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `credential_ref`, `credential_uid`, `credential_digest`, `resource_name_digest`, and every resource-name-derived label key; reject Credential name/ref/UID/digest canaries from all OTEL Resource attributes, span attributes, and metric labels; preserve generic collector-allowlisted Resource attributes including `d2b.zone`, `d2b.provider`, `d2b.component`, and service fields; reject Zone-name span/label canaries and sensitive shapes; pass complete managed-identity metric/span frames through the shared collector ingress validator and prove that adding `d2b.credential.name` or any Credential identity key/value rejects the whole frame", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-contracts/src/v3/credential.rs" + ], + "detailedDesign" : "Contract types: define `CredentialSpec`, `CredentialStatus`, `CredentialLeaseHandle`, `OperationClass`, `PlacementBinding`, `CredentialConditionType`, and serde/validation/redaction helpers. Reuse `OpaqueAzureRef` from the v3 baseline. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt current credential/status concepts into v3 ResourceType DTOs; reuse `OpaqueAzureRef` directly where applicable.", + "entryContracts" : [ + "ADR-046-provider-credential-secret-service" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cred-ss-001", + "kind" : "work-item", + "owner" : "Dependency for ADR046-cred-ss-003; owner: `packages/d2b-contracts` Credential ResourceType contract", + "parallelGroup" : "wi:ADR-046-provider-credential-secret-service", + "prerequisites" : [ + "ADR-046-provider-credential-secret-service" + ], + "specId" : "ADR-046-provider-credential-secret-service", + "topologicalRank" : 11, + "validation" : "Credential ResourceType schema/serde/redaction tests from `ADR-046-resources-credential`; credential-secret-service conformance consumes the shared types", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-contracts/proto/v3/credential.proto; packages/d2b-credential-service/" + ], + "detailedDesign" : "Service proto: define the `d2b.credential.v3` protobuf/ttrpc service and generate typed client/server code. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `create`. Preserved source-plan detail: net-new service contract replacing the v2 in-process `CredentialProvider` trait.", + "entryContracts" : [ + "ADR-046-provider-credential-secret-service" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cred-ss-002", + "kind" : "work-item", + "owner" : "Dependency for ADR046-cred-ss-003; owner: credential service contract/codegen", + "parallelGroup" : "wi:ADR-046-provider-credential-secret-service", + "prerequisites" : [ + "ADR-046-provider-credential-secret-service" + ], + "specId" : "ADR-046-provider-credential-secret-service", + "topologicalRank" : 11, + "validation" : "Generated-code compile tests and credential service contract tests from `ADR-046-resources-credential`; credential-secret-service lifecycle/delivery tests consume the generated service", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-credential-secret-service/src/{lib.rs, controller.rs, service.rs, main.rs}`; `packages/d2b-provider-credential-secret-service/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}`; `packages/d2b-provider-credential-secret-service/integration/{container-service.sh, host-placement.nix, guest-placement.nix, cleanup-rollback.sh}`; `packages/d2b-provider-credential-secret-service/README.md`" + ], + "detailedDesign" : "Adapt `SecretServiceCredentialProvider` and `SecretServiceCredentialProviderFactory` to v3 `d2b.credential.v3` service; replace v2 `CredentialProvider` trait with v3 controller/service handler; retain `Oo7SecretServicePort` trait methods unchanged; ensure `SecretServiceOwner::Userd` placement guard rejects system-domain and guest-agent construction; validate `collectionAlias` against provider-internal charset (not `OpaqueAzureRef`; collection aliases may include spaces); integrate with Provider resource descriptor and controller toolkit; test that `credential_canary` never appears in any service response; create a Process resource per `(Zone, User, executionRef)` triple with `template = \"secret-service-controller\"` (plain string), canonical `sandbox` fields (`namespaceClasses`, `capabilityClasses`, `seccompClass`, `noNewPrivileges`, `startRoot`, `environmentClass`, `readOnlyRoot`), `budget` with nested `cpu`/`memory`/`pids`/`fds` sub-fields, `networkUsage: null`, no inline endpoint fields, an owned credential-service `Endpoint` resource, and `readiness.class = \"provider-defined\"`; component descriptor declares the required authenticated `dbus-session` FD attachment carried privately by the LaunchTicket; D087 status-first state model: no Provider state Volume is declared, ProviderStateSet is optional/query-time and empty, no Volume mount or layout principal is required, and the storage-need test is not met; bounded non-secret lease/acquisition/retry observation lives in `Credential.status` plus the core Operation ledger; any opaque status handle is non-secret, non-authorizing, bounded, safe for authorized status readers, and independently revalidated; no token/object-path/lease bytes persist anywhere; finalize() emits revoke outcome audit but MUST NOT emit the resource-deleted closure audit (audit subsystem only); controller writes only scoped Credential/Process health (core aggregates Provider status) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-provider-credential-secret-service" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cred-ss-003", + "kind" : "work-item", + "owner" : "`ADR046-cred-ss-001` (contract types); `ADR046-cred-ss-002` (service proto); `ADR046-reconcile-001`; credential-secret-service owner", + "parallelGroup" : "wi:ADR-046-provider-credential-secret-service", + "prerequisites" : [ + "ADR-046-provider-credential-secret-service", + "ADR046-cred-ss-001", + "ADR046-cred-ss-002", + "ADR046-reconcile-001" + ], + "specId" : "ADR-046-provider-credential-secret-service", + "topologicalRank" : 12, + "validation" : "See §16", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-credential-/src/controller.rs" + ], + "detailedDesign" : "Controller toolkit: implement the common Credential controller handler conforming to the `ADR-046-resource-reconciliation` async loop. Secret-service-specific controller code plugs into this handler while keeping provider bytes out of status/store/audit. Primary reuse disposition: `create`. Preserved source-plan detail: net-new shared controller handler pattern specialized by each Credential Provider.", + "entryContracts" : [ + "ADR-046-provider-credential-secret-service" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cred-ss-004", + "kind" : "work-item", + "owner" : "Dependency for ADR046-cred-ss-003; owner: common Credential controller/reconciliation toolkit", + "parallelGroup" : "wi:ADR-046-provider-credential-secret-service", + "prerequisites" : [ + "ADR-046-provider-credential-secret-service" + ], + "specId" : "ADR-046-provider-credential-secret-service", + "topologicalRank" : 11, + "validation" : "Shared reconciliation tests from `ADR-046-resources-credential`; credential-secret-service lifecycle/fault tests verify the handler integration", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "nixos-modules/options-resources.nix; nixos-modules/activation-nixos-cleanup.nix" + ], + "detailedDesign" : "Nix compiler: implement `d2b.zones..resources.` authoring, eval-time assertions, canonical JSON emission, artifact catalog, bundle digest, and generation cleanup contract. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `create`. Preserved source-plan detail: net-new Nix resource emission and cleanup contract.", + "entryContracts" : [ + "ADR-046-provider-credential-secret-service" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cred-ss-005", + "kind" : "work-item", + "owner" : "Dependency for ADR046-cred-ss-003; owner: Nix resource compiler and activation cleanup", + "parallelGroup" : "wi:ADR-046-provider-credential-secret-service", + "prerequisites" : [ + "ADR-046-provider-credential-secret-service" + ], + "specId" : "ADR-046-provider-credential-secret-service", + "topologicalRank" : 11, + "validation" : "Nix eval/assertion/golden tests from `ADR-046-resources-credential`; credential-secret-service cleanup rollback integration fixture", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-credential-secret-service/src/{audit.rs,telemetry.rs}" + ], + "detailedDesign" : "Audit/OTEL: emit authorized bounded audit records with Credential identity represented only by `resource_name_digest`, and emit OTEL spans/metrics for all credential service methods and controller events with canary enforcement, expiry aggregated across user-agent leases, no Credential resource name, ResourceRef, UID, digest, derived identity token, Zone/Credential/resource-name-derived metric label, or non-allowlisted OTEL Resource attribute; retain applicable generic collector-allowlisted Resource attributes (`d2b.zone`, `d2b.provider`, `d2b.component`, and service fields); no token/object-path/lease bytes in status, delivery outer headers, audit, metrics, spans, or logs. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt zero-secret invariant and canary test pattern to credential-secret-service audit/OTEL surfaces.", + "entryContracts" : [ + "ADR-046-provider-credential-secret-service" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-cred-ss-006", + "kind" : "work-item", + "owner" : "Dependency for ADR046-cred-ss-003; owner: credential-secret-service audit and telemetry implementation", + "parallelGroup" : "wi:ADR-046-provider-credential-secret-service", + "prerequisites" : [ + "ADR-046-provider-credential-secret-service" + ], + "specId" : "ADR-046-provider-credential-secret-service", + "topologicalRank" : 11, + "validation" : "Credential audit/OTEL tests from `ADR-046-resources-credential` require `resource_name_digest` in authorized audit records and reject raw Credential name/ResourceRef/UID; `tests/canary.rs` structurally asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `credential_ref`, `credential_uid`, `credential_digest`, `resource_name_digest`, and every resource-name-derived metric key; Credential name/ref/UID/digest canaries are absent from all OTEL Resource attributes, span attributes, and metric labels; Zone-name canaries are absent from spans and labels while generic collector-allowlisted Resource attributes remain; complete secret-service metric/span frames pass the shared collector ingress validator, while adding `d2b.credential.name` or any Credential identity key/value rejects the whole frame; `tests/delivery.rs` covers credential-secret-service delivery", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/credential.rs`" + ], + "detailedDesign" : "Define `CredentialSpec`, `CredentialStatus`, `CredentialLeaseHandle` (opaque bounded newtype), `CredentialRotationPolicy`, `CredentialRevocationPolicy`, `CredentialScope`, `OperationClass` enum, `CredentialLeaseState`, `PlacementBinding`, `CredentialConditionType`, and all serde/validation/redaction helpers; reuse `OpaqueAzureRef` from v3 baseline directly; enforce zero-secret-bytes charset validation on all string fields at construction; prevent Credential name/ResourceRef/UID or any identity-derived digest from serializing into any status layer or typed error Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-credential" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-credential-001", + "kind" : "work-item", + "owner" : "`ADR046-object-001` (resource envelope); `ADR046-identities-001` (types); W0 shared contract root; `d2b-contracts`", + "parallelGroup" : "wi:ADR-046-resources-credential", + "prerequisites" : [ + "ADR-046-resources-credential", + "ADR046-identities-001", + "ADR046-object-001" + ], + "specId" : "ADR-046-resources-credential", + "topologicalRank" : 9, + "validation" : "Schema golden vectors; charset/length tests; serde unknown-field rejection; `OpaqueAzureRef` round-trip and secret-shape rejection parity; `leaseHandle` and `sourceVersion` opaque newtype tests; status/error redaction tests with Credential name/ResourceRef/UID/digest canaries", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/proto/v3/credential.proto`; `packages/d2b-credential-service/src/{service.rs, client.rs, server.rs}`" + ], + "detailedDesign" : "Define `d2b.credential.v3` protobuf service with exactly five operation methods: `AcquireToken`, `RefreshToken`, `RevokeToken`, `SignChallenge`, and `InspectMetadata`; map those methods one-to-one to the same exact operation class in `spec.allowedOperations` and the existing Role `subresources` field (`acquire-token`, `refresh-token`, `revoke-token`, `sign-challenge`, or `inspect-metadata`) under the canonical `use-credential` resource verb; derive the operation class from the method rather than accepting a caller-selected operation-class field; each request carries `credential_ref`, `operation_id`, `idempotency_key`, `requested_expiry_unix_ms`, `deadline_unix_ms`; map administrative lifecycle to matching `admin-credential` subresources `create`, `update-spec`, and `delete`, supplemental to ordinary CRUD, never to a new Role field or method-name alias; `RevokeToken` and `InspectMetadata` responses carry only non-secret metadata (leaseHandle digest, rotationGeneration, sourceVersion, expiresAtUnixMs, state, outcome code); `AcquireToken`, `RefreshToken`, and `SignChallenge` responses additionally include a `delivery_session_params` field carrying the binding contract fields required to establish the end-to-end credential-delivery ComponentSession (see §Credential-delivery endpoint contract); the token bytes themselves travel in the separate Noise-encrypted delivery session, never in the outer DTO; strict unknown-field rejection; bounded message sizes; all record wrappers for delivery sessions must be zeroizing types", + "entryContracts" : [ + "ADR-046-resources-credential" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-credential-002", + "kind" : "work-item", + "owner" : "`ADR046-credential-001`; `ADR046-api-001` (resource API); `ADR046-bus-001` (d2b-bus); Credential service owner", + "parallelGroup" : "wi:ADR-046-resources-credential", + "prerequisites" : [ + "ADR-046-resources-credential", + "ADR046-api-001", + "ADR046-bus-001", + "ADR046-credential-001" + ], + "specId" : "ADR-046-resources-credential", + "topologicalRank" : 10, + "validation" : "Protocol golden vectors for each method; Role matrix for the five exact allowed-operation subresources under `use-credential` and exact `create`, `update-spec`, and `delete` subresources under `admin-credential`; prove admin permission is supplemental to ordinary CRUD; deny empty/wildcard/unknown/mismatched subresources, alternate Credential-operation Role fields, and method-name aliases before Provider dispatch; malformed/oversize rejection; `leaseHandle` opacity tests (secret-canary must not appear in outer DTO or delivery routing metadata); locked/unavailable/denied/expired state tests; delivery session binding contract round-trip; zeroizing record type unit tests; delivery channel never materialized in non-delivery method tests", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-credential-secret-service/src/{lib.rs, controller.rs, service.rs, main.rs}` (implementation + binary); `packages/d2b-provider-credential-secret-service/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}` (hermetic Cargo integration); `packages/d2b-provider-credential-secret-service/integration/{container-service.sh, host-placement.nix, cleanup-rollback.sh}` (orchestration fixtures); `packages/d2b-provider-credential-secret-service/README.md` (all §Provider README required sections)" + ], + "detailedDesign" : "Adapt `SecretServiceCredentialProvider` and `SecretServiceCredentialProviderFactory` to use v3 `d2b.credential.v3` service interface; replace v2 `CredentialProvider` trait impl with v3 controller/service handler; adapt `Oo7SecretServicePort` trait (retain all methods unchanged); ensure `SecretServiceOwner::Userd` placement restriction rejects system-domain and guest-agent construction; validate `collectionAlias` against provider-internal charset (not `OpaqueAzureRef`; collection aliases may include spaces); integrate with Provider resource descriptor and controller toolkit; test `credential_canary` never appears in any service response Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-resources-credential" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-credential-003", + "kind" : "work-item", + "owner" : "`ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`; credential-secret-service owner", + "parallelGroup" : "wi:ADR-046-resources-credential", + "prerequisites" : [ + "ADR-046-resources-credential", + "ADR046-credential-001", + "ADR046-credential-002", + "ADR046-reconcile-001" + ], + "specId" : "ADR-046-resources-credential", + "topologicalRank" : 11, + "validation" : "**`src/` unit** (`#[cfg(test)]` in `src/`): `Oo7SecretServicePort` trait API surface, `SecretServiceOwner` placement guard, `collectionAlias` charset, `lockPolicy` state transitions. **`tests/` Cargo integration** (`cargo test -p d2b-provider-credential-secret-service`): copied test suite from main with v3 type substitutions; add `lifecycle.rs` (acquire/refresh/revoke/inspect end-to-end with `FakeOo7Port`); `conformance.rs` (all 11 `check_provider_conformance` arms pass); `faults.rs` (locked state → `credential-provider-unavailable`, unavailable, cardinality limit); `canary.rs` (`credential_canary` and `object_path_canary` absent from every response, status field, and delivery record); `delivery.rs` (delivery-session binding contract, zeroizing buffer, replay-safe sequence); `placement.rs` (system-domain and guest-agent construction rejected). **`integration/` fixtures**: `container-service.sh` (container-backed Provider service start/stop/drain); `host-placement.nix` (user-domain Host/Process placement in runNixOSTest); `cleanup-rollback.sh` (Nix-generation removal triggers async Delete and Provider-revoke finalizer).", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-credential-entra/src/{lib.rs, controller.rs, service.rs, main.rs}`; `packages/d2b-provider-credential-entra/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}`; `packages/d2b-provider-credential-entra/integration/{container-service.sh, guest-placement.nix, cleanup-rollback.sh}`; `packages/d2b-provider-credential-entra/README.md` (all §Provider README required sections)" + ], + "detailedDesign" : "Adapt `EntraCredentialProvider` and `EntraCredentialProviderFactory` to v3 service; replace v2 `AgentPlacementBinding` with v3 `PlacementBinding` enum (user-agent, guest-agent only; reject host-system); validate `tenantId` config field using `OpaqueAzureRef::parse` from v3 baseline `d2b-realm-provider/src/credential.rs` (note: current v3 source field is named via `AzureControlPlaneRef`; target field name is `tenantId`); retain `EntraCredentialClient` trait unchanged; map `EntraClientError::InteractionRequired` to `credential-provider-unavailable` (not denied); enforce `EntraCredentialOwner::ExactConsumer` so only the declared `consumerRef` may acquire; enforce canonical Credential verb/subresource admission, provider-visible Endpoint plus exact consumer policy, and audit-only Credential identity as frozen in the dedicated Entra dossier Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-resources-credential" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-credential-004", + "kind" : "work-item", + "owner" : "`ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`; credential-entra owner", + "parallelGroup" : "wi:ADR-046-resources-credential", + "prerequisites" : [ + "ADR-046-resources-credential", + "ADR046-credential-001", + "ADR046-credential-002", + "ADR046-reconcile-001" + ], + "specId" : "ADR-046-resources-credential", + "topologicalRank" : 11, + "validation" : "**`src/` unit**: `EntraCredentialClient` trait API, `OpaqueAzureRef::parse` on `tenantId`, `EntraCredentialOwner::ExactConsumer` guard, `EntraClientState` transitions. **`tests/` Cargo integration**: `lifecycle.rs` (acquire/refresh/revoke/inspect with `FakeEntraClient`); `conformance.rs` (all conformance arms, canonical Endpoint visibility/consumer policy, and exact Role verb/subresource mapping); `faults.rs` (interaction-required → unavailable, generation-mismatch, colocated-consumer and mismatched Role-subresource rejection); `canary.rs` (secret plus Credential name/ResourceRef/UID/digest canaries absent from status/errors/logs and every OTEL Resource/span attribute/metric label; authorized audit retains only `resource_name_digest`); `delivery.rs` (delivery-session binding, zeroizing, replay-safe); `placement.rs` (host-system placement rejected). **`integration/` fixtures**: `container-service.sh`; `guest-placement.nix` (user-domain and system-domain Process on Guest in runNixOSTest); `cleanup-rollback.sh`.", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-credential-managed-identity/src/{lib.rs, controller.rs, service.rs, main.rs}`; `packages/d2b-provider-credential-managed-identity/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}`; `packages/d2b-provider-credential-managed-identity/integration/{container-service.sh, host-guest-placement.nix, aca-credential-ref.sh, cleanup-rollback.sh}`; `packages/d2b-provider-credential-managed-identity/README.md` (all §Provider README required sections)" + ], + "detailedDesign" : "Adapt `ManagedIdentityCredentialProvider` to v3 service; enforce `ManagedIdentityCredentialOwner::ExactSdkConsumer`; reject user-agent placement (IMDS is machine-local, not user-session); validate `clientId` config using `OpaqueAzureRef::parse` directly from v3 baseline (note: current v3 source field is named via `ManagedIdentityRef.client_id`; target field name is `clientId`); retain `ManagedIdentityCredentialClient` trait unchanged; map `ManagedIdentityClientState::Unavailable` to `credential-provider-unavailable`; `sign-challenge` operation class returns schema-invalid immediately Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-resources-credential" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-credential-005", + "kind" : "work-item", + "owner" : "`ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`; credential-managed-identity owner", + "parallelGroup" : "wi:ADR-046-resources-credential", + "prerequisites" : [ + "ADR-046-resources-credential", + "ADR046-credential-001", + "ADR046-credential-002", + "ADR046-reconcile-001" + ], + "specId" : "ADR-046-resources-credential", + "topologicalRank" : 11, + "validation" : "**`src/` unit**: `ManagedIdentityCredentialClient` trait, `OpaqueAzureRef::parse` on `clientId`, `ManagedIdentityCredentialOwner::ExactSdkConsumer` guard, `imdsEndpointAlias` validation, `sign-challenge` schema-invalid fast path. **`tests/` Cargo integration**: `lifecycle.rs` (acquire/refresh/revoke/inspect with `FakeClient`); `conformance.rs`; `faults.rs` (unavailable state, colocated-consumer rejection); `canary.rs` (canary absent from all responses and delivery records); `delivery.rs`; `placement.rs` (user-agent placement rejected). **`integration/` fixtures**: `container-service.sh`; `host-guest-placement.nix` (system-domain Host and Guest placement in runNixOSTest); `aca-credential-ref.sh` (ACA Provider config uses `credentialRef`; raw `managed_identity_client_id` absent); `cleanup-rollback.sh`.", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-credential-/src/controller.rs`; `packages/d2b-contracts/src/v3/credential_controller.rs`" + ], + "detailedDesign" : "Implement Credential controller handler conforming to `ADR-046-resource-reconciliation` async loop; implement `reconcile`, `observe`, `finalize`, `drain`, and `health` handlers; implement rotation state machine (proactive/on-expiry/on-demand policies); implement `provider-revoke` finalizer execution with `revocation.onOwnerDelete` policy; implement provider-generation-change detection and revocation; implement `CredentialReady`, `RotationDue`, `ProviderUnavailable`, `LeaseRevoked` condition logic; use controller Roles with the exact allowed-operation subresource matching each `use-credential` service call, while status/finalizer writes retain their separate structural ownership checks; never add a Credential-specific Role field or coarse alias; implement bounded idempotency key derivation (Credential UID + rotationGeneration + operation class, no secret material); implement `observeInterval=30s` health check calling `InspectMetadata`; bounded retry/backpressure with typed `credential-rotation-failed` outcome; enforce `MAX_LOCAL_LEASES=256` per controller provider instance", + "entryContracts" : [ + "ADR-046-resources-credential" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-credential-006", + "kind" : "work-item", + "owner" : "`ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`, `ADR046-reconcile-002`; Credential controller owner", + "parallelGroup" : "wi:ADR-046-resources-credential", + "prerequisites" : [ + "ADR-046-resources-credential", + "ADR046-credential-001", + "ADR046-credential-002", + "ADR046-reconcile-001", + "ADR046-reconcile-002" + ], + "specId" : "ADR-046-resources-credential", + "topologicalRank" : 11, + "validation" : "Controller state-machine golden vectors; rotation-policy matrix (proactive/on-demand/on-expiry × success/locked/unavailable/expired); exact Role-subresource admission matrix; finalizer execution tests; provider-generation-change revocation tests; idempotency key derivation tests; observe-interval drift detection test; canary tests confirm zero secret bytes and no Credential name/ResourceRef/UID/digest in any controller-written status or error", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/options-resources.nix` (generic schema-derived resource options; not type-specific), `nixos-modules/activation-nixos-cleanup.nix`" + ], + "detailedDesign" : "**(1) Schema-derived options and eval-time validation**: implement `d2b.zones..resources. = { type = \"...\"; spec = { ... }; }` as a generic attrset option; an optional `metadata` sub-attr may contain `ownerRef` and/or presentation `labels`/`annotations`. Nix option types, defaults, and inline docs for `spec` fields are generated from the committed `ResourceTypeSchema` JSON (`docs/reference/schemas/v3/credential.json`) and the signed Provider schema — no bespoke options module is maintained separately. `metadata.name` derives from the attr key; `metadata.zone` from the Zone attr key; `apiVersion` defaults to `resources.d2bus.org/v3`; `status`/`uid`/`generation`/`revision`/timestamps/`managedBy`/`configurationGeneration` are not authored. Core assigns `metadata.managedBy = \"configuration\"` and `metadata.configurationGeneration = ` to all configuration-managed resources at create/update time; these are never authored in Nix. Eval-time assertions (applied to all entries with `type = \"Credential\"`): `spec.providerRef` resolves a Provider in the same Zone whose `credentialDomains` includes `spec.scope.domainFilter` and whose `spec.artifactId` (a sibling of `spec.config` on the Provider resource, not inside `spec.config`) resolves an artifact catalog entry of `type = \"provider\"`; `spec.audience` charset (`^[A-Za-z0-9._:/@-]+$`, max 256); `spec.rotation.proactiveWindowMs < maxLeaseLifetimeMs / 2`; `spec.consumerRef`/`scope.executionRef`/`scope.userRef` resolve declared Zone resources; duplicate `(providerRef, executionRef, userRef, audience)` tuple rejected; `contains_sensitive_shape` on all string fields; Provider-specific placement constraints; `allowedOperations` ⊆ `providerRef.supportedOperations`. **(2) Canonical JSON and bundle emission**: render `spec` attr directly to `spec` object in canonical JSON (no field renames/re-nesting); `metadata` in output contains only derived `name`/`zone` and optionally Nix-authored `ownerRef`/`labels`/`annotations`; `apiVersion` is top-level, not inside `metadata`; `finalizers` is omitted from the Nix-rendered input (core manages finalizers, never accepts them from the bundle); no management labels are emitted by Nix; sort bundle by `(type, name)`; write to `/etc/d2b/zones//resource-bundle.json` with digest. **(2b) Artifact catalog emission**: derivation-valued inputs (`d2b.artifacts.`) are compiled separately into an integrity-pinned artifact catalog (`/etc/d2b/zones//artifact-catalog.json`) with its own digest header; each entry records `id`, `type`, `sha256`, and bounded closure metadata; store paths are private catalog implementation data absent from the resource bundle, status, audit, and logs; `activation-nixos` verifies both digests before any create/update; missing or wrong-type `artifactId` references fail the NixOS build. **(3) Build-time schema validation**: validate rendered JSON against `docs/reference/schemas/v3/credential.json` and Provider-specific schema; enforce `secretRef` fields use `Credential/` refs; enforce no store paths in any resource bundle or status output; drift gate (`make test-drift`) regenerates schemas with `cargo xtask gen-schemas` and asserts `git diff --exit-code`; Nix options module drift checked in the same gate. **(4) Generation transition and cleanup contract**: activation-nixos controller verifies SHA-256 digest of both resource bundle and artifact catalog, creates/updates desired-set resources, activates without blocking on cleanup, issues async Delete for absent configuration-managed resources (those with `metadata.managedBy = \"configuration\"`), sets Degraded/Cleanup=True on removed resources; retains up to `retainedConfigurationMax` (default 3, range 1..16) prior bundles; oldest prune when count exceeded; no time-based rollback window. **(5) Configuration-managed vs controller/API isolation**: `managedBy` and `ownerRef` are orthogonal; configuration-managed and API-created resources may each carry an optional same-Zone `ownerRef` and participate in owner cascade. Cleanup checks `metadata.managedBy = \"configuration\"` before issuing Delete; resources with `metadata.managedBy = \"controller\"` or `metadata.managedBy = \"api\"` are never deleted by this path; API-created resources persist until explicit Delete and are never generation-swept.", + "entryContracts" : [ + "ADR-046-resources-credential" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-credential-007", + "kind" : "work-item", + "owner" : "`ADR046-credential-001`; `ADR046-identities-002` (Nix resource compiler); `ADR046-api-001` (resource API, for create/update/delete); `ADR046-reconcile-001` (activation-nixos controller); Nix integrator", + "parallelGroup" : "wi:ADR-046-resources-credential", + "prerequisites" : [ + "ADR-046-resources-credential", + "ADR046-api-001", + "ADR046-credential-001", + "ADR046-identities-002", + "ADR046-reconcile-001" + ], + "specId" : "ADR-046-resources-credential", + "topologicalRank" : 10, + "validation" : "**(eval/build)**: nix-unit golden JSON envelope for each example (spec shape, no management labels in Nix output, sort, digest); assertion-failure tests for secret-shaped audience, mismatched providerRef/domainFilter, proactiveWindow > half maxLifetime, duplicate binding tuple, unresolved refs; generated activation Role contains `admin-credential` with exact `subresources = [ \"create\" \"update-spec\" \"delete\" ]`, no coarse alias, and no undeclared Role field; permission tests prove each lifecycle action also requires its ordinary CRUD verb; artifact catalog: assertion-failure for missing `artifactId`, wrong-type `artifactId`, duplicate catalog ID; bundle + artifact catalog digest round-trip; artifact catalog store-path absence from resource bundle and status; Provider-specific schema cross-check; `make test-drift` schema drift gate. **(runtime integration in `tests/host-integration/`)**: `credential-cleanup-basic` (removed resource reaches Deleted); `credential-cleanup-nonblocking` (activation Ready before cleanup finalizer finishes); `credential-cleanup-pending-status` (Cleanup=True on removed resource, PendingCleanup=True on Provider); `credential-cleanup-stalled` (Degraded stall detection and recovery); `credential-cleanup-controller-children-preserved` (ownerRef children cleaned by Credential controller); `credential-cleanup-no-dynamic-deletion` (controller-created Credential with `managedBy = \"controller\"` not deleted); `credential-retained-generation-count` (up to retainedConfigurationMax bundles retained; rollback re-creates from retained bundle; oldest pruned when count exceeded); `credential-bundle-digest-mismatch` (tampered bundle aborts activation).", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-credential-/src/audit.rs`, `telemetry.rs`; `packages/d2b-contract-tests/tests/credential_audit.rs`" + ], + "detailedDesign" : "Implement audit record emission for all credential service methods and controller events using the field set defined in §Audit, with Credential identity represented only by the authorized bounded `resource_name_digest` after authorization and no identity-bearing record elicited by a denied request; implement OTEL span/metric emission using the closed semantic label set in §OTEL and metrics, with expiry reported as a provider/placement aggregate and no Credential resource name, ResourceRef, UID, digest, derived identity token, Zone/resource-name-derived label, or non-allowlisted OTEL Resource attribute; retain applicable generic collector-allowlisted Resource attributes (`d2b.zone`, `d2b.provider`, `d2b.component`, and service fields); implement `contains_sensitive_shape` checks in all string fields of audit records and metric label values (adapted from `d2b-realm-provider/src/error.rs:contains_sensitive_shape`); add canary-enforcement tests that verify `\"secret-canary\"`, `\"entra-token-canary\"`, `\"managed-identity-canary\"`, Credential name/ref/UID/digest canaries, and Zone name values never appear in any metric label, span attribute, log line, status field, error, or collector diagnostic, and that Credential identity canaries are also absent from OTEL Resource attributes", + "entryContracts" : [ + "ADR-046-resources-credential" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-credential-008", + "kind" : "work-item", + "owner" : "`ADR046-credential-001`, `ADR046-credential-006`; audit/OTEL integrator", + "parallelGroup" : "wi:ADR-046-resources-credential", + "prerequisites" : [ + "ADR-046-resources-credential", + "ADR046-credential-001", + "ADR046-credential-006" + ], + "specId" : "ADR-046-resources-credential", + "topologicalRank" : 12, + "validation" : "Canary tests across all three Provider crates; authorized audit record field-presence tests require `resource_name_digest` and reject raw Credential name/ResourceRef/UID, while denied-request tests emit no identity-bearing audit; structural metric descriptor tests assert exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `credential_ref`, `credential_uid`, `credential_digest`, `resource_name_digest`, and every resource-name-derived key; Credential name/ref/UID/digest canaries are absent from every status field, error, log/Debug line, collector diagnostic, OTEL Resource attribute, span attribute, and metric label; Zone-name canaries are absent from spans and labels while resource-attribute tests preserve the generic collector allowlist including `d2b.zone`, `d2b.provider`, `d2b.component`, and service fields; complete Credential metric/span frames pass the shared collector ingress validator, while adding `d2b.credential.name` or any Credential identity key/value rejects the whole frame", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`docs/specs/ADR-046-decision-register.md`" + ], + "detailedDesign" : "Record every evidence-underdetermined choice before dependent spec work proceeds", + "entryContracts" : [ + "ADR-046-decision-register" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-decisions-001", + "kind" : "work-item", + "owner" : "ADR 0046 integrator", + "parallelGroup" : "wi:ADR-046-decision-register", + "prerequisites" : [ + "ADR-046-decision-register" + ], + "specId" : "ADR-046-decision-register", + "topologicalRank" : 1, + "validation" : "Zero unresolved entries before pre-panel review; manifest/link consistency", + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/heavy_gate.rs`; `Makefile` targets `heavy-check`, `heavy-test-integration`, `heavy-test-host-integration`, `heavy-test-hardware`, `heavy-cargo-test`, `heavy-flake-check`" + ], + "detailedDesign" : "Two-slot per-UID OFD-locked semaphore, 250 ms nonblocking retry up to 30 minutes, fail-closed on unsupported locking, duplicated locked-FD handoff to child, wrapper-owned group-signal/reap, as specified in §11 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt paths/crate names to this repository's `packages/xtask` layout.", + "entryContracts" : [ + "ADR-046-validation-and-delivery" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-delivery-001", + "kind" : "work-item", + "owner" : "`ADR046-W0`; delivery-tooling integrator", + "parallelGroup" : "wi:ADR-046-validation-and-delivery", + "prerequisites" : [ + "ADR-046-validation-and-delivery" + ], + "specId" : "ADR-046-validation-and-delivery", + "topologicalRank" : 13, + "validation" : "Unit tests for slot acquisition/timeout/fail-closed paths; integration test spawning two concurrent heavy-gate invocations and asserting the second blocks until the first releases", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/delivery/snapshot.rs`" + ], + "detailedDesign" : "Binds base/head OIDs, dependency graph, repository set into `candidate_id`/`content_id`/`snapshot_sha256` per §12.1 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt.", + "entryContracts" : [ + "ADR-046-validation-and-delivery" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-delivery-002", + "kind" : "work-item", + "owner" : "`ADR046-W0`; delivery-tooling integrator", + "parallelGroup" : "wi:ADR-046-validation-and-delivery", + "prerequisites" : [ + "ADR-046-validation-and-delivery" + ], + "specId" : "ADR-046-validation-and-delivery", + "topologicalRank" : 13, + "validation" : "Unit tests asserting identical inputs produce identical digests and any single-byte content change produces a different `content_id`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/delivery/validate_import.rs`; external candidate-ID-addressed evidence directory (never under Git)" + ], + "detailedDesign" : "Imports CI/local/host validator command/result evidence, keyed by `candidate_id`, per §12.2 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt.", + "entryContracts" : [ + "ADR-046-validation-and-delivery" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-delivery-003", + "kind" : "work-item", + "owner" : "`ADR046-delivery-002`; delivery-tooling integrator", + "parallelGroup" : "wi:ADR-046-validation-and-delivery", + "prerequisites" : [ + "ADR-046-validation-and-delivery", + "ADR046-delivery-002" + ], + "specId" : "ADR-046-validation-and-delivery", + "topologicalRank" : 14, + "validation" : "Test asserting evidence for a stale `candidate_id` is rejected; test asserting raw command output never lands in a tracked file", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/gen_spec_set.rs`; `docs/specs/ADR-046-spec-set.json`, `docs/specs/ADR-046-work-items.json`" + ], + "detailedDesign" : "Enumerates every `docs/specs/ADR-046-*.md` and `docs/specs/providers/ADR-046-provider-*.md` file, its metadata table, bytewise-sorted `workItemPrefixes` registry, content digest, and every `### ADR046--` work item, per §8. Each prefix belongs globally to exactly one member, and generation resolves ownership only through the registry rather than by splitting IDs or filenames. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (new generator, following the existing `xtask gen-schemas`/`gen-nix-options` pattern already used for other generated artifacts).", + "entryContracts" : [ + "ADR-046-validation-and-delivery" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-delivery-004", + "kind" : "work-item", + "owner" : "`ADR046-delivery-002`; spec-set integrator", + "parallelGroup" : "wi:ADR-046-validation-and-delivery", + "prerequisites" : [ + "ADR-046-validation-and-delivery", + "ADR046-delivery-002" + ], + "specId" : "ADR-046-validation-and-delivery", + "topologicalRank" : 14, + "validation" : "Golden-fixture test against a small synthetic spec directory; drift test against the real `docs/specs/` tree", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/delivery/panel.rs`" + ], + "detailedDesign" : "`panel-request` writes the candidate-bound request naming the exact ten roles and required model; `panel-attest` validates a directory of exactly ten strict 13-field records, rejecting wrong model/candidate binding, duplicate provider/run provenance, or inconsistent `signoff`/`recommendations`, per §12.3 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt to bind the fixed `gemini-3.1-pro-preview`/`github-copilot` model/provider pair and this repository's existing ten-role roster (§12.3).", + "entryContracts" : [ + "ADR-046-validation-and-delivery" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-delivery-005", + "kind" : "work-item", + "owner" : "`ADR046-delivery-002`, `ADR046-delivery-003`; panel-tooling integrator", + "parallelGroup" : "wi:ADR-046-validation-and-delivery", + "prerequisites" : [ + "ADR-046-validation-and-delivery", + "ADR046-delivery-002", + "ADR046-delivery-003" + ], + "specId" : "ADR-046-validation-and-delivery", + "topologicalRank" : 15, + "validation" : "Unit tests for every rejection class (wrong model, missing role, duplicate run_id, `signoff:true` with non-empty `recommendations`); integration test with ten synthetic valid records passing", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/delivery/{seal,eligibility,history_proof}.rs`" + ], + "detailedDesign" : "`seal` requires all ten panel records unanimous and bound to the same candidate/content/snapshot digests plus every validator lane passing; `merge-eligibility` checks each stacked PR's current base/head against the sealed OIDs or a passing history-proof; `history_proof` verifies byte-identical integrated content/generated artifacts/dependency diff/repository set across a rebase, per §12.4/§12.6 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt.", + "entryContracts" : [ + "ADR-046-validation-and-delivery" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-delivery-006", + "kind" : "work-item", + "owner" : "`ADR046-delivery-002`, `ADR046-delivery-004`, `ADR046-delivery-005`; delivery-tooling integrator", + "parallelGroup" : "wi:ADR-046-validation-and-delivery", + "prerequisites" : [ + "ADR-046-validation-and-delivery", + "ADR046-delivery-002", + "ADR046-delivery-004", + "ADR046-delivery-005" + ], + "specId" : "ADR-046-validation-and-delivery", + "topologicalRank" : 16, + "validation" : "Unit tests for seal rejection on any missing/mismatched record; integration test proving a history-only rebase with identical content passes `history_proof` and reuses panel evidence, while any content change fails it", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/test_runtime_ledger.rs`; a `make`-invokable timing gate reusing `make test-rust`/Layer-1 shard targets" + ], + "detailedDesign" : "Measures execution-only time (after build, warm cache) per test/crate/shard against §10.16 budgets, records the reference runner/repetitions/p95, reports the top slow tests, applies a historical regression threshold, and emits a machine-readable CI artifact; the placement lint rejects a hermetic-tier test that sleeps, spawns a process, or touches network/containers/DBus/systemd/broker/Nix/KVM/hardware/live cloud, and the deterministic-clock/sleep lint rejects wall-clock sleep/retry in `src/`/`tests/`", + "entryContracts" : [ + "ADR-046-validation-and-delivery" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-delivery-007", + "kind" : "work-item", + "owner" : "`ADR046-W0`; delivery-tooling integrator", + "parallelGroup" : "wi:ADR-046-validation-and-delivery", + "prerequisites" : [ + "ADR-046-validation-and-delivery" + ], + "specId" : "ADR-046-validation-and-delivery", + "topologicalRank" : 13, + "validation" : "Policy self-tests: an intentional slow/sleep/process/network hermetic test is rejected; a synthetic timing regression fails the gate; parallel isolation holds under shuffled/parallel execution; a retired legacy selector is absent from `tests/layer1-jobs.json`, closed gate manifests, and CI shards", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`docs/specs/ADR-046-implementation-graph.json`, `docs/specs/ADR-046-implementation-graph.md` (generated by `ADR046-streamline-001`'s `xtask implementation-graph`); the artifact contract, generation, validation, and ready-wave query are owned by §3.5 of this spec" + ], + "detailedDesign" : "Owns the D095 implementation-graph contract: `artifactKind`/`schemaVersion`/`adr`/`status`; one node per member spec and per work item mapped exactly once to a `W0`–`W7` wave and a file-disjoint `parallelGroup`, with `owner`/`destinations`/`entryContracts`/`prerequisites`/`blockers`/`exitGate`/`topologicalRank`; work-item nodes additionally embed the manifest's exact `detailedDesign` and `validation` text byte-for-byte; typed `spec-depends-on`/`shared-contract`/`work-item-depends-on`/`implements-spec`/`file-overlap-order` edges; the §3.5.1 ready-wave query; and the anti-serialization invariant that every ready file-disjoint group launches concurrently while a same-wave dependency is a prep barrier, not whole-wave serialization. The graph is a generated non-member artifact and does not change the 55-member `ADR-046-spec-set.json` count. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (D095 artifact contract).", + "entryContracts" : [ + "ADR-046-validation-and-delivery" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-delivery-008", + "kind" : "work-item", + "owner" : "`ADR046-streamline-001`, `ADR046-W0`; delivery-tooling integrator", + "parallelGroup" : "wi:ADR-046-validation-and-delivery", + "prerequisites" : [ + "ADR-046-validation-and-delivery", + "ADR046-streamline-001" + ], + "specId" : "ADR-046-validation-and-delivery", + "topologicalRank" : 13, + "validation" : "Every 55 spec node and every work item present exactly once; all edge endpoints resolve; graph acyclic; waves monotonic (dependencies earlier or explicit same-wave prep barrier); parallel groups claim no ordering absent a dependency/file-overlap edge; deterministic JSON with no timestamps/host paths; every Mermaid node ID valid; the ready-wave query returns the expected concurrently-launchable groups on a seeded fixture", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/gen_spec_set.rs`; `packages/d2b-contract-tests/tests/policy_adr046_work_items.rs`; generated spec-set, work-item, and implementation-graph drift checks" + ], + "detailedDesign" : "Parse every normative member's exact level-three `### ADR046--` headings and tables; `##` or `####` item declarations are invalid. Require an exact Markdown/manifest bijection; exact `specId` and `specPath`; a bytewise-sorted, nonempty `workItemPrefixes` list for each item-owning member; global one-member ownership for every registered prefix; registry-based ID ownership; three-digit nonzero ordinals; every mandatory field exactly once and nonempty; one closed scalar `reuseAction`; and `reuseSource: null` for `create`. Reject dropped, extra, malformed, wrong-level, duplicate, ambiguous, heuristic-split, unregistered-prefix, or unconsumed items before writing any artifact. Validate all dependency endpoints, DAG acyclicity, wave monotonicity, and single-wave parallel groups before atomically publishing all generated files.", + "entryContracts" : [ + "ADR-046-validation-and-delivery" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-delivery-009", + "kind" : "work-item", + "owner" : "`ADR046-delivery-004`, `ADR046-delivery-008`; spec-set policy-test owner", + "parallelGroup" : "wi:ADR-046-validation-and-delivery", + "prerequisites" : [ + "ADR-046-validation-and-delivery", + "ADR046-delivery-004", + "ADR046-delivery-008" + ], + "specId" : "ADR-046-validation-and-delivery", + "topologicalRank" : 15, + "validation" : "Fixtures fail for a dropped heading, `##`/`####` item heading, extra manifest row, duplicate ID, duplicate cross-member prefix, unsorted/empty required prefix registry, wrong owner/path/prefix, heuristic-only prefix match, two-digit/zero ordinal, missing/duplicate mandatory field, free-form/compound action, `create` with a reuse source, dangling dependency, cyclic DAG, backward-wave dependency, and cross-wave parallel group; the exact 55-spec real tree passes with every item once", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/device.rs`" + ], + "detailedDesign" : "Device ResourceType schema (spec/status/conditions/claims/inventory); closed-set error codes; Device RBAC verbs; broker operation effect-limit constants; shared Device telemetry contract requires fixed semantic metric labels with no Zone/resource-name-derived identity and retains `d2b.zone`/`d2b.provider` only as OTEL resource attributes. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-device" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-001", + "kind" : "work-item", + "owner" : "W0 shared contract root; `d2b-contracts`", + "parallelGroup" : "wi:ADR-046-resources-device", + "prerequisites" : [ + "ADR-046-resources-device" + ], + "specId" : "ADR-046-resources-device", + "topologicalRank" : 10, + "validation" : "Schema golden vectors; unknown-field denial; exclusive/shared conflict rejection; arbitration/maxClaims invariant; cross-Provider structural descriptor test asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus Device/Zone-name canary absence while preserving `d2b.zone` resource attributes", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-tpm/src/` (controller, swtpm runner, state-dir logic); `packages/d2b-provider-device-tpm/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-tpm/integration/` (container/Host scenarios); `packages/d2b-provider-device-tpm/README.md`" + ], + "detailedDesign" : "Device spec/status; flush EphemeralProcess → swtpm Process sequencing; state-dir hardening; tamper-marker; finalizer non-deletion of Volume; Nix emitter; all four required crate paths present (see \"Provider crate layout\") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-device" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-002", + "kind" : "work-item", + "owner" : "ADR046-device-001; device-tpm provider owner", + "parallelGroup" : "wi:ADR-046-resources-device", + "prerequisites" : [ + "ADR-046-resources-device", + "ADR046-device-001" + ], + "specId" : "ADR-046-resources-device", + "topologicalRank" : 11, + "validation" : "`src/`: swtpm argv golden, state-dir, flush sequencing, finalizer no-delete; `tests/`: `controller_state_machine.rs`, `conformance.rs`, `fault_swtpm_missing.rs`; `integration/`: `provision_and_reboot/`, `tamper_marker_survives/`, `finalizer_no_delete/`; workspace policy check: `make test-policy` passes with all four paths present", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-usbip/src/` (controller, daemon Process, bind/unbind EphemeralProcess, firewall); `packages/d2b-provider-device-usbip/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-usbip/integration/` (container/Host scenarios); `packages/d2b-provider-device-usbip/README.md`" + ], + "detailedDesign" : "Device spec/status; bus ID validation; firewall rule ownership-marker; bind/unbind EphemeralProcess; per-Device daemon Process (owned by device-usbip; Network supplies dependency/firewall interface); Nix emitter; all four required crate paths present (see \"Provider crate layout\") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-device" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-003", + "kind" : "work-item", + "owner" : "ADR046-device-001; device-usbip provider owner", + "parallelGroup" : "wi:ADR-046-resources-device", + "prerequisites" : [ + "ADR-046-resources-device", + "ADR046-device-001" + ], + "specId" : "ADR-046-resources-device", + "topologicalRank" : 11, + "validation" : "`src/`: bus ID corpus, firewall marker format, EphemeralProcess creation; `tests/`: `arbitration_conflict.rs`, `conformance.rs`, `firewall_marker.rs`, `explicit_attach_split.rs`; `integration/`: `arbitration_conflict/`, `busid_bind_cycle/`, `network_firewall_coexistence/`; workspace policy check: `make test-policy` passes with all four paths present", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-security-key/src/` (controller, relay Process, guest frontend Process, lease/session ring); `packages/d2b-provider-device-security-key/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-security-key/integration/` (container/Host/Guest scenarios); `packages/d2b-provider-device-security-key/README.md`" + ], + "detailedDesign" : "Device spec/status; unprivileged relay Process (`device--sk-relay`); guest frontend Process (`device--sk-frontend`, `executionRef: Guest/`); ceremony/CID/lease/session ring (max 1 session per Device); broker hidraw-only access; mandatory Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` claim shared with every USB Provider before effects; Nix emitter; all four required crate paths present (see \"Provider crate layout\") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-device" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-004", + "kind" : "work-item", + "owner" : "ADR046-device-001; device-security-key provider owner", + "parallelGroup" : "wi:ADR-046-resources-device", + "prerequisites" : [ + "ADR-046-resources-device", + "ADR046-device-001" + ], + "specId" : "ADR-046-resources-device", + "topologicalRank" : 11, + "validation" : "`src/`: lease transitions, session ring eviction, broker op path-free, CID round-trip; `tests/`: `lease_state_machine.rs`, `session_ring.rs`, `mutual_exclusion.rs` proves security-key and USB implementations resolve one token to a byte-identical physical backing key and the loser receives `physical-usb-backing-conflict` before any effect, `conformance.rs`, `guest_frontend_process.rs`; `integration/`: `lease_acquire_cancel/`, `session_ring_capacity/`, `guest_frontend_connect/`; workspace policy check: `make test-policy` passes with all four paths present", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-gpu/src/` (controller, GPU/render-node/video worker Processes, broker token set); `packages/d2b-provider-device-gpu/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-gpu/integration/` (container/Host/Guest scenarios); `packages/d2b-provider-device-gpu/README.md`" + ], + "detailedDesign" : "Combined Device spec/status; GPU worker Process (`device--gpu`, exclusive); render-node Process (`device--render-node`, exclusive default, shared when explicit); video-decoder Process (`device--video`); broker token set; wire-contract constants; shared render-node arbitration enforcement; Nix emitter; all four required crate paths present (see \"Provider crate layout\") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-device" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-005", + "kind" : "work-item", + "owner" : "ADR046-device-001; device-gpu provider owner", + "parallelGroup" : "wi:ADR-046-resources-device", + "prerequisites" : [ + "ADR-046-resources-device", + "ADR046-device-001" + ], + "specId" : "ADR-046-resources-device", + "topologicalRank" : 11, + "validation" : "`src/`: process role selection, wire-constant snapshot, render-node vs full-GPU branching; `tests/`: `combined_reconcile.rs`, `render_node_enforcement.rs`, `wire_constant_snapshot.rs`, `conformance.rs`; `integration/`: `gpu_worker_start/`, `render_node_shared/`, `video_dependency/`; workspace policy check: `make test-policy` passes with all four paths present", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/resources-device.nix`; `nixos-modules/bundle-artifacts.nix` (bundle emission for resource store); `nixos-modules/assertions.nix` (six eval-time device assertions)" + ], + "detailedDesign" : "Nix authoring shape `d2b.zones..resources. = { type = \"Device\"; metadata.ownerRef = ...; spec = { ...exact ResourceSpec fields... }; };` as specified in \"Nix configuration\" section; `metadata.name`/`metadata.zone`/`apiVersion` derived automatically; `status` and Core management fields (`managedBy`, `configurationGeneration`, uid, generation, revision, timestamps, finalizers) omitted from emitted JSON; `spec` field names/types/defaults identical to ResourceTypeSchema with no renaming; per-Provider `spec.provider.settings` validated against signed Provider schema; no `artifactId` or store-path fields in Device `spec.provider.settings` — binary paths are resolved from Provider package closures; Credential-ref enforcement; artifact catalog emitted as a separate private integrity-pinned map (ID→type/digest/closure) by its own emitter; six eval-time validation assertions; canonical sorted-key full resource-envelope JSON emission (`apiVersion`, `type`, `metadata`, `spec` only); Zone resource bundle with `contentDigest` as specified in \"Zone resource bundle/generation\" section; Core sets `metadata.managedBy=configuration` and `metadata.configurationGeneration` at activation", + "entryContracts" : [ + "ADR-046-resources-device" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-006", + "kind" : "work-item", + "owner" : "ADR046-device-001 through ADR046-device-005; Nix integrator", + "parallelGroup" : "wi:ADR-046-resources-device", + "prerequisites" : [ + "ADR-046-resources-device", + "ADR046-device-001", + "ADR046-device-002", + "ADR046-device-003", + "ADR046-device-004", + "ADR046-device-005" + ], + "specId" : "ADR-046-resources-device", + "topologicalRank" : 12, + "validation" : "nix-unit: `device-tpm-eval.nix`, `device-usbip-eval.nix`, `device-security-key-eval.nix`, `device-gpu-eval.nix`, `device-schema-validation.nix`, `device-gen-cleanup-eval.nix`, `device-bundle-canonical.nix`, `device-inline-secret-rejected.nix`, `device-artifact-catalog.nix`; contract tests: `device_resource_schema.rs`, `device_bundle_canonical.rs`, `device_gen_cleanup.rs`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/configuration.rs`; `packages/d2b-contract-tests/tests/device_gen_cleanup.rs`" + ], + "detailedDesign" : "Implement the cleanup contract described in \"Zone generation and cleanup\": (1) on new generation activation, diff `resources` against prior generation's `resources` by (type, name) — resources absent from new generation that have `managedBy=configuration` go into `pendingDeletion`; (2) Zone phase transitions to `Degraded/pending-cleanup` until all items in `pendingDeletion` reach terminal Delete; (3) items in `pendingDeletion` with `managedBy=controller` or `managedBy=api` are rejected with `cleanup-config-ownership-mismatch`; (4) `managedBy=controller` and `managedBy=api` resources are never touched by generation cleanup — `cleanup-controller-resource-protected` emitted if attempted; `managedBy=api` resources persist until explicit Delete; (5) prior generations retained by count: default 3, range 1..16, configured via `d2b.zones..retainedGenerations`; (6) each deletion is non-blocking; (7) finalizer-stuck timeout emits `cleanup-finalizer-stuck` and leaves Zone in `Degraded/pending-cleanup`; (8) all deletions emit `config-resource-deletion-requested` audit record with digested resource identity", + "entryContracts" : [ + "ADR-046-resources-device" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-007", + "kind" : "work-item", + "owner" : "ADR046-device-006; Zone runtime implementer", + "parallelGroup" : "wi:core-config-hub:w5", + "prerequisites" : [ + "ADR-046-resources-device", + "ADR046-core-001", + "ADR046-device-006" + ], + "specId" : "ADR-046-resources-device", + "topologicalRank" : 13, + "validation" : "Fast hermetic (D094): in-process Rust selectors under the owning crate `tests/` — `packages/d2b-core-controller/tests/gen_cleanup.rs`, `packages/d2b-core-controller/tests/gen_cleanup_controller_protected.rs`, `packages/d2b-core-controller/tests/gen_cleanup_audit.rs` — driven by a fake Zone store/clock with deterministic ordering and no wall-clock sleep; slower coverage reuses the existing integration directory scenarios `tests/integration/containers/device-gen-cleanup.sh`, `device-controller-resource-protected.sh`, and `device-gen-cleanup-audit.sh`; feasibility proof: generation cleanup with fake Zone runtime. No new top-level shell gate is added.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/main.rs` (`check-provider-layout` subcommand); `packages/d2b-contract-tests/tests/workspace_policy.rs` (provider-layout policy assertions)" + ], + "detailedDesign" : "Add `cargo xtask check-provider-layout`: enumerate workspace members matching `d2b-provider-*`; for each, assert `src/`, `tests/`, `integration/`, and `README.md` all exist relative to the crate root; report all missing paths before failing; no opt-out flag. Add companion test in `workspace_policy.rs` that asserts the same invariant against the static crate list in `Cargo.toml`. Wire `cargo xtask check-provider-layout` into `make test-policy` alongside existing workspace naming and sort checks. The check must also run in CI as part of the policy gate. Primary reuse disposition: `adapt`. Preserved source-plan detail: extend.", + "entryContracts" : [ + "ADR-046-resources-device" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-008", + "kind" : "work-item", + "owner" : "ADR046-device-002 through ADR046-device-005; workspace/tooling maintainer", + "parallelGroup" : "wi:ADR-046-resources-device", + "prerequisites" : [ + "ADR-046-resources-device", + "ADR046-device-002", + "ADR046-device-003", + "ADR046-device-004", + "ADR046-device-005" + ], + "specId" : "ADR-046-resources-device", + "topologicalRank" : 12, + "validation" : "Policy gate passes once all four Device Provider crates exist with required paths; gate fails predictably when any path is removed from a Provider crate fixture; test fixture crate with one missing path must produce a named error identifying the exact missing path", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-tpm/{src/,tests/,integration/README.md,README.md}; Cargo workspace membership" + ], + "detailedDesign" : "Crate scaffold: create `packages/d2b-provider-device-tpm/` with `src/`, `tests/`, `integration/README.md`, and `README.md`; add it to the Cargo workspace; workspace policy test must pass. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new crate scaffold; later items adapt baseline TPM behavior into the new Provider boundary.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-001", + "kind" : "work-item", + "owner" : "P0; unblocked; owner: `d2b-provider-device-tpm` crate", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 11, + "validation" : "Workspace policy test for required crate paths and Cargo workspace membership", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-tpm/src/{effect_port.rs,effect_impl.rs}; packages/d2b-provider-device-tpm/tests/effect_fake.rs" + ], + "detailedDesign" : "TpmEffectPort and FakeTpmEffectPort: implement the effect trait, typed TPM EndpointRef handoff, and fake test port. Prove non-test files contain no `use d2b_priv_broker::` and the controller sees only opaque resource IDs and EndpointRefs. Primary reuse disposition: `wrap`. Preserved source-plan detail: wrap privileged effects behind an injected async `TpmEffectPort`; keep broker operations only behind `volume-local` and `system-minijail`.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-002", + "kind" : "work-item", + "owner" : "P0; blocked by ADR046-device-tpm-001; owner: device-tpm effect boundary", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-001" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 12, + "validation" : "`tests/effect_fake.rs`; static proof that non-test files do not import `d2b_priv_broker`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-tpm/src/controller.rs; packages/d2b-provider-device-tpm/tests/controller_fsm.rs" + ], + "detailedDesign" : "Controller reconcile state machine: implement the Device reconcile algorithm from §11.1 against `FakeTpmEffectPort`, covering happy path, Volume not-ready, marker fail-closed, flush failure, swtpm maxRestarts, and finalizer behavior where Process is deleted and Volume retained; emit §14 metrics with only closed `phase`/`outcome`/`status` semantics and no Zone/resource-name-derived label. Primary reuse disposition: `replace`. Preserved source-plan detail: replace direct daemon lifecycle with Provider reconcile against `FakeTpmEffectPort` and resource status.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-003", + "kind" : "work-item", + "owner" : "P0; blocked by ADR046-device-tpm-002; owner: device-tpm controller FSM", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-002" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 13, + "validation" : "`tests/controller_fsm.rs` covering happy path, Volume not-ready, marker fail-closed, flush failed, swtpm maxRestarts, and finalizer behavior; `tests/metrics_labels.rs` structurally asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys and Device/Zone-name canary absence", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-tpm/src/resources.rs; packages/d2b-provider-device-tpm/tests/volume_create.rs" + ], + "detailedDesign" : "Controller-created Volume spec: implement `build_tpm_state_volume_spec` with `cleanupPolicy: never`, `repairPolicy: fail-closed`, `adoptionPolicy: quarantine-on-ambiguity`, `sensitivity: secret-adjacent`, required invariants, `source.sourceId`, no `hostPath`, no top-level identityMarker/persistenceClass/quotaBytes/stateSchema, `ownerRef: Device/`, `managedBy: controller`, empty attachments, and `quota: null`. Primary reuse disposition: `replace`. Preserved source-plan detail: replace VM-level TPM option/state path with controller-created Device-owned Volume spec.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-004", + "kind" : "work-item", + "owner" : "P0; blocked by ADR046-device-tpm-001; owner: device-tpm resource builders", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-001" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 12, + "validation" : "`tests/volume_create.rs` proving every canonical Volume field and forbidden field listed in this item", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-tpm/src/resources.rs; Process spec tests under packages/d2b-provider-device-tpm/tests/" + ], + "detailedDesign" : "Canonical swtpm Process spec: implement `build_swtpm_process_spec` with `readOnlyRoot: true`, `userNamespace.mappingClass: process-principal-root`, namespace classes `[pid, mount, user]`, empty capability classes, `seccompClass: w1-swtpm`, two Device-owned Endpoint resources (`tpm` and `ctrl`), `mounts[0].required: true`, and no socket path, binary path, UID integer, or GID integer in any spec field. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract swtpm argv/sandbox intent into canonical Process resources; remove caller-supplied binary path, UID, GID, and socket path fields.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-005", + "kind" : "work-item", + "owner" : "P0; blocked by ADR046-device-tpm-004; owner: device-tpm Process resource builder", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-004" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 13, + "validation" : "Process spec golden tests proving all required and forbidden fields; preserved `minijail_swtpm_video.rs` contract tests", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-tpm/src/resources.rs; packages/d2b-provider-device-tpm/tests/flush_mandatory.rs" + ], + "detailedDesign" : "Mandatory flush EphemeralProcess spec: implement `build_flush_ephemeral_process_spec`; no `startupClear` field exists; flush is always created before swtpm Process start with no skip path; TTLs are `successfulTtl: \"1h\"` and `failedTtl: \"24h\"`; no userNamespace on flush Process; deadlines are `startDeadline: \"30s\"` and `runtimeDeadline: \"60s\"`. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract flush intent into mandatory EphemeralProcess resource; remove configurable `startupClear` path and caller-supplied binary path fields.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-006", + "kind" : "work-item", + "owner" : "P0; blocked by ADR046-device-tpm-003; owner: device-tpm EphemeralProcess resource builder", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-003" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 14, + "validation" : "`tests/flush_mandatory.rs` plus contract proof that user-NS is long-lived only", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-tpm/src/status.rs; packages/d2b-provider-device-tpm/tests/{endpoint_ref.rs,redaction.rs}" + ], + "detailedDesign" : "Device status builder: implement `build_device_status`; `tpmEndpointRef` is an `Endpoint/` ResourceRef with no opaque endpoint ID compatibility alias and never a filesystem path; `stateVolumeRef` and `swtpmProcessRef` are canonical ResourceRef strings; no path, socket name, UID, GID, PID, or pidfd appears in status; `markerStatus` is one of `verified`, `missing`, `replaced`, or `unknown`. Primary reuse disposition: `create`. Preserved source-plan detail: net-new bounded Device status projection; do not reuse path/socket/UID/GID/PID observations.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-007", + "kind" : "work-item", + "owner" : "P1; blocked by ADR046-device-tpm-003; owner: device-tpm status builder", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-003" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 14, + "validation" : "`tests/endpoint_ref.rs`; `tests/redaction.rs`; status builder tests for allowed `markerStatus` values", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-tpm/src/{effect_port.rs,status.rs}; packages/d2b-provider-device-tpm/integration/guest_endpoint.rs" + ], + "detailedDesign" : "EndpointRef handoff: hermetic tests prove `tpmEndpointRef` is an EndpointRef and never a path; integration proves Guest runtime Provider reads `tpmEndpointRef` and obtains the socket fd from the Zone runtime endpoint resolver with no path string in Guest spec or LaunchTicket API surface. Primary reuse disposition: `replace`. Preserved source-plan detail: replace path handoff with EndpointRef and Zone runtime endpoint resolver fd acquisition.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-008", + "kind" : "work-item", + "owner" : "P1; blocked by ADR046-device-tpm-007; owner: device-tpm endpoint handoff integration", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-007" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 15, + "validation" : "`tests/endpoint_ref.rs`; `integration/guest_endpoint.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-tpm/tests/marker_fail_closed.rs; packages/d2b-provider-device-tpm/integration/marker_tamper.rs" + ], + "detailedDesign" : "Marker fail-closed test: FakeTpmEffectPort returning `markerStatus: replaced` makes Device Failed, prevents a second `ensure_state_volume` call, and prevents swtpm Process creation. Integration physically replaces `swtpm/`; volume-local sets Volume Failed; Device fails with no auto-recovery. Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve fail-closed marker semantics while moving ownership to Volume/Device resources.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-009", + "kind" : "work-item", + "owner" : "P0; blocked by ADR046-device-tpm-004; owner: device-tpm marker/fail-closed tests", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-004" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 13, + "validation" : "`tests/marker_fail_closed.rs`; `integration/marker_tamper.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-tpm/src/resources.rs; packages/d2b-provider-device-tpm/tests/controller_process.rs" + ], + "detailedDesign" : "Controller Process (status-first; no Provider state Volume): implement the controller Process spec from §4.1 with `processClass: controller`, `readOnlyRoot: true`, empty mounts, no controller-scratch namespace, no scratch mount, no `User/device-tpm-controller-system` state-layout principal, no permission to create Provider-owned Volumes, and restart re-derivation from resource store plus external marker/process observations while treating status as observation. Primary reuse disposition: `create`. Preserved source-plan detail: net-new status-first controller Process spec; no Provider state Volume is reused or created.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-010", + "kind" : "work-item", + "owner" : "P1; blocked by ADR046-device-tpm-001; owner: device-tpm controller Process descriptor", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-001" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 12, + "validation" : "Controller Process spec tests proving the bullets in this item", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "nixos-modules/options-resources.nix and Nix eval/golden tests for §17.1 Device JSON" + ], + "detailedDesign" : "Nix roundtrip test: Device Nix spec from §17.1 round-trips through the Nix emitter to expected resource JSON; emitted bundle contains no Volume, Process, or EphemeralProcess resources because controller-managed resources are not in the Nix bundle. Primary reuse disposition: `replace`. Preserved source-plan detail: replace VM-level TPM enable option with v3 Device resource emission and assertions.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-011", + "kind" : "work-item", + "owner" : "P1; blocked by ADR046-device-tpm-001; owner: Nix Resource compiler for Device declarations", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-001" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 12, + "validation" : "Nix roundtrip/golden test for §17.1 and emitted-bundle absence of controller-managed resources", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-tpm/src/controller.rs; packages/d2b-provider-device-tpm/tests/finalizer.rs" + ], + "detailedDesign" : "Finalizer: Volume retained on Device deletion. Hermetic tests cover Device deletion finalizer leading to swtpm Process deletion, TPM state Volume not deleted because `cleanupPolicy: never`, Volume persists, Core emits `phase=Deleted` for Device after finalizer clears, and audit carries no path/UID. Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve TPM identity retention while moving deletion sequencing to Resource finalizers.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-012", + "kind" : "work-item", + "owner" : "P0; blocked by ADR046-device-tpm-003; owner: device-tpm finalizer lifecycle", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-003" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 14, + "validation" : "`tests/finalizer.rs` proving Process deletion, retained Volume, core Deleted phase, and redacted audit", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2bd/src/*; packages/d2b-core/src/processes.rs; packages/d2b-provider-device-tpm/src/; packages/d2b-host/src/swtpm_argv.rs" + ], + "detailedDesign" : "Remove direct broker references: remove pre-ADR-0046 daemon swtpm broker call sites, retire `ProcessRole::Swtpm` and `ProcessRole::SwtpmPreStartFlush`, move argv builders from `d2b-host/src/swtpm_argv.rs` to `d2b-provider-device-tpm/src/` with binary path fields removed, while retaining `d2b-priv-broker/src/ops/swtpm_dir.rs` for `volume-local` and `spawn_runner.rs` for `system-minijail`. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: remove direct broker references from daemon and move argv builders into the Provider with binary path fields removed; retain broker ops only behind resource providers.", + "entryContracts" : [ + "ADR-046-provider-device-tpm" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-device-tpm-013", + "kind" : "work-item", + "owner" : "P0; blocked by ADR046-device-tpm-002; owner: device-tpm migration/removal cleanup", + "parallelGroup" : "wi:ADR-046-provider-device-tpm", + "prerequisites" : [ + "ADR-046-provider-device-tpm", + "ADR046-device-tpm-002" + ], + "specId" : "ADR-046-provider-device-tpm", + "topologicalRank" : 13, + "validation" : "Static search/proof for no direct broker swtpm references in daemon/controller plus preserved swtpm contract tests", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-display-wayland/src/`" + ], + "detailedDesign" : "Create Provider crate layout (`src/`, `tests/`, `integration/`, `README.md`); extract `FilterPolicy`, `PolicyInput`, `DecorationManager`, `BridgeConfig`, `ProxyReadinessEvent`, `ProxyIdentity`, `ClipboardGlobalDisposition` from `d2b-wayland-proxy`; implement single `display-controller` using toolkit `ResourceClient`/`Reconciler` to manage both `WaylandSession` and `WaylandPolicy` resources; implement `display-user-portal` as a separately sandboxed user-domain service that receives pre-opened compositor connections from the fixed user session supervisor (never reads `WAYLAND_DISPLAY`), validates same-user via `SO_PEERCRED`, and issues bounded per-session compositor connection attachment grants to `ProviderSupervisor`; implement LaunchTicket composition with opaque attachment grant handles (compositor, GPU endpoint) so no fd transits through the controller; implement pool-slot acquisition using opaque hash-derived account names (`d2b-wlp-` for bundle sessions, `d2b-wlp-p` for pool) that fails closed with `NoPrincipalAvailable` when all pool slots are occupied; implement `wl-cross-domain-proxy` guest frontend binary at `src/bin/wl-cross-domain-proxy.rs` within the Provider package; implement provider-neutral `display_fails_closed_when_unsupported` conformance; assert D087 status-first state: ProviderStateSet is empty and bounded operational state is in resource status plus the core Operation ledger Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-display-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-display-001", + "kind" : "work-item", + "owner" : "`ADR046-provider-001`, `ADR046-process-001`; display Provider owner", + "parallelGroup" : "wi:ADR-046-provider-display-wayland", + "prerequisites" : [ + "ADR-046-provider-display-wayland", + "ADR046-process-001", + "ADR046-provider-001" + ], + "specId" : "ADR-046-provider-display-wayland", + "topologicalRank" : 11, + "validation" : "conformance vectors, fake-bus tests, filter policy golden tests (migrate from `packages/d2b-wayland-proxy/`), redaction/audit contract tests, no-fallback test, `controller_unknown_interface_fails_closed`, `controller_finalizer_ambiguous_retained`, `user_portal_unavailable_blocks_pending`, `provider_state_set_empty_status_first`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Zone bundle emitter for `WaylandSession` / `WaylandPolicy` ResourceSpecs under `d2b.zones..resources.*`; `WaylandSession` color resolution in Nix bundle emitter" + ], + "detailedDesign" : "Emit `WaylandSession` and `WaylandPolicy` ResourceSpecs from Nix; derive colors from `d2b-niri-border` palette (§16.2); enforce `crossDomainTrusted = true` at eval time; emit v3 `display-wayland-provider` artifact catalog entry with `spec.config.principalPoolSize` (default 4, bounds 1..32) and `spec.config.runtimeVolumePolicyId`; provision opaque hash-derived OS accounts (`d2b-wlp-` for bundle-declared sessions, `d2b-wlp-p` for pool; `isSystemUser = true`, no raw UID/GID) for all sessions and pool slots; validate that account names are unique and do not expose guest/session identity; validate signed Provider config schema; enforce eval-time bound that bundle session count + pool size does not exceed provisioned principal count; configure `display-controller` and `display-user-portal` in the signed ProviderDeployment manifest; nix-unit tests for color derivation, opaque principal name derivation, spec shape, JSON round-trip, and principal provisioning uniqueness", + "entryContracts" : [ + "ADR-046-provider-display-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-display-002", + "kind" : "work-item", + "owner" : "`ADR046-display-001`; Nix integrator", + "parallelGroup" : "wi:ADR-046-provider-display-wayland", + "prerequisites" : [ + "ADR-046-provider-display-wayland", + "ADR046-display-001" + ], + "specId" : "ADR-046-provider-display-wayland", + "topologicalRank" : 12, + "validation" : "nix-unit spec-shape tests, eval-time guard tests (crossDomainTrusted=false rejected), color derivation golden tests, principal provisioning count bound test", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-display-wayland/src/audit.rs`, `packages/d2b-provider-display-wayland/src/metrics.rs`" + ], + "detailedDesign" : "Implement audit record types for all events in §14.1; implement OTEL metric counters/gauges in §14.2; adapt `DiagRateLimiter` to use closed semantic label sets with no Zone UID/name or resource-name-derived key; retain Zone identity in `d2b.zone` resource attributes; validate that no socket path, user identity, window title, or app-id appears in any log/audit/metric surface. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-display-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-display-003", + "kind" : "work-item", + "owner" : "`ADR046-display-001`; telemetry/audit owner", + "parallelGroup" : "wi:ADR-046-provider-display-wayland", + "prerequisites" : [ + "ADR-046-provider-display-wayland", + "ADR046-display-001" + ], + "specId" : "ADR-046-provider-display-wayland", + "topologicalRank" : 12, + "validation" : "Redaction contract tests (`policy_observability.rs` pattern), audit record schema tests, structural label-policy tests asserting exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus WaylandSession/Zone-name canary absence", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-display-wayland/integration/`" + ], + "detailedDesign" : "Container/Host/Guest/cross-process integration fixtures for: (a) end-to-end WaylandSession create → proxy Process ready → guest frontend ready; (b) GPU endpoint unavailable → Pending; (c) proxy crash → Failed backoff; (d) policy policy warning production; (e) clipboard boundary denial; (f) crossDomainTrusted=false admission rejection. Follows `ADR-046-provider-model-and-packaging` integration/ convention.", + "entryContracts" : [ + "ADR-046-provider-display-wayland" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-display-004", + "kind" : "work-item", + "owner" : "`ADR046-display-001`; integration test owner", + "parallelGroup" : "wi:ADR-046-provider-display-wayland", + "prerequisites" : [ + "ADR-046-provider-display-wayland", + "ADR046-display-001" + ], + "specId" : "ADR-046-provider-display-wayland", + "topologicalRank" : 12, + "validation" : "All six scenarios above pass; no socket paths in test output", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/zone_doctor.rs`, `packages/d2b/tests/zone_doctor_contract.rs`" + ], + "detailedDesign" : "`d2b zone doctor [--zone ] [--json]` reads resource status from Zone API (read-only verb), OTEL self-metrics from `observability-otel` Provider endpoint (optional), and audit segment inventory from `d2b-audit` segment reader. Named check set from this spec. Exit 0 on all-ready; 1 on any warn/error. Env-redirect sandbox for all test fixtures. Current `MANIFEST_JSON` fixture pattern adapted: `\"_observability\": {\"enabled\": false}` test ensures OTEL probe short-circuits cleanly. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt env-redirect sandbox test harness; adapt `defects` array pattern for `audit-hash-chain-clean` check; adapt `broker_ready` → `zone_phase` field.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-doctor-001", + "kind" : "work-item", + "owner" : "ADR046-core-001 + ADR046-audit-001; CLI/doctor owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-audit-001", + "ADR046-core-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "`zone_doctor_contract.rs`: all-ready/degraded/quarantine/otel-absent/audit-absent fixtures; no resource names/paths/argv/PIDs; `zone_phase` field present; no legacy `broker_ready` field", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/zone_support_bundle.rs`, `packages/d2b/tests/zone_support_bundle_contract.rs`" + ], + "detailedDesign" : "`d2b zone support-bundle [--zone ]` invokes only `d2b.support.v3.SupportService/GenerateBundle` under the admin-only `support-bundle` session verb; the caller receives no resource read authority. The service reads bounded resource status snapshots (32 per type, 512 total; metadata + status only; no spec bytes; no `metadata.name`), controller queue depths, schema catalog (names+versions only), audit segment inventory, OTEL collector metrics summary, and a bounded structured log ring (2000 entries). NDJSON output. On quarantine: `bundle_completeness: \"partial\"`, exit 1. Primary reuse disposition: `adapt`. Preserved source-plan detail: reuse env-redirect sandbox scaffold.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-doctor-002", + "kind" : "work-item", + "owner" : "ADR046-doctor-001; CLI/doctor owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-doctor-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 12, + "validation" : "`zone_support_bundle_contract.rs`: complete/partial bundles; no spec/name/path/argv; field completeness", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/host.rs`, `packages/d2b-contracts/src/v3/guest.rs`, `packages/d2b-contracts/src/v3/execution_policy.rs`, `packages/d2b-contracts/src/v3/process.rs`, `packages/d2b-contracts/src/v3/ephemeral_process.rs`, `packages/d2b-contracts/src/v3/user.rs`, `packages/d2b-contracts/src/v3/endpoint.rs`" + ], + "detailedDesign" : "Implement strict typed Rust structs for HostSpec, GuestSpec, ExecutionPolicy, ExecutionSpec, SandboxSpec, BudgetSpec, MountSpec, NetworkUsageSpec, DeviceUsageSpec, EndpointSpec, EndpointConsumerPolicy, TelemetrySpec, ProcessSpec, EphemeralProcessSpec, UserSpec; EndpointSpec accepts exactly `owner|provider|zone`, and EndpointConsumerPolicy owns the only finer gates (`allowedSubjects`, `allowedProviderComponents`, `allowedOperations`) with no schema aliases; strict serde deny_unknown_fields; bounds/redaction on all string fields; stable error types; `UserSpec.osUsername` validated as OS username (1..255 bytes, no NUL/control/path-separator), not ResourceName grammar Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-001", + "kind" : "work-item", + "owner" : "W0 shared contract root; `d2b-contracts`", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 10, + "validation" : "Golden JSON vectors for each ResourceType; Endpoint vectors accept only `owner|provider|zone`, reject every legacy/private visibility alias, reject scalar/array `consumerPolicy` aliases, and cover each canonical consumer allowlist; a docs drift test parses every `type: Endpoint` YAML/Nix example and fails unless visibility is canonical and finer gates occur only under `consumerPolicy`; serde unknown-field rejection; bounds enforcement; `UserSpec.osUsername` OS-username validation (underscore allowed, NUL rejected)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/process_provider.rs`: LaunchTicket, ProcessIdentityDigest, AdoptionCandidate, PidfdEvidence, WaitReapOwner, BrokerTerminalResult, ProcessOutcome, ExitClass" + ], + "detailedDesign" : "LaunchTicket (Process/EphemeralProcess ref/UID/revision/generation, owner Provider/component/template, executionRef/domain/userRef, providerRef, compiled sandbox/budget/mount/device/network/endpoint digest, inherited FD table, operation/deadline/cancellation, expected identity/readiness); ProcessIdentityDigest (opaque bounded hex string); AdoptionCandidate (cgroup leaf path relative to controller root, start-time token, executable hash); BrokerTerminalResult binds process identity/operation to the clone3 parent's exact-once wait/reap status and cannot be constructed from pidfd readability; all types zeroizing where credential-adjacent Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-002", + "kind" : "work-item", + "owner" : "ADR046-exec-001; `d2b-contracts`", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-001" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 11, + "validation" : "Golden LaunchTicket and BrokerTerminalResult vectors; field redaction test; digest-binding test; duplicate/mismatched/non-parent terminal relay rejection", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-core/src/host.rs`: HostReconciler; status/conditions/capability probe implementation; `packages/d2b-provider-system-core/tests/`: hermetic reconcile/conformance/fault tests; `packages/d2b-provider-system-core/integration/`: Host probe and lifecycle integration scenarios; `packages/d2b-provider-system-core/README.md`: Provider identity, `spec.provider.settings` schema, ResourceTypes, placement, RBAC, security posture, telemetry labels, build/test commands (provider crate standard layout — see §Provider crate standard layout)" + ], + "detailedDesign" : "Async Host reconcile loop per this spec's Reconcile section; HostCapabilityClass probe set (kvm/pidfd/cgroup-v2/user-namespace/wayland/audio-pipewire/gpu-render/tpm2/usbip); bounded OS probes with timeout; mandatory system-minijail placement gate for Linux ≥5.14 plus writable delegated-leaf `cgroup.kill` independent of optional `kernelVersionMin`; `isolationPosture` validation and status; aggregate budget reservation tracking via List; status write with expected revision Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-003", + "kind" : "work-item", + "owner" : "ADR046-exec-001; system-core Provider owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-001" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 11, + "validation" : "Multiple Hosts per Zone; system-only and user-only Hosts; capability probe mocks; Linux <5.14 and missing/unwritable `cgroup.kill` reject system-minijail before spawn; Linux ≥5.14 positive probe; `isolationPosture=\"none\"` rejection of system processes; budget overcommit rejection; `tests/` all pass under `cargo test`; `integration/` scenario passes in container fixture; `README.md` present and covers all required sections (provider crate standard layout acceptance)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-core/src/user.rs`: UserReconciler; NSS lookup implementation" + ], + "detailedDesign" : "Async User reconcile loop per this spec's Reconcile section; NSS `getpwnam(spec.osUsername)` with bounded timeout (default 5 s); `spec.osUsername` validated on admission (1..255 bytes, no NUL/control/path-separator); `metadata.name` used only as Zone-local resource identity and `User/` ref; uid/gid/home/shell/group discovery written to status; session manager availability check via fixed user supervisor; status write; phase Degraded on SessionManagerReady=False Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-004", + "kind" : "work-item", + "owner" : "ADR046-exec-001; system-core Provider owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-001" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 11, + "validation" : "User found/not-found; group membership; sessionManagerAvailable; multiple Users; deletion blocked by Process userRef; `spec.osUsername` with underscore succeeds NSS lookup where ResourceName grammar would reject it; `metadata.name` and `spec.osUsername` differ (e.g. `alice-admin` / `alice_admin`); `spec.osUsername` containing NUL/control/path-separator rejected at admission", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-core/src/host.rs` (continued); bootstrap startup sequence" + ], + "detailedDesign" : "ProviderSystem-core fixed bootstrap: start with embedded Zone runtime and compiled bootstrap authorization; run Host reconcile before any Process Provider launches; create initial User resources from Nix; publish initial Role/RoleBinding from Nix config; hand off to stored RBAC", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-005", + "kind" : "work-item", + "owner" : "ADR046-exec-001 + ADR046-exec-004; system-core Provider owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-001", + "ADR046-exec-004" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 12, + "validation" : "Bootstrap without prior Host; User-before-Process ordering; compilation bootstrap authorization closed-set tests", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-systemd/src/`: launch.rs, adoption.rs, pidfd.rs, wait.rs, user_supervisor.rs; `packages/d2b-provider-system-systemd/tests/`: hermetic lifecycle/conformance/fault tests; `packages/d2b-provider-system-systemd/integration/`: transient-unit and user-supervisor integration scenarios; `packages/d2b-provider-system-systemd/README.md`: Provider identity, `spec.provider.settings` schema, ResourceTypes, placement, RBAC, security posture, telemetry labels, build/test commands (provider crate standard layout — see §Provider crate standard layout)" + ], + "detailedDesign" : "system-systemd Process/EphemeralProcess provider conformance per this spec's system-systemd conformance section; transient system unit (Type=exec); InvocationID+cgroup+MainPID+start-time binding before pidfd_open; systemd-owned wait/reap; user domain via fixed user supervisor; adoption re-verification; sandboxSpec compilation to systemd hardening directives; runtimeDeadline enforcement; drainTimeout SIGTERM/SIGKILL sequence Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-006", + "kind" : "work-item", + "owner" : "ADR046-exec-001 + ADR046-exec-002; system-systemd Process Provider owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-001", + "ADR046-exec-002" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 12, + "validation" : "Shared process conformance test matrix (lifecycle/readiness/crash/drain/adoption/user-domain/sandboxSpec/pidfd); system-specific InvocationID binding; no-static-unit test; `tests/` all pass under `cargo test`; `integration/` scenario passes in container fixture; `README.md` present and covers all required sections (provider crate standard layout acceptance)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-minijail/src/`: sandbox_compiler.rs, launch.rs, adoption.rs, pidfd.rs, wait.rs, user_ns.rs; `packages/d2b-provider-system-minijail/tests/`: hermetic sandbox-compilation/lifecycle/conformance/fault tests; `packages/d2b-provider-system-minijail/integration/`: clone3/user-namespace and broker-spawn integration scenarios; `packages/d2b-provider-system-minijail/README.md`: Provider identity, `spec.provider.settings` schema, ResourceTypes, placement, RBAC, security posture (capabilities, namespaces, seccomp), telemetry labels, build/test commands (provider crate standard layout — see §Provider crate standard layout)" + ], + "detailedDesign" : "system-minijail Process/EphemeralProcess provider conformance per this spec's system-minijail conformance section; SandboxSpec-to-minijail plan compilation; Linux ≥5.14/cgroup.kill placement gate; broker clone3(CLONE_PIDFD|CLONE_INTO_CGROUP) parent retains sole waitid(P_PIDFD)/reap/exit-status ownership and relays a typed terminal result; ProviderSupervisor polls a verified duplicate and retains exact-main pidfd_send_signal semantics but never waits/reaps; adoption verifies original broker parent; runtimeDeadline/drainTimeout use graceful main signal then anchored leaf cgroup.kill, broker wait/reap, and empty-leaf proof; no PID/PGID fallback; EphemeralProcess one-shot launch Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-007", + "kind" : "work-item", + "owner" : "ADR046-exec-001 + ADR046-exec-002; system-minijail Process Provider owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-001", + "ADR046-exec-002" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 12, + "validation" : "Shared process conformance test matrix; minijail-specific sandbox compilation tests; user namespace tests; clone3 parent-only wait/reap and broker-relay tests; poll-readability-not-status test; pidfd_send_signal duplicate-holder test; setsid descendant/recycled-PGID cgroup.kill teardown test; Linux 5.14/cgroup.kill platform-gate tests; adoption quarantine asserts no signal/cgroup.kill; `tests/` all pass under `cargo test`; `integration/` scenario passes; `README.md` present and covers all required sections (provider crate standard layout acceptance)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-process-conformance/src/`: shared conformance test matrix run against both system-systemd and system-minijail providers" + ], + "detailedDesign" : "Full process lifecycle tests: start/ready/crash/restart/maxRestarts/drain/stop/delete; EphemeralProcess start/succeed/fail/ttl/cleanup; adoption after controller restart (fresh/quarantine); sandboxSpec compilation contract; pidfd rules (never-serialized/never-exported/re-verified-after-supervisor-restart; clone3 broker parent alone waits/reaps; non-parent readability is not status; verified duplicate holder may pidfd_send_signal exact main); system-minijail intentional teardown uses anchored cgroup.kill against setsid descendant and recycled-PGID fixtures, with no kill on ambiguity; Linux ≥5.14/cgroup.kill platform gate; user domain (system-systemd only); desiredLifecycle=stopped; fast path latency gate (<=5ms/<=20ms p95); 1/10/100 concurrent Process start", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-008", + "kind" : "work-item", + "owner" : "ADR046-exec-006 + ADR046-exec-007; conformance test owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-006", + "ADR046-exec-007" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 13, + "validation" : "Hard pass/fail per-test; latency gates enforced; no exception for partial conformance", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-core/src/host.rs` (user-only no-isolation Host); `nixos-modules/options-zones.nix` (Nix unsafe-local Host declaration)" + ], + "detailedDesign" : "v3 unsafe-local migration: `kind = \"unsafe-local\"` in the current Nix Realm workload model becomes a Host resource with `providerRef: Provider/system-core`, `spec.isolationPosture: \"none\"`, `defaultDomain: user`, `allowedDomains: [user]`, `defaultUserRef: User/`. This is a Host ResourceType, not a Guest and not a v3 Provider. Child Process and EphemeralProcess resources on this Host use the normal Process Providers (Provider/system-systemd for user-domain transient user scope; Provider/system-minijail is also valid for callers that explicitly request namespace isolation within the user session). No special unsafe-local-specific Provider is introduced. The explicit no-isolation posture and its warnings are preserved: Host status reflects `isolationPosture=\"none\"` and this is surfaced in every operator CLI/UI view as an explicit \"no isolation boundary\" warning; `ProcessEffect` audit records (launch, stop, adopt, quarantine) for child Processes and EphemeralProcesses carry the stable `no_isolation=true` attribute; operator CLI/UI always shows the warning and may not suppress it. The `no_isolation=true` attribute belongs on ProcessEffect records only — it must NOT appear on OTEL metric labels, span attributes, log fields, or audit records for other event kinds. The legacy helper protocol (`d2b-unsafe-local-helper`) is not exposed as a v3 ComponentSession service.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-009", + "kind" : "work-item", + "owner" : "ADR046-exec-001; unsafe-local migration owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-001" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 11, + "validation" : "User-only no-isolation Host rejected for system processes; `isolationPosture=\"none\"` Host rejected for `allowedDomains` containing `system`; `allowedDomains=[\"user\"]`+`defaultDomain=user`+`defaultUserRef` set with `isolationPosture=null` rejected at eval time (bidirectional evasion test); posture warning visible in CLI/UI status; `no_isolation=true` attribute present on ProcessEffect launch/stop/adopt/quarantine audit records for child Processes/EphemeralProcesses; `no_isolation=true` absent from OTEL span attributes, metric labels, log fields, and non-ProcessEffect audit records; user-domain Process under user-only Host starts correctly with normal Process Provider", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-systemd/src/guest_exec.rs` (guest-domain EphemeralProcess launch via systemd-run inside guest); `packages/d2b-session/` (ComponentSession replacing ad-hoc guest ttrpc); runtime Provider guest bootstrap Process" + ], + "detailedDesign" : "Guest-side execution transitions to EphemeralProcess/Process resources under the owning Guest; guestd service becomes a fixed bootstrap Process owned by the runtime Provider controller; exec operations become EphemeralProcess creates via the Zone ResourceClient; detached sessions become EphemeralProcess with failedTtl=24h; shell sessions become shell-terminal Provider Processes; guestd auth.rs becomes ComponentSession via d2b-session (copy/adapt from ADR046-session-001 per ComponentSession spec) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-010", + "kind" : "work-item", + "owner" : "ADR046-exec-001 + ADR046-exec-007; guestd migration owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-001", + "ADR046-exec-007" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 13, + "validation" : "Guest EphemeralProcess exec lifecycle; detached exec TTL; guestd auth replaced by ComponentSession; no SSH fallback", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "guest-domain process attachment becomes a ComponentSession named stream to the EphemeralProcess running in the guest; `UserExecSession` trait reimplemented as a typed ResourceClient+ComponentSession attachment" + ], + "detailedDesign" : "`UserdConfig.socket_name` replaced by Zone-local EphemeralProcess ResourceRef and ComponentSession attach verb; `UserSessionIdentity.uid`/`gid` become Process `userRef` resolved against User resource status; `UserAttachRequest.exec_id`/`tty`/`initial_size` become ComponentSession method parameters on the EphemeralProcess attachment service; `UserdError` becomes stable v3 error codes; `validate_attach_request` tty/size validation retained in the ComponentSession session schema Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (logic retained; protocol replaced).", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-011", + "kind" : "work-item", + "owner" : "ADR046-exec-004 + ADR046-exec-010; userd migration owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-004", + "ADR046-exec-010" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 14, + "validation" : "Attach/detach; TTY size; stream close; error path parity with UserdError variants", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/options-zones.nix`: `d2b.zones..resources` option as `types.attrsOf (types.submodule resourceModule)` where each resource module has `type` (required enum), optional `metadata` submodule (`ownerRef`, `labels`, `annotations`), and `spec` (type-dependent, auto-generated submodule); `nixos-modules/zone-bundle.nix`: zone resource bundle emitter (see ADR046-exec-014); `nixos-modules/resource-schemas/`: generated per-type Nix option modules imported by `options-zones.nix`" + ], + "detailedDesign" : "`d2b.zones..resources` is a flat attrset; each entry has `type`, optional `metadata` (`ownerRef`, `labels`, `annotations`), and `spec`. The attrset key is the resource name. `spec` submodule fields and their Nix types/defaults/docs are auto-generated from the committed `packages/d2b-contracts/src/v3/schemas/.json` via `xtask gen-resource-nix-options`; no second vocabulary and no renaming of `spec.*` fields. `spec.provider.settings` sub-fields are constrained to the specific Provider's `providerNixSettingsSchema` attribute if present. All 17 eval-time validation rules from the \"Eval-time validation rules\" section are enforced by `lib.assertMsg` on the flat resource attrset. The `spec` object in the emitted JSON is the direct 1:1 serialization of the `spec` submodule. `metadata.ownerRef`, `metadata.labels`, `metadata.annotations` are serialized into the `metadata` object of the ResourceEnvelope JSON. `metadata.managedBy` and `metadata.configurationGeneration` are NOT in the bundle; they are set by the activation controller at runtime. `metadata.name` is the attrset key; `metadata.zone` is the enclosing zone key. Eval errors carry stable rule codes (1–17). Status is never present in the Nix option. `Guest.spec.systemArtifactId` (top-level spec field, not in `spec.provider.settings`) is validated against `d2b.artifacts` by rule 17; the `spec` submodule never contains derivation values or store paths.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-012", + "kind" : "work-item", + "owner" : "ADR046-exec-001; Nix resource compiler owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-001" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 11, + "validation" : "nix-unit eval/build tests 1–18 from the \"Tests\" section; eval validation rule tests 1–17 with expected error messages and stable codes; `spec` fields in emitted JSON match `spec` submodule values exactly (1:1 invariant test); `type` field in JSON matches `type` option value; `metadata.name` = attrset key; `metadata.zone` = enclosing zone key; `Guest.spec.systemArtifactId` plain string in resource bundle JSON at top-level spec (no store path, not in `spec.provider.settings`); missing/wrong-type artifact ID raises rule 17 eval error", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/cleanup.rs`: EphemeralProcess TTL cleanup controller handler" + ], + "detailedDesign" : "Cleanup controller handler as specified in core-controllers spec; watches EphemeralProcess resources for terminal phase; computes cleanupEligibleAt from successfulTtl/failedTtl + completedAt; handles incidentHold; respects finalizers; issues normal Delete via ResourceClient; does not remove rows directly; bounded requeue-at for TTL expiry", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-013", + "kind" : "work-item", + "owner" : "ADR046-exec-008; EphemeralProcess cleanup controller owner", + "parallelGroup" : "wi:core-config-hub:w5", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-core-001", + "ADR046-exec-008" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 14, + "validation" : "Succeeded TTL default 1h; Failed TTL default 24h; incidentHold blocking; finalizer blocking; Delete with expected revision; cleanup controller restart recovery", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/zone-bundle.nix`: Zone resource bundle emitter; `nixos-modules/resource-schemas/`: generated per-type Nix option submodules; `packages/d2b-contracts/src/v3/resource_bundle.rs`: `ResourceBundle`, `ResourceEnvelope`, `BundleManifest`, `BundleIntegrityPin` Rust types; `packages/xtask/src/gen_resource_schemas.rs`: `xtask gen-resource-schemas` (generates schema JSON and Nix option modules)" + ], + "detailedDesign" : "`zone-bundle.nix` iterates the flat `d2b.zones..resources` attrset (all types share the same attrset; each entry has `type`, optional `metadata`, and `spec`). For each entry it serializes the `spec` submodule to canonical JSON 1:1 (field names unchanged; keys sorted at every level; order-significant arrays preserve declaration order; semantically unordered arrays sorted). `metadata.ownerRef`, `metadata.labels`, `metadata.annotations` are serialized from the author-supplied `metadata` submodule into the envelope's `metadata` object. `metadata.managedBy` and `metadata.configurationGeneration` are NOT in the bundle envelope; they are set by the activation controller at runtime. `metadata.name` is the attrset key; `metadata.zone` is the zone key. `xtask gen-resource-schemas` generates both the schema JSON files AND the per-type Nix option submodule files under `nixos-modules/resource-schemas/` from the Rust DTO definitions; both must be regenerated when any ResourceType spec or Rust struct changes (same drift-gate pattern as current `make test-drift` / `xtask gen-schemas`). The bundle sorts all envelopes by `type` then `metadata.name` alphabetically, computes `bundleSha256` from the canonical JSON of the `resources` array, and emits the bundle manifest as a NixOS store artifact. No secret values, credentials, or OS paths not already declared in `spec` or `metadata` fields may appear in any envelope JSON. The bundle file includes private integrity metadata (resource type schema fingerprints, per-Provider schema fingerprints) alongside the `resources` array; these are bundle-level fields, never per-envelope fields. Alongside the resource bundle, the emitter installs the global private artifact catalog at `/etc/d2b/artifact-catalog.json` (root:d2bd 0640) mapping each `d2b.artifacts.` to `{ \"sha256\", \"size\", \"storePath\", \"type\" }`; this catalog is never included in public resource bundle envelopes. The bundle outer wrapper includes a `catalogSha256` binding the catalog to the bundle for activation-time integrity verification.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-014", + "kind" : "work-item", + "owner" : "ADR046-exec-001 + ADR046-exec-012; Nix resource compiler owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-001", + "ADR046-exec-012" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 12, + "validation" : "nix-unit test: bundle sort order (Host < Guest < Process; within type, names alphabetical); nix-unit test: `bundleSha256` recomputed from resources array matches recorded value; nix-unit test: schema fingerprints appear in private bundle file fields, not in any individual ResourceEnvelope `metadata` or `spec` object; nix-unit test: `catalogSha256` changes when any artifact derivation changes; nix-unit test: `schemaFingerprint` changes when ResourceTypeSchema JSON changes; nix-unit test: `providerSchemaFingerprint` changes when Provider settings schema changes and is null when Provider declares no schema; nix-unit test: no inline secret value passes through to bundle JSON; nix-unit test: identical configuration produces byte-identical bundle JSON across two builds; nix-unit test: artifact catalog JSON contains `storePath` field for each entry; nix-unit test: no envelope in `resources` array contains `storePath`, `nixSystem`, `schemaFingerprint`, or `providerSchemaFingerprint`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/configuration.rs`: `ZoneConfigController`, `GenerationState`, `PendingCleanup`, `BundleActivation`, `ActivationResult`, `ActivationError`, `GenerationDiff`, `DiffEntry`, `DiffKind`, `CleanupRecord`, `CleanupPhase`, `CleanupOutcome`, `RetentionPolicy`, `RetentionState`; `packages/d2b-core-controller/src/audit.rs`: audit event emission per the \"Audit events\" section" + ], + "detailedDesign" : "The Zone configuration controller (`packages/d2b-core-controller/src/configuration.rs`) runs as a fixed process in the `d2b-core-controller` crate. It watches `/etc/d2b/zones//resource-bundle.json` (inotify or polling). On change: (1) read and verify `bundleSha256` integrity; fail closed on mismatch, emit `d2b.zone.config.activate.error` with `config-bundle-integrity-failed`, make no changes. (2) Compare candidate `bundleSha256` against the currently active bundle record; if identical, this is a no-op re-activation (return immediately). (3) Verify resource type schema fingerprints (from bundle private fields) against committed schemas at `packages/d2b-contracts/src/v3/schemas/.json`; any mismatch fails the entire bundle closed with `config-schema-mismatch`, emits error, makes no changes. (4) Verify Provider schema fingerprints against installed Provider schemas; any mismatch fails the entire bundle closed with `provider-schema-mismatch`, emits error, makes no changes. (5) Verify `catalogSha256` binding against `/etc/d2b/artifact-catalog.json`; mismatch fails bundle closed. (6) Fetch current `metadata.managedBy=\"configuration\"` resources via ResourceClient List (one call per type). (7) Compute `GenerationDiff` (new/changed/unchanged/removed) by type+name key. (8) Submit Create, UpdateSpec, and Delete intents concurrently with bounded async concurrency (default max 32 in-flight); activation returns after all intents are durably queued by the resource store, without waiting for reconcile loops to complete. For unchanged specs: issue UpdateConfigGeneration to refresh `metadata.configurationGeneration` to the new generation number; no controller reconcile triggered. For new resources: core sets `metadata.managedBy=\"configuration\"` and `metadata.configurationGeneration=` on Create; if a same-name resource already exists with `metadata.managedBy=\"controller\"` or `metadata.managedBy=\"api\"`, record a per-item `config-collision` error for that resource without seizing it, emit error, continue other intents. For changed specs: submit UpdateSpec with `expectedRevision`; retry on optimistic lock conflict. For removed resources: submit Delete; set `metadata.deletionRequestedAt`. (9) Set Zone `phase=Pending` while create/update intents are outstanding. (10) Return after durable queue commit; do not block on reconcile completion. (11) The cleanup controller watches ResourceClient Watch streams for `Deleted` revision events (not polling GET) for each pending-cleanup resource by type+name+expectedRevision. When all finalizers release, the resource store commits the `Deleted` revision event atomically with row and index removal in a single transaction; following this commit, the audit subsystem appends the deletion audit record using a dedup/exactly-once recovery key (audit is NOT part of the atomic store transaction). Zone transitions to `phase=Degraded` immediately whenever any pending-cleanup item is outstanding. When all pending-cleanup items receive `Deleted` Watch events, `pendingCleanup` empties and Zone transitions to `phase=Ready`. Cleanup-stuck threshold: 10 min default; configurable; stuck resources remain Degraded without blocking later activations. Prior generation retention: controller retains the N most recently activated, cleanup-complete bundle records (default N=3; range 1..16; no time-based TTL).", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-015", + "kind" : "work-item", + "owner" : "ADR046-exec-014 + ADR046-exec-001 + ADR046-exec-022; configuration generation controller owner", + "parallelGroup" : "wi:core-config-hub:w5", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-core-001", + "ADR046-exec-001", + "ADR046-exec-014", + "ADR046-exec-022" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 15, + "validation" : "Runtime/integration tests 19–23 from the \"Tests for Nix configuration and ResourceType-specific lifecycle\" section in this spec; additionally: `GenerationDiff` hermetic unit tests (new/changed/unchanged/removed classification); `bundleSha256` integrity failure aborts and emits correct audit event; `catalogSha256` mismatch aborts bundle; UpdateSpec optimistic lock conflict retried correctly; Watch `Deleted` revision events consumed (not polling GET) to track cleanup completion; Zone `phase=Pending` while intents outstanding; Zone `phase=Degraded` immediately when any cleanup outstanding (no grace window); Zone `phase=Ready` when complete; activation returns after durable queue commit, not after reconcile; same-name `managedBy=controller` OR `managedBy=api` collision emits per-item `config-collision` error without seizing resource, other intents continue; unchanged spec refreshes `configurationGeneration` without triggering controller reconcile; final deletion: atomic tx commits `Deleted` revision event + row/index removal only; audit append follows committed revision via dedup/exactly-once recovery (NOT part of atomic tx); recovery retry produces no duplicate audit record; prior bundle record released after cleanup-complete and retention count exceeded; activation with zero diff and identical bundleSha256 is a no-op", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus-session/src/`: all above modules verbatim; `packages/d2b-bus-session/tests/`: all above tests verbatim" + ], + "detailedDesign" : "The entire `d2b-session` portable ComponentSession runtime is transport-agnostic and contains no ADR 0045 realm-specific types. All Noise handshake parameters, record framing, fragmentation, named stream mux, fair scheduler, cancellation, deadlines, bootstrap PSK, and attachment bindings are directly reusable. `ComponentSessionDriver` trait and `serve_ttrpc_services` are the primary integration surface for every v3 bus service. The `sessions/lib.rs` re-export boundary (`pub use d2b_contracts::v2_component_session as contract`) must be updated to point at the v3 wire contract module. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy verbatim; rename crate from `d2b-session` to `d2b-bus-session` or retain name.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-016", + "kind" : "work-item", + "owner" : "ADR046-exec-001; `d2b-session` v3 crate owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-001" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 11, + "validation" : "Copy tests verbatim; all tests must pass on v3 baseline without modification; re-run `tests/noise_vectors.rs` golden vectors; add one v3-specific test: endpoint policy identity uses v3 `ZoneId`/zone-name binding rather than ADR45 `RealmId`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus-session-unix/src/`: all above modules verbatim; `packages/d2b-bus-session-unix/tests/`: all above tests verbatim" + ], + "detailedDesign" : "Provides the Linux-specific `OwnedTransport` implementation for Unix seqpacket and stream sockets. `CreditPool`/`ProcessCreditLimit` enforces per-scope FD attachment budget (ADR45 constants: `MAX_PROCESS_ATTACHMENT_CREDITS = 2048`, `MAX_HOST_ATTACHMENT_CREDITS = 8192`, `RESERVED_CONTROL_FDS = 64`). `PidfdIdentityVerifier` provides the `/proc//fdinfo/` parse path that Process controllers use to verify process identity before `pidfd_open(2)` — this is a direct dependency of system-systemd and system-minijail Process Providers (ADR046-exec-006, ADR046-exec-007). Primary reuse disposition: `adapt`. Preserved source-plan detail: copy verbatim; rename crate from `d2b-session-unix` to `d2b-bus-session-unix` or retain name.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-017", + "kind" : "work-item", + "owner" : "ADR046-exec-016; `d2b-bus-session-unix` crate owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-016" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 12, + "validation" : "Copy all 20+ tests verbatim; all must pass; add v3-specific test: `PathnamePeerVerifier` verifies against `d2b-zonert` daemon uid; credit pool per-Zone-runtime limits match v3 constants", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus-wire/src/session.rs`: v3 bus protocol constants and wire types; all numeric constants copied verbatim; `PREFACE_MAGIC` retained; `EndpointPolicy` and `EndpointPolicyIdentity` adapted to use v3 `ZoneId`/`ProviderId` instead of ADR45 `RealmId` in the policy identity fingerprint" + ], + "detailedDesign" : "All numeric constants (frame sizes, credit limits, deadline values, reconnect limits) are directly reusable without change — they are derived from protocol analysis, not from realm semantics. `LimitProfile::local_default()` is the source for `serve_ttrpc_services` capacity; retain the value. `EndpointPolicy` carries the Noise static key and schema fingerprint; the fingerprint computation does not embed realm names and is reusable. The `EndpointPolicyIdentity` type carries the zone runtime's static public key — update from ADR45 `RealmId` to v3 `ZoneId` string encoding. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-018", + "kind" : "work-item", + "owner" : "ADR046-exec-016; `d2b-bus-wire` contract owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-016" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 12, + "validation" : "Compile-time assertions on all copied numeric constants matching source values; `EndpointPolicyIdentity` golden-vector test with v3 zone name encoding; `LimitProfile::local_default()` round-trip test", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-runtime/src/`: `registry.rs`, `rpc.rs`, `instance.rs`, `context.rs`, `error.rs`; provider trait objects moved to `d2b-bus-wire` or `d2b-provider-contracts`" + ], + "detailedDesign" : "`ProviderRegistry` manages in-flight permits, draining, and provider lifecycle. `RegistryLimits { total_in_flight, per_provider_in_flight }` is directly reusable. `RpcProviderProxy` wraps a `ComponentSessionDriver` and dispatches typed `RpcCall` to an `AuthenticatedProviderRpc` implementation — the proxy pattern is fully reusable for v3 Provider resource controllers. `SessionIdentity { peer_role, service, provider_id, provider_type, provider_generation }` maps directly to a v3 Provider session credential. `InFlightPermit` RAII guard is directly reusable. The provider trait object set (`RuntimeProvider`, `StorageProvider`, etc.) adapts to v3 Provider resource typed methods; the trait hierarchy is preserved but `ProviderMethod` enum variant names may be renamed to drop ADR45 workload terminology. `ProviderRegistry::MAX_PROVIDER_REGISTRY_ENTRIES` bound from v2_provider is retained. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-019", + "kind" : "work-item", + "owner" : "ADR046-exec-016 + ADR046-exec-018; `d2b-provider-runtime` crate owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-016", + "ADR046-exec-018" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 13, + "validation" : "Copy `d2b-provider` tests; `RegistryLimits::validate` enforces non-zero and per≤total; `InFlightPermit` RAII release; drain/retire state machine; `RpcProviderProxy` round-trip over `FakeProvider` (from toolkit ADR046-exec-020)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-toolkit/src/`: retain all modules verbatim; adapt `ProviderAgentAdapter` to use v3 `ProviderRegistry` (ADR046-exec-019) and v3 bus wire types; adapt `GeneratedProviderServiceServer` to use v3 generated service stubs (ADR046-exec-021); `packages/d2b-provider-agent/src/`: adapted from `gateway-runtime/src/provider_agent.rs`" + ], + "detailedDesign" : "`ProviderAgentAdapter` is the core: it drives a `ComponentSessionDriver` receive loop, dispatches decoded ttrpc frames to `ProviderRegistry`, and forwards responses. `GeneratedProviderServiceServer` closes the loop by registering all generated service stubs with `serve_ttrpc_services`. `FakeProvider` implements every v2 Provider trait with deterministic outputs — adapt each trait method to v3 Provider resource semantics while retaining the fixture pattern. `Redacted` / `Secret` zero-copy wrappers are used in every audit log path; copy verbatim. Provider conformance check pattern is retained: descriptor validation, capability publication, fixture round-trip, observability query result. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-020", + "kind" : "work-item", + "owner" : "ADR046-exec-019; `d2b-provider-toolkit` owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-019" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 14, + "validation" : "Copy conformance tests verbatim; `check_descriptor_conformance` passes on a `FakeProvider` descriptor; `check_provider_conformance` covers all five `ConformanceError` variants; `ProviderAgentProcess` shutdown within deadline test; `MAX_DISPATCH_IN_FLIGHT` semaphore back-pressure test", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus-contracts/src/generated_v3_services/`: v3 generated ttrpc stubs for Zone service methods (Resource CRUD, Watch, ComponentSession service verbs); `packages/d2b-zone-service/src/`: Zone runtime service handler adapted from `DaemonServiceV2` pattern; `packages/d2b-zone-service/src/admission.rs`, `handler.rs`, `routing.rs`" + ], + "detailedDesign" : "`StrictWireMessage` trait (decode_strict, encode_strict) is directly reusable — it enforces deny-unknown-fields decode and schema-pinned fingerprint validation. `ServiceInventoryDocument` / `service_schema_fingerprint` pattern provides the service schema publication mechanism that v3 Provider resources use to advertise their ComponentSession service interface. `DaemonServiceV2` / `DaemonOperationHandler` pattern becomes the v3 Zone service handler base: `DaemonCallContext` → v3 `ZoneCallContext` with `ZoneId`, principal `User/`, operation deadline; `DaemonMethod` enum → v3 `ZoneMethod` (ResourceGet, ResourceList, ResourceWatch, ResourceCreate, ResourceUpdateSpec, ResourceUpdateStatus, ResourceDelete, BusAttach); `daemon_endpoint_policy` → v3 zone endpoint policy with v3 `ZoneId`-bound static key and schema fingerprint. `server_stream_name`/`parse_server_stream_name` for Watch stream naming is reusable verbatim. `TerminalStreamValidator` / `ServerStreamLease` for terminal byte stream safety is reusable for EphemeralProcess attach. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt; v3 generates new protobuf definitions and new ttrpc stubs; reuse message shaping, method dispatch patterns, and service inventory pattern.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-021", + "kind" : "work-item", + "owner" : "ADR046-exec-016 + ADR046-exec-018; v3 Zone service contract owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-016", + "ADR046-exec-018" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 13, + "validation" : "`StrictWireMessage` decode rejects unknown fields on all v3 message types; `service_schema_fingerprint` is stable across builds; `ZoneCallContext` deadline enforcement test; `DaemonSeqpacketTransport` → `ZoneSeqpacketTransport` end-to-end roundtrip; generated v3 stub compile check", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus-client/src/`: all above modules; `DaemonClient` → `ZoneClient` (v3 Resource CRUD/Watch verbs); `GuestClient` → `ProcessAttachClient`; `HostSocketConnector` → `ZoneSocketConnector`; `LocalDaemonSession` → `LocalZoneSession`" + ], + "detailedDesign" : "`Client` is the core typed async client with bounded retry, wall-clock injection, and cancellation. `ConnectedSession` wraps a `ComponentSessionDriver` and provides `call()`, `open_stream()`, and `close()`. `ComponentSessionConnector` trait decouples connection establishment from the client — v3 `ZoneSocketConnector` implements this for the local Zone runtime public socket. `TargetResolver`/`RouteTable` provides request routing to local vs remote Zone runtimes. `HostSocketConnector::local_daemon_endpoint_identity` provides the peer identity pinning that prevents MITM on the local socket — this is a security-critical invariant; copy verbatim, rename from `d2bd` to `d2b-zonert` uid. `DaemonClient` method table adapts to v3 Resource verbs: `ResourceGet`, `ResourceList`, `ResourceWatch` (streaming), `ResourceCreate`, `ResourceUpdateSpec`, `ResourceUpdateStatus`, `ResourceDelete`. `ServiceHandle`/`MethodHandle`/`GeneratedClient` provide the typed client stub generation pattern. `RetryPolicy`/`RetryClass`/`RemoteErrorKind` error classification is directly reusable. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-022", + "kind" : "work-item", + "owner" : "ADR046-exec-016 + ADR046-exec-021; `d2b-bus-client` crate owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-016", + "ADR046-exec-021" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 14, + "validation" : "Copy `tests/client.rs` verbatim; add v3-specific tests: `ResourceWatch` streaming teardown; `ZoneSocketConnector` peer-uid mismatch rejection; retry policy respects `RetryClass::Transient`/`Permanent`; `TargetInput`→`ResolvedTarget` for local-only v3 Zone; `local_daemon_endpoint_identity` returns correct v3 zone-rt uid", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-zone-router/src/`: `router.rs` (v3 `ZoneOperationRouter` — idempotency semantics copied verbatim; dedup key namespace adapted from `(realm, principal, node, kind, key)` to `(zone, resource-type, resource-name, verb, idempotency-key)`); `service.rs` (v3 `ZoneServiceLimits`, `ZoneServiceServer`, `ZoneAuditEvent`); `resolver.rs` (v3 `ZoneTargetResolver`, `ZoneEntrypointTable`)" + ], + "detailedDesign" : "The idempotency/dedup semantics from `OperationRouter` are security-critical and must be copied exactly: (1) dedup key is the full 5-tuple namespace — reusing a key under a different principal is a conflict, not a replay; (2) expired keys leave tombstones for a no-reuse horizon; (3) same-key/same-request returns the original `operation_id` and recorded result; (4) same-key/different-request returns conflict error fail-closed. These semantics apply to all v3 Resource mutation verbs (Create, UpdateSpec, UpdateStatus, Delete) that carry an idempotency key. `RealmServiceLimits` numeric bounds are copied verbatim: `MAX_DISPATCH_IN_FLIGHT=64` gates concurrent resource mutations per Zone session. `RealmSessionAuthority` principal-binding model (session principal MUST match request principal field, derived in trusted code from authenticated session) maps directly to v3 Zone RBAC: `ZoneCallContext` carries the authenticated `User/` principal from `SO_PEERCRED`; no caller-supplied principal field is accepted. `DurableExecTable`/`DEFAULT_MAX_EXECUTIONS` from `execution.rs` provides the EphemeralProcess in-flight table bound for the Zone router.", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-exec-023", + "kind" : "work-item", + "owner" : "ADR046-exec-021 + ADR046-exec-019; Zone bus routing owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-exec-019", + "ADR046-exec-021" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 14, + "validation" : "Idempotency replay returns original result; conflict returns error; expired tombstone fails closed; `MAX_DISPATCH_IN_FLIGHT` semaphore back-pressure; principal-binding enforcement (mismatched principal returns auth-denied); `DurableExecTable` capacity limit; v3 5-tuple dedup key golden vector test", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`proofs/redb-resource-store-spike/`" + ], + "detailedDesign" : "Implements SPIKE-01 and SPIKE-02: the eight-table schema, fair write queue, blocking store-actor, watch registrar, and hint bus described in those two spike entries Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (the atomic-write/idempotency discipline in `storage.rs`/`sync.rs` is adapted into the spike's write-transaction algorithm; redb itself is used unmodified).", + "entryContracts" : [ + "ADR-046-feasibility-and-spikes" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-feasibility-001", + "kind" : "work-item", + "owner" : "W0 shared contract root; store/reconciliation integrator", + "parallelGroup" : "wi:ADR-046-feasibility-and-spikes", + "prerequisites" : [ + "ADR-046-feasibility-and-spikes" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 11, + "validation" : "SPIKE-01 metrics (1)-(5) and SPIKE-02 metrics (1) across all 3 concurrency profiles, per those entries' exact pass/fail thresholds", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`proofs/process-fastlaunch-spike/`" + ], + "detailedDesign" : "Implements SPIKE-03: the fake Process controller loop, fake `ProcessLaunchEffectPort`, and the 1/10/100-concurrency commit-to-launch-attempt and next-dispatch-independence benchmarks Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (current DAG ordering/readiness concepts are adapted into the spike's per-resource single-flight/parallel-semaphore loop).", + "entryContracts" : [ + "ADR-046-feasibility-and-spikes" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-feasibility-002", + "kind" : "work-item", + "owner" : "`ADR046-feasibility-001`; reconciliation/process integrator", + "parallelGroup" : "wi:ADR-046-feasibility-and-spikes", + "prerequisites" : [ + "ADR-046-feasibility-and-spikes", + "ADR046-feasibility-001" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 12, + "validation" : "SPIKE-03 metrics (1)-(3) and thresholds", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`proofs/effectport-async-spike/`" + ], + "detailedDesign" : "Implements SPIKE-04: the four fake EffectPort traits, the deliberately slow blocking-primitive backends, and the current-thread-runtime heartbeat-jitter detector", + "entryContracts" : [ + "ADR-046-feasibility-and-spikes" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-feasibility-003", + "kind" : "work-item", + "owner" : "Independent of `-001`/`-002`; EffectPort/ProviderSupervisor integrator", + "parallelGroup" : "wi:ADR-046-feasibility-and-spikes", + "prerequisites" : [ + "ADR-046-feasibility-and-spikes" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 11, + "validation" : "SPIKE-04 heartbeat-jitter metric and threshold", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`proofs/provider-packaging-spike/`" + ], + "detailedDesign" : "Implements SPIKE-05: the two-binary crate, hand-authored manifest, fake `ProviderDeployment`, and the `cargo metadata` dependency-edge check Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (the crate-layout policy check reuses the same `src/`/`tests/`/`integration/`/`README.md` structure already enforced elsewhere in this repository's workspace policy tests).", + "entryContracts" : [ + "ADR-046-feasibility-and-spikes" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-feasibility-004", + "kind" : "work-item", + "owner" : "Independent of `-001`/`-002`/`-003`; Provider packaging/toolkit integrator", + "parallelGroup" : "wi:ADR-046-feasibility-and-spikes", + "prerequisites" : [ + "ADR-046-feasibility-and-spikes" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 11, + "validation" : "SPIKE-05 metrics (1)-(4) across 20 repeated randomized-order manifest loads", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`proofs/bus-routing-noise-spike/`, `proofs/transport-opaque-streams-spike/`, `proofs/credential-kk-e2e-spike/`" + ], + "detailedDesign" : "Implements SPIKE-06 (exact-addressed routing + per-recipient Noise isolation), SPIKE-07 (Unix/vsock/relay-shaped opaque byte-stream conformance across 3 backends), and SPIKE-08 (Credential Provider → consumer Provider KK delivery with the 13-field binding contract) Primary reuse disposition: `adapt`. Preserved source-plan detail: `copy-unchanged` for the Noise/record/transport machinery (path-dependency on a pinned local checkout of `a1cc0b2d`); `adapt` for the fake router/relay/credential-delivery wrapper code that SPIKE-06/07/08 add on top.", + "entryContracts" : [ + "ADR-046-feasibility-and-spikes" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-feasibility-005", + "kind" : "work-item", + "owner" : "Independent of `-001` through `-004`; bus/session/transport/credential integrator", + "parallelGroup" : "wi:ADR-046-feasibility-and-spikes", + "prerequisites" : [ + "ADR-046-feasibility-and-spikes" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 11, + "validation" : "SPIKE-06 metrics (1)-(3), SPIKE-07 metrics (1)-(4) against the exact numeric gates already committed in the transport-unix/vsock dossiers, SPIKE-08 metrics (1)-(6)", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`proofs/provider-state-export-spike/`, `proofs/volume-policy-spike/`" + ], + "detailedDesign" : "Implements SPIKE-09 (optional declared state-Volume creation order, guest-local/host-backed-guest placement, virtiofs Export child ownership) and SPIKE-10 (Volume ACL/`sourcePolicyId`/quota/lifecycle-marker policy conformance)", + "entryContracts" : [ + "ADR-046-feasibility-and-spikes" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-feasibility-006", + "kind" : "work-item", + "owner" : "Independent of `-001` through `-005`; Provider-state/Volume integrator", + "parallelGroup" : "wi:ADR-046-feasibility-and-spikes", + "prerequisites" : [ + "ADR-046-feasibility-and-spikes" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 11, + "validation" : "SPIKE-09 metrics (1)-(4); SPIKE-10 metrics (1)-(5), zero-tolerance on path leakage", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`proofs/process-provider-conformance-spike/`" + ], + "detailedDesign" : "Implements SPIKE-11: the shared `ProcessProviderHarness` trait, the minijail-shaped `clone3(CLONE_PIDFD)` launcher, and the systemd transient-user-scope launcher, plus the identity-drift/quarantine and clean-exit cases", + "entryContracts" : [ + "ADR-046-feasibility-and-spikes" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-feasibility-007", + "kind" : "work-item", + "owner" : "Independent of `-001` through `-006`; Process Provider integrator", + "parallelGroup" : "wi:ADR-046-feasibility-and-spikes", + "prerequisites" : [ + "ADR-046-feasibility-and-spikes" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 11, + "validation" : "SPIKE-11 metrics (1)-(4), zero-tolerance on false adoption", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`proofs/nix-authoring-spike/`" + ], + "detailedDesign" : "Implements SPIKE-12: the minimal flake, the two synthetic ResourceTypes, the hand-written committed schemas, the standalone `gen-schemas`-shaped drift check, and the two-generation removed-resource cleanup simulation", + "entryContracts" : [ + "ADR-046-feasibility-and-spikes" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-feasibility-008", + "kind" : "work-item", + "owner" : "Independent of `-001` through `-007`; Nix/xtask integrator", + "parallelGroup" : "wi:ADR-046-feasibility-and-spikes", + "prerequisites" : [ + "ADR-046-feasibility-and-spikes" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 11, + "validation" : "SPIKE-12 metrics (1)-(5), byte-for-byte reproducibility across 3 hermetic builds", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`proofs/cli-discovery-spike/`, `proofs/clean-cutover-spike/`" + ], + "detailedDesign" : "Implements SPIKE-13 (dynamic Provider-projection discovery, bounds, latency isolation) and SPIKE-14 (zero v2 dispatch, fresh Zone bootstrap ignoring legacy state)", + "entryContracts" : [ + "ADR-046-feasibility-and-spikes" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-feasibility-009", + "kind" : "work-item", + "owner" : "Independent of `-001` through `-008`; CLI integrator", + "parallelGroup" : "wi:ADR-046-feasibility-and-spikes", + "prerequisites" : [ + "ADR-046-feasibility-and-spikes" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 11, + "validation" : "SPIKE-13 metrics (1)-(7); SPIKE-14 metrics (1)-(3), zero-tolerance on legacy-file access", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`proofs/e2e-composition-spike/`" + ], + "detailedDesign" : "Implements SPIKE-15: the three representative compositions (local/cloud-hypervisor, cloud/azure, interaction/shell-terminal-or-wayland), wired from the fakes built by `-001` through `-009`, plus the combined 3-Zone aggregate RSS measurement", + "entryContracts" : [ + "ADR-046-feasibility-and-spikes" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-feasibility-010", + "kind" : "work-item", + "owner" : "`ADR046-feasibility-001` through `ADR046-feasibility-009` (integrates their fakes; must run last)", + "parallelGroup" : "wi:ADR-046-feasibility-and-spikes", + "prerequisites" : [ + "ADR-046-feasibility-and-spikes", + "ADR046-feasibility-001", + "ADR046-feasibility-009" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 12, + "validation" : "SPIKE-15 metrics (1)-(4) across all three compositions", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`proofs/test-runtime-budget-spike/`; the committed baseline ledger consumed by `ADR046-delivery-007`" + ], + "detailedDesign" : "Establishes the D094 measurement baseline: records the reference runner class, repetition count, and per-test/crate/shard p95 for a representative hermetic crate; proves the §10.16 budgets (individual normal test p95 ≤50 ms, per-crate `--lib --tests` ≤2 s, Layer-1 hermetic shard ≤60 s) are met on the reference runner and that an injected slow/sleeping test is detected as a regression", + "entryContracts" : [ + "ADR-046-feasibility-and-spikes" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-feasibility-011", + "kind" : "work-item", + "owner" : "`ADR046-delivery-007`; delivery/test-tooling integrator", + "parallelGroup" : "wi:ADR-046-feasibility-and-spikes", + "prerequisites" : [ + "ADR-046-feasibility-and-spikes", + "ADR046-delivery-007" + ], + "specId" : "ADR-046-feasibility-and-spikes", + "topologicalRank" : 14, + "validation" : "The representative crate meets every budget; a synthetic slow/sleep/process/network test is flagged; cold compile time is recorded on a separate line and excluded from the execution budgets", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-gpu/` with `src/`, `tests/`, `integration/`, `README.md`; add to workspace `Cargo.toml` members list (alphanumerically sorted)" + ], + "detailedDesign" : "Crate scaffold: `Cargo.toml` with `d2b-host`, `d2b-contracts`, `d2b-provider-toolkit`, `d2b-core` dependencies; `lib.rs` exporting controller binary entry points; `error.rs` with `DeviceGpuError` closed-set enum; placeholder `controller.rs` Primary reuse disposition: `extract`. Preserved source-plan detail: `extract` both argv files into `d2b-provider-device-gpu/src/argv.rs` as re-exports; do not copy logic.", + "entryContracts" : [ + "ADR-046-provider-device-gpu" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-gpu-001", + "kind" : "work-item", + "owner" : "`ADR046-resources-device` accepted; `ADR046-provider-model-and-packaging` accepted; workspace root must add crate", + "parallelGroup" : "wi:ADR-046-provider-device-gpu", + "prerequisites" : [ + "ADR-046-provider-device-gpu" + ], + "specId" : "ADR-046-provider-device-gpu", + "topologicalRank" : 11, + "validation" : "`cargo build -p d2b-provider-device-gpu`; workspace policy crate-layout check passes", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-gpu/src/{controller.rs,telemetry.rs}`" + ], + "detailedDesign" : "Five triggers: `spec-generation-changed`, `deletion-requested`, `dependency-changed`, `scheduled-observe`, `owned-resource-changed`. Each trigger handler writes optimistic `ResourceMutationBatch`. Status writer in `status.rs`. Async watch task + per-resource reconcile tasks. Independent resources in parallel. Telemetry uses only closed `mode`/`video_sidecar`/`arbitration` semantics; `d2b.zone` and `d2b.provider` remain resource attributes and no Zone/resource-name-derived key is a metric label. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — implement the five-trigger reconcile loop using Provider toolkit async reconciler.", + "entryContracts" : [ + "ADR-046-provider-device-gpu" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-gpu-002", + "kind" : "work-item", + "owner" : "ADR046-gpu-001; `ADR-046-resource-reconciliation` implementation present; Provider toolkit `ResourceClient` available", + "parallelGroup" : "wi:ADR-046-provider-device-gpu", + "prerequisites" : [ + "ADR-046-provider-device-gpu", + "ADR046-gpu-001" + ], + "specId" : "ADR-046-provider-device-gpu", + "topologicalRank" : 12, + "validation" : "`cargo test -p d2b-provider-device-gpu --test combined_reconcile`; all five trigger handlers must reach their expected output state; structural metric descriptor test asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus GPU Device/Zone-name canary absence while preserving `d2b.zone` resource attributes", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-gpu/src/probe.rs`" + ], + "detailedDesign" : "Call `GpuEffectPort::probe_drm_device(selector)` on each `scheduled-observe` trigger; the effect port resolves device presence against the trusted device table and returns a presence/health result without exposing raw sysfs or device paths to the controller. Three-strike failure counter; `observe_interval_secs` (10–60, default 30); emit `DevicePresent` condition and update `lastProbedAt`.", + "entryContracts" : [ + "ADR-046-provider-device-gpu" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-gpu-003", + "kind" : "work-item", + "owner" : "ADR046-gpu-002", + "parallelGroup" : "wi:ADR-046-provider-device-gpu", + "prerequisites" : [ + "ADR-046-provider-device-gpu", + "ADR046-gpu-002" + ], + "specId" : "ADR-046-provider-device-gpu", + "topologicalRank" : 13, + "validation" : "`tests/conformance.rs` contains probe-mock path; `cargo test` passes", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-gpu/src/arbitration.rs`" + ], + "detailedDesign" : "On `spec-generation-changed` and each new claim: check `arbitration` vs `maxConcurrentClaims` vs current `holderRefs` length. Exclusive: reject any second claim with `ClaimConflict` condition, set requesting Device phase `Degraded`. Shared render-node: accept up to `maxConcurrentClaims`. Admission: `shared + renderNodeOnly=false` fails with `shared-arbitration-requires-render-node-only`.", + "entryContracts" : [ + "ADR-046-provider-device-gpu" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-gpu-004", + "kind" : "work-item", + "owner" : "ADR046-gpu-002", + "parallelGroup" : "wi:ADR-046-provider-device-gpu", + "prerequisites" : [ + "ADR-046-provider-device-gpu", + "ADR046-gpu-002" + ], + "specId" : "ADR-046-provider-device-gpu", + "topologicalRank" : 13, + "validation" : "`cargo test -p d2b-provider-device-gpu --test arbitration_conflict`; `cargo test -p d2b-provider-device-gpu --test render_node_enforcement`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-gpu/src/worker_gpu.rs`" + ], + "detailedDesign" : "Build and commit `Process` resource record with `template: gpu-worker` or `template: render-node-worker`; set `sandbox.seccompClass` (`w1-gpu` or `w1-gpu-render-node`), `sandbox.userNamespace: {mappingClass: process-principal-root}` (uid/gid resolved privately by core from signed worker template — controller does NOT write numeric values), `sandbox.namespaceClasses`, `sandbox.capabilityClasses=[]`, `sandbox.startRoot=false`; set `deviceUsage[{deviceRef,access,purpose}]`, `networkUsage: null`, `endpoints[{name,transport,purpose}]`, `budget` (including `pids` and `fds` bounded limits), `readiness` (with `class`, `initialDelay`, `timeout`, `failureThreshold`, `successThreshold`), and `restartPolicy` (with `class`, `backoffBase`, `backoffMax`, `backoffMultiplier`, `maxRestarts`, `resetAfter`). Provider/system-minijail validates and resolves the LaunchTicket and sends effect requests via `MinijailProcessEffectPort`; the core EffectPort adapter routes them to the **privileged broker** which performs `SpawnRunner`, `OpenDevice`, `clone3`, `uid_map`/`gid_map` writes, and fd transfer — the device-gpu controller does not have execution authority or fd access. `crossDomainTrusted` gating: the signed descriptor is static; `crossDomainTrusted` is projected from the Device setting into the LaunchTicket by Provider/system-minijail, which omits `GpuContextType::CrossDomain` from runtime argv when false. Primary reuse disposition: `adapt`. Preserved source-plan detail: `extract` argv builder logic into `argv.rs` as re-export from `d2b-host` (used by Provider/system-minijail at LaunchTicket resolution time; the signed component descriptor is static and is not rewritten per Device); `adapt` device allowlist token set from `bundle_resolver.rs` into `worker_gpu.rs` `GPU_DEVICE_ALLOWLIST` constant for `deviceUsage` population.", + "entryContracts" : [ + "ADR-046-provider-device-gpu" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-gpu-005", + "kind" : "work-item", + "owner" : "ADR046-gpu-002; `ADR046-components-processes-and-sandbox` (Provider/system-minijail present and able to handle Process resources with `gpu-worker`/`render-node-worker` templates)", + "parallelGroup" : "wi:ADR-046-provider-device-gpu", + "prerequisites" : [ + "ADR-046-provider-device-gpu", + "ADR046-gpu-002" + ], + "specId" : "ADR-046-provider-device-gpu", + "topologicalRank" : 13, + "validation" : "`cargo test -p d2b-provider-device-gpu`; `cargo test -p d2b-contract-tests --test minijail_gpu` continues to pass", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-gpu/src/worker_video.rs`, `tests/wire_constant_snapshot.rs`" + ], + "detailedDesign" : "Controller creates `Process/device--video` only after `GpuWorkerReady=True`. `worker_video.rs` builds `VideoArgvInput` from resolved device spec and signed descriptor binary path. Validates `wire_contract_snapshot()` matches committed golden at startup; fails closed if mismatch (error `device-wire-contract-mismatch`). NVIDIA device gating: include `nvidia-ctl`, `nvidia-device`, `nvidia-uvm` tokens in `deviceUsage[]` entries only when `videoNvidiaDecode=true`; the **privileged broker** opens the fds when executing the effect request from the core EffectPort adapter. Distinct allocator-assigned principal enforced by LaunchTicket (internal invariant; not expressed in the resource spec); `template: video-worker` descriptor declares no Wayland/audio endpoint capability. `sandbox.seccompClass: w1-video`; `sandbox.namespaceClasses` includes `pid`; `userNamespace: null` (explicit, tested invariant). Primary reuse disposition: `adapt`. Preserved source-plan detail: `extract` argv generator (re-export from `argv.rs`); `copy-unchanged` wire-contract constants into `tests/wire_constant_snapshot.rs` golden comparison.", + "entryContracts" : [ + "ADR-046-provider-device-gpu" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-gpu-006", + "kind" : "work-item", + "owner" : "ADR046-gpu-005 (video depends on GPU Process being Ready)", + "parallelGroup" : "wi:ADR-046-provider-device-gpu", + "prerequisites" : [ + "ADR-046-provider-device-gpu", + "ADR046-gpu-005" + ], + "specId" : "ADR-046-provider-device-gpu", + "topologicalRank" : 14, + "validation" : "`cargo test -p d2b-provider-device-gpu --test video_dependency`; `cargo test -p d2b-provider-device-gpu --test wire_constant_snapshot`; `cargo test -p d2b-contract-tests --test video_binary_contract` continues to pass", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/assertions.nix` (new GPU Device eval assertions); `tests/unit/nix/cases/device-gpu-eval.nix` (new Nix eval case); committed settings schema `docs/reference/schemas/v3/providers/device-gpu.settings.json`" + ], + "detailedDesign" : "Eval assertions as documented in § Nix configuration / Eval-time assertions. Canonical JSON golden as documented. Settings schema drift gate via `make test-drift`. `d2b.vms..graphics.*` options are deprecated (emit deprecation warning) until a transition generation removes them; they are not removed in the same commit that adds the Device spec option. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — map old `d2b.vms..graphics.*` fields to `d2b.zones..resources.` Device spec settings fields; add eval assertions.", + "entryContracts" : [ + "ADR-046-provider-device-gpu" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-gpu-007", + "kind" : "work-item", + "owner" : "ADR046-gpu-001; ADR 0046 Zone Nix emitter wired; `ADR-046-nix-configuration` Nix emitter present", + "parallelGroup" : "wi:ADR-046-provider-device-gpu", + "prerequisites" : [ + "ADR-046-provider-device-gpu", + "ADR046-gpu-001" + ], + "specId" : "ADR-046-provider-device-gpu", + "topologicalRank" : 12, + "validation" : "`nix-unit tests/unit/nix/cases/device-gpu-eval.nix`; `make test-drift`; `make test-flake`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-gpu/` component descriptor; controller/status tests" + ], + "detailedDesign" : "Do **not** declare a controller Provider state Volume. The device-gpu component descriptor declares an empty ProviderStateSet; controller and worker Process templates contain no `/state` mount. Bounded non-secret operational state is published to Device/Provider status and the core Operation ledger. GPU has no Device-payload Volume; render-node access remains a Device attachment resolved by LaunchTicket and broker policy. Primary reuse disposition: `create`. Preserved source-plan detail: `new` — status-first state assertions in the component descriptor and controller tests.", + "entryContracts" : [ + "ADR-046-provider-device-gpu" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-gpu-008", + "kind" : "work-item", + "owner" : "ADR046-gpu-001; D087 status-first state model present in the foundational ADR-046 specs", + "parallelGroup" : "wi:ADR-046-provider-device-gpu", + "prerequisites" : [ + "ADR-046-provider-device-gpu", + "ADR046-gpu-001" + ], + "specId" : "ADR-046-provider-device-gpu", + "topologicalRank" : 12, + "validation" : "`cargo test -p d2b-provider-device-gpu --test status_state`; component descriptor golden has no Provider state Volume declaration; controller Process template has no `/state` mount; ProviderStateSet query is empty; status/core-ledger fields carry bounded operational observations", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-gpu/README.md`" + ], + "detailedDesign" : "Must include: Provider identity, supported ResourceTypes, controller/service/worker binary descriptions, placement (Host, system domain), dependencies (system-minijail, volume-local, observability-otel), RBAC roles, security model summary, state/telemetry contract, build command (`cargo build -p d2b-provider-device-gpu`), test commands (`cargo test -p d2b-provider-device-gpu`), integration command (`make test-integration`), hardware test note (see `integration/README.md`), standalone-repository consumption stub.", + "entryContracts" : [ + "ADR-046-provider-device-gpu" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-gpu-009", + "kind" : "work-item", + "owner" : "ADR046-gpu-001", + "parallelGroup" : "wi:ADR-046-provider-device-gpu", + "prerequisites" : [ + "ADR-046-provider-device-gpu", + "ADR046-gpu-001" + ], + "specId" : "ADR-046-provider-device-gpu", + "topologicalRank" : 12, + "validation" : "`make test-policy` (workspace crate layout policy check)", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-core/src/{host_reconciler.rs,host_status.rs,host_process_audit.rs}`; adapted `nixos-modules/unsafe-local-workloads-json.nix`; `packages/d2b-provider-system-core/tests/host_posture_contract.rs`" + ], + "detailedDesign" : "`Provider/system-core` reconciler: (1) On user-only `Host` resource creation (`defaultDomain=user`, `allowedDomains=[user]`), set `status.isolationPosture = \"none\"` and `status.isolationPostureMessage = \"...\"` unconditionally; reject any operator-supplied value for these fields. Host resources with other execution policies do not receive `isolationPosture`. (2) On every user-only Host process launch: emit `ProcessEffect{event:\"launch\", provider:\"system-core-user\", domain:\"user\", no_isolation:true, ...}` audit record. (3) On every user-only Host process stop: emit `ProcessEffect{event:\"stop\", ...}`. (4) `d2b zone list`/`inspect` CLI renders `⚠ no isolation boundary (user domain)` annotation only for `Host` resources with `isolationPosture: \"none\"`; annotation is not suppressible. (5) `isolation-posture-declared` doctor check: passes when user-only `Host` resource status has `isolationPosture: \"none\"`; omitted when Zone has no user-only `Host` resources. (6) `no_isolation=true` is emitted in `ProcessEffect` records only; it does not appear in any OTEL span attribute, log field, or metric label. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `UnsafeLocalWorkload` private-bundle contract for the `Host` resource spec payload; adapt `HelperRegistry::allowed_uids` constraint as `defaultUserRef=User/` validation; adapt Nix `unsafe-local-workloads-json.nix` emitter for the new Host resource shape; gap-fill: add `ProcessEffect{no_isolation:true}` at `dispatch_launch` / stop call sites.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-host-posture-001", + "kind" : "work-item", + "owner" : "ADR046-audit-001 + ADR046-core-001; `Provider/system-core` owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-audit-001", + "ADR046-core-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "`host_posture_contract.rs` tests from the Host posture tests section of this spec; `d2b-contract-tests/tests/policy_telemetry_redaction.rs` asserts `no_isolation` key absent from all span/metric/log surfaces", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/identity.rs`, `packages/d2b-contracts/src/v3/resource_ref.rs`" + ], + "detailedDesign" : "Add ZoneId, ResourceTypeName, ResourceName, ResourceUid, ResourceRef, generation/revision newtypes, exact parsing/serde/Debug/redaction, and golden vectors Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-identities-001", + "kind" : "work-item", + "owner" : "W0 shared contract root; `d2b-contracts`", + "parallelGroup" : "wi:ADR-046-terminology-and-identities", + "prerequisites" : [ + "ADR-046-terminology-and-identities" + ], + "specId" : "ADR-046-terminology-and-identities", + "topologicalRank" : 2, + "validation" : "Rust property/vector tests; pure-Nix vector parity; malformed/collision/UID-recreate tests", + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/options-zones.nix`, `nixos-modules/resources.nix`, `nixos-modules/index.nix`" + ], + "detailedDesign" : "Validate Zone names, ResourceTypes/names/refs, shared Host/Guest ExecutionPolicy, and canonical sorted resource identities", + "entryContracts" : [ + "ADR-046-terminology-and-identities" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-identities-002", + "kind" : "work-item", + "owner" : "ADR046-identities-001; Nix integrator", + "parallelGroup" : "wi:ADR-046-terminology-and-identities", + "prerequisites" : [ + "ADR-046-terminology-and-identities", + "ADR046-identities-001" + ], + "specId" : "ADR-046-terminology-and-identities", + "topologicalRank" : 3, + "validation" : "nix-unit vectors and rendered contract tests", + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-credential-managed-identity/src/{controller.rs,agent.rs}; packages/d2b-provider-credential-managed-identity/{controller/main.rs,agent/main.rs}; packages/d2b-provider-credential-managed-identity/tests/topology.rs" + ], + "detailedDesign" : "Implement the controller/agent process split: separate `d2b-managed-identity-controller` binary with no IMDS client and no KK delivery, and `d2b-managed-identity-agent` binary with injected IMDS client via effect port and KK delivery. Controller manages Credential resources, spawns/monitors agent Processes, uses canonical Process templates, attaches LaunchTickets projecting `imdsEndpointAlias` and `credentialRef`, monitors agent Process health with bounded backoff, performs Deleted-phase cleanup without emitting Deleted closure audit, and applies D087 status-first state with no Provider state Volume. Agent validates `ExactSdkConsumer` via `AuthenticatedSubjectContext`, serves token-delivery methods, terminates Noise_KK delivery sessions, reports lease state, declares no direct IMDS egress, and keeps token bytes transient. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt the main managed-identity Provider; replace v2 provider registry/session assumptions with v3 controller/agent Process topology and `d2b.credential.v3` service.", + "entryContracts" : [ + "ADR-046-provider-credential-managed-identity" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-mi-topology-001", + "kind" : "work-item", + "owner" : "ADR046-credential-001 and ADR046-credential-002; owner: credential-managed-identity controller/agent topology. This topology contract is consumed by the later managed-identity controller work.", + "parallelGroup" : "wi:ADR-046-provider-credential-managed-identity", + "prerequisites" : [ + "ADR-046-provider-credential-managed-identity", + "ADR046-credential-001", + "ADR046-credential-002" + ], + "specId" : "ADR-046-provider-credential-managed-identity", + "topologicalRank" : 11, + "validation" : "`tests/topology.rs`; `integration/host-guest-placement.nix`; `make test-rust`; `make test-integration`; `make test-host-integration`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-minijail/src/sandbox_compiler.rs`" + ], + "detailedDesign" : "Accept `SandboxSpec` from common contracts; compile NamespaceClass/CapabilityClass/SeccompClass/UserNamespaceSpec/mount/environment/rlimit/umask into a versioned `CompiledSandboxPlan`; compute `sandboxRevisionDigest`; all rejection conditions from §12.1; no raw bitmask/BPF/argv/path in any output type; golden round-trip test vectors Primary reuse disposition: `adapt`. Preserved source-plan detail: EXTRACT/ADAPT.", + "entryContracts" : [ + "ADR-046-provider-system-minijail" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-minijail-001", + "kind" : "work-item", + "owner" : "`ADR046-process-001` (common spec/status types); `ADR046-provider-001` (toolkit/contracts); system-minijail Provider owner", + "parallelGroup" : "wi:ADR-046-provider-system-minijail", + "prerequisites" : [ + "ADR-046-provider-system-minijail", + "ADR046-process-001", + "ADR046-provider-001" + ], + "specId" : "ADR-046-provider-system-minijail", + "topologicalRank" : 11, + "validation" : "`tests/sandbox_compilation.rs`; `tests/schema.rs`; golden vectors", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-minijail/src/launch.rs`" + ], + "detailedDesign" : "LaunchTicket construction with compiled sandbox/budget/mount digests; ticket verification on ProviderSupervisor receipt; `d2b.supervisor.v3/IssueLaunchTicket` service call; expired/revoked/malformed ticket rejection", + "entryContracts" : [ + "ADR-046-provider-system-minijail" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-minijail-002", + "kind" : "work-item", + "owner" : "ADR046-minijail-001; common `LaunchTicket` contract", + "parallelGroup" : "wi:ADR-046-provider-system-minijail", + "prerequisites" : [ + "ADR-046-provider-system-minijail", + "ADR046-minijail-001" + ], + "specId" : "ADR-046-provider-system-minijail", + "topologicalRank" : 12, + "validation" : "`tests/lifecycle.rs`; `tests/fault_injection.rs`; `tests/fast_path.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Broker-side: `d2b-priv-broker` retains `SpawnRunner` op, invoked by the `MinijailProcessEffectPort` implementation owned by core/ProviderSupervisor; Provider-side: `packages/d2b-provider-system-minijail/src/launch.rs` calls `MinijailProcessEffectPort` with opaque Process/LaunchTicket/profile IDs; `user_ns.rs` implements the user namespace pre-establishment protocol" + ], + "detailedDesign" : "Linux ≥5.14 and delegated-leaf `cgroup.kill` platform gate; `clone3(CLONE_PIDFD | CLONE_INTO_CGROUP)` with pre-declared cgroup leaf FD; broker retained as child parent and sole `waitid(P_PIDFD)`/reap/exit-status owner; verified duplicate returned privately to ProviderSupervisor for poll/readiness and exact-main `pidfd_send_signal`; anchored `cgroup.kill` write for unambiguous intentional teardown; user namespace pre-establishment sequence (§7.7) when `userNamespace` set; host UID 0 rejection; parent name-to-inode re-validation; zero-host-capability invariant (ADR 0021); `MinijailProcessEffectPort` privately maps opaque IDs to SpawnRunner/OpenDevice/clone3/uid-map/FD effects; Provider crate imports no broker service/client/DTO", + "entryContracts" : [ + "ADR-046-provider-system-minijail" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-minijail-003", + "kind" : "work-item", + "owner" : "ADR046-minijail-001; broker integration owner", + "parallelGroup" : "wi:ADR-046-provider-system-minijail", + "prerequisites" : [ + "ADR-046-provider-system-minijail", + "ADR046-minijail-001" + ], + "specId" : "ADR-046-provider-system-minijail", + "topologicalRank" : 12, + "validation" : "`tests/fault_injection.rs`; `tests/platform_gate.rs`; `tests/broker_wait_contract.rs`; `tests/cgroup_kill_finalize.rs`; `integration/clone3_pidfd/`; `integration/user_namespace/`; `integration/broker_parent_reap/`; `integration/cgroup_kill_subtree/`; `integration/kernel_platform_gate/`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Broker-side parent wait/reap and typed terminal relay in `packages/d2b-priv-broker/src/`; non-parent observation/status consumption in `packages/d2b-provider-system-minijail/src/{pidfd,wait}.rs`" + ], + "detailedDesign" : "Broker that called `clone3` alone calls `waitid(P_PIDFD)`, collects exit status, and reaps exactly once; ProviderSupervisor `AsyncFd` readability is a hint only and never a wait/status source; controller consumes the identity-bound broker relay and holds no raw pidfd; ProviderSupervisor duplicate reacquisition is dispatched through a bounded blocking adapter with explicit timeout; pidfd never serialized; verified broker/ProviderSupervisor holder retains exact-main `pidfd_send_signal`; no PID/PGID fallback; graceful deadline followed by mandatory anchored leaf `cgroup.kill`; empty-leaf proof before rmdir; exit class classification (clean-exit/crash/signal/timeout/unknown) Primary reuse disposition: `adapt`. Preserved source-plan detail: EXTRACT/ADAPT.", + "entryContracts" : [ + "ADR-046-provider-system-minijail" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-minijail-004", + "kind" : "work-item", + "owner" : "ADR046-minijail-003; wait/pidfd owner", + "parallelGroup" : "wi:ADR-046-provider-system-minijail", + "prerequisites" : [ + "ADR-046-provider-system-minijail", + "ADR046-minijail-003" + ], + "specId" : "ADR-046-provider-system-minijail", + "topologicalRank" : 13, + "validation" : "`tests/lifecycle.rs`; `tests/broker_wait_contract.rs` (only clone3 parent calls waitid/reaps; poll readability cannot supply status); `tests/cgroup_kill_finalize.rs` (setsid descendant and PGID reuse); `tests/redaction.rs` (PID never in log/status/audit); `tests/blocking_adapter.rs` (duplicate/status relay via adapter; timeout → error)", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-minijail/src/` — controller binary entry point; reconcile loop; adoption; quarantine; bootstrap authz; health/status; restart; finalize" + ], + "detailedDesign" : "Full Process/EphemeralProcess reconcile algorithm (§8); fast path ≤5/≤20 ms gates; spawn via `MinijailProcessEffectPort` (opaque IDs; no broker DTO imported); adoption algorithm (§8.5) with `/proc` reads, cgroup enumeration, and original-broker-parent verification via bounded blocking adapters; quarantine on ambiguity; quarantine reuse blocked until externally established process-absence proof or full Zone reset; no signal or cgroup.kill write to quarantined/ambiguous identity; restart/backoff driven only by broker-relayed terminal status; finalize (§8.6) with exact-main SIGTERM, bounded grace, mandatory cgroup.kill, broker wait/reap, empty-leaf proof, and no PGID ownership; EphemeralProcess continuation recovery (§9); bootstrap authz scope (§3); post-bootstrap RBAC; metric label closed-set enforcement (no `zone` label); controller writes status only on Process/EphemeralProcess resources; Provider resource status aggregated by core; the controller declares no Provider state Volume and mounts none — its bounded non-secret operational state lives in `status`/the core Operation ledger (§5.1, D087) and running units are re-adopted from cgroup leaves + fresh pidfds on restart", + "entryContracts" : [ + "ADR-046-provider-system-minijail" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-minijail-005", + "kind" : "work-item", + "owner" : "All of ADR046-minijail-001 through ADR046-minijail-004; ComponentSession/d2b-bus (ADR046-session-001, ADR046-bus-001); bootstrap authz", + "parallelGroup" : "wi:ADR-046-provider-system-minijail", + "prerequisites" : [ + "ADR-046-provider-system-minijail", + "ADR046-bus-001", + "ADR046-minijail-001", + "ADR046-minijail-002", + "ADR046-minijail-003", + "ADR046-minijail-004", + "ADR046-session-001" + ], + "specId" : "ADR-046-provider-system-minijail", + "topologicalRank" : 14, + "validation" : "`tests/lifecycle.rs`; `tests/ephemeral_lifecycle.rs`; `tests/conformance.rs`; `tests/adoption_quarantine.rs`; `tests/broker_wait_contract.rs`; `tests/cgroup_kill_finalize.rs`; `tests/platform_gate.rs`; `tests/bootstrap_authz.rs`; `tests/fast_path.rs`; `tests/blocking_adapter.rs`; `integration/adoption_restart/`; `integration/quarantine_scenario/`; `integration/broker_parent_reap/`; `integration/cgroup_kill_subtree/`; `integration/kernel_platform_gate/`; `integration/latency_gate/`; shared conformance suite in `d2b-process-conformance`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/` — v3 Nix `Process`/`EphemeralProcess` resource authoring; Provider catalog entry; `docs/reference/schemas/v3/Process.json`; `docs/reference/schemas/v3/EphemeralProcess.json`; `make test-drift` schema drift gate" + ], + "detailedDesign" : "Nix module accepts `d2b.zones..resources.` with `type = \"Process\"` or `\"EphemeralProcess\"`; eval-time validation rules (§16.4); build-time JSON validation (§16.5); artifact catalog integration; cleanup contract tests (§16.5)", + "entryContracts" : [ + "ADR-046-provider-system-minijail" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-minijail-006", + "kind" : "work-item", + "owner" : "ADR046-minijail-005; Nix integrator; test infrastructure owner", + "parallelGroup" : "wi:ADR-046-provider-system-minijail", + "prerequisites" : [ + "ADR-046-provider-system-minijail", + "ADR046-minijail-005" + ], + "specId" : "ADR-046-provider-system-minijail", + "topologicalRank" : 15, + "validation" : "`nix-unit` eval cases for every validation rule; schema drift gate; `tests/schema.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/network.rs`: NetworkSpec, NetworkStatus, AttachmentSpec, AttachmentStatus, ExternalAttachmentSpec, ExternalAttachmentStatus, PortForwardSpec, NetworkConditionType, opaque AttachmentHandle, and AttachmentGenerationFence; `packages/d2b-contracts/src/v3/ifname.rs`: IfName newtype, derivation, collision detection (extracted from `d2b-host/src/ifname.rs`). Also defines `User/net-local-controller` as a proper Resource with explicit lifecycle: `Provider/network-local`'s Nix package/module provisions the reserved `net-local-controller` OS account with a private fixed UID/GID in Host prerequisites and in the generic net-VM nixos-system artifact (same account, same UID/GID inside the Guest); the network-local controller creates and owns the User Resource (`spec.osUsername: net-local-controller`, `ownerRef: Provider/network-local`, `managedBy: controller`); `Provider/system-core` verifies the account via NSS lookup and reconciles the User Resource to Ready — it does not provision the OS account. No numeric UID/GID enters any ResourceSpec field, authz check, or audit record; `User.status` MAY carry diagnostic `uid`/`gid` values discovered by NSS lookup, but those are informational only and are never authorization inputs. The network-local controller waits for `User/net-local-controller` to reach `Ready` before creating any config Volume (reconcile precondition, not a bootstrap side effect)." + ], + "detailedDesign" : "Strict ResourceEnvelope with Network-specific spec/status. IfName newtype: IFNAMSIZ-1 validated, FNV-1a 64-bit derivation, base32 Crockford, 8-char suffix, bridge/tap role prefixes, detect_collisions over IfNameMapping slice. cidrOverlaps: pure Rust IPv4 arithmetic, same algorithm as lib.nix. NetworkSpec validators: /24 lanCidr with .0 base, /30 uplinkCidr, unique attachment indices 2–250, default hostBlocklist enforcement. The opaque attachment realization binds Network UID/generation and attachment UID/generation so deletion can supply a non-printable ID plus explicit expected generation fence without an IfName/path. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-network" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-network-001", + "kind" : "work-item", + "owner" : "W0 shared contract root; `d2b-contracts`", + "parallelGroup" : "wi:ADR-046-resources-network", + "prerequisites" : [ + "ADR-046-resources-network" + ], + "specId" : "ADR-046-resources-network", + "topologicalRank" : 9, + "validation" : "Golden JSON/CBOR vectors; CIDR overlap property tests; IfName collision and derivation determinism tests; default hostBlocklist enforcement; attachment index uniqueness; `User/net-local-controller` User resource lifecycle/readiness test: controller creates User Resource with `spec.osUsername = \"net-local-controller\"` (`ownerRef: Provider/network-local`); controller waits for User resource to reach `Ready` before proceeding; controller aborts with `ConfigVolumeReady=False/user-not-ready` if User resource is not Ready; verifies no numeric UID/GID appears in the Resource spec, authz check, or audit record; verifies that any diagnostic `uid`/`gid` in `User.status` is never used as an authorization input", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-network-local/src/ifname.rs`, `bridge_port.rs`, `nftables.rs`, `routes.rs`, `netlink.rs`" + ], + "detailedDesign" : "Move IfName derivation to `d2b-contracts` (ADR046-network-001); keep bridge_port, nftables, routes, netlink in Provider crate. nftables: retain the Network-owned `inet d2b` chain layout, ownership markers, and coexistence matrix; emit no USBIP/TCP-3240 rule and compute drift over only the Network UID ownership projection. routes: adapt dnsmasq-bound check to use Network status instead of `HostJson.environments`. netlink: keep IPv6-off sequence; add defense-in-depth re-application path. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract (ifname, bridge_port, nftables) into shared network-local Provider library; adapt (routes, netlink) into controller observe loop.", + "entryContracts" : [ + "ADR-046-resources-network" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-network-002", + "kind" : "work-item", + "owner" : "ADR046-network-001; d2b-host network modules owner", + "parallelGroup" : "wi:ADR-046-resources-network", + "prerequisites" : [ + "ADR-046-resources-network", + "ADR046-network-001" + ], + "specId" : "ADR-046-resources-network", + "topologicalRank" : 10, + "validation" : "Existing `bridge_port::tests::readback_matches_defaults`, `ops::tap::tests::set_bridge_port_flags_readback_drift_fails_closed`, `netlink::tests::ipv6_off_sequence_runs_in_order`, nftables coexistence matrix tests; all pinned in `tests/golden/pinned/host-prepare-network.txt` and `tests/golden/pinned/net-canaries.txt`", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-network-local/` — artifact catalog integration for net-VM nixos-system artifact resolution; `packages/d2b-provider-network-local/nix/` — default net-VM NixOS module (parameterized successor to net.nix), built and registered as a nixos-system artifact in `d2b.artifacts`" + ], + "detailedDesign" : "`Network.spec.netVmSystemArtifactId` is REQUIRED. It must reference a declared `d2b.artifacts` entry with `type = \"nixos-system\"`; verified at Nix build time (Stage 2 check, hard build error if absent or wrong type). No implicit default exists; Provider artifacts cannot silently provide a separately typed system artifact. The controller sets `Guest.spec.systemArtifactId` to the artifact ID value at reconcile time (the value is already validated by the build; the controller fails closed if absent at runtime). The net-VM nixos-system artifact is **generic** (INV-NET-008): it contains the guest-agent binary and runtime, kernel, base NixOS services, systemd-networkd NIC bootstrap, and the `net-local-controller` **OS account** provisioned by `Provider/network-local`'s Nix module (same private fixed UID/GID as on the Host, so that virtiofs view ACLs on config Volume layout entries are enforced consistently inside the Guest; `Provider/system-core` performs NSS lookup reconciliation, not OS account provisioning; no numeric UID/GID appears in any ResourceSpec field, authz check, or audit record; `User.status` MAY carry diagnostic `uid`/`gid` from NSS lookup but those are informational only and never authorization inputs). It does NOT encode per-Network desired data; per-Network config (dnsmasq, nftables, routing, attachments) is delivered via the controller-owned config Volume and applied by the guest-agent Process. The artifact preserves compile-time-fixed content: `lib.mkForce` on 10-eth-dhcp (INV-NET-001); two systemd-networkd interface units matched by MAC; IPv6 suppression sysctls on NIC interfaces; ip6 filter table drop-all policy. **mDNS reflector and local dnsmasq DNS bridge are separate owned Process resources** (D-NETWORK-001); they are not inline services in the artifact.", + "entryContracts" : [ + "ADR-046-resources-network" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-network-003", + "kind" : "work-item", + "owner" : "ADR046-network-001, ADR046-network-002; Provider/runtime-cloud-hypervisor dossier owner", + "parallelGroup" : "wi:ADR-046-resources-network", + "prerequisites" : [ + "ADR-046-resources-network", + "ADR046-network-001", + "ADR046-network-002" + ], + "specId" : "ADR-046-resources-network", + "topologicalRank" : 11, + "validation" : "nix-unit: `tests/unit/nix/cases/net-vm-network.nix` (adapted to v3 resource API); INV-NET-001 assertion in new nix-unit case; no mDNS inline service appears in the generated artifact; no per-Network dnsmasq or nftables data in artifact (INV-NET-008); integration test: mDNS Process resources are created when `spec.mdns.enable = true`; Stage 2 build test: absent `netVmSystemArtifactId` fails with required-field build error; wrong artifact type fails with `artifact-type-mismatch` error; `packages/d2b-provider-network-local/tests/net_vm_artifact_is_generic.rs` — two Networks with different CIDRs produce same `systemArtifactId` and different config Volume content", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/resources-network.nix`: Nix resource object emitter for Network ResourceType; `nixos-modules/index.nix`: network resource compilation section" + ], + "detailedDesign" : "The emitter replaces `d2b.envs.` with `d2b.zones..resources. = { type = \"Network\"; spec = { ... }; }` (attr key = resource name; `type` explicit field; `spec` fields identical to the canonical ResourceSpec JSON — no bespoke Nix vocabulary). It validates CIDR shape, attachment index uniqueness, external attachment constraints, and CIDR overlap (reusing `cidrOverlaps` from `lib.nix`). **Bridges are NOT emitted as `systemd.network.netdevs` entries** (D-NETWORK-003 resolved; bridges are created dynamically by the broker `CreateBridge` op at reconcile time). The Nix emitter provisions only bootstrap/static prerequisites that do not require runtime bridge IfNames: `networking.networkmanager.unmanaged` pattern for the `d2b-*` prefix (covers all dynamically-created bridges and taps regardless of specific IfNames; emitted to `00-d2b-unmanaged.conf`); schema validation and controller binary deployment artifacts. Current `d2b.vms..env` + `d2b.vms..index` attachment semantics (`options-vms.nix` lines 944/962) become `Network.spec.attachments[].executionRef` + `index`. Current `d2b.site.allowUnsafeEastWest` (`options-site.nix` line 48) moves to the per-Network `isolation.allowEastWest` field; Zone.spec is empty in v3. Current `d2b.hostLanCidrs` (`options-site.nix` line 382) becomes the Host resource's network inventory, queried at runtime; at Nix build time the eval may validate CIDRs against declared host configuration input. The emitter does not emit `boot.kernel.sysctl` entries per bridge IfName (bridges do not exist at activation time; IPv6 suppression is applied by `CreateBridge` and `ApplySysctl` per INV-NET-002). **Nix option types** for `spec.*` fields are generated from `Network.schema.json`; they are not hand-written. **Bundle generation**: the emitter collects all declared `Network` resource objects, sorts them lexicographically by `(type, name)`, serializes each as canonical JSON **omitting `managedBy` and `configurationGeneration`** (core sets these at activation), and assembles the Zone resource bundle at `$out/bundle.json` (see [Nix configuration contract — Stage 3](#stage-3--build-output-zone-resource-bundle)). The emitter records a `providerSchemaDigest` entry for `Provider/network-local` in the bundle resolved from the artifact catalog. The bundle's `contentHash` is a SHA-256 of the sorted canonical resource array; the derivation is fixed-output so that identical configuration always produces the same store path. The `managedBy` field is NOT set by the emitter; core sets `managedBy = \"configuration\"` and assigns the `configurationGeneration` counter when applying the bundle. The core controller retains prior bundle copies under `/var/lib/d2b/zones//configuration/prior/` per [Generation lifecycle and cleanup contract](#generation-lifecycle-and-cleanup-contract).", + "entryContracts" : [ + "ADR-046-resources-network" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-network-004", + "kind" : "work-item", + "owner" : "ADR046-network-001, ADR046-network-002, ADR046-network-003; Nix integrator", + "parallelGroup" : "wi:ADR-046-resources-network", + "prerequisites" : [ + "ADR-046-resources-network", + "ADR046-network-001", + "ADR046-network-002", + "ADR046-network-003" + ], + "specId" : "ADR-046-resources-network", + "topologicalRank" : 12, + "validation" : "nix-unit CIDR overlap, assertion eval, and bridge-sysctl cases; `make test-flake` with updated examples; `make test-drift` for schema/emitter parity; `packages/d2b-contracts/tests/generation_bundle.rs` for bundle format and `contentHash` stability; nix-unit `tests/unit/nix/cases/generation-cleanup-absent-network.nix` for removed-resource scheduling (added by ADR046-network-008)", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-network-local/src/controller.rs`: async NetworkReconciler; `packages/d2b-provider-network-local/src/plan.rs`: ReconcilePlan computation; `packages/d2b-provider-network-local/src/observe.rs`: drift-detection observe loop. Full crate layout required (see [Package and crate boundary](#package-and-crate-boundary)): `src/` (controller/plan/observe + colocated unit tests), `tests/` (hermetic conformance and state-machine tests), `integration/` (provider-system reconcile fixtures), `README.md` (Network ResourceType, controller binary, placement, RBAC, security invariants, build/test/integration commands)." + ], + "detailedDesign" : "Implements full async reconcile interface from `ADR-046-resource-reconciliation`. `plan()` computes desired vs. actual bridge-presence, sysctl, host-side nftables, hosts-file, NM-unmanaged, config Volume content, Guest, guest-agent Process, and mDNS-Process states. `reconcile()` dispatches in order: `CreateBridge` for each bridge not present (broker applies IPv6 sysctls atomically; `CreateBridge` failure sets `FabricReady=False/bridge-create-error` and aborts) → `ApplySysctl` (defense-in-depth IPv6) → `ApplyNftables` (host-side `inet d2b` table) → `ApplyNmUnmanaged` → `ApplyRoute` → `UpdateHostsFile` → `SeedDnsmasqLease` for new reservations → **Volume upsert** (two-phase): Phase 1 — create `Volume/net--config` with `kind: ephemeral`, `source.executionRef: Host/`, `source.settings.kind: tmpfs`, `quota: {maxBytes: 4194304, maxInodes: 128, enforcement: hard}` (tmpfs quota charged to Host memory budget), `layout` entries (root directory with `type: directory` and four config files each with `type: file`, `ownerRef: User/net-local-controller`, `groupRef: User/net-local-controller`, `mode: \"0640\"`, `accessAcl: []`, `defaultAcl: []`, `noFollow: true`, conservative create/repair/cleanup policies), `views: {guest-readonly: {path: \"\", rights: [read, traverse]}}`, `attachments: []` (no Guest attachment); abort on terminal error with `ConfigVolumeReady=False/config-volume-error`. Wait for Volume backing to reach `Ready`; requeue on `Degraded`/`Failed` with `ConfigVolumeReady=False/volume-not-ready`. Write rendered config content through Volume write service (no raw host paths). Phase 2 — create Guest upsert with `systemArtifactId` from REQUIRED `Network.spec.netVmSystemArtifactId`. Wait for Guest `Ready`. Then update Volume with Guest attachment: `attachments: [{executionRef: Guest/, transport: virtiofs, view: guest-readonly, access: read-only, mountPath: \"/run/d2b/net-config\", settings: {posixAcl: false, xattr: false, cache: auto, inodeFileHandles: never, threadPoolSize: null, socketGroup: null}}]`; wait for attachment `Ready`; requeue on `Degraded` with `ConfigVolumeReady=False/attachment-not-ready`. Create or update guest-agent Process `Process/net--agent` with `processClass: worker`, `sandbox: {namespaceClasses: [], capabilityClasses: [network-admin, network-bind, network-raw]}` (inherits Guest network namespace; `network-admin`→`CAP_NET_ADMIN`, `network-bind`→`CAP_NET_BIND_SERVICE`, `network-raw`→`CAP_NET_RAW`, all effective in Guest network namespace only; INV-NET-009), `mounts: [{volumeRef: Volume/net--config, view: guest-readonly, mountPath: \"/run/d2b/net-config\", access: read-only, required: true}]`. mDNS Process upsert when `spec.mdns.enable = true` (D-NETWORK-001) → `SetBridgePortFlags` per tap. Removed attachments first wait for Guest/VMM FD ownership to close, then issue `DeletePersistentTap` with the retained opaque attachment ID and current expected Network/attachment generations; the handle remains retained until confirmed effect or validated absence. Stale generation refreshes/requeues, transient kernel error retries, and foreign marker fails closed. Each broker op returns typed audit evidence. `observe()` re-reads `firewallDigest` (host-side), bridge isolation flags, IPv6 sysctls, and guest-agent Process status (`dnsmasq-bound`, `firewall-applied` predicates); queues reconcile on drift. Metrics use only the fixed semantic labels in §OTEL spans and metrics; Zone/Network identity remains in OTEL resource attributes and permitted audit fields and never enters metric labels or span attributes. **Finalizer** (strictly child-first): `NetworkDraining` → stop workload Guests and await VMM FD closure → generation-fenced `DeletePersistentTap` for each retained attachment, awaiting confirmation → delete guest-agent Process and mDNS Processes; wait for each Deleted watch event → update Volume to remove Guest attachment (`attachments: []`); wait for attachment removal confirmed → delete `Guest/`; wait for Deleted watch event → delete `Volume/net--config`; wait for Deleted watch event → `ApplyNftables` (empty) → `ApplyNmUnmanaged` (empty) → `UpdateHostsFile` (empty) → `DeleteBridge` for each bridge (idempotent) → clear finalizer. No USBIP rules installed by Network; device-usbip issues the existing `UsbipBindFirewallRule` request with action `Ensure` for apply or `Remove` for release (D-NETWORK-002).", + "entryContracts" : [ + "ADR-046-resources-network" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-network-005", + "kind" : "work-item", + "owner" : "ADR046-network-001–004; network-local controller owner; D-NETWORK-001, D-NETWORK-002, and D-NETWORK-003 resolved", + "parallelGroup" : "wi:ADR-046-resources-network", + "prerequisites" : [ + "ADR-046-resources-network", + "ADR046-network-001" + ], + "specId" : "ADR-046-resources-network", + "topologicalRank" : 10, + "validation" : "`ADR046-reconcile-001` toolkit conformance; latency gates (p95 ≤5 ms hint-to-handler); Network-specific: CIDR conflict blocks reconcile, `CreateBridge` failure sets `FabricReady=False`, Volume creation failure sets `ConfigVolumeReady=False/config-volume-error`, `User/net-local-controller` not Ready aborts with `ConfigVolumeReady=False/user-not-ready`, Volume schema round-trip (kind=ephemeral, source.settings.kind=tmpfs, quota.enforcement=hard, layout type=file entries, views.guest-readonly.rights=[read,traverse]), tmpfs quota charged to Host memory budget (test Volume creation fails when Host memory budget exceeded), Guest not created before Volume backing `Ready`, Guest attachment not added before Guest `Ready`, guest-agent Process created after attachment `Ready` (`processClass: worker`, `namespaceClasses: []`, `capabilityClasses: [network-admin, network-bind, network-raw]`, `access: read-only`, `required: true`), host-capability leakage test: no host-netns process gains `CAP_NET_ADMIN`/`CAP_NET_BIND_SERVICE`/`CAP_NET_RAW` as result of guest-agent launch (INV-NET-009; `tests/host-integration/guest-agent-cap-confinement.nix`), removed attachment and finalizer call `DeletePersistentTap` only after Guest/VMM FD closure with opaque ID/current generations, validated absence succeeds, transient failure retains handle/retries, stale generation refreshes, foreign marker blocks without deletion, request/audit contain no IfName/path, `DeleteBridge` called only after tap confirmations, Volume attachment removed before net-VM Guest deletion in finalizer (test order: workload FD closure → persistent taps deleted → agent Deleted → Volume attachment removed → net-VM Guest Deleted → Volume Deleted → bridges), east-west invariant (INV-NET-003), hostBlocklist enforcement (INV-NET-004), macvtap attachment status (delegated to runtime-ch), mDNS Process created/deleted with `spec.mdns.enable` toggle, broker INV-NET-002 tests, config-only spec change updates Volume content and triggers agent reload without Guest restart (INV-NET-008); golden tests updated for v3 IfNames; structural metric descriptor test asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `network`, and every resource-name-derived label and verifies a Network-name canary is absent from emitted label values", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`tests/unit/nix/cases/net-vm-network.nix` (adapted to v3 resource API); updated golden pins; `tests/host-integration/bridge-isolation.nix` (adapted); `packages/d2b-priv-broker/tests/{bridge_lifecycle,persistent_tap_lifecycle}.rs` (new hermetic broker tests). Provider crate test directories: `packages/d2b-provider-network-local/tests/` — hermetic Cargo integration tests (conformance suite, controller state machine, CIDR validation vectors, IfName determinism, invariant tests INV-NET-001–007, reconcile/observe/finalize with deterministic clock, fault injection); `packages/d2b-provider-network-local/integration/` — container/Host/Guest lifecycle fixtures invoked by `make test-integration` (bridge isolation, east-west double opt-in, nftables drift detection, persistent-tap and macvtap lifecycle). Both directories required by package policy." + ], + "detailedDesign" : "Rust integration tests: NetworkSpec CIDR validation golden vectors; AttachmentSpec index uniqueness; ExternalAttachmentSpec mutual-exclusion validation; IfName derivation determinism; CIDR overlap arithmetic; INV-NET-001 through INV-NET-009 invariant tests; reconcile/observe/finalize state machine (deterministic clock). Broker tests: `create_bridge_applies_ipv6_sysctl` (INV-NET-002 layer 1); `delete_bridge_is_idempotent`; `delete_bridge_never_cascades_attached_tap`; `create_bridge_parameters_match_spec` (MTU, STP disabled, multicast snooping disabled); `delete_persistent_tap_pairs_with_create`; `delete_persistent_tap_absent_is_idempotent_after_ownership_validation`; `delete_persistent_tap_rejects_stale_network_generation`; `delete_persistent_tap_rejects_stale_attachment_generation`; `delete_persistent_tap_foreign_marker_fails_closed`; `delete_persistent_tap_request_and_audit_have_no_ifname_or_path`. Controller tests: `reconcile_applies_sysctl_defense_in_depth` (INV-NET-002 layer 2); `volume_created_before_guest`; `guest_not_created_until_volume_ready`; `agent_process_created_after_guest`; `removed_attachment_waits_for_vmm_then_delete_persistent_tap`; `finalizer_order_vmm_then_taps_then_agent_then_guest_then_volume_then_bridges`; `delete_persistent_tap_transient_retry_retains_handle`; `delete_persistent_tap_generation_mismatch_refreshes`; `delete_persistent_tap_foreign_marker_blocks_finalizer`; `config_only_spec_change_updates_volume_no_guest_restart` (INV-NET-008); `finalizer_calls_delete_bridge`; `mdns_process_created_on_enable`; `mdns_process_deleted_on_disable`; `host_capability_leakage` (INV-NET-009). nix-unit: INV-NET-001 lib.mkForce assertion; net-VM artifact has no inline mDNS service and no per-Network dnsmasq/nftables data (INV-NET-008); Network emitter CIDR constraint assertions; no `systemd.network.netdevs` bridge entries emitted. Host integration: bridge isolation with east-west opt-in; nftables drift detection; persistent-tap and macvtap create/delete lifecycle; config Volume update propagates to guest-agent without Guest restart; `tests/host-integration/guest-agent-cap-confinement.nix` (INV-NET-009 zero leakage to host netns).", + "entryContracts" : [ + "ADR-046-resources-network" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-network-006", + "kind" : "work-item", + "owner" : "ADR046-network-001, ADR046-network-005; test owner", + "parallelGroup" : "wi:ADR-046-resources-network", + "prerequisites" : [ + "ADR-046-resources-network", + "ADR046-network-001", + "ADR046-network-005" + ], + "specId" : "ADR-046-resources-network", + "topologicalRank" : 11, + "validation" : "All listed tests must pass before `nixos-modules/network.nix` removal is eligible", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`Provider/device-usbip` owns one relay Process/Endpoint authority per Network plus the typed EffectPort adapter for the existing closed `UsbipBindFirewallRule` request with closed action enum `Ensure|Remove`. The controller watches only the `networkRef` resource's identity/readiness/generation; Core privately resolves Network UID to relay attachment and firewall intent. Network spec/status is not mutated with USBIP fields. Full crate layout required for `packages/d2b-provider-device-usbip/` (see [Package and crate boundary](#package-and-crate-boundary)): `src/` (controller and usbip runner + unit tests), `tests/` (hermetic conformance, dependency-watch state machine, `UsbipBindFirewallRule` `Ensure|Remove` round-trip), `integration/` (Host/Guest USBIP attach/detach lifecycle fixtures), `README.md` (Provider identity, provider-neutral USB Service/Binding types, USBIP Processes/Endpoints, Network least-privilege dependency contract, RBAC, security invariants, standalone-repo path)." + ], + "detailedDesign" : "Device-usbip's typed EffectPort is the sole semantic owner of every USBIP TCP/3240 rule. Its Core adapter resolves the opaque per-Network/per-busid intent and issues the same `UsbipBindFirewallRule` request with action `Ensure` for apply and `Remove` for release; no separate release op exists. `Remove` is generation-bound, ownership-scoped, idempotent after validated absence, and foreign-marker fail-closed. The controller retains firewall token/status and the relay authority reference until the broker confirms `Remove`; its strict provider status owns firewall digest/drift. Network-local emits no generic host or net-VM TCP/3240 allow and ignores device-usbip ownership markers in Network drift. The device Provider owns exactly one multiplexed relay Endpoint authority per Network and supplies Binding proxies only authorized connected streams through LaunchTickets. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-network" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-network-007", + "kind" : "work-item", + "owner" : "ADR046-network-005; device-usbip Provider dossier; D-NETWORK-002 resolved", + "parallelGroup" : "wi:ADR-046-resources-network", + "prerequisites" : [ + "ADR-046-resources-network", + "ADR046-network-005" + ], + "specId" : "ADR-046-resources-network", + "topologicalRank" : 11, + "validation" : "device-usbip conformance tests cover the exact closed `Ensure|Remove` enum (unknown actions rejected), same-request broker mapping for apply/release, expected Network/Service generation binding, exact per-Network/per-busid scoping, idempotent validated-absence `Remove`, one relay Endpoint authority, ownership-scoped drift/status, foreign-marker rejection, transient retry, and retention of status/token/authority until effect confirmation; network-local nftables tests assert no TCP/3240/USBIP rule on host or net VM and prove USBIP rule churn does not change Network `FirewallReady`; the pinned USBIP firewall golden moves to device-usbip ownership", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/configuration.rs`: bundle application, diff, generation-transition logic (including per-item name-conflict handling), prior-bundle retention under `/var/lib/d2b/zones//configuration/prior/`; `packages/d2b-core-controller/src/cleanup.rs`: removal scheduling and `PendingCleanup` condition tracking; `packages/d2b-contracts/src/generation_bundle.rs`: `ZoneBundle`/`BundleResource`/`BundleMetadata` **input** DTOs — MUST NOT include `managedBy` or `configurationGeneration` (both are persisted resource metadata set by core at activation, not bundle input fields); `ManagedBy` closed enum `{ Configuration, Controller, Api }` and `configurationGeneration: u64` live in `packages/d2b-core-controller/src/resource_store.rs` as persisted resource metadata fields; `nixos-modules/resources-network.nix` (emits bundle with `managedBy`/`configurationGeneration` absent; core sets both at activation per ADR046-network-004); `d2b.zones..retainedGenerations` Nix/compiler-level Zone option (outside `Zone.spec`; default `3`, range `1..16`); `tests/unit/nix/cases/generation-cleanup-absent-network.nix`; `packages/d2b-contracts/tests/generation_bundle.rs`; `tests/host-integration/nix-generation-cleanup.nix`" + ], + "detailedDesign" : "**Core generation tracking** (`packages/d2b-core-controller/src/configuration.rs`): core maintains a monotone `configurationGeneration` counter per Zone in its durable state. On each bundle application it compares the incoming `contentHash` against the prior applied hash. If different, it increments `configurationGeneration`, sets `managedBy = \"configuration\"` and the new counter value on each resource in the bundle, and performs the resource diff (create/update/delete scheduling). The `managedBy` and `configurationGeneration` fields are absent from the Nix-emitted bundle and are set exclusively by core at activation time. **`managedBy` field and per-item name-conflict handling**: `ManagedBy` is a closed enum (`Configuration`, `Controller`, `Api`) persisted in resource metadata at `packages/d2b-core-controller/src/resource_store.rs`. It is NOT a field in `ZoneBundle`/`BundleResource` input DTOs; core sets it at activation. Controllers set `ManagedBy::Controller` when creating owned children (net-VM Guest, config Volume, guest-agent Process, mDNS Processes); exact controller identity/UID/generation are tracked in separate internal metadata, not embedded in the `managedBy` value. API-created resources carry `ManagedBy::Api` and persist until explicit delete with no bundle-driven lifecycle. Core's generation-transition logic only schedules bundle-driven Delete for `ManagedBy::Configuration` resources. **Per-item name-conflict handling**: when a bundle item's `(zone, name)` already exists with `managedBy ≠ \"configuration\"`, core skips that item and records it with `phase = Degraded, reason: name-conflict`; a `ResourceConflictSkipped` audit record is emitted for that item. All non-conflicting items in the bundle proceed normally (Provider-state contract). The existing resource is left completely untouched. The operator deletes the conflicting resource via the resource API; the next bundle application applies the item. **Removal scheduling**: on generation N+1 activation, core performs a set difference: `prev_configuration_managed - new_configuration_managed` = resources to delete. For each, it sets `metadata.deletionRequestedAt` in the resource store and emits a `ResourceDeletionScheduled` audit record. Normal finalizer-path Delete proceeds asynchronously. **`PendingCleanup` condition**: the Zone self resource carries a `PendingCleanup = True` condition while any `managedBy = Configuration` resource has `deletionRequestedAt` set and has not yet been atomically removed. Aggregate Zone `phase = Degraded` applies. The condition transitions to `False` and Zone phase returns to `Ready` when all scheduled deletions complete. **Prior generation bundle retention** (`cleanup.rs`): count-based (`d2b.zones..retainedGenerations`, outside `Zone.spec`, default 3, range 1..16); no TTL. Core copies prior bundles to `/var/lib/d2b/zones//configuration/prior/.json`. A generation is eligible for pruning when all configuration-managed resources from it have either been atomically removed or are present unchanged in a newer generation, AND the count would be exceeded. **`BundleActivated` audit record**: emitted at each generation transition with `contentHash`, `configurationGeneration`, `resourceCount`, and `providerSchemaDigests` map (digests from `type=provider` artifacts via `Provider.spec.artifactId`); no spec contents, CIDRs, or resource names appear in the record. Provider schema digests in the bundle are re-verified against installed Provider artifact digests at application time; a mismatch aborts application with a `BundleRejected` audit record.", + "entryContracts" : [ + "ADR-046-resources-network" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-network-008", + "kind" : "work-item", + "owner" : "ADR046-network-004, ADR046-network-005; Zone runtime integrator", + "parallelGroup" : "wi:core-config-hub:w4", + "prerequisites" : [ + "ADR-046-resources-network", + "ADR046-core-001", + "ADR046-network-004", + "ADR046-network-005" + ], + "specId" : "ADR-046-resources-network", + "topologicalRank" : 13, + "validation" : "**nix-unit**: `tests/unit/nix/cases/generation-cleanup-absent-network.nix` — verifies that a Network resource present in generation N and absent from generation N+1 receives `deletionRequestedAt` and appears in the `PendingCleanup` condition; verifies that a controller-owned `Guest` (`managedBy = \"controller\"`) does NOT receive a direct bundle-driven Delete; verifies that a re-declared (identical spec) Network is NOT scheduled for Delete; verifies `retainedGenerations` default is 3. **Rust contract tests**: Two separate test files — (1) `packages/d2b-contracts/tests/generation_bundle.rs`: tests the **input** bundle DTO only: `ZoneBundle`/`BundleResource`/`BundleMetadata` JSON round-trip, `contentHash` stability across serialization, `providerSchemaDigests` presence, `managedBy` and `configurationGeneration` fields ABSENT from `BundleResource` input struct (verified by both compile-time type check: the fields must not exist on the `BundleResource` type, and runtime JSON serialization: the serialized object must not contain those keys). (2) `packages/d2b-core-controller/tests/resource_metadata.rs`: `ManagedBy` closed enum round-trip with `\"configuration\"`/`\"controller\"`/`\"api\"` values tested separately here since `ManagedBy` is persisted resource metadata in `resource_store.rs`, not a field of the input bundle DTO. **Controller integration tests**: async Delete triggered through finalizers for Network; mDNS Process child deleted before Network finalizer clears; bridge `DeleteBridge` broker call made exactly once during finalizer; controller waits for Deleted watch event (not a persistent phase=Deleted row) before proceeding. **Host integration**: `tests/host-integration/nix-generation-cleanup.nix` — runNixOSTest scenario: apply generation 1 with Network resource, then apply generation 2 with that Network absent; assert Zone enters `Degraded/PendingCleanup`; assert Network `phase = Degraded` with `NetworkDraining = True` and `deletionRequestedAt` set and `reason: configuration-generation-removed`; assert cleanup completes (single store transaction: Deleted REVISION event + row/index removal; dedup-guarded audit append follows committed transaction) and Zone returns to `Ready`; assert no controller-owned children deleted directly by core; assert prior bundle copied to `/var/lib/d2b/zones//configuration/prior/` and retained until cleanup complete; assert bundle pruned when `retainedGenerations` exceeded and generation eligible. **INV-NET-LIFECYCLE-001**: core never schedules bundle-driven Delete for `managedBy ≠ \"configuration\"` resources; verified by static analysis of core's generation-transition diff function, which is bounded at compile time to iterate only the `configuration_managed_resources` set. **INV-NET-LIFECYCLE-002**: per-item name-conflict — when a bundle item collides with `managedBy = \"controller\"` or `\"api\"`, that item is recorded as `Degraded/name-conflict`; the existing resource is left untouched; non-conflicting items continue to activate; tested by `packages/d2b-core-controller/tests/configuration_name_conflict.rs` (three cases: collision with a controller-owned child, an API-created resource, and a same-name configuration resource from a prior generation that completed deletion; each case asserts non-conflicting items still activate).", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/network.rs` external-attachment sharing schema/status; `packages/d2b-core-controller/src/authority.rs` Core-derived physical-NIC identity and Host-global claim; Provider/network-local descriptor/reconcile/finalizer" + ], + "detailedDesign" : "Resolve operator-declared `parentInterface` against trusted Host inventory and derive an opaque `external-physical-nic/v1` digest; index `(Host, external-physical-nic, opaqueKeyDigest)` before any macvtap/VMM effect. `passthru`, `private`, and `vepa` are exclusive. `bridge` defaults exclusive and is multiplexed only under explicitly authored compatible policy. Use typed `external-physical-nic-conflict`; expose only bounded authority availability/holder-count/queue/arbitration/update-currency and conditions; keep digest, interface identity, and owner proof private. Parent/mode/policy update drains and releases the old claim before replacement; deletion closes macvtap/VMM ownership before releasing the claim; restart adopts exact owner proof and quarantines ambiguity. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt the existing private macvtap-FD spawn path; add authority admission before it.", + "entryContracts" : [ + "ADR-046-resources-network" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-network-009", + "kind" : "work-item", + "owner" : "D097 Host-global authority index; ADR046-network-001, ADR046-network-005; Provider/network-local and Core authority owners", + "parallelGroup" : "wi:ADR-046-resources-network", + "prerequisites" : [ + "ADR-046-resources-network", + "ADR046-network-001", + "ADR046-network-005" + ], + "specId" : "ADR-046-resources-network", + "topologicalRank" : 11, + "validation" : "Hermetic authority tests cover same-Zone and cross-Zone exclusive collisions, mixed-policy conflicts, non-bridge multiplex rejection, explicit compatible bridge multiplex admission, Core-derived key equality for two selectors resolving to one fake NIC, caller-supplied digest rejection, no-effect conflict, owner-proof adoption/ambiguity, disruptive update, and release-after-close ordering. Nix eval covers schema and declared cross-Zone conflicts; host integration covers create/update/delete with a fake macvtap parent and status/condition transitions without raw identity exposure.", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/options-zones.nix` (Zone-level options: `label`, `parentZone`, `retainedGenerations`, `trustedPublishers` — compiler settings, not Zone spec fields); `nixos-modules/options-zones-resources.nix` (unified `resources` attrset)" + ], + "detailedDesign" : "`d2b.zones..resources. = { type = \"\"; spec = { ... }; }` — single attrset covering all ResourceTypes; `type` discriminates dispatch; `spec` fields mirror exact ResourceTypeSchema field names and nesting; Nix option types/defaults/docs generated from `docs/reference/schemas/v3/.json`; no Nix-only fields inside resource declarations; `metadata.name` derives from attr key; `metadata.zone` derives from enclosing zone attr key; `apiVersion` defaulted; `uid`/`generation`/`revision`/`status`/`managedBy` never in Nix; `resource_name` regex `^[a-z][a-z0-9-]*$`; ref validation assertions; `WorkloadProviderKind` → Guest/Host mapping per disposition table above; `Capability` → Role verb mapping per resource-api/authz foundation spec; Zone self-resource spec is `{}`; `parentZone` is a required non-root/forbidden-root compiler-only plain Zone name compiled into sealed allocator topology; `retainedGenerations`/`trustedPublishers` are likewise Zone-level compiler settings not emitted in Zone spec", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-001", + "kind" : "work-item", + "owner" : "W0; `d2b-contracts` identities (ADR046-identities-001, ADR046-identities-002)", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-identities-001", + "ADR046-identities-002" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 10, + "validation" : "nix-unit vectors for each ResourceType; ref-validation rejection vectors; malformed ref error shape; resource/session verb closed-enum vectors accept `relay` only in `sessionVerbs`; relay wildcard/unbounded/Provider-self-asserted fixtures fail before activation while an exact core-generated ZoneLink fixture passes; Endpoint visibility accepts exactly `owner|provider|zone` and uses `consumerPolicy` for finer bounds; `parentZone` is missing on non-root/forbidden on local-root/unknown/self/cyclic/over-depth rejection; scalar module conflicts prove one parent; child-local ZoneLink `childZoneName` must equal its enclosing Zone key; a second uplink resource (even disabled) and a local-root uplink fail eval; missing/local-unresolved `transportProviderRef` fails eval; `managedBy` in spec rejected at eval; Zone spec is `{}` (no `parentZone`, `parentRef`, `retainedGenerations`, etc.)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`Network` resource fields in `nixos-modules/options-zones-resources.nix`; `Guest` resource fields" + ], + "detailedDesign" : "`d2b.envs.work.lanSubnet` → `d2b.zones.work.resources.work-lan = { type = \"Network\"; spec = { lanSubnet = \"...\"; ... }; }`; CIDR overlap assertion migrated; `sys-` reserved prefix and VM-name regex retained; `d2b.vms..tpm.enable` → `d2b.zones..resources.vm-tpm = { type = \"Device\"; spec = { providerRef = \"Provider/device-tpm\"; ... }; }`", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-002", + "kind" : "work-item", + "owner" : "ADR046-nix-001; env/VM migration", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-001" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 11, + "validation" : "nix-unit CIDR rejection; eval assertion for `sys-` prefix; VM-name regex", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/options-site.nix` (retained); per-Zone options in `options-zones.nix`" + ], + "detailedDesign" : "`d2b.zones..retainedGenerations` (default 3, range 1..16, compiler setting — not emitted in Zone spec); `d2b.site.stateDir` maps to Zone storage roots; `d2b.site.usePrebuiltHostTools` retained Primary reuse disposition: `adapt`. Preserved source-plan detail: retain and extend.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-003", + "kind" : "work-item", + "owner" : "ADR046-nix-001; site options", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-001" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 11, + "validation" : "nix-unit case verifies `retainedGenerations` default/range, `site.stateDir` mapping, and `usePrebuiltHostTools` retention.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/index.nix` (rewritten); emits `/etc/d2b/index.json`" + ], + "detailedDesign" : "Cross-Zone normalized index: zone/host/guest/network/closure entries; executionIndex; networkIndex; closureIndex; sorted output; `cfg._index` attribute tree retained as internal helper during migration Primary reuse disposition: `adapt`. Preserved source-plan detail: rewrite.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-004", + "kind" : "work-item", + "owner" : "ADR046-nix-001, ADR046-nix-002", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-001", + "ADR046-nix-002" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 12, + "validation" : "nix-unit golden vectors for index shape; drift gate: `xtask gen-index` round-trip", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/bundle-zones.nix` (per-Zone bundle derivation); common helpers retained in `bundle-artifacts.nix`" + ], + "detailedDesign" : "Per-Zone `bundle.json` with `candidateId`/`contentId` binding; SHA256 digest chain; `generationIndex`; atomic activation pointer; `manifestVersion` → `schemaVersion` rename Primary reuse disposition: `adapt`. Preserved source-plan detail: extend and rewrite.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-005", + "kind" : "work-item", + "owner" : "ADR046-nix-004", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-004" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 13, + "validation" : "Artifact-shape contract tests in `packages/d2b-contract-tests/tests/`; determinism test (build twice, diff outputs)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/resources-zones-processes.nix`; emits `zones//processes.json`" + ], + "detailedDesign" : "Process/EphemeralProcess resource serialization per disposition table and the frozen `ExecutionSpec`: exact `providerRef`/`executionRef`/domain/user/processClass/template/config/credential/mount/sandbox/budget/`networkUsage`/`deviceUsage`/telemetry names; semantic owner from `metadata.ownerRef` with template fallback through the Process Provider; no `packageRef`, free-form `binaryPath`, `argv`, renamed network/device fields, or inline endpoints; stable produced endpoints are separate owned `Endpoint` resources with `producerRef`; Process uses canonical `restartPolicy` fields; EphemeralProcess uses `runtimeDeadline` and has no restart policy; VsockRelay → `Process` under `Provider/transport-vsock`; GuestSshReadiness retired at v3 cutover; Usbip long-lived backend/proxy → `Process`, Usbip per-busid attach/detach → `EphemeralProcess`, all owned by `Provider/device-usbip` Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-006", + "kind" : "work-item", + "owner" : "ADR046-nix-005; Process Provider work items (ADR046-primitives-002)", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-005", + "ADR046-primitives-002" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 14, + "validation" : "Process and EphemeralProcess exact-schema vectors; reject `packageRef`, `network`, `devices`, `endpoints`, `restart`, and `runDeadline`; accept canonical `networkUsage`, `deviceUsage`, `restartPolicy`, `runtimeDeadline`, and separately owned Endpoint with `producerRef`; no-raw-path assertion; ProcessRole parity test (every variant has a test case)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/resources-zones-volumes.nix`; emits `zones//volumes.json`; OFD lock rows move to `d2b-contracts` internals" + ], + "detailedDesign" : "Volume layout/views/ACL/no-follow/repair preserving current policy; `PrincipalRef { kind: \"uid\" }` → `User/` typed ref only; OFD rows removed from Nix artifacts Primary reuse disposition: `adapt`. Preserved source-plan detail: extract storage policy → adapt; retire sync rows.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-007", + "kind" : "work-item", + "owner" : "ADR046-nix-005; Volume Provider work items (ADR046-primitives-003)", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-005", + "ADR046-primitives-003" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 14, + "validation" : "Volume schema vectors; ACL/no-follow/view policy tests", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "Compiler-only `parentZone` map in `nixos-modules/options-zones.nix`; local-root allocator bootstrap compiler/sealer; runtime-created Zone self-resource with `spec = {}`; child-local ZoneLink resource; `realm-controllers.json` RETAINED during migration (live d2bd reads it)" + ], + "detailedDesign" : "Resolve each non-root `parentZone` to one declared Zone; reject a root value, missing/unknown/self/conflicting/cyclic parent, and ancestry deeper than 16 names. Canonicalize sorted child→parent rows and feed them to the local-root allocator's private bootstrap sealer. Parent allocators derive/pre-bind privileged socket/listener state from that topology; no socket path or `parentZone` enters `Zone.spec` or a ResourceRef. Child-local ZoneLinks provide transport/local route state. `realm-controllers.json` remains published until `realm_access_resolver` is replaced. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt and retire.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-008", + "kind" : "work-item", + "owner" : "ADR046-nix-004; Zone/Network migration", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-004" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 13, + "validation" : "Zone self-resource round-trip with exact empty spec; `parentZone` required/root-forbidden/existence/one-parent/self/cycle/16-name-depth vectors; topology seal excludes `parentZone` from Zone/resource bundles; bootstrap socket/listener regression tests", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "Provider/display-wayland and Provider/shell-terminal Process configs in `zones//processes.json`; `Provider/credential-entra` Credential resource; `realm-identity.json` RETAINED during migration" + ], + "detailedDesign" : "Launcher metadata folded into Process resource annotations; identity config → Credential resource fields (`providerRef`, `identityGuestRef`, `loginEndpointRef`, `scope`, `audience`, `consumerRef`, `allowedOperations`, canonical `spec.provider` extension where Provider schema declares it; no secret bytes); `realm-identity.json` must remain until d2bd `RealmIdentityConfigJson` loading is replaced by Credential resource reader", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-009", + "kind" : "work-item", + "owner" : "ADR046-nix-006; display/credential Provider work items", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-006" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 15, + "validation" : "Launcher metadata shape regression; Entra identity-Guest fixture requires login Endpoint purpose `credential-entra.d2bus.org/entra-login-token`, `visibility = \"provider\"` (and rejects `\"zone\"`), exact `consumerPolicy.allowedSubjects` containing both `Provider/credential-entra` and the Credential's configured `consumerRef`, and exact canonical `resolve` operation; no-secret assertion vectors", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "User-only `Host` resource in `zones//hosts.json` (`spec.isolationPosture: \"none\"`, `defaultDomain: user`, `allowedDomains: [user]`, `defaultUserRef: User/`); child `Process` resources in `zones//processes.json` using normal Process Providers; shell session supervisor → `Process` under `Provider/shell-terminal`; never a `Guest`; not a v3 Provider" + ], + "detailedDesign" : "`isolationPosture: \"none\"` is a promoted Host base field declared at top-level `spec.isolationPosture` in the Host schema; enforced at eval time; user-only Host rejects system-domain Process refs; `NoIsolation` condition in Host status; `status.isolationPosture: none`; every `ProcessEffect` audit event under this Host carries `no_isolation=true`; OTEL telemetry never carries an isolation label; CLI/UI warning non-suppressible", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-010", + "kind" : "work-item", + "owner" : "ADR046-nix-001; unsafe-local migration", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-001" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 11, + "validation" : "User-only Host rejection of system-domain Process refs; `spec.isolationPosture != \"none\"` assertion rejection for user-only system-core Hosts; `NoIsolation` condition present in status; `no_isolation=true` in `ProcessEffect` audit event; OTEL attribute absent; no Guest emitted for unsafe-local declaration", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/privileges-json.nix` (retained); `/etc/d2b/privileges.json` (retained, site-wide)" + ], + "detailedDesign" : "Broker op catalog is not Zone-scoped; no structural change required Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: retain.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-011", + "kind" : "work-item", + "owner" : "Broker privileges owner", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 10, + "validation" : "Existing `tests/unit/gates/drift-check.sh`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/closures-json.nix` (rewritten, keyed by artifact ID from `d2b.artifacts` with `type = \"nixos-system\"`); `nixos-modules/minijail-profiles.nix` (retained, adapted to reference Zone Guests)" + ], + "detailedDesign" : "Closure emitter iterates `d2b.artifacts` entries with `type = \"nixos-system\"`, computes `pkgs.closureInfo`, records `storePath`/digest/size in artifact catalog (private root:d2bd 0640 field; absent from all public ResourceSpecs/status/audit/OTEL); `Guest.spec.systemArtifactId` links Guest to artifact; `Volume.source.systemArtifactId` links Volume to artifact; minijail profile emitter structurally unchanged; old `d2b.vms.` keying retired", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-012", + "kind" : "work-item", + "owner" : "ADR046-nix-005; ADR046-nix-022 (artifact catalog emitter); Provider/volume-virtiofs", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-005", + "ADR046-nix-022" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 15, + "validation" : "Closure map round-trip; per-VM store hardlink integrity; `storePath` present in private catalog; `storePath` absent from all emitted public ResourceSpecs and status/audit/OTEL surfaces", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "Per-Zone `zones//bundle.json` (`schemaVersion`); Host resource in `zones//hosts.json`" + ], + "detailedDesign" : "`manifestVersion` → `schemaVersion`; `host.json` host config folded into Host resource spec; CHANGELOG entry for rename required", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-013", + "kind" : "work-item", + "owner" : "ADR046-nix-005; manifest contract", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-005" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 14, + "validation" : "Schema drift gate; CHANGELOG enforcement", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/assertions.nix`" + ], + "detailedDesign" : "Migrate existing assertions to Zone/Resource terminology; add ref validation, owner cycles, CIDR overlap (Zones), provider resolution, RoleBinding verb set assertions Primary reuse disposition: `adapt`. Preserved source-plan detail: extend in place.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-014", + "kind" : "work-item", + "owner" : "ADR046-nix-001, ADR046-nix-002", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-device-006", + "ADR046-nix-001", + "ADR046-nix-002" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 13, + "validation" : "Each new assertion has a failing-config test vector", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "Same files; updated to use Zone bundle activation path and Zone resource state dirs" + ], + "detailedDesign" : "`d2b-activation-helper` updated to validate/stage per-Zone bundles; `d2bd.service` updated to read Zone bundle; `d2b` group retained for `SO_PEERCRED` Primary reuse disposition: `adapt`. Preserved source-plan detail: retain and adapt.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-015", + "kind" : "work-item", + "owner" : "ADR046-nix-001; host activation", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-001" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 11, + "validation" : "Host-integration test with Zone bundle activation and daemon readiness", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "Network reconciliation by `Provider/network-local` Process resources" + ], + "detailedDesign" : "Current bridge/NAT/DHCP/firewall Nix units retained; `Provider/network-local` controller emits equivalent configuration from Network resources; `lib.mkForce` neutralization preserved Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: retain until Provider successor.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-016", + "kind" : "work-item", + "owner" : "ADR046-nix-002; Provider/network-local dossier", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-002" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 12, + "validation" : "`tests/net-vm-network-eval.sh` passes against Network resource spec", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "Per-VM store reconciliation by `Provider/volume-virtiofs` EphemeralProcess/Process resources" + ], + "detailedDesign" : "`store.nix` retained; `Provider/volume-virtiofs` controller creates equivalent EphemeralProcess; per-VM store path derived from Zone stateDir + Guest name via Provider, not raw path in spec Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: retain until Provider successor.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-017", + "kind" : "work-item", + "owner" : "ADR046-nix-012; Provider/volume-virtiofs", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-012" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 16, + "validation" : "Store hardlink integrity; no direct `/nix/store` export", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`Provider/device-tpm`, `Provider/device-usbip`, `Provider/device-gpu`, `Provider/audio-pipewire` resource install declarations in `options-zones-resources.nix`" + ], + "detailedDesign" : "`d2b.vms..tpm.enable = true` → `d2b.zones..resources.vm-tpm = { type = \"Device\"; spec = { providerRef = \"Provider/device-tpm\"; ... }; }`; all component eval assertions migrated to `assertions.nix`; GuestSshReadiness retired at v3 cutover; Usbip long-lived backend/proxy → `Process`, per-busid attach/detach → `EphemeralProcess`, both owned by `Provider/device-usbip` Primary reuse disposition: `replace`. Preserved source-plan detail: each component becomes a Provider install resource + Device/Guest spec field.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-018", + "kind" : "work-item", + "owner" : "ADR046-nix-002; Provider dossiers for graphics, tpm, usbip, audio", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-002" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 12, + "validation" : "Existing component eval tests; `tests/usbip-gating-eval.sh`; `tests/video-contract-eval.sh`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`docs/reference/schemas/v3/.json` for each ResourceType; `nixos-modules/resource-schema-validation.nix` (validates emitted spec against committed JSON Schema at build time); `nixos-modules/provider-settings-validation.nix` (validates `spec.provider.settings` where declared in schema, and Provider `config`, against Provider-embedded schema at build time); `nixos-modules/assertions.nix` (Credential ref enforcement, secret-pattern rejection)" + ], + "detailedDesign" : "`cargo xtask gen-schemas` emits one JSON Schema per ResourceType under `docs/reference/schemas/v3/`; Nix derivation reads these schemas from `pkgs.d2b-resource-schemas` and validates every emitted `spec` JSON before producing the Zone bundle; Provider-settings validation reads `settingsSchemaDigest` from `provider-catalog.json` and resolves the schema from the Provider package closure; Credential ref enforcement: eval assertion rejects any `spec` string field matching `-----BEGIN`, `eyJ`, or a hex string ≥ 32 bytes in a secret-typed field; `managedBy` in any input spec rejected at eval (core-set runtime field, never in Nix input); bundle integrity: `candidateId`/`contentId` computed over canonical sorted output Primary reuse disposition: `adapt`. Preserved source-plan detail: extend xtask schema generation; new Nix eval/build validation hooks.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-019", + "kind" : "work-item", + "owner" : "ADR046-nix-005; ADR046-nix-001; ADR046-cli-011 assertions/resource-option shape; `d2b-contracts` schema generation (ADR046-nix-027)", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-cli-011", + "ADR046-nix-001", + "ADR046-nix-005", + "ADR046-nix-027" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 15, + "validation" : "Schema round-trip: emit spec, validate against schema, verify byte-identical re-emit; `spec.provider.settings` rejection test (unknown field, out-of-bounds value, raw store path, secret bytes) where schema declares the field; Provider `config` rejection test (unknown field); Credential ref enforcement: PEM-in-spec rejected; secret-pattern-in-spec rejected; valid `Credential/` ref accepted; `managedBy` in spec input rejected at eval", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "Configuration-publication controller handler in `packages/d2bd/src/config_publication.rs`; `ConfigurationOwnedClassifier`; `AbsentResourceReaper`; `Zone` status conditions in `d2b-contracts/src/v3/zone_status.rs`; cleanup audit emitter in `d2b-state/src/audit_segments.rs`" + ], + "detailedDesign" : "`ConfigurationOwnedClassifier`: classify resources by core-set `managedBy` field only — `managedBy=configuration` resources are owned by config publication; `managedBy=controller` and `managedBy=api` resources are never touched. At activation, diff new-generation bundle name+type set against all resources with `managedBy=configuration` in the Zone store; resources absent from the new bundle are enqueued for Delete. Never infer ownership from `ownerRef`, labels, or absence from emitted files. `AbsentResourceReaper`: processes the Absent queue asynchronously; does not block pointer swap (step 4); sets `status.phase=Pending` + `PendingDeletion` condition (`reason: AbsentFromConfiguration`); waits for all finalizers to clear; commits a single store transaction that appends the `Deleted` revision and removes the resource row and its indexes; subsequent `Get` returns not-found; the `ResourceDelete` audit event is appended afterward with dedup/exactly-once recovery. Zone phase: `Pending` during pointer-swap-to-first-reconcile window; `Degraded` while any `managedBy=configuration` resource carries `PendingDeletion` or a ZoneLink lags; `Ready` when all reconciled. Generation pruning: prune when `generationIndex ≤ activeIndex - retainedGenerations` AND all enqueued resources from that generation have reached `Deleted`. Rollback: re-adopt `managedBy=configuration` resources in `Pending/PendingDeletion` back to the rollback target generation's owned set", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-020", + "kind" : "work-item", + "owner" : "ADR046-nix-005; ADR046-nix-001; configuration-publication controller (ADR-046-core-controllers)", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-001", + "ADR046-nix-005" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 14, + "validation" : "Classification: `managedBy=controller` resource never enqueued (even if absent from bundle). `managedBy=api` resource never enqueued. `managedBy=configuration` resource absent from new bundle always enqueued. Finalizer safety: resource with active finalizer enters DeletionBlocked; not force-deleted; stays in `Pending`. Final deletion: a single store transaction appends the `Deleted` revision and removes the resource row and its indexes; subsequent `Get` returns not-found; `ResourceDelete` audit event is appended afterward with dedup/exactly-once recovery. Zone status: `Pending` during activation; `Degraded` while PendingDeletion outstanding; `Ready` when clean. Audit: `ResourceDelete` event includes `configurationGeneration` field.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/provider-crate-layout.rs`; workspace scan in `packages/xtask/src/main.rs` (extend `check-workspace` or add `check-provider-layout` subcommand)" + ], + "detailedDesign" : "Parse the root `packages/Cargo.toml` workspace member list; for every member path matching `packages/d2b-provider-*-*`: assert (1) `src/` directory exists and contains at least one `.rs` file; (2) `tests/` directory exists and contains at least one `.rs` file; (3) `integration/` directory exists and contains at least one `.rs` or fixture file; (4) `README.md` exists and is ≥ 200 bytes. All four conditions required; any single failure fails the test with a structured message naming the crate and missing path. Test runs as `cargo test -p d2b-contract-tests provider_crate_layout`; wired into `make test-policy`.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-021", + "kind" : "work-item", + "owner" : "ADR046-nix-001; `d2b-contracts` workspace policy tests", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-001" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 11, + "validation" : "Fixture: add a stub `packages/d2b-provider-test-missing-integration/` with `src/lib.rs` and `tests/smoke.rs` but no `integration/` and no `README.md`; assert test fails naming both missing paths. Add complete stub with all four paths; assert test passes.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/artifact-catalog.nix` (new emitter); `nixos-modules/options-artifacts.nix` (new option: `d2b.artifacts. = { package; type; }`); `/etc/d2b/artifact-catalog.json` (output artifact, `root:d2bd` 0640); `nixos-modules/bundle-zones.nix` (extend to include artifact catalog digest in `bundle.json`); `nixos-modules/options-zones-resources.nix` (replace `closureRef` / `nixosSystem` helpers with `systemArtifactId` validation)" + ], + "detailedDesign" : "`d2b.artifacts.` attrset option: `id` matches `^[a-z][a-z0-9-]*$`; `type ∈ { \"provider\", \"nixos-system\", \"nixos-module-set\", \"config-bundle\" }`; no other fields. Emitter computes `pkgs.closureInfo` for each entry and writes `artifact-catalog.json` with sorted entries (by `artifactId`) containing `artifactId`, `type`, `storePath` (private, for activation-helper staging), `packageDigest`, `closureDigest`, `closureSize`. `storePath` is a private field of the root:d2bd 0640 file; it is never emitted in public ResourceSpecs, status fields, audit records, or OTEL telemetry. The `bundle.json` manifest includes the artifact catalog file entry and its SHA256 digest. `d2b-activation-helper` reads `storePath` from the catalog to resolve and stage each artifact; verifies catalog digest before staging. Build-time validation: `artifactId` / `systemArtifactId` / `source.systemArtifactId` fields in resource specs resolve against `d2b.artifacts`; type-mismatch fails with a structured error. `d2b.providerCatalog..package` option is removed; replaced by `d2b.providerCatalog..artifactId`. `Guest.spec.systemArtifactId` replaces the former `nixosSystem` Nix-only helper. `Volume.source.systemArtifactId` replaces `source.closureRef`.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-022", + "kind" : "work-item", + "owner" : "ADR046-nix-005 (bundle derivation); `d2b-contracts` schema generation (ADR046-nix-027)", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-005", + "ADR046-nix-027" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 14, + "validation" : "Artifact catalog round-trip: declare artifact, build, verify JSON entry present with correct type/digests/storePath; missing artifact ID fails build with structured error; wrong-type artifact fails build; `storePath` absent from all public ResourceSpecs and status/audit/OTEL surfaces", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/session/` (new crate `d2b-bus`); `ComponentSessionDriver` becomes the central abstraction for all Zone bus sessions." + ], + "detailedDesign" : "Complete session protocol: preface negotiation, Noise handshake profiles, record protection, replay cache, fair scheduler, fragmentation/reassembly, keepalive, ttrpc bridge, generation discovery, and `ComponentSessionDriver` application surface. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; adapt ADR45 EndpointPurpose/ServicePackage names listed in `ADR45 exclusions` while preserving wire tag values..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-023", + "kind" : "work-item", + "owner" : "ADR-046-componentsession-and-bus spec; ADR046-nix-027 (wire contract) must land first", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-027" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 11, + "validation" : "Port `component_session.rs` and `noise_vectors.rs`; run `make test-rust`; Noise KAT vectors must pass after copy.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/session/` (same crate as ADR046-nix-023)." + ], + "detailedDesign" : "Attachment ownership validates descriptors only after authenticated decrypt; unconsumed payloads close on drop. NamedStreamMux preserves half-close, reset, per-stream limits, aggregate limits, and credit-based flow control. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; keep attachment/stream semantics and descriptor validation, with no ADR45 realm adaptation required for this item..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-024", + "kind" : "work-item", + "owner" : "ADR046-nix-023; ADR046-nix-027", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-023", + "ADR046-nix-027" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 12, + "validation" : "Port `component_session.rs` attachment ownership, stream mux, and credit-accounting tests.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/session/`." + ], + "detailedDesign" : "Per-generation `RequestRegistry`, request-wide cancellation, `DeadlineBudget::admit_metadata()` as the inbound metadata gate, and single-use `BootstrapAdmission::consume()` replay protection. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; adapt `CancelRequest`/`CancelResponse` module paths if v3 services move the common proto types..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-025", + "kind" : "work-item", + "owner" : "ADR046-nix-023; ADR046-nix-027", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-023", + "ADR046-nix-027" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 12, + "validation" : "Port cancel-before-dispatch, cancel-after-dispatch, generation-mismatch cancel, and deadline admit tests; add v3 common-proto path adapter test.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/transport/unix/`." + ], + "detailedDesign" : "Unix seqpacket/stream transports, pidfd identity verification, six-scope credit pool with host/process bounds, systemd-activated seqpacket listener support, and length-prefixed vsock framing. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; retain host-socket/native-vsock feature gates and replace only ADR45 socket-name strings with Zone bootstrap config..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-026", + "kind" : "work-item", + "owner" : "ADR046-nix-023", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-023" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 12, + "validation" : "Port `unix_session.rs` seqpacket, stream, pidfd, FD attachment, and credit pool tests; add Zone bootstrap socket-name fixture.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/component_session.rs`." + ], + "detailedDesign" : "Canonical wire values, bounded vectors, binary size constants, closed-enum tag codecs, and fail-closed validation for the ComponentSession protocol. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse wire constants/types from main commit `a1cc0b2d`; rename ADR45 realm-oriented variants only as directed by the bus owning spec and preserve numeric tags..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-027", + "kind" : "work-item", + "owner" : "ADR-046-componentsession-and-bus; naming and wire enumeration decisions per ADR-046-componentsession-and-bus owning spec before final adoption", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 10, + "validation" : "Port component session and Noise vector tests; add wire-tag stability gates for renamed role/service/purpose variants.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/services/`." + ], + "detailedDesign" : "Versioned ttrpc service modules preserve server dispatch tables, shared request/cancel/outcome/error types, and service fingerprint inventory. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse generated ttrpc service tables from main commit `a1cc0b2d`; adapt ADR45 Realm/Workload fields and service names to v3 Zone/Resource naming per owning bus spec..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-028", + "kind" : "work-item", + "owner" : "ADR046-nix-027; Zone service naming per ADR-046-componentsession-and-bus owning spec", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-027" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 11, + "validation" : "Port provider-toolkit conformance and service fingerprint assertions; add v3 service-name gate for Zone service and ResourceName field adaptation.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider/src/` (adapt in place)." + ], + "detailedDesign" : "ProviderRegistry snapshot admission/drain, bounded in-flight caps, typed RpcProviderProxy dispatch, and build-time RegistryLimits validation. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse ProviderRegistry/RPC runtime from main commit `a1cc0b2d`; adapt `SessionIdentity` to Zone naming and update EndpointRole/ServicePackage names per owning bus spec..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-029", + "kind" : "work-item", + "owner" : "ADR046-nix-027; ADR046-nix-028; EndpointRole naming per ADR-046-componentsession-and-bus owning spec", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-027", + "ADR046-nix-028" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 12, + "validation" : "Port `d2b-provider` `runtime.rs` tests; add v3 AdmissionOptions peer-role and SessionIdentity zone-name gates.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-toolkit/src/` (adapt in place)." + ], + "detailedDesign" : "ProviderAgentAdapter validates ComponentSession identity, GeneratedProviderServiceServer dispatches all ProviderType method families with bounded stores/in-flight state, and conformance kit gates every Provider implementation. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse provider toolkit from main commit `a1cc0b2d`; adapt endpoint role/service variant names and v2_identity imports while preserving wire tags..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-030", + "kind" : "work-item", + "owner" : "ADR046-nix-029; ADR046-nix-028; EndpointRole and ServicePackage naming per ADR-046-componentsession-and-bus owning spec", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-028", + "ADR046-nix-029" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 13, + "validation" : "Port `conformance.rs`; assert endpoint role/service wire tags remain 7/4 after any variant rename.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/resources-sharing.nix`; `nixos-modules/assertions.nix`; `tests/unit/nix/cases/resource-sharing.nix`" + ], + "detailedDesign" : "Compile the eight exact D098 semantic Service/Binding types plus `ResourceExport`/`ResourceImport` without a second vocabulary. Nix authors authority Services and consumer Bindings, never projection Services; Binding requires same-Zone `serviceRef` and allowed target, while all observed realization remains absent from spec. Core receives a stable projection name/factory fingerprint and creates exactly one same-type Service with ResourceImport ownership, `providerRef`, semantic base/import fields, and no `spec.provider`. Reject Device/Endpoint/Binding export/projection, implementation-qualified/former `*State` aliases, implementation detail in base schemas, cross-Zone refs, USB export without every policy opt-in, and any USB/security-key physical resource that does not use the exact Core-derived `physical-usb-backing` tuple class.", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-031", + "kind" : "work-item", + "owner" : "ADR046-provider-004, ADR046-zone-control-019, ADR046-zone-control-020, ADR046-zone-control-024; Nix resource compiler owner", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-019", + "ADR046-provider-004", + "ADR046-zone-control-019", + "ADR046-zone-control-020", + "ADR046-zone-control-024" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 16, + "validation" : "Fast Nix eval/build tests cover all four exact pairs, common base schema discovery, canonical minimal base without `spec.provider`, stable lowering across repeated evaluations, same-Zone refs/targets, strict Provider extension placement, Service-only export, exactly-one Core projection metadata with no `spec.provider`, no auto-Binding, status-field rejection in spec, USB/security-key resolution to one byte-identical Host-global `(Host, physical-usb-backing, opaqueKeyDigest)` collision before effects, Provider-private-class bypass rejection, and rejection of all forbidden names/aliases.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-client/src/` (adapt in place)." + ], + "detailedDesign" : "Transport-neutral typed async client, ComponentSessionConnector abstraction, precise SessionFailure classification, signed metadata envelopes, named-stream client abstraction, and host-socket connector. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse async client from main commit `a1cc0b2d`; adapt TargetInput, ServiceOwner, service packages, and local socket identity to Zone/ResourceRef semantics..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-032", + "kind" : "work-item", + "owner" : "ADR046-nix-029; ADR046-nix-027; TargetInput v3 shape per ADR-046-componentsession-and-bus owning spec", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-027", + "ADR046-nix-029" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 13, + "validation" : "Port `d2b-client` tests; add ResourceRef TargetInput and Zone bootstrap socket identity gates.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/routing/zone_service.rs`." + ], + "detailedDesign" : "ZoneSessionAuthority preserves no-host-credential custody, gateway/ZoneLink custody behavior, bounded concurrent dispatch, and graceful shutdown semantics. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse routing service behavior from main commit `a1cc0b2d`; rename RealmSessionAuthority/service identifiers to Zone terms per owning bus spec while preserving custody behavior..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-033", + "kind" : "work-item", + "owner" : "ADR046-nix-023; ADR046-nix-027; naming and service name per ADR-046-componentsession-and-bus owning spec", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-023", + "ADR046-nix-027" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 12, + "validation" : "Port `realm_service_v2.rs` and topology harness tests with Zone naming; add service-name wire gate.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2bd/src/provider_registry.rs` (adapt in place)." + ], + "detailedDesign" : "Named ProviderCompositionError variants, first-party factory instantiation, bundle resolver validation, version/schema constants, and monotone lifecycle operation IDs. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse fail-closed provider composition from main commit `a1cc0b2d`; adapt v2 identity/bundle artifact types to v3 provider registry contracts and bump bundle versions..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-034", + "kind" : "work-item", + "owner" : "ADR046-nix-029; ADR046-nix-028; ADR046-nix-027; PROVIDER_BUNDLE_VERSION bump required on any bundle artifact format change", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-027", + "ADR046-nix-028", + "ADR046-nix-029" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 13, + "validation" : "Port provider_registry composition tests; add PROVIDER_BUNDLE_VERSION/SCHEMA_VERSION v3 bump gate and ZoneId/ResourceName binding test.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2bd/src/provider_effects.rs` (adapt in place)." + ], + "detailedDesign" : "DaemonEffectAdapters bind semantic effect ports; ProviderLifecycleDispatch bounds lifecycle mutations and routes broker start/stop through blocking adapters with idempotency-key deduplication. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse descriptor-bound effect dispatch from main commit `a1cc0b2d`; adapt lifecycle request wire type from VM name to Zone-scoped Guest ResourceRef..", + "entryContracts" : [ + "ADR-046-nix-configuration" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nix-035", + "kind" : "work-item", + "owner" : "ADR046-nix-034; ADR046-nix-029; GuestLifecycleRequest ResourceRef addressing per ADR-046-componentsession-and-bus owning spec", + "parallelGroup" : "wi:ADR-046-nix-configuration", + "prerequisites" : [ + "ADR-046-nix-configuration", + "ADR046-nix-029", + "ADR046-nix-034" + ], + "specId" : "ADR-046-nix-configuration", + "topologicalRank" : 14, + "validation" : "Port `provider_effects.rs` lifecycle dispatch tests; add ResourceRef Guest lifecycle request and BrokerCallerRole compatibility gates.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`d2b-contracts` trait plus `d2b-core` core adapter; maps to broker wire operations and audit emission." + ], + "detailedDesign" : "Implement `NetworkEffectPort` core adapter in `d2b-core`; map to broker wire ops; emit audit records. `revoke_attachment_tap` accepts only an opaque `AttachmentHandle` plus `AttachmentGenerationFence { expected_network_generation, expected_attachment_generation }` and maps to `DeletePersistentTap`; no IfName/path or caller-authored marker crosses the trait. Versioning: minor releases may add methods with default impls; major releases require Provider upgrade. The trait lives in `d2b-contracts`; the adapter in `d2b-core`.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-001", + "kind" : "work-item", + "owner" : "Core; owns `NetworkEffectPort` contract/versioning in `d2b-contracts` and adapter implementation in `d2b-core`.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-network-local/tests/fault_injection.rs` verifies fake `NetworkEffectPort` behavior, error mapping, no broker socket in provider context, and audit-safe adapter boundaries.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Broker wire contract and broker/core adapter operation table for `DeletePersistentTap`, `CreateBridge`, `DeleteBridge`, `ReadNftablesDigest`, `ReadSysctlState`, and `ReadBridgePortFlags`." + ], + "detailedDesign" : "Add canonical closed `DeletePersistentTap` paired with `CreatePersistentTap`, plus `CreateBridge`, `DeleteBridge`, `ReadNftablesDigest`, `ReadSysctlState`, and `ReadBridgePortFlags`. `DeletePersistentTapRequest` contains only an opaque attachment ID and expected Network/attachment generations. The broker resolves trusted realization state, validates generations and ownership marker, treats validated absence as success, rejects foreign markers without deletion, and emits path-free post-effect audit. No request accepts an IfName or path. Primary reuse disposition: `adapt`. Preserved source-plan detail: extend broker wire with net-new operations and reuse existing closed broker-operation dispatch shape.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-002", + "kind" : "work-item", + "owner" : "Core; broker/core contract work consumed by ADR046-nl-001.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local", + "ADR046-nl-001" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 12, + "validation" : "Broker tests cover `DeletePersistentTap` success, validated already-absent idempotency, stale Network/attachment generations, foreign-marker fail-closed behavior, path-free audit, and rejection of any IfName/path field; `integration/host_fabric.rs` covers persistent-tap deletion, bridge create/delete, nftables apply/digest, IPv6 suppression, NetworkManager unmanaged handling, and real `NetworkEffectPort` implementation.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`d2b-contracts` opaque byte-array newtypes; core-held HMAC key and provider-facing redacted handle types." + ], + "detailedDesign" : "Implement `AttachmentHandle` and `FabricHandle` as opaque byte-array newtypes (32 bytes of HMAC-SHA-256 over internal identity material; key held by core). Each attachment handle identifies one generation-fenced realization and is retained until explicit `DeletePersistentTap` confirmation during attachment removal or Network finalization. These types are declared in `d2b-contracts`, not in the provider crate.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-003", + "kind" : "work-item", + "owner" : "Core; handle DTOs are owned by `d2b-contracts` and consumed by `d2b-core` plus `Provider/network-local`.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "`tests/fault_injection.rs` and `tests/controller_state.rs` cover opaque-handle mismatch, generation-fenced `DeletePersistentTap`, retained handle until confirmation, and no raw IfName/IP/MAC/path public surface.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Core LaunchTicket builder and dependency resolver that walks `Guest.ownerRef: Network/` to resolved tap FDs." + ], + "detailedDesign" : "Implement LaunchTicket FD resolution: when core builds the LaunchTicket for a Guest with `ownerRef: Network/`, it walks the owner graph, locates the Network, reads its internally-held `AttachmentHandle` set, and includes the corresponding tap FDs in the ticket. No API surface for the provider or runtime is required beyond the existing LaunchTicket mechanism. Primary reuse disposition: `create`. Preserved source-plan detail: net-new LaunchTicket integration; reuse existing LaunchTicket mechanism without adding provider/runtime API surface.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-004", + "kind" : "work-item", + "owner" : "Core; depends on ADR046-nl-003 handles and runtime-cloud-hypervisor LaunchTicket consumption.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local", + "ADR046-nl-003" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 12, + "validation" : "`integration/guest_lifecycle.rs` validates net-VM/workload Guest lifecycle, opaque attachment handle resolution, and `systemArtifactId` binding.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Core adapter imports `d2b-host` modules; `packages/d2b-provider-network-local/src/ifname.rs` re-exports `d2b_host::ifname::derive_ifname` only." + ], + "detailedDesign" : "The `d2b-host` IfName/nftables/bridge/route modules are consumed directly by the core adapter (not by the provider crate). The provider crate re-exports only `d2b_host::ifname::derive_ifname` for validation purposes. No additional extraction work is required beyond confirming the `d2b-host` API surface is stable. Primary reuse disposition: `adapt`. Preserved source-plan detail: reuse directly in core adapter; provider re-exports only `derive_ifname` for validation.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-005", + "kind" : "work-item", + "owner" : "Provider plus Core; provider validates through `d2b_host::ifname::derive_ifname`, while core adapter consumes host networking modules.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "`tests/ifname_derive.rs`, `tests/fault_injection.rs`, and `integration/host_fabric.rs` prove derivation, adapter reuse, and real host-fabric behavior.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-network-local/src/{controller.rs,metrics.rs}`." + ], + "detailedDesign" : "Implement `controller.rs` reconcile/observe/finalize handlers with `NetworkEffectPort` injection and the §21 metric descriptors with closed semantic labels. Attachment removal and finalization wait for Guest/VMM FD ownership to close, then call `revoke_attachment_tap` with the retained opaque handle and current Network/attachment generation fence; transient deletion retries retain the handle, stale generations refresh/requeue, and ownership conflict blocks finalizer clearing. No descriptor may carry `vm`, `zone`, `zone_id`, `zone_uid`, `network`, or another resource-name-derived key; Network/Zone identity stays only in OTEL resource attributes and permitted audit fields. Primary reuse disposition: `adapt`. Preserved source-plan detail: port semantics into provider reconcile state machine; do not reuse static per-env systemd/Nix ownership.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-006", + "kind" : "work-item", + "owner" : "Provider; depends on ADR046-nl-001 through ADR046-nl-005 and owns the Network reconcile/observe/finalize handlers.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local", + "ADR046-nl-001", + "ADR046-nl-002", + "ADR046-nl-003", + "ADR046-nl-004", + "ADR046-nl-005" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 13, + "validation" : "`tests/controller_state.rs` covers normal reconcile, errors, finalizer ordering, adoption on restart, and observe/drift cycles with deterministic clock; `tests/metrics_labels.rs` structurally asserts exact identity-key absence and that a Network-name canary never enters metric label values.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-network-local/src/process_specs.rs` agent template plus agent service implementation in the net-VM artifact." + ], + "detailedDesign" : "Implement `NetworkAgentService` Noise-KK vsock ComponentSession (Reload + ReadinessQuery methods). Agent reconnect policy: if the controller cannot reach the agent vsock (Guest restart in progress), it retries with exponential backoff up to `drainTimeout` of the agent Process; after timeout it deletes and re-creates the agent Process resource. Primary reuse disposition: `create`. Preserved source-plan detail: net-new service; preserve semantic nftables/routes reload behavior from v1 net VM configuration.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-007", + "kind" : "work-item", + "owner" : "Provider; owns net-agent ComponentSession service and depends on ComponentSession/bus and net-VM Process lifecycle.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "`integration/agent_reload.rs` validates Reload, `nft_applied` and `routes_applied` predicates, reconnect behavior, and config digest matching.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-network-local/src/config_volume.rs`." + ], + "detailedDesign" : "Implement config Volume content rendering (dnsmasq.conf, nftables.rules, routing.conf, attachments.json). Primary reuse disposition: `adapt`. Preserved source-plan detail: port and render into bounded config Volume files.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-008", + "kind" : "work-item", + "owner" : "Provider; config rendering owned by `Provider/network-local`, storage reconciliation owned by `Provider/volume-local`.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "`tests/controller_state.rs`, `integration/agent_reload.rs`, and `integration/delete_sequence.rs` validate rendering, write flow, reload, and cleanup ordering.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-network-local/src/process_specs.rs`." + ], + "detailedDesign" : "Implement canonical Process spec builders for agent, dnsmasq, mdns-reflector, mdns-dnsbridge. Primary reuse disposition: `adapt`. Preserved source-plan detail: port service semantics into canonical Process resource specs.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-009", + "kind" : "work-item", + "owner" : "Provider; Process resource builders owned by `d2b-provider-network-local`.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "`tests/controller_state.rs`, `integration/mdns_reflector.rs`, and eval case `process-sandbox-netns.nix` validate Process shape, optional mDNS, and guest-netns capability isolation.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`net-vm-base` nixos-system artifact and artifact catalog entry `d2b.artifacts.net-vm-base`." + ], + "detailedDesign" : "Build generic `net-vm-base` nixos-system artifact with net-agent binary, agent-service endpoint, guest-agent binary, standard NIC bootstrap, lib.mkForce override; bake `net-local-controller` account with the UID/GID allocated from the host-users reservation table (documented in `nixos-modules/host-users.nix`). Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve generic boot/safety invariants; exclude per-Network static config.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-010", + "kind" : "work-item", + "owner" : "net-vm artifact; owns generic `net-vm-base` nixos-system artifact and shared net-local-controller UID/GID reservation.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "Eval cases `net-vm-artifact-id-eval.nix` and `network-spec-eval.nix`, plus `tests/net-vm-network-eval.sh` for the lib.mkForce invariant.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Nix module resource emission for `Provider/network-local`, `User/net-local-controller`, host OS account, `provider-network-local`, and `net-vm-base` artifacts." + ], + "detailedDesign" : "Nix module for `Provider/network-local` resource declaration; `User/net-local-controller` declaration; OS account provisioning; artifact catalog entries. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt Nix option/resource emission and account provisioning to v3 resources.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-011", + "kind" : "work-item", + "owner" : "Nix; owns resource declaration, User declaration, host account provisioning, and artifact catalog wiring.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "Eval cases `network-spec-eval.nix`, `user-no-managed-by-eval.nix`, `net-vm-artifact-id-eval.nix`, and `make test-policy` for artifact/package paths.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Nix flake/resource schema checks for declared Networks and provider `validate.rs` parity." + ], + "detailedDesign" : "Build-time CIDR overlap check for declared Networks in flake check. Primary reuse disposition: `adapt`. Preserved source-plan detail: port/reuse overlap semantics in v3 eval checks and provider validation.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-012", + "kind" : "work-item", + "owner" : "Nix; depends on Network resource schema and CIDR validation rules.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "Eval case `network-cidr-overlap-eval.nix` and `tests/cidr_overlap.rs` cover same-Network, cross-Network, external CIDR, and adjacency cases.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-network-local/tests/schema_roundtrip.rs`, `tests/ifname_derive.rs`, and `tests/cidr_overlap.rs`." + ], + "detailedDesign" : "Conformance suite: NetworkSpec round-trip, IfName derivation, CIDR validation matrix. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing IfName/CIDR assertions into provider conformance tests.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-013", + "kind" : "work-item", + "owner" : "Tests; owned by `d2b-provider-network-local` hermetic test suite.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "The listed conformance tests themselves are the validation, with workspace policy ensuring `tests/` exists.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-network-local/tests/controller_state.rs`." + ], + "detailedDesign" : "Controller state-machine unit tests with fake `NetworkEffectPort` (from d2b-contracts mock) and deterministic clock, including attachment removal/finalizer calls to generation-fenced `DeletePersistentTap`.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-014", + "kind" : "work-item", + "owner" : "Tests; depends on ADR046-nl-006 controller and fake `NetworkEffectPort` from `d2b-contracts`.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local", + "ADR046-nl-006" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 14, + "validation" : "`tests/controller_state.rs` covers normal path, CIDR conflict, User not Ready, Volume error, Guest timeout, agent reload failure, finalizer sequence, `DeletePersistentTap` validated absence, transient retry with retained handle, stale-generation refresh, foreign-marker block, adoption, and drift.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-network-local/integration/host_fabric.rs`, `guest_lifecycle.rs`, `agent_reload.rs`, and `delete_sequence.rs`." + ], + "detailedDesign" : "Integration tests: full Network lifecycle (create, config update, agent Reload, generation-fenced persistent-tap deletion, delete sequence) in container environment. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt reusable semantic assertions into v3 integration coverage.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-015", + "kind" : "work-item", + "owner" : "Tests; integration coverage for the complete Network lifecycle.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "`make test-integration` for container tests and `make test-host-integration` where guest lifecycle requires host/KVM coverage.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Process templates for agent and dnsmasq plus sandbox/eval tests." + ], + "detailedDesign" : "Verify INV-NET-008 (Guest-network-admin isolation): Process Provider correctly inherits Guest netns for agent/dnsmasq. Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve and verify existing guest-netns isolation invariant in v3 Process specs.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-016", + "kind" : "work-item", + "owner" : "Security; depends on Process sandbox schema and `Provider/system-minijail` guest namespace inheritance.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "`tests/unit/nix/cases/process-sandbox-netns.nix` and provider Process-template tests assert no host capability or host network namespace grant.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-network-local/README.md`." + ], + "detailedDesign" : "`packages/d2b-provider-network-local/README.md` covering all 7 required topics. Primary reuse disposition: `create`. Preserved source-plan detail: net-new documentation.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-017", + "kind" : "work-item", + "owner" : "Docs; owned by `d2b-provider-network-local` package documentation.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "`make test-policy` / `xtask workspace-policy` verifies required provider crate paths and README presence.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Device-usbip EffectPort/adapter owns USBIP rules, drift, and strict provider status; network-local host/net-VM renderers and status cover only Network-owned policy." + ], + "detailedDesign" : "`UsbipBindFirewallRule` remains the sole broker mutation path for exact per-Network/per-busid TCP/3240 exposure. Network-local emits no TCP/3240 match, excludes device-usbip ownership markers from its digest, and never reports USBIP drift. Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve the typed closed broker op; remove both generic network-local USBIP allows.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-018", + "kind" : "work-item", + "owner" : "Broker plus device provider boundary; `UsbipBindFirewallRule` remains owned by `Provider/device-usbip`.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "Network-local host and net-VM firewall intent tests assert no TCP/3240/USBIP rule; USBIP rule churn leaves Network digest/`FirewallReady` unchanged; device-usbip tests own exact scoping, drift, status, and release.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Provider descriptor, controller-main deployment, `tests/state_schema_roundtrip.rs`, and eval case `provider-state-volume-eval.nix`." + ], + "detailedDesign" : "Confirm `controller-main` declares no stateNamespace and core ProviderDeployment creates no Provider state Volume or state mount; validate ProviderStateSet query returns empty for `Provider/network-local`; validate bounded operational state is written to revisioned/redacted status and the core Operation ledger with `status-oversize` conformance; confirm per-Network config Volumes remain `ownerRef: Network/` runtime/config operational Volumes outside the ProviderStateSet and `Volume` is not in `ResourceTypes implemented`. Primary reuse disposition: `create`. Preserved source-plan detail: net-new status-first provider-state conformance.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-019", + "kind" : "work-item", + "owner" : "Provider; depends on D087 ProviderStateSet and status-first storage rules.", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "`tests/state_schema_roundtrip.rs` and `tests/unit/nix/cases/provider-state-volume-eval.nix` validate empty ProviderStateSet, status bounds/redaction, and config Volume exclusion.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Network schema/Provider descriptor, Core authority index, Network reconcile/update/finalizer, runtime LaunchTicket resolver, and authority tests" + ], + "detailedDesign" : "Register the external physical-NIC `AuthorityDescriptor`: Host-global `external-physical-nic/v1` Core-derived identity, `zero-or-one` authority, exclusive `passthru`/`private`/`vepa`, exclusive-by-default `bridge`, explicitly compatible bounded multiplexing only for bridge, `external-physical-nic-conflict`, exact owner proof, drain-release-reacquire update, forbidden export, and bounded FIFO holder policy. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt the existing broker-internal macvtap-FD creation path behind mandatory Core authority admission.", + "entryContracts" : [ + "ADR-046-provider-network-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-nl-020", + "kind" : "work-item", + "owner" : "D097 Core authority index, Network contracts, runtime-cloud-hypervisor private attachment path", + "parallelGroup" : "wi:ADR-046-provider-network-local", + "prerequisites" : [ + "ADR-046-provider-network-local" + ], + "specId" : "ADR-046-provider-network-local", + "topologicalRank" : 11, + "validation" : "`external_nic_authority.rs` covers Core-derived identity, same-/cross-Zone conflicts, explicit bridge multiplexing, incompatible policy, non-bridge multiplex denial, no-effect rejection, adoption ambiguity, owner transfer, disruptive update, release ordering, and redaction; Nix eval and host integration cover declared configuration and lifecycle.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-notification-desktop/src/{types,redact,action_nonce}.rs`" + ], + "detailedDesign" : "`NotificationRequest`/`NotificationResult` DTOs and stream record types; bounded fields; closed category set; icon catalog contract; `ActionNonce`/`ActionNonceStore` adapted from main; no ResourceType DTO Primary reuse disposition: `adapt`. Preserved source-plan detail: copy/adapt.", + "entryContracts" : [ + "ADR-046-provider-notification-desktop" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-notify-001", + "kind" : "work-item", + "owner" : "W0 shared contract root; session/bus owner", + "parallelGroup" : "wi:ADR-046-provider-notification-desktop", + "prerequisites" : [ + "ADR-046-provider-notification-desktop" + ], + "specId" : "ADR-046-provider-notification-desktop", + "topologicalRank" : 11, + "validation" : "`tests/stream_record.rs` — DTO schema vectors; `tests/action_nonce.rs` — single-use/TTL/capacity/replay", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-notification-desktop/src/stream_admission.rs`" + ], + "detailedDesign" : "Session admission checks, Noise profile enforcement, transport class validation Primary reuse disposition: `adapt`. Preserved source-plan detail: copy/adapt.", + "entryContracts" : [ + "ADR-046-provider-notification-desktop" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-notify-002", + "kind" : "work-item", + "owner" : "ADR046-session-001, ADR046-bus-001; session/bus wiring", + "parallelGroup" : "wi:ADR-046-provider-notification-desktop", + "prerequisites" : [ + "ADR-046-provider-notification-desktop", + "ADR046-bus-001", + "ADR046-session-001" + ], + "specId" : "ADR-046-provider-notification-desktop", + "topologicalRank" : 11, + "validation" : "`tests/stream_admission.rs` — all rejection vectors", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-notification-desktop/src/controller.rs`" + ], + "detailedDesign" : "Async Process placement controller; watches `guestSources` Guest refs; creates/drains/deletes guest-source Processes; creates/stops host-sink Process on display-wayland readiness change; declares no Provider state Volume and does not own/add/create/delete Volumes; bounded non-secret operational state lives in `status`/the core Operation ledger (D087); notification delivery state (in-memory projection, action nonce store) is host-sink process memory only; no ResourceType reconcile loop", + "entryContracts" : [ + "ADR-046-provider-notification-desktop" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-notify-003", + "kind" : "work-item", + "owner" : "ADR046-notify-001, ADR046-reconcile-001; controller owner", + "parallelGroup" : "wi:ADR-046-provider-notification-desktop", + "prerequisites" : [ + "ADR-046-provider-notification-desktop", + "ADR046-notify-001", + "ADR046-reconcile-001" + ], + "specId" : "ADR-046-provider-notification-desktop", + "topologicalRank" : 12, + "validation" : "Unit tests for placement FSM in `tests/stream_record.rs`; Volume creation/deletion lifecycle in `tests/volume_lifecycle.rs`; see also `integration/cross_zone_source.rs` end-to-end", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-notification-desktop/src/host_sink.rs`" + ], + "detailedDesign" : "D-Bus client; `DesktopNotificationSink` stream consumer; action nonce issuance; `DesktopNotificationObserver` projection (in-memory, not persisted); display-wayland ComponentSession bootstrap for pre-opened D-Bus FD", + "entryContracts" : [ + "ADR-046-provider-notification-desktop" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-notify-004", + "kind" : "work-item", + "owner" : "ADR046-notify-002, ADR046-notify-003; host-sink owner", + "parallelGroup" : "wi:ADR-046-provider-notification-desktop", + "prerequisites" : [ + "ADR-046-provider-notification-desktop", + "ADR046-notify-002", + "ADR046-notify-003" + ], + "specId" : "ADR-046-provider-notification-desktop", + "topologicalRank" : 13, + "validation" : "`integration/dbus_sink.rs`, `integration/observer_client.rs`, `integration/action_invoke.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-notification-desktop/src/guest_source.rs`" + ], + "detailedDesign" : "Guest-side vsock ComponentSession; `NotificationRequest` record validation and field bounding; category filter; `DesktopNotificationSink` stream forwarding; `NotificationResult` handling; no host-side resource creation", + "entryContracts" : [ + "ADR-046-provider-notification-desktop" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-notify-005", + "kind" : "work-item", + "owner" : "ADR046-notify-002; guest-source owner", + "parallelGroup" : "wi:ADR-046-provider-notification-desktop", + "prerequisites" : [ + "ADR-046-provider-notification-desktop", + "ADR046-notify-002" + ], + "specId" : "ADR-046-provider-notification-desktop", + "topologicalRank" : 12, + "validation" : "`integration/cross_zone_source.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Nix: Zone resource authoring in `nixos-modules/`; metrics: `packages/d2b-provider-notification-desktop/src/`" + ], + "detailedDesign" : "Zone Provider resource and RoleBinding Nix compiler output; `spec.config.guestSources` authoring and eval-time assertions; OTEL metric emitters with closed semantic labels and no Zone/resource-name-derived dimensions while retaining `d2b.zone` as a resource attribute; audit record emitters", + "entryContracts" : [ + "ADR-046-provider-notification-desktop" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-notify-006", + "kind" : "work-item", + "owner" : "ADR046-notify-001; Nix/telemetry owner", + "parallelGroup" : "wi:ADR-046-provider-notification-desktop", + "prerequisites" : [ + "ADR-046-provider-notification-desktop", + "ADR046-notify-001" + ], + "specId" : "ADR-046-provider-notification-desktop", + "topologicalRank" : 12, + "validation" : "Eval tests for category enforcement, displayWaylandRef assertion, guestRef resolution; `tests/stream_redaction.rs` for content-free telemetry; `tests/metrics_labels.rs` for structural exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus notification/Zone-name canary absence", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/resource.rs`, `resource_status.rs`, `resource_schema.rs`" + ], + "detailedDesign" : "Implement strict ResourceEnvelope, metadata, the three-layer spec shape (universal envelope + ResourceType base `spec.*` incl. `spec.providerRef` + optional canonical `spec.provider` `{ schemaId, schemaVersion, settings }`), the three-layer status shape (universal base + `status.resource` + optional `status.provider` with `providerRef`/`schemaId`/`schemaVersion`/`observedProviderGeneration`/`details`), phase/condition/outcome, canonical JSON, per-layer bounds/redaction, ownerRef/UID fields Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resource-object-model" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-object-001", + "kind" : "work-item", + "owner" : "W0 shared contract root; `d2b-contracts`", + "parallelGroup" : "wi:ADR-046-resource-object-model", + "prerequisites" : [ + "ADR-046-resource-object-model" + ], + "specId" : "ADR-046-resource-object-model", + "topologicalRank" : 3, + "validation" : "Golden JSON/protobuf vectors; serde unknown-field; three-layer spec shape round-trip; canonical minimal base-spec acceptance; base-schema version/fingerprint conformance; `spec.provider` deny-unknown/version-mismatch/shadow rejection and providerRef-binding; three-layer status shape round-trip; base-only projection (universal + `status.resource`) ignores/omits `status.provider`; `status.provider` unknown-field/version-mismatch rejection; status redaction/size/time/phase tests; `status.update` currency object round-trip (state/reasons/disruption/preserveState/owned+dependency refs bounded); `spec.updatePolicy` base round-trip", + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-store-redb/src/ownership.rs`, `packages/d2b-controller-toolkit/src/owner_hints.rs`" + ], + "detailedDesign" : "Singular ownerRef resolution/UID binding, cycle/depth property checks, reverse index, owner hints, child-first deletion", + "entryContracts" : [ + "ADR-046-resource-object-model" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-object-002", + "kind" : "work-item", + "owner" : "ADR046-object-001; native resource store", + "parallelGroup" : "wi:ADR-046-resource-object-model", + "prerequisites" : [ + "ADR-046-resource-object-model", + "ADR046-object-001" + ], + "specId" : "ADR-046-resource-object-model", + "topologicalRank" : 4, + "validation" : "Property tests for cycles/reparent/name reuse; integration tests for child drift repair and owner cascades", + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-observability-otel/src/{forwarder_bin,controller,binding}.rs`" + ], + "detailedDesign" : "Binding-owned forwarder: accept OTLP only from the exact Guest producer, relay through the Binding-private Endpoint/Volume to its edge collector, enforce bounded frames/quota/session timeout, and use no OTEL SDK. Map forwarder readiness to `status.provider`, then derive provider-neutral Binding ingest readiness; Process Provider owns launch/pidfd. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (`OtelHostBridgeArgvInputs` vsock logic → native Rust OTLP relay); adapt (`OtelHostBridgeReadiness` → `TelemetryBinding` conditions); delete-after-cutover (`RunnerRole::OtelHostBridge`, `ProcessRole::OtelHostBridge`).", + "entryContracts" : [ + "ADR-046-provider-observability-otel" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-otel-001", + "kind" : "work-item", + "owner" : "ADR046-telem-001 (BoundedEmitter crate); W0/W2; telemetry/observability owner", + "parallelGroup" : "wi:ADR-046-provider-observability-otel", + "prerequisites" : [ + "ADR-046-provider-observability-otel", + "ADR046-telem-001" + ], + "specId" : "ADR-046-provider-observability-otel", + "topologicalRank" : 11, + "validation" : "`integration/scenario_obs_zone_forwarding.rs`; adapted `minijail_relay_otel.rs` shape test for Provider-managed runner; assert `RunnerRole::OtelHostBridge` is absent from `d2b-contracts` after removal", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-observability-otel/src/{collector_bin,emitter_socket,ingress_policy,exporter,controller,service,binding}.rs`; updated Nix observability modules" + ], + "detailedDesign" : "Register the initial implementation of both provider-neutral qualified ResourceTypes by binding the exact ADR046-provider-004 base versions/fingerprints, then define only strict observability-otel Service/Binding spec and status extensions. Reconcile each Binding into an edge collector, private Endpoints, runtime Volume, and optional forwarder. Collector links the full OTEL SDK, resolves `serviceRef`, stamps trusted producer identity into allow-listed OTEL Resource attributes, and routes Unix-emitter, OTLP-Unix, OTLP/vsock, and import-stream metrics through one structural `METRIC_LABEL_POLICY` gate before aggregation, queueing, batching, retry, or export. The gate rejects exact/suffix identity keys and trusted `metadata.name`/resource-identity canary values, applies bounded non-echoing error classes and connection quarantine, and never consumes audit. Write generic Service/Binding observations to `status.resource` and SigNoz/OTLP/OTEL observations only to `status.provider`; no state file or Provider state Volume. Provider root config remains installation-only. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt Nix pipeline shape (replace per-VM `vmName` with per-Zone name; replace socat runner with vsock-forwarder long-lived Process; adapt `ingressSources` per-Zone entry).", + "entryContracts" : [ + "ADR-046-provider-observability-otel" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-otel-002", + "kind" : "work-item", + "owner" : "ADR046-otel-001 + ADR046-telem-001 + ADR046-provider-001 (Provider toolkit) + ADR046-provider-004 (common telemetry Service/Binding base) + resource/Endpoint/Volume contracts; W2; observability owner", + "parallelGroup" : "wi:ADR-046-provider-observability-otel", + "prerequisites" : [ + "ADR-046-provider-observability-otel", + "ADR046-otel-001", + "ADR046-provider-001", + "ADR046-provider-004", + "ADR046-telem-001" + ], + "specId" : "ADR-046-provider-observability-otel", + "topologicalRank" : 12, + "validation" : "Common-fixture/fingerprint and canonical-minimal-base conformance (including a fake alternate telemetry Provider); `tests/emitter_socket_receive.rs`; table-driven `tests/ingress_metric_policy.rs` across all four ingress adapters; `tests/exporter_outage.rs`; `tests/exporter_backpressure.rs`; `integration/scenario_full_pipeline.rs`; adapted `policy_observability.rs` (retain all existing assertions; add new `d2b.zone`, `d2b.provider` allowlist entries)", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-observability-otel/src/nix/journald.nix`; `packages/d2b-provider-observability-otel/src/journald.rs`" + ], + "detailedDesign" : "Per `ADR-046-telemetry-audit-and-support` §journald stdout/stderr ingestion: cgroup filter derived from trusted `producerRef`; redaction drops credential/secret/path fields, `_CMDLINE`, `_EXE`, and `INVOCATION_ID`; the strict `TelemetryBinding.spec.provider.settings.journald.enable` defaults false. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt journald receiver config for per-Zone cgroup filter.", + "entryContracts" : [ + "ADR-046-provider-observability-otel" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-otel-003", + "kind" : "work-item", + "owner" : "ADR046-otel-002 + ADR046-telem-001 + ADR046-volume-001; Nix/observability owner", + "parallelGroup" : "wi:ADR-046-provider-observability-otel", + "prerequisites" : [ + "ADR-046-provider-observability-otel", + "ADR046-otel-002", + "ADR046-telem-001", + "ADR046-volume-001" + ], + "specId" : "ADR-046-provider-observability-otel", + "topologicalRank" : 13, + "validation" : "Nix eval test: filter expression set when enabled; assert `_CMDLINE` and `INVOCATION_ID` in drop list; `tests/redaction.rs` for journald field redaction", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/policy_observability.rs` (updated); `packages/d2b-contract-tests/tests/policy_telemetry_redaction.rs` (new, per ADR046-telem-008); `packages/d2b-provider-observability-otel/tests/{no_vm_label_in_metrics,ingress_metric_policy}.rs`" + ], + "detailedDesign" : "(1) Extend `loki_native_otel_resource_attributes` allowlist with `d2b.zone`, `d2b.provider`, `d2b.component`, `service.version`. (2) Add gate: `no_isolation` must not appear in any Provider `MetricDescriptor` label or span attribute catalog. (3) Adapt `minijail_relay_otel.rs` shape test for Provider-managed runner (no broker `RunnerRole::OtelHostBridge`). (4) Add metric inventory gates for `d2b_otel_*` instruments from this spec. (5) Structurally prove that all four metrics ingress adapters call the shared pre-batch policy gate; run exact forbidden-key, suffix, `metadata.name`, ResourceRef, UID, and resource-identity canaries through each adapter; assert bounded non-echoing outcomes/quarantine/backpressure and valid OTEL Resource identity preservation. (6) Retain: `startup_tracing_avoids_host_path_fields`; SigNoz-only backend assertion; `tempo_guest_collector_shape`; `config_source = \"realm-controllers\"` absence gate. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt and extend existing tests; keep existing test assertions.", + "entryContracts" : [ + "ADR-046-provider-observability-otel" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-otel-004", + "kind" : "work-item", + "owner" : "ADR046-otel-002; policy/contract-tests owner", + "parallelGroup" : "wi:ADR-046-provider-observability-otel", + "prerequisites" : [ + "ADR-046-provider-observability-otel", + "ADR046-otel-002" + ], + "specId" : "ADR-046-provider-observability-otel", + "topologicalRank" : 13, + "validation" : "All contract-tests pass after update; existing allowlist test does not regress", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-observability-otel/src/share_adapter.rs`" + ], + "detailedDesign" : "Implement the signed adapter: `sys-obs` exports only the authority `TelemetryService`; its local ingest Endpoint remains Service-owned implementation transport and is never an Export field. ResourceExport uses only canonical `serviceType`, `projectionSchemaFingerprint`, and `factoryFingerprint`; ResourceImport uses the corresponding `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`. Every producer import causes Core to create one local `TelemetryService` projection with ResourceImport ownership, `providerRef`, semantic base/import fields, and no `spec.provider`. Routing derives from the signed local descriptor and ResourceImport record. The semantic factory fingerprint binds factory metadata plus projection-protocol version, never Provider/adapter identity, which the signed descriptor authenticates separately. Binding `serviceRef` targets that projection. Enforce many-to-one quota/credit/backpressure/schema/source-stamp/redaction/cardinality over bounded encrypted streams; no FD/socket crosses a Zone and audit authority stays local. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (implement the signed observability export/import adapter).", + "entryContracts" : [ + "ADR-046-provider-observability-otel" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-otel-005", + "kind" : "work-item", + "owner" : "ADR046-zone-control-019, ADR046-zone-control-020; observability Provider owner", + "parallelGroup" : "wi:ADR-046-provider-observability-otel", + "prerequisites" : [ + "ADR-046-provider-observability-otel", + "ADR046-zone-control-019", + "ADR046-zone-control-020" + ], + "specId" : "ADR-046-provider-observability-otel", + "topologicalRank" : 13, + "validation" : "Fast `projection_chain.rs` proves Service projection semantics with a fake stream, exact canonical Export/Import type and fingerprint fields, rejection of obsolete `endpointRef`/`exportedType`/`baseSchemaFingerprint`/`expectedType`/`expectedBaseSchemaFingerprint`/`projectionType`, rejection of projection `spec.provider`, and semantic-fingerprint stability under Provider/adapter identity mutation while signed descriptor authentication remains exact; integration alone runs real encrypted streams and SigNoz; revocation/reconnect, quotas, source stamp, redaction/cardinality, no FD crossing, and audit locality", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-observability-otel/src/{authority,service,binding,projection}.rs`; `AuthorityDescriptor` on the `sys-obs` `TelemetryService`" + ], + "detailedDesign" : "Implement one provider-neutral D097 authority Service with generic telemetry-ingest Endpoint refs and common service/signals/quota/policy fields. Its strict observability-otel `spec.provider` alone selects the loopback SigNoz stack, backend Endpoints, and OTLP. Core rejects duplicates and adopts by owner proof. Implement core-owned imported Service projections with no `spec.provider` or backend ownership. Implement per-producer Bindings with common service/producer/signals/quota/policy fields and strict implementation extension; Bindings own/cause edge collector/forwarder/private Endpoints/runtime Volume. Keep generic observations in `status.resource` and SigNoz/OTLP/OTEL observations in `status.provider`. Preserve trusted source upsert, retry/queue, bounded cardinality/redaction, audit non-transfer, status-first state, and no OTEL SDK in core. Endpoint is transport only. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new universal provider-neutral `telemetry.d2bus.org.TelemetryService`/`telemetry.d2bus.org.TelemetryBinding` pair; adapt existing authority/edge behavior as the initial observability-otel implementation.", + "entryContracts" : [ + "ADR-046-provider-observability-otel" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-otel-006", + "kind" : "work-item", + "owner" : "ADR046-otel-005, ADR046-zone-control-019, ADR046-zone-control-020; observability Provider owner", + "parallelGroup" : "wi:ADR-046-provider-observability-otel", + "prerequisites" : [ + "ADR-046-provider-observability-otel", + "ADR046-otel-005", + "ADR046-zone-control-019", + "ADR046-zone-control-020" + ], + "specId" : "ADR-046-provider-observability-otel", + "topologicalRank" : 14, + "validation" : "Fast `resource_service_binding.rs` and `projection_chain.rs` plus reused nix-unit/policy tests prove provider-neutral names, base/Provider field separation, projection `spec.provider` rejection, exact D088 placement of semantic observations under `status.resource` and implementation observations under `status.provider`, ownership, schemas, stamping, quotas, redaction, and projection chain. Real SigNoz and real stream scenarios are integration-only.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/policy_provider_crate_layout.rs` (new file; gated under D2B_FIXTURES in existing `tests/tools/rust-workspace-checks.sh`)" + ], + "detailedDesign" : "Implement `policy_provider_crate_layout.rs` with the following test functions: (1) `every_provider_crate_has_src` — walk `packages/d2b-provider-*/` directories in the workspace, assert each contains `src/`; failure names crate and missing path; (2) `every_provider_crate_has_tests` — assert `tests/` present; (3) `every_provider_crate_has_integration` — assert `integration/` present; (4) `every_provider_crate_has_readme` — assert `README.md` present; (5) `every_provider_readme_has_required_sections` — read `README.md`, check for all nine section headings from §4.8.3 (case-insensitive, after stripping `#` and whitespace); failure names the missing heading(s); (6) `every_integration_file_has_target_declaration` — for each `integration/*.rs` file, scan first 20 lines for exactly one `//! integration-target: (container|host-integration)` declaration; failure names the file and the violation (missing/multiple/invalid value); (7) `non_provider_crates_exempt` — verify the check does not run on non-`d2b-provider-*` crates. All checks are filesystem-only (no compilation). Workspace member list is discovered by parsing `packages/Cargo.toml` `[workspace].members`. Gate: add the new test file to `tests/tools/rust-workspace-checks.sh` D2B_FIXTURES list alongside existing policy tests", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pkg-001", + "kind" : "work-item", + "owner" : "ADR046-zone-control-003; workspace policy owner", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-003" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 13, + "validation" : "§15.3 layout conformance tests: `provider-crate-layout-src-required`, `provider-crate-layout-tests-required`, `provider-crate-layout-integration-required`, `provider-crate-layout-readme-required`, `provider-readme-sections-all-present`, `provider-readme-sections-partial-missing`, `provider-integration-target-declared`, `provider-integration-target-unique`, `provider-integration-target-valid-values`, `provider-crate-naming-convention`, `provider-crate-layout-non-provider-exempt`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/host.rs`, `guest.rs`, `execution_policy.rs`, `process.rs`, `volume.rs`, `user.rs`, `network.rs`, `device.rs`, `credential.rs`" + ], + "detailedDesign" : "Complete minimal ResourceType schemas and shared execution/Volume sub-schemas Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-primitive-resource-composition" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-primitives-001", + "kind" : "work-item", + "owner" : "W0; resource contracts", + "parallelGroup" : "wi:ADR-046-primitive-resource-composition", + "prerequisites" : [ + "ADR-046-primitive-resource-composition" + ], + "specId" : "ADR-046-primitive-resource-composition", + "topologicalRank" : 7, + "validation" : "Schema vectors and folded-field/no-duplicate-type policy tests", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-systemd/`, `packages/d2b-provider-system-minijail/`, shared neutral process conformance library" + ], + "detailedDesign" : "Common Process/EphemeralProcess, provider-specific launch/pidfd/wait/adoption/status Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-primitive-resource-composition" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-primitives-002", + "kind" : "work-item", + "owner" : "Process contracts; system Provider slices", + "parallelGroup" : "wi:ADR-046-primitive-resource-composition", + "prerequisites" : [ + "ADR-046-primitive-resource-composition" + ], + "specId" : "ADR-046-primitive-resource-composition", + "topologicalRank" : 7, + "validation" : "Shared conformance plus Host/Guest/user integration", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-*/`; `nixos-modules/resources-volume.nix`" + ], + "detailedDesign" : "Fine-grained Volume layout/views, host-path policy, virtiofs attachments/status/owned Process Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-primitive-resource-composition" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-primitives-003", + "kind" : "work-item", + "owner" : "Volume contract; Volume Provider slices", + "parallelGroup" : "wi:ADR-046-primitive-resource-composition", + "prerequisites" : [ + "ADR-046-primitive-resource-composition" + ], + "specId" : "ADR-046-primitive-resource-composition", + "topologicalRank" : 7, + "validation" : "ACL/no-follow/marker, sharing/views, virtiofs host/guest mount tests", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-process/src/`, `packages/d2b-provider-supervisor/src/`" + ], + "detailedDesign" : "Common spec/status/tickets/pidfd/adoption/naming/cgroup/fast path Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-process-001", + "kind" : "work-item", + "owner" : "W0/W2; Process contracts/supervisor", + "parallelGroup" : "wi:ADR-046-components-processes-and-sandbox", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox" + ], + "specId" : "ADR-046-components-processes-and-sandbox", + "topologicalRank" : 9, + "validation" : "Shared conformance/fault/latency tests", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-systemd/`, `packages/d2b-provider-system-minijail/`" + ], + "detailedDesign" : "Two Process/EphemeralProcess implementations, pidfd/wait ownership, system/user domains Primary reuse disposition: `adapt`. Preserved source-plan detail: extract/adapt.", + "entryContracts" : [ + "ADR-046-components-processes-and-sandbox" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-process-002", + "kind" : "work-item", + "owner" : "ADR046-process-001; systemd/minijail Provider owners", + "parallelGroup" : "wi:ADR-046-components-processes-and-sandbox", + "prerequisites" : [ + "ADR-046-components-processes-and-sandbox", + "ADR046-process-001" + ], + "specId" : "ADR-046-components-processes-and-sandbox", + "topologicalRank" : 10, + "validation" : "Identical schema/status conformance plus provider-specific adoption", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/provider.rs`, `packages/d2b-provider/src/lib.rs`, `packages/d2b-provider-toolkit/`" + ], + "detailedDesign" : "Provider resource/manifest/components/dependencies/services/trust/compatibility/toolkit Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-model-and-packaging" + ], + "exitGate" : "ADR046-W3 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-provider-001", + "kind" : "work-item", + "owner" : "W0; Provider contract/catalog owner", + "parallelGroup" : "wi:ADR-046-provider-model-and-packaging", + "prerequisites" : [ + "ADR-046-provider-model-and-packaging" + ], + "specId" : "ADR-046-provider-model-and-packaging", + "topologicalRank" : 8, + "validation" : "Contract vectors, fake/malicious Provider, one-crate/one-identity policy", + "wave" : "W3" + }, + { + "blockers" : [], + "destinations" : [ + "one `packages/d2b-provider--/` per Provider with mandatory src/, tests/, integration/, README.md; generic Nix Provider package/catalog emitter" + ], + "detailedDesign" : "Split current combined/composition crates; exact outputs/manifests/conformance/layout/documentation", + "entryContracts" : [ + "ADR-046-provider-model-and-packaging" + ], + "exitGate" : "ADR046-W3 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-provider-002", + "kind" : "work-item", + "owner" : "Provider contract; package/Nix integrator", + "parallelGroup" : "wi:ADR-046-provider-model-and-packaging", + "prerequisites" : [ + "ADR-046-provider-model-and-packaging" + ], + "specId" : "ADR-046-provider-model-and-packaging", + "topologicalRank" : 8, + "validation" : "Workspace naming/dependency/output/dossier/catalog parity policy", + "wave" : "W3" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-core/`, `d2b-provider-system-systemd/`, `d2b-provider-system-minijail/`" + ], + "detailedDesign" : "Bootstrap system-core; common Process/EphemeralProcess providers and pidfd conformance Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-model-and-packaging" + ], + "exitGate" : "ADR046-W3 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-provider-003", + "kind" : "work-item", + "owner" : "Process contracts; system Provider owners", + "parallelGroup" : "wi:ADR-046-provider-model-and-packaging", + "prerequisites" : [ + "ADR-046-provider-model-and-packaging" + ], + "specId" : "ADR-046-provider-model-and-packaging", + "topologicalRank" : 8, + "validation" : "Shared conformance and host/user/non-Host tests", + "wave" : "W3" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/semantic_services/{mod,audio,security_key,telemetry,usb}.rs`; generated schema artifacts for the eight exact qualified ResourceTypes" + ], + "detailedDesign" : "Define one shared strict base spec/status DTO and schema contract for each frozen D098 Service/Binding pair, including exact semantic type/schema IDs, versions, fingerprints, minimal valid base fixtures without `spec.provider`, authority/projection Service union, same-Zone Binding `serviceRef`/target rules, D088 `status.resource` layering, status-only observations, and projection-factory type binding. A Core-generated projection permits only `providerRef`, semantic base/import fields, and ResourceImport ownership; it rejects `spec.provider`. Register no implementation-qualified or former `*State` alias.", + "entryContracts" : [ + "ADR-046-provider-model-and-packaging" + ], + "exitGate" : "ADR046-W3 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-provider-004", + "kind" : "work-item", + "owner" : "ADR046-provider-001; shared semantic Service/Binding contract owner", + "parallelGroup" : "wi:ADR-046-provider-model-and-packaging", + "prerequisites" : [ + "ADR-046-provider-model-and-packaging", + "ADR046-provider-001" + ], + "specId" : "ADR-046-provider-model-and-packaging", + "topologicalRank" : 9, + "validation" : "Shared contract tests cover exact names, strict serde/schema round trips, common base discoverability without any Provider package, canonical minimal base acceptance without `spec.provider`, same-Zone refs/targets, owner/projection discrimination, Core projection rejection of `spec.provider`, common fields only under `status.resource`, implementation observation only under `status.provider`, status-only observations, no Device/Endpoint/Binding projection, implementation-detail rejection, semantic factory-fingerprint stability under Provider/adapter identity changes, and rejection of every implementation-qualified/former `*State` alias. Each initial and fake alternate Provider must pass the identical base conformance fixture.", + "wave" : "W3" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider/src/agent.rs` (v3 provider agent dispatch); `packages/d2b-contracts/src/v3/provider_registry.rs` (v3 provider registry wire contract)" + ], + "detailedDesign" : "**Selected**: `ProviderAgentProcess::serve` dispatch loop with semaphore in-flight limit; unsupported-service/method → ttrpc error; negative-timeout rejection; `SessionClosed` clean termination; `ProtocolViolation` audit-and-terminate path; bounded audit ring; `GeneratedProviderServiceServer` single-service-per-agent requirement; `ProviderRegistryV2` entry validation: provider-id derivation, schema fingerprint, scope digest, generation exactness; `MAX_PROVIDER_MAPPING_IDS=64` → Provider component mapping bound; `ProviderBindingV2` non-exhaustive + explicit `UnsupportedProviderBindingV2` fallback (never panics on unknown axis); `ProviderIntentId` `max 128 bytes` label rules → Provider component `spec.intentRef`; all 10 `provider_registry_v2.rs` tests ported. **Excluded ADR45 assumptions**: `aca_workload.rs` (`AcaGatewayWorkload`): ADR45 ACA external provider adapter, excluded entirely. `waypipe_display.rs` (`WaypipeDisplayProvider`): ADR45 display provider, excluded. `ProviderRegistryV2.registry_generation` / `configuration_fingerprint` bind to ADR45 bundle generation; v3 Provider resource version is tracked in redb store, not a JSON bundle. `run()` binary entrypoint uses the fixed `d2b-provider-agent` command; v3 provider processes use normal Zone runtime Process launch. `TrustedFirstPartyInProcess` is the only placement in v2; v3 Provider resources extend to `HostLocal`/`GatewayVm` etc. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-provider-agent-001", + "kind" : "work-item", + "owner" : "ADR046-zone-control-011, ADR046-zone-control-017, ADR046-zone-control-018", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-011", + "ADR046-zone-control-017", + "ADR046-zone-control-018" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 13, + "validation" : "`provider-agent-dispatch-unsupported-service-returns-ttrpc-error`, `provider-agent-negative-timeout-rejected`, `provider-agent-session-closed-terminates-serve-loop`, `provider-agent-audit-ring-capacity-bounded` (ported from `audit_capacity_is_bounded`), `provider-registry-entry-fingerprint-generation-exact` (ported from `rejects_generation_and_exact_identity_mismatches`), `provider-registry-unknown-axis-fallback-non-exhaustive` (ported from `unknown_binding_axis_remains_rejected_on_the_wire`), `provider-registry-duplicate-ids-rejected` (ported from `rejects_duplicate_or_unbounded_mapping_ids`)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/volume_state.rs`" + ], + "detailedDesign" : "`VolumeStateSchema` struct (`schemaId`, `schemaVersion`, `schemaDigest`, `migrationPolicy`); `PersistenceClass` and `SensitivityClass` enums; `VolumeStateStatus` extension (`stateSchemaPhase`, `installedSchemaVersion`, `markerStatus`, `sealingStatus`, `quotaUsage`, `lastMigrationAt`); `StateEnvelope` (replaces v2 `StateEnvelope`); canonical JSON serde and digest helpers Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-001", + "kind" : "work-item", + "owner" : "ADR046-primitives-001; v3 contracts owner", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-primitives-001" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 9, + "validation" : "Schema golden vectors; phase/reason round-trip; StateEnvelope digest tests", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/provider.rs` (component descriptor `stateNamespaces` field)" + ], + "detailedDesign" : "Add `stateNamespaces: Vec` to the component descriptor (zero or more entries; a component declares an entry only when a payload passes the storage-need test; stateless components declare none); each entry includes `id`, `kind` (always `state`), `schemaId` (non-null), `schemaVersion`, `schemaDigest`, `persistenceClass` (must be `persistent`; `ephemeral`/`cache` rejected), `sensitivityClass`, `migrationPolicy`, `quotaBytes` (nonzero; minimum 4096), `storageNeed` (`secret` | `large-binary` | `private-unsafe-for-status` | `revision-unsuitable`), `sealingRequired`, `placementMode` (`guest-local` or `host-backed-guest` for Guest-targeted; omitted for Host-targeted), `hostCustodyPermitted` (required `true` for `host-backed-guest`; absent/false for `guest-local`), and `views`; there is no empty-payload (`schemaId: null`) namespace", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-002", + "kind" : "work-item", + "owner" : "ADR046-pstate-001; Provider contracts owner", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-pstate-001" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 10, + "validation" : "Descriptor schema golden vectors; descriptor-Volume consistency property test; stateless-component-declares-no-namespace round-trip; storage-need justification enforcement (namespace whose payload is derivable from spec/status/core ledger/external observation → `component-state-not-justified`); `kind != state` → `component-kind-invalid`; `persistenceClass: ephemeral` → `component-persistence-class-forbidden`; `quotaBytes: 0` or `1024` → `component-quota-too-small`; base `quota.maxBytes == quotaBytes` and `quota.maxInodes > 0`; Guest-targeted with `placementMode: guest-local` → source.executionRef=Guest; Guest-targeted with `host-backed-guest` + `hostCustodyPermitted: true` → source on Host, Export created; `host-backed-guest` without `hostCustodyPermitted: true` → `placement-host-custody-violation`; credential/audit schema with `host-backed-guest` → `guest-local-required`; `placementMode` change → descriptor version increment enforced", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-local/` (new crate, full scaffold required): `src/{atomic.rs, path.rs, lock.rs, marker.rs, effect_port.rs}`; `tests/volume_local.rs` (marker missing/replaced/mismatch, domain-isolation rejection, quota enforcement); `integration/volume_local.rs` (real Host filesystem provision, broker-maintained marker check, domain-isolation rejection cross-process); `README.md`" + ], + "detailedDesign" : "Anchored Volume root provision, identity marker write/check, quota soft-check on write, domain-isolation validation, fd-relative layout creation/repair/cleanup, broker-maintained marker root protocol; layout `ownerRef`/`groupRef` must reference a Nix-preprovisioned User principal or bounded system pool — Volume admission rejects runtime-created principals; `VolumeEffectPort` returns opaque IDs and named view dirfds only — no raw host path returned by any EffectPort operation; volume-local must support `source.executionRef: Guest/` for `guest-local` placement (controller running inside the Guest): when executing in a Guest domain, volume-local may not create, read, or hold dirfds/paths for Volumes sourced in another domain; `host-backed-guest` placement creates a `virtiofs.d2bus.org.Export` child per attachment entry and validates `hostCustodyPermitted: true` in the signed descriptor Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged (path.rs) / adapt (atomic.rs, lock.rs) / adapt (swtpm_dir.rs marker algorithm).", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-003", + "kind" : "work-item", + "owner" : "ADR046-pstate-001; volume-local Provider owner", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-pstate-001" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 10, + "validation" : "Marker missing/replaced/mismatch tests; domain-isolation rejection tests; quota enforcement tests; crash at every provision step", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-local/src/migration.rs`; `packages/d2b-provider-volume-local/tests/migration_unit.rs` (hermetic staging Volume and prepare/commit/rollback); `packages/d2b-provider-volume-local/integration/migration.rs` (real Host crash-injection at each migration step, N-Volume cross-component coordination); `packages/d2b-controller-toolkit/src/state_migration.rs`" + ], + "detailedDesign" : "Pre-launch migration EphemeralProcess template, staging Volume create/destroy lifecycle, prepare/commit/rollback protocol implementation, roll-forward on restart detection, migration idempotency", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-004", + "kind" : "work-item", + "owner" : "ADR046-reconcile-001, ADR046-pstate-003; volume-local Provider and controller-toolkit owners", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-pstate-003", + "ADR046-reconcile-001" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 11, + "validation" : "Migration with and without crash at each step; rollback after failed EphemeralProcess; roll-forward after interrupted commit; cross-component N-Volume coordination", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-local/src/sealing.rs`; `packages/d2b-provider-volume-local/tests/sealing_unit.rs` (hermetic seal/rotation state machine); `packages/d2b-provider-volume-local/integration/sealing.rs` (real Credential lease flow, interrupted key rotation with live Credential Provider)" + ], + "detailedDesign" : "Envelope encryption on write using Credential lease key material; key rotation EphemeralProcess re-encrypt; no raw key on disk; `sealingStatus` transitions; `rotation-failed` fail-safe", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-005", + "kind" : "work-item", + "owner" : "ADR046-pstate-003; volume-local Provider and Credential Provider owners", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-pstate-003" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 11, + "validation" : "Seal/read/rotation tests; rotation interrupted at commit; credential revoked during runtime", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-local/src/snapshot.rs`; `packages/d2b-provider-volume-local/tests/snapshot_unit.rs` (hermetic `snapshotPolicy` enforcement, retention count and TTL logic); `packages/d2b-provider-volume-local/integration/snapshot.rs` (real Host filesystem snapshot creation, retention expiry, pre-migration auto-snapshot with interrupted migration)" + ], + "detailedDesign" : "Snapshot EphemeralProcess; bounded `.snapshots/` sub-tree; `snapshotPolicy` enforcement; retention count and TTL cleanup; snapshot status tracking in Volume status", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-006", + "kind" : "work-item", + "owner" : "ADR046-pstate-003, ADR046-pstate-004; volume-local Provider and snapshot toolkit owners", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-pstate-003", + "ADR046-pstate-004" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 12, + "validation" : "Create/read/expire/retention-limit tests; snapshot before interrupted migration; snapshot list in Volume status", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-local/src/relocation.rs`; `packages/d2b-provider-volume-local/tests/relocation_unit.rs` (hermetic finalizer set/clear, commit/failure state machine); `packages/d2b-provider-volume-local/integration/relocation.rs` (real Host-to-Host anchored file copy, crash at copy midpoint, virtiofs source re-point after successful relocation)" + ], + "detailedDesign" : "Relocation EphemeralProcess; source finalizer; anchored copy; source Volume (volume-local) relocation for components with Guest attachment (the attachment Volume backed by volume-virtiofs is re-pointed to the new source after copy; see `ADR-046-primitive-resource-composition` Volume attachment spec); commit/failure handling", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-007", + "kind" : "work-item", + "owner" : "ADR046-pstate-003; volume-local Provider owner", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-pstate-003" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 11, + "validation" : "Relocation with crash at copy midpoint; failed relocation source preservation; virtiofs source relocation test", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-local/src/audit.rs`; `packages/d2b-provider-volume-local/src/otel.rs`; `packages/d2b-provider-volume-local/tests/audit_unit.rs` (hermetic audit golden records, OTEL cardinality label tests)" + ], + "detailedDesign" : "Volume-state audit event types and Zone audit emission; OTEL metric definitions with closed semantic label sets and no Zone or resource-name-derived label keys; Zone identity remains in the `d2b.zone` OTEL resource attribute", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-008", + "kind" : "work-item", + "owner" : "ADR046-pstate-001; Zone audit stream, OTEL provider owners", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-pstate-001" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 10, + "validation" : "Audit event golden records; no content/path/credential in audit payload; structural OTEL label-policy tests assert exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys and preserve `d2b.zone` as a resource attribute", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-local/tests/state.rs` (ported hermetic atomic/lock/quarantine/lease tests); `packages/d2b-provider-volume-local/tests/migration.rs` (ported migration fault-injection, cross-component N-Volume coordination); `packages/d2b-provider-volume-local/integration/provider_state.rs` (end-to-end: live daemon, real Host Volume mount, cross-process worker subview, full audit stream); `packages/d2b-provider-volume-local/integration/audit.rs` (live Zone audit stream emission and OTEL export)" + ], + "detailedDesign" : "Port all d2b-state integration tests replacing ADR 0045 contract setup with v3 Volume/StateEnvelope; add provider-state-specific migration, marker, quota, sealing, relocation, snapshot, incident-hold, and unclaimed-GC tests; include cross-component N-Volume coordination test", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-009", + "kind" : "work-item", + "owner" : "ADR046-pstate-001 through ADR046-pstate-008; integration test owner", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-pstate-001", + "ADR046-pstate-002", + "ADR046-pstate-003", + "ADR046-pstate-004", + "ADR046-pstate-005", + "ADR046-pstate-006", + "ADR046-pstate-007", + "ADR046-pstate-008" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 13, + "validation" : "All ported tests pass under v3 contracts; test coverage includes every fault-injection scenario listed in d2b-state/tests/state.rs plus new provider-state cases; stateless-component-declares-no-Volume test passes; shared-Volume attempt rejected; `guest-local` Volume creation inside Guest domain (source.executionRef=Guest, no Export created, Host volume-local holds no dirfd/path); `host-backed-guest` Volume creation (source on Host, Export created, Export reaches Ready, Guest Process mounts source Volume view); `host-backed-guest` without `hostCustodyPermitted: true` → `placement-host-custody-violation`; credential/audit schema with `host-backed-guest` → `guest-local-required`; cross-domain isolation: Guest-local volume-local does not create or observe Host-domain Volumes", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/zone-resources.nix` (per-Zone bundle emitter NixOS module); `packages/d2b-core/src/v3/zone_bundle.rs` (shared bundle DTOs: `ZoneResourceBundle`, `BundleResource`, `contentId` computation); `packages/d2b-core-controller/src/configuration.rs` (diff/apply loop, name-conflict detection, `pending-cleanup` Zone status, `maxFinalizerDurationSeconds` stall detection — NOT in `d2b-provider-volume-local`); `packages/d2b-core-controller/tests/configuration.rs` (hermetic bundle diff, absent-resource Delete dispatch, name-conflict `Degraded/name-conflict` item, integrity-failure abort); `packages/d2b-core-controller/integration/configuration.rs` (container-based generation activation with running Providers, absent-resource cleanup, rollback, finalizer-timeout stall detection)" + ], + "detailedDesign" : "Generic `d2b.zones..resources. = { type = \"…\"; spec = { …exact ResourceTypeSpec fields… }; }` attrset; `metadata.name` derived from attr key, `metadata.zone` from Zone key, `apiVersion` defaulted; `status` omitted (read-only); Nix option types for `spec.*` generated from committed `ResourceTypeSchema` for each `type`; Nix option types for `spec.config.*` generated from the Provider artifact's config schema module (resolved at eval time via `spec.artifactId` from `d2b.artifacts`); credential-ref guard (`credentialRef: true` schema fields accept only `Credential/[a-z][a-z0-9-]*`); build-phase full JSON Schema validation of rendered `spec` against Provider manifest; canonical sorted bundle emission with `contentId` (SHA-256 of sorted `resources` array); configuration service sets `metadata.managedBy: configuration` and `configurationGeneration` in the resource store when persisting activated bundle resources (not in the bundle input; user authors only `type` + `spec`); diff compares new configured set against persisted resource store entries where `managedBy: configuration` (not against the prior bundle file); name-conflict detection: `(type, name)` collision with existing `managedBy: controller` or `managedBy: api` resource → `Degraded/name-conflict` activation item, existing resource untouched, `managedBy` never seized; unchanged-spec resources receive updated `configurationGeneration` with no controller reconcile triggered; absent-resource async Delete with owner-child/finalizer ordering; `Degraded/pending-cleanup` Zone status condition; per-Zone prior bundle count retention (range 1..16, default 3; no TTL); `maxFinalizerDurationSeconds` stall detection and `Degraded/finalizer-timeout` condition (no force-clear)", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-010", + "kind" : "work-item", + "owner" : "ADR046-pstate-001; Zone resource bundle owner, NixOS module owner, `d2b-core-controller` owner (ADR-046-core-controllers)", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-pstate-001" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 10, + "validation" : "All eight removed-resource cleanup tests enumerated in [Required tests for removed-resource cleanup](#required-tests-for-removed-resource-cleanup); eval credential-ref guard (test: raw value in `credentialRef` field → NixOS eval error, no bundle emitted); Provider schema conformance golden vector (test: unknown `config` key → build fails); `contentId` determinism (test: two independent builds of identical Nix inputs produce byte-identical bundles)", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/provider_crate_policy.rs`; `tests/unit/gates/provider-crate-layout-check.sh`" + ], + "detailedDesign" : "`cargo xtask check-provider-crate-layout` command: walks every workspace member matching `d2b-provider-*`; for each, asserts presence of `src/`, `tests/`, `integration/`, and `README.md`; asserts `integration/` contains at least one `.rs` file and a `README.md`; fails closed with a typed `missing-provider-crate-path` error listing every absent path; wired into `make test-policy` via `tests/unit/gates/provider-crate-layout-check.sh`; output is machine-readable JSON (`{ \"crate\": \"…\", \"missing\": [\"integration/\"] }` per violation) Primary reuse disposition: `adapt`. Preserved source-plan detail: extend.", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-011", + "kind" : "work-item", + "owner" : "ADR046-pstate-003; workspace policy and xtask owners", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-pstate-003" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 11, + "validation" : "Policy gate detects missing `src/` → error; missing `tests/` → error; missing `integration/` → error; missing `README.md` → error; empty `integration/` (no `.rs` files) → error; all four paths present and non-empty → pass; existing non-provider `d2b-*` crates not flagged; gate is idempotent across re-runs", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/optional_state_admission.rs` (storage-need admission: reject a declared namespace whose payload is derivable from spec/status/core ledger/external observation with `component-state-not-justified`; only declared namespaces produce a Volume; stateless components produce none); `packages/d2b-core-controller/tests/optional_state_admission.rs` (hermetic: stateless component → no Volume; declared `storageNeed` variants accepted; unjustified namespace rejected; status-first restart revalidation — controller re-derives observed state from status/core ledger/external observation after restart and never treats a status field as authority); `packages/d2b-provider-volume-local/tests/status_bounds.rs` (hermetic: total canonical serialized status cap and provider-specific detail cap enforced; oversize status write → typed rejection; status carries no secret/path/argv/PID/unit/stream/ring content)" + ], + "detailedDesign" : "Optional state-Volume admission is a fixed step in Core ProviderDeployment: for each component, if it declares no `stateNamespaces` entry it gets no Volume; for each declared entry, verify the `storageNeed` justification (`secret` | `large-binary` | `private-unsafe-for-status` | `revision-unsuitable`) and reject a namespace whose payload is fully derivable from spec/status/core ledger/external observation with `component-state-not-justified`. Fixed bootstrap components (`system-core`, `system-minijail`, first `volume-local` instance) declare no state Volume and reach Ready using resource `status`, the core Operation ledger, and external observation only; there is no bootstrap-storage mechanism. Status-bound enforcement: reject a status write whose total canonical serialized size exceeds the single canonical status cap, or whose provider-specific detail exceeds the detail cap, or whose condition/count/list/map entries exceed the bounded limits, with the typed status-oversize rejection; status writes occur only on material change.", + "entryContracts" : [ + "ADR-046-provider-state" + ], + "exitGate" : "ADR046-W4 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-pstate-012", + "kind" : "work-item", + "owner" : "ADR046-pstate-001, ADR046-pstate-002; Zone runtime owner (`d2b-core-controller`), volume-local Provider owner", + "parallelGroup" : "wi:ADR-046-provider-state", + "prerequisites" : [ + "ADR-046-provider-state", + "ADR046-pstate-001", + "ADR046-pstate-002" + ], + "specId" : "ADR-046-provider-state", + "topologicalRank" : 11, + "validation" : "Stateless component → no Volume created; each `storageNeed` variant accepted with a declared Volume; unjustified namespace → `component-state-not-justified`; status-first restart: controller re-derives observed state and reverifies against external reality, never treating status as authority; oversize/over-detail/over-cardinality status write → typed rejection; status contains no secret/path/argv/PID/unit/stream/ring content", + "wave" : "W4" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/{src/lib.rs,tests/provider_layout.rs,integration/mod.rs,README.md}" + ], + "detailedDesign" : "Crate scaffold and layout gate: create the crate with the four required paths, commit a README.md stub meeting §1 requirements, and wire the workspace policy gate so the crate cannot land without `src/`, `tests/`, `integration/`, and `README.md`.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-001", + "kind" : "work-item", + "owner" : "P0; blocks all other runtime-qemu-media work items; owner: `runtime-qemu-media` Provider crate", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 11, + "validation" : "`make test-policy` (workspace policy gate)", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/types/guest.rs" + ], + "detailedDesign" : "Guest ResourceType schema and serde: define `GuestSpec`, `GuestStatus`, and `GuestProviderSpecSettings` with serde and `schemars` JSON Schema. Fields must match §4, §5, and §16 exactly. Enforce `bootMediaRef` as a `Volume/` ResourceRef, `removableVolumeRefs` max 4 entries, `providerPhase` max 64 chars with the closed value set, and no argv/path/credential bytes in any serialized type. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt selected baseline field concepts; discard raw paths, argv, and credential-carrying fields.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-002", + "kind" : "work-item", + "owner" : "P0; depends on ADR046-qemu-media-001; owner: runtime-qemu-media type/schema implementation", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-001" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 12, + "validation" : "`tests/guest_schema_roundtrip.rs`; `tests/guest_provider_settings_bounds.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/config.rs" + ], + "detailedDesign" : "Provider config schema and projection: define `ProviderConfig`, derive JSON Schema, require `controllerExecutionRef`, validate bounds, and project config only to the controller component. Worker processes receive no root config, no ResourceAPI authority, and no d2b-bus authority. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt bounded timeout/quota concepts into v3 Provider config; project only to the controller component.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-003", + "kind" : "work-item", + "owner" : "P0; depends on ADR046-qemu-media-001; owner: runtime-qemu-media Provider config/schema implementation", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-001" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 12, + "validation" : "`tests/config_schema_projection.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/{descriptor.rs,state.rs}; no Volume management code for Provider state" + ], + "detailedDesign" : "Controller status-first operational state (no state Volume): controller component descriptor declares an empty `stateNamespaces` list; ProviderDeployment creates no controller state Volume; controller writes reconcile stage, per-Guest launch/adoption observations, bounded counters, and closed-enum error detail to `status` on material change without secrets, paths, argv, PIDs, or unit names; restart re-derives observed state from the Zone resource store, core Operation ledger, and independent external observation with fresh pidfds. Worker Processes and the controller receive no state-Volume mount.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-004", + "kind" : "work-item", + "owner" : "P0; depends on ADR046-qemu-media-001 and ADR046-qemu-media-003; owner: runtime-qemu-media controller descriptor/state implementation", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-001", + "ADR046-qemu-media-003" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 13, + "validation" : "`tests/state_status_spec.rs`; `tests/state_status_restart.rs`; `tests/state_mount_exclusivity.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/controller/volume.rs" + ], + "detailedDesign" : "Runtime tmpfs Volume resource: controller creates the per-Guest runtime tmpfs Volume specified in §6.1. The emitted spec must exactly match the canonical YAML, including all layout entries, views, quota, and `cleanupPolicy: vm-stop-with-proof`. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt naming intent into controller-created Volume resources; replace raw runtime directory paths with `Volume` specs.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-005", + "kind" : "work-item", + "owner" : "P1; depends on ADR046-qemu-media-001 and ADR046-qemu-media-003; owner: runtime-qemu-media controller Volume reconciliation", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-001", + "ADR046-qemu-media-003" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 13, + "validation" : "`tests/runtime_volume_spec.rs`; `tests/volume_cleanup_policy.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/controller/media_watch.rs" + ], + "detailedDesign" : "Media Volume watch and virtio-blk attachment validation: controller watches `bootMediaRef` and `removableVolumeRefs` Volumes for `Ready` status and validates that each has a `virtio-blk` attachment for the owning Guest. It performs no path inspection. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt media kind concepts to Volume source-kind assertions and ResourceRef watches.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-006", + "kind" : "work-item", + "owner" : "P1; depends on ADR046-qemu-media-002 and ADR046-qemu-media-005; owner: runtime-qemu-media media dependency controller", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-002", + "ADR046-qemu-media-005" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 14, + "validation" : "`tests/media_volume_watch.rs`; `tests/media_attachment_validation.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/controller/device_watch.rs" + ], + "detailedDesign" : "KVM Device watch: controller watches `Device/host-kvm` from `spec.deviceAttachments` for `Ready` status and gates runner launch on it, propagating Pending/Ready/Failed transitions to Guest conditions.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-007", + "kind" : "work-item", + "owner" : "P1; depends on ADR046-qemu-media-002; owner: runtime-qemu-media Device dependency controller", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-002" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 13, + "validation" : "`tests/kvm_device_watch.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/controller/display.rs" + ], + "detailedDesign" : "WaylandSession resource management: when `spec.provider.settings.displayWindow = true`, controller creates, updates, deletes, and watches a `display-wayland.d2bus.org.WaylandSession` resource using the exact ResourceSpec from the display-wayland dossier. It reads the EndpointRef attachment from status using only display-wayland-defined field names. Primary reuse disposition: `create`. Preserved source-plan detail: net-new against the display-wayland Resource contract.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-008", + "kind" : "work-item", + "owner" : "P1; depends on ADR046-qemu-media-002 and the `display-wayland` Provider dossier; owner: runtime-qemu-media display integration", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-002" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 13, + "validation" : "`tests/wayland_session_create.rs`; `tests/wayland_session_attachment_read.rs`; `tests/wayland_session_missing_provider.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/controller/process_builder.rs" + ], + "detailedDesign" : "Process spec builder and LaunchTicket attachment resolution: build the canonical `qemu-media-runner` Process ResourceSpec from §10.1 and supply only opaque Network/Endpoint refs to Core's attachment resolver. Core, not the qemu controller, resolves authorized kvm, tap, media, and optional display attachments and seals their fd slots in the LaunchTicket. The qemu Provider/controller receives no broker operation or fd. No raw path, argv, executable path, or principal appears in any public field. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt sandbox/budget concepts to canonical Process resources and Core-sealed LaunchTickets; do not copy raw argv strings or path construction.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-009", + "kind" : "work-item", + "owner" : "P1; depends on ADR046-qemu-media-002, ADR046-qemu-media-005, ADR046-qemu-media-006, ADR046-qemu-media-007, ADR046-qemu-media-008, and ADR046-qemu-media-012; owner: runtime-qemu-media Process launch builder", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-002", + "ADR046-qemu-media-005", + "ADR046-qemu-media-006", + "ADR046-qemu-media-007", + "ADR046-qemu-media-008", + "ADR046-qemu-media-012" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 15, + "validation" : "`tests/process_spec_golden.rs`; `tests/launch_ticket_fd_slots.rs`; `tests/launch_ticket_tap_fd_lifetime.rs`; `tests/no_controller_fd_or_broker_op.rs`; `tests/no_raw_argv_in_spec.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/qmp/" + ], + "detailedDesign" : "QMP endpoint attachment handling: consume `qmp` and `serial` Endpoint connection attachments delivered by the ProviderSupervisor ComponentSession channel; implement QMP capability negotiation, command dispatch, and health check using only the delivered fd, never direct socket path access. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt QMP command payloads to internal DTOs; discard broker wire ops and all socket path/fd-open code.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-010", + "kind" : "work-item", + "owner" : "P1; depends on ADR046-qemu-media-009; owner: runtime-qemu-media QMP client implementation", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-009" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 16, + "validation" : "`tests/qmp_capability_negotiation.rs`; `tests/qmp_command_dispatch.rs`; `tests/qmp_greeting_timeout.rs`; `tests/qmp_health_check.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/controller/hotplug.rs" + ], + "detailedDesign" : "Hotplug attach/detach protocol: on `removableVolumeRefs` update, request a Volume fd from the `volume-local` ComponentSession service and issue `blockdev-add`/`device_add` QMP commands; reverse the sequence for detach; QMP failures set Degraded with `hotplug-media-failed`. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt QMP hotplug command bodies; delete broker op wiring.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-011", + "kind" : "work-item", + "owner" : "P2; depends on ADR046-qemu-media-006 and ADR046-qemu-media-010; owner: runtime-qemu-media hotplug controller", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-006", + "ADR046-qemu-media-010" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 17, + "validation" : "`tests/hotplug_attach_sequence.rs`; `tests/hotplug_detach_sequence.rs`; `tests/hotplug_qmp_failure.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/controller/network.rs" + ], + "detailedDesign" : "Network attachment routing: project each Guest network attachment as an opaque `Network/` ref and condition only. The network-local controller declares the opaque semantic effect; its Core-owned NetworkEffectPort adapter maps it to `CreatePersistentTap`, then `SetBridgePortFlags`, and transfers the already-authorized connected `OwnedFd` directly to ProviderSupervisor for the QEMU Process LaunchTicket. The adapter and supervisor keep `FD_CLOEXEC` set on parent copies; only the declared child slot is made inheritable immediately before exec. On cancellation, ticket rejection, or spawn failure, all fd copies close before generation-fenced `DeletePersistentTap`, and the opaque realization is retained until deletion confirmation. The qemu Provider/controller receives no broker operation, fd, bridge name, or interface name, and the fd is never serialized through ResourceAPI, ComponentSession, or d2b-bus. Primary reuse disposition: `create`.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-012", + "kind" : "work-item", + "owner" : "P1; depends on ADR046-qemu-media-002, ADR046-network-005, and Provider config `networkProviderRef`; owner: runtime-qemu-media network dependency integration", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-network-005", + "ADR046-qemu-media-002" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 13, + "validation" : "`tests/tap_launch_routing.rs` proves `CreatePersistentTap → SetBridgePortFlags → ProviderSupervisor LaunchTicket` ordering; `tests/tap_fd_lifetime.rs` proves CLOEXEC, single child ownership, and close-before-`DeletePersistentTap`; `tests/tap_fd_no_bus_serialization.rs` rejects fd/broker DTOs at the qemu controller boundary; `tests/tap_fd_unavailable.rs` covers authorization and resolution failure", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/controller/reconcile.rs" + ], + "detailedDesign" : "Reconcile loop and finalize: implement the full async reconcile loop from §11.3 and finalize sequence from §11.4, including dependency gating, providerPhase transitions, condition management, runner exit handling, and WaylandSession cleanup. Primary reuse disposition: `create`. Preserved source-plan detail: net-new reconcile/finalize implementation using the v3 Resource API.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-013", + "kind" : "work-item", + "owner" : "P1; depends on ADR046-qemu-media-005 through ADR046-qemu-media-012; owner: runtime-qemu-media controller", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-005", + "ADR046-qemu-media-006", + "ADR046-qemu-media-007", + "ADR046-qemu-media-008", + "ADR046-qemu-media-009", + "ADR046-qemu-media-010", + "ADR046-qemu-media-011", + "ADR046-qemu-media-012" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 18, + "validation" : "`tests/reconcile_dependency_gating.rs`; `tests/reconcile_runner_exit_handling.rs`; `tests/finalize_sequence.rs`; `tests/finalize_wayland_session_cleanup.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/controller/status.rs" + ], + "detailedDesign" : "Status, conditions, and error reporting: implement all phase transitions from §16.1, providerPhase values from §16.2, condition types from §16.3, error codes from §16.4, and bounds enforcement on `providerPhase`. Primary reuse disposition: `create`. Preserved source-plan detail: net-new status/error projection for the v3 Guest ResourceType.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-014", + "kind" : "work-item", + "owner" : "P1; depends on ADR046-qemu-media-013; owner: runtime-qemu-media status/error implementation", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-013" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 19, + "validation" : "`tests/status_phase_transitions.rs`; `tests/condition_reason_codes.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/audit.rs" + ], + "detailedDesign" : "Audit event emission: emit all audit events in §17 and verify that no sensitive fields such as paths, argv, fds, or socket paths appear in any payload. Primary reuse disposition: `create`. Preserved source-plan detail: net-new audit emission for the Provider events in §17.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-015", + "kind" : "work-item", + "owner" : "P2; depends on ADR046-qemu-media-013 and ADR046-qemu-media-014; owner: runtime-qemu-media audit integration", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-013", + "ADR046-qemu-media-014" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 20, + "validation" : "`tests/audit_event_shapes.rs`; `tests/audit_no_sensitive_fields.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/src/telemetry.rs" + ], + "detailedDesign" : "Metrics and OTEL spans: implement all metrics from §18 and OTEL trace spans with structural closed-label enforcement and no Zone/VM/resource name, user identity, path, or other sensitive value in any metric label. Span attributes use only the exact fixed semantic fields and `outcome` listed in §18; no resource name, UID, shortened UID, digest, ref, or derived identity is admitted. Retain identity only in allow-listed OTEL Resource attributes and permitted bounded audit fields. Primary reuse disposition: `create`. Preserved source-plan detail: net-new telemetry emission for the Provider metrics and spans in §18.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-016", + "kind" : "work-item", + "owner" : "P2; depends on ADR046-qemu-media-013 and ADR046-qemu-media-014; owner: runtime-qemu-media telemetry integration", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-013", + "ADR046-qemu-media-014" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 20, + "validation" : "`tests/metrics_label_cardinality.rs` asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus Guest/Zone-name canary absence; `tests/otel_span_attributes.rs` asserts the exact per-span semantic allowlist, preserves allowed OTEL Resource identity attributes, and rejects Zone/Guest/Process/Provider-resource names, refs, UIDs, shortened UIDs, digests, and identity canary values in span attributes", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "nixos-modules/options-guest-qemu-media.nix; nixos-modules/assertions.nix" + ], + "detailedDesign" : "Nix module and assertions: implement the Guest resource declaration from §19 and eval-time assertions from §19.8. Rewrite qemu-media options as v3 spec fields and reject raw path options. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt option names into v3 Guest/Provider spec fields; remove raw path options; extend existing assertion predicates.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-017", + "kind" : "work-item", + "owner" : "P1; depends on ADR046-qemu-media-002 and ADR046-qemu-media-003; owner: Nix resource compiler and runtime-qemu-media options", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-002", + "ADR046-qemu-media-003", + "ADR046-transport-unix-009" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 14, + "validation" : "`tests/unit/nix/cases/guest-qemu-media-spec.nix`; `tests/assertions-eval.sh` new assertion cases", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/tests/conformance_guest.rs" + ], + "detailedDesign" : "d2b-provider-toolkit conformance: pass the Provider conformance kit for the Guest ResourceType axis, including reconcile/finalize contract, phase machine, condition typing, audit shape, and telemetry cardinality. Primary reuse disposition: `adapt`. Preserved source-plan detail: reuse conformance harness; add runtime-qemu-media Guest ResourceType coverage.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-018", + "kind" : "work-item", + "owner" : "P2; depends on ADR046-qemu-media-013 through ADR046-qemu-media-016; owner: runtime-qemu-media conformance tests", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-013", + "ADR046-qemu-media-014", + "ADR046-qemu-media-015", + "ADR046-qemu-media-016" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 21, + "validation" : "`make test-rust` (runs conformance suite)", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-runtime-qemu-media/integration/" + ], + "detailedDesign" : "Integration tests: implement container/fake-Host scenarios for full reconcile from Created to Ready with fake dependencies, finalize sequence, hotplug attach/detach, and restart recovery. Primary reuse disposition: `create`. Preserved source-plan detail: net-new integration fixtures.", + "entryContracts" : [ + "ADR-046-provider-runtime-qemu-media" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-qemu-media-019", + "kind" : "work-item", + "owner" : "P2; depends on ADR046-qemu-media-005 through ADR046-qemu-media-018; owner: runtime-qemu-media integration fixtures", + "parallelGroup" : "wi:ADR-046-provider-runtime-qemu-media", + "prerequisites" : [ + "ADR-046-provider-runtime-qemu-media", + "ADR046-qemu-media-005", + "ADR046-qemu-media-006", + "ADR046-qemu-media-007", + "ADR046-qemu-media-008", + "ADR046-qemu-media-009", + "ADR046-qemu-media-010", + "ADR046-qemu-media-011", + "ADR046-qemu-media-012", + "ADR046-qemu-media-013", + "ADR046-qemu-media-014", + "ADR046-qemu-media-015", + "ADR046-qemu-media-016", + "ADR046-qemu-media-017", + "ADR046-qemu-media-018" + ], + "specId" : "ADR-046-provider-runtime-qemu-media", + "topologicalRank" : 22, + "validation" : "`make test-integration`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-controller-toolkit/src/lib.rs`, `runner.rs`, `queue.rs`, `context.rs`, `result.rs`" + ], + "detailedDesign" : "Async ResourceReconciler, watch receiver, coalescing, per-resource serialization, parallel tasks, retry/checkpoint/finalize; expedited priority lane and `CommittedRevisionProof`-gated effects (D090); `assess_update`/`plan_upgrade`/`execute_upgrade` methods serialized in the same single-flight (D091) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resource-reconciliation" + ], + "exitGate" : "ADR046-W1 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reconcile-001", + "kind" : "work-item", + "owner" : "W0/W1a; controller toolkit owner", + "parallelGroup" : "wi:ADR-046-resource-reconciliation", + "prerequisites" : [ + "ADR-046-resource-reconciliation" + ], + "specId" : "ADR-046-resource-reconciliation", + "topologicalRank" : 6, + "validation" : "Golden state-machine vectors, deterministic clocks, conflict/restart/queue tests; D090: commit-fails/Abort → no effect, controller finishes-before-commit gated on proof, effects-gate, status-write-delayed (`statusPersistence: pending`), normal-queued no-op/rejoin, concurrent mutation, delete event-only projection, expedited timeout committed-but-pending, restart re-entry no duplicate; D091: current/non-disruptive/each-trigger assess, UpgradeRequired-not-in-place, dependency propagation/topological drain-recycle-restart, GPU blocking, state/TPM preservation, crash/re-entry resume, single-flight reconcile-vs-upgrade serialization", + "wave" : "W1" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/hints.rs`, `dependencies.rs`, `owner_reconcile.rs`" + ], + "detailedDesign" : "Watch-plan validation, indexes, suppression, owner/dependency hints, leases, startup relist, fair admission", + "entryContracts" : [ + "ADR-046-resource-reconciliation" + ], + "exitGate" : "ADR046-W1 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reconcile-002", + "kind" : "work-item", + "owner" : "Store/API + ADR046-reconcile-001; core controller", + "parallelGroup" : "wi:ADR-046-resource-reconciliation", + "prerequisites" : [ + "ADR-046-resource-reconciliation", + "ADR046-reconcile-001" + ], + "specId" : "ADR-046-resource-reconciliation", + "topologicalRank" : 7, + "validation" : "Owner/dependency chains, suppression/no-loss, restart/relist, lease withdrawal", + "wave" : "W1" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-controller-toolkit/benches/reaction.rs`, Process Provider integration tests" + ], + "detailedDesign" : "Commit-to-handler/launch fast path, nonblocking watch, parallel ready resources", + "entryContracts" : [ + "ADR-046-resource-reconciliation" + ], + "exitGate" : "ADR046-W1 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reconcile-003", + "kind" : "work-item", + "owner" : "Process Providers + benchmark owner", + "parallelGroup" : "wi:ADR-046-resource-reconciliation", + "prerequisites" : [ + "ADR-046-resource-reconciliation" + ], + "specId" : "ADR-046-resource-reconciliation", + "topologicalRank" : 6, + "validation" : "Hard <=5 ms/<=20 ms p95 gates and 1/10/100 Process concurrency", + "wave" : "W1" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-cutover/src/{inventory,snapshot,checkpoint}.rs`" + ], + "detailedDesign" : "Implement the seven closed inventories of [Authoritative inventories](#authoritative-inventories); the `checkpoint_id` digest algorithm of [Preflight and immutable snapshot](#preflight-and-immutable-snapshot); the atomic snapshot-write sequence (temp file, fsync, rename, parent fsync, post-rename immutability) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-reset-and-cutover" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reset-001", + "kind" : "work-item", + "owner" : "W0 shared contract root; storage/broker integrator", + "parallelGroup" : "wi:ADR-046-reset-and-cutover", + "prerequisites" : [ + "ADR-046-reset-and-cutover" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 11, + "validation" : "`checkpoint_id` determinism property test; snapshot atomic-write crash-injection test; `cutover_preflight_refuses_dirty_flake_check`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-cutover/src/{bundle_validate,trust_preflight}.rs`" + ], + "detailedDesign" : "Independent legacy-flake-check gate; candidate v3 bundle schema/cross-ref/determinism validation per [Config/artifact/schema validation](#configartifactschema-validation); Provider trust preflight per `ADR-046-provider-model-and-packaging` \"Trust\"", + "entryContracts" : [ + "ADR-046-reset-and-cutover" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reset-002", + "kind" : "work-item", + "owner" : "ADR046-reset-001; `d2b-resource-store-redb` owner; `d2b-provider` catalog owner", + "parallelGroup" : "wi:ADR-046-reset-and-cutover", + "prerequisites" : [ + "ADR-046-reset-and-cutover", + "ADR046-reset-001" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 12, + "validation" : "`cutover-candidate-bundle-validation.nix`; trust-preflight rejection tests for each of digest/publisher/signature/deny/provenance/conformance failure modes", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-cutover/src/{consent,drain,disposition}.rs`" + ], + "detailedDesign" : "Exact-consent-phrase gate bound to `checkpoint_id`; Phase 3 drain algorithm (§ [Old daemon/unit/process drain](#old-daemonunitprocess-drain)); the [Disposition framework](#disposition-framework)'s Adopt/Preserve/Destroy executor, delegating every Adopt to ADR046-reset-004 Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-reset-and-cutover" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reset-003", + "kind" : "work-item", + "owner" : "ADR046-reset-001, ADR046-reset-002; Process/Guest lifecycle owner", + "parallelGroup" : "wi:ADR-046-reset-and-cutover", + "prerequisites" : [ + "ADR-046-reset-and-cutover", + "ADR046-reset-001", + "ADR046-reset-002" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 13, + "validation" : "`cutover_apply_requires_exact_consent_phrase`; `cutover_drain_refuses_on_live_process`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-cutover/src/adopt.rs`, thin wrapper invoking `ADR-046-provider-state`'s migration `EphemeralProcess` prepare/stage/commit/rollback machinery with cutover-specific source paths" + ], + "detailedDesign" : "One Adopt invocation per matrix row tagged `Adopt` in the [migration/disposition matrix](#migrationdisposition-matrix); marker re-validation before every step; new-marker-before-old-removal ordering; idempotent re-run safety per [Crash/power-loss/retry/idempotency journals](#crashpower-lossretryidempotency-journals)", + "entryContracts" : [ + "ADR-046-reset-and-cutover" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reset-004", + "kind" : "work-item", + "owner" : "ADR046-reset-003; `ADR-046-provider-state` owner; `device-tpm`/`volume-local` Provider owners", + "parallelGroup" : "wi:ADR-046-reset-and-cutover", + "prerequisites" : [ + "ADR-046-reset-and-cutover", + "ADR046-reset-003" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 14, + "validation" : "Crash-injection at every step boundary (Type 10 `cutover-crash-resume.nix`); TPM/durable-Volume Destroy-exclusion property test", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-cutover/src/{store_bootstrap,provider_sequence}.rs`" + ], + "detailedDesign" : "Phase 5 store creation per [Resource-store initialization](#resource-store-initialization); Phase 6 topological Provider install per [Provider install/topological start](#provider-installtopological-start), including the fixed staged default order and cycle-rejection check", + "entryContracts" : [ + "ADR-046-reset-and-cutover" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reset-005", + "kind" : "work-item", + "owner" : "ADR046-reset-004; `d2b-resource-store-redb` owner (`ADR046-store-003`); core-controller owner (`ADR046-core-001`)", + "parallelGroup" : "wi:ADR-046-reset-and-cutover", + "prerequisites" : [ + "ADR-046-reset-and-cutover", + "ADR046-core-001", + "ADR046-reset-004", + "ADR046-store-003" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 15, + "validation" : "Provider install topological-order determinism test; cycle-rejection test; store-identity mismatch fail-closed test", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-cutover/src/{zonelink_cutover,guest_activation}.rs`" + ], + "detailedDesign" : "Phase 7 translation from `EntrypointMode::GatewayBacked`/`HostResident` per [Zone/ZoneLink cutover](#zonezonelink-cutover): the child authors/stores one local uplink with self-matching `childZoneName`; compiler-only `parentZone` selects the allocator; the parent retains only sealed allocator/route state and owns no reciprocal row or ZoneLink handler. Phase 8 follows Network→Volume→Device→Guest ordering per [Guest/runtime/network/store view activation](#guestruntimenetworkstore-view-activation). The parent inventory never enumerates gateway-guest-internal credential/audit state", + "entryContracts" : [ + "ADR-046-reset-and-cutover" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reset-006", + "kind" : "work-item", + "owner" : "ADR046-reset-005; `ADR-046-zone-routing` owner; `ADR-046-resources-zone-control` owner", + "parallelGroup" : "wi:ADR-046-reset-and-cutover", + "prerequisites" : [ + "ADR-046-reset-and-cutover", + "ADR046-reset-005" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 16, + "validation" : "Gateway-custody-boundary test asserting the parent inventory never contains a gateway-guest-internal path; child-local ZoneLink test asserting one uplink, self-matching `childZoneName`, and child-store ownership; compiler test asserting `parentZone` selects the allocator but appears only in sealed bootstrap state; no-reciprocal-parent-row/no-parent-handler test; child-local ZoneLink `Degraded/waiting-on-remote` non-blocking test", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-cutover/src/{verify,doctor,degraded}.rs`" + ], + "detailedDesign" : "The ten `verify` checks in [Post-cutover verification](#post-cutover-verification); the `cutover-quarantined` degraded class and `doctor` reporting in [Failure/quarantine/manual recovery](#failurequarantinemanual-recovery); audit chain closure/genesis-record cross-check (check 9) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-reset-and-cutover" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reset-007", + "kind" : "work-item", + "owner" : "ADR046-reset-006; telemetry-audit-and-support owner (`d2b-audit`)", + "parallelGroup" : "wi:ADR-046-reset-and-cutover", + "prerequisites" : [ + "ADR-046-reset-and-cutover", + "ADR046-reset-006" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 17, + "validation" : "Injected-digest-mismatch test for TPM/durable-Volume verify checks; audit-genesis-cross-check test; `cutover-full-rehearsal.nix`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-cutover/src/finalize.rs`" + ], + "detailedDesign" : "Per-candidate independent gate evaluation exactly as tabled in [Old artifact/unit/schema removal gates](#old-artifactunitschema-removal-gates); separate consent phrase from `apply`; never partial-destroys a candidate", + "entryContracts" : [ + "ADR-046-reset-and-cutover" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reset-008", + "kind" : "work-item", + "owner" : "ADR046-reset-007; owner of each retiring artifact's ADR 0046 successor work item", + "parallelGroup" : "wi:ADR-046-reset-and-cutover", + "prerequisites" : [ + "ADR-046-reset-and-cutover", + "ADR046-reset-007" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 18, + "validation" : "`policy_no_destroy_without_gate`; `policy_legacy_cli_verbs_absent_after_gate`; `tpm-adopt-retirement.nix`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-cutover/src/{journal,rollback,hold}.rs`" + ], + "detailedDesign" : "Append-only journal per [Crash/power-loss/retry/idempotency journals](#crashpower-lossretryidempotency-journals); [Rollback boundary](#rollback-boundary) enforcement (`cutover-rollback-window-closed` past phase 4); cutover-wide incident hold per [Incident hold (cutover-wide)](#incident-hold-cutover-wide)", + "entryContracts" : [ + "ADR-046-reset-and-cutover" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reset-009", + "kind" : "work-item", + "owner" : "ADR046-reset-003 through ADR046-reset-006; `ADR-046-provider-state` incident-hold owner", + "parallelGroup" : "wi:ADR-046-reset-and-cutover", + "prerequisites" : [ + "ADR-046-reset-and-cutover", + "ADR046-reset-003", + "ADR046-reset-004", + "ADR046-reset-005", + "ADR046-reset-006" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 17, + "validation" : "`cutover_rollback_window_closes_after_phase_5`; incident-hold-blocks-destructive-step test", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-cutover/src/reset_scope.rs`; `d2b host reset` CLI command" + ], + "detailedDesign" : "The three reset scopes and their comparison table in [Full Zone reset vs Provider reset vs Guest reset](#full-zone-reset-vs-provider-reset-vs-guest-reset); durable-Volume preserve-by-default with explicit `--destroy-durable-volumes`/`--destroy-volumes` opt-in; OS-level authentication for the zone scope only", + "entryContracts" : [ + "ADR-046-reset-and-cutover" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reset-010", + "kind" : "work-item", + "owner" : "ADR046-reset-005 through ADR046-reset-007; `ADR-046-resources-zone-control` owner (§2.6/§9.4)", + "parallelGroup" : "wi:ADR-046-reset-and-cutover", + "prerequisites" : [ + "ADR-046-reset-and-cutover", + "ADR046-reset-005", + "ADR046-reset-006", + "ADR046-reset-007" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 18, + "validation" : "`host_reset_scope_isolation`; `zone-provider-guest-reset-isolation.nix`; durable-Volume-preserved-by-default property test for both Provider and Guest scopes", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`tests/integration/live/cutover-real-host.sh`, `tests/integration/live/cutover-real-host-cloud-guest.sh`, `tests/host-integration/hardware/cutover-real-tpm.sh`, `tests/host-integration/hardware/cutover-real-usbip-security-key.sh`" + ], + "detailedDesign" : "Manual, `D2B_LIVE=1`/hardware-gated validation scripts described in [Tests](#tests) Type 11/12 rows; never run in CI; require operator sign-off and an independent out-of-band backup before execution", + "entryContracts" : [ + "ADR-046-reset-and-cutover" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reset-011", + "kind" : "work-item", + "owner" : "ADR046-reset-001 through ADR046-reset-010, fully landed", + "parallelGroup" : "wi:ADR-046-reset-and-cutover", + "prerequisites" : [ + "ADR-046-reset-and-cutover", + "ADR046-reset-001", + "ADR046-reset-002", + "ADR046-reset-003", + "ADR046-reset-004", + "ADR046-reset-005", + "ADR046-reset-006", + "ADR046-reset-007", + "ADR046-reset-008", + "ADR046-reset-009", + "ADR046-reset-010" + ], + "specId" : "ADR-046-reset-and-cutover", + "topologicalRank" : 19, + "validation" : "Manual pass/fail sign-off recorded per the project's existing live-host/hardware validation conventions", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-session/` copied verbatim; v3 contract extension renames endpoint purpose values while preserving backward wire compatibility during transition." + ], + "detailedDesign" : "Noise handshake, record protection, scheduler, named streams, cancellation, attachments, deadlines, metrics injection, ttrpc serving, and generation-discovery behavior from the selected source. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy/adopt the core ComponentSession runtime; adapt EndpointPurpose/ServicePackage/RealmSessionAuthority names listed in Excluded ADR45 assumptions and inject a `d2b-telemetry` MetricsSink..", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reuse-001", + "kind" : "work-item", + "owner" : "ADR046-telem-001 (d2b-telemetry must exist first for MetricsSink injection); session owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-telem-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "Adopt `tests/component_session.rs` and `tests/noise_vectors.rs` unchanged; extend with v3 `EndpointPurpose` enum gate test; add `d2b-contract-tests/tests/component_session_v2_vectors.rs` (existing at `a1cc0b2d`) as-is.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-session-unix/` copied verbatim." + ], + "detailedDesign" : "SO_PASSCRED verification, pidfd identity verification, multi-scope credit reservation, emergency headroom, seqpacket atomic CLOEXEC transfer, stream reassembly, vsock transport, and descriptor policy enforcement. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy verbatim; retain `host-socket`/`native-vsock` feature flags; no ADR45 topology adaptation required..", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reuse-002", + "kind" : "work-item", + "owner" : "ADR046-reuse-001; Unix transport owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-reuse-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 12, + "validation" : "Adopt all `unix_session.rs` tests unchanged.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-client/` copied; DaemonClient/GuestClient adapted to v3 service packages; `MetadataInput::with_trace` drives `TraceContext` propagation." + ], + "detailedDesign" : "MetadataInput trace/correlation/idempotency, retry policy, cancellation token, generic client over resolver/connector/clock, typed service proxies, stream terminal, and host-socket verifier. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy client layer; adapt service package/method sets, local-root Zone identity, and TraceContext propagation for v3..", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reuse-003", + "kind" : "work-item", + "owner" : "ADR046-reuse-001; client owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-reuse-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 12, + "validation" : "Adopt typed-route, proxy-reuse, and cancel tests unchanged. Add v3 service-package name gate test.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider/` and `packages/d2b-provider-toolkit/` copied with v3 session admission and bus routing adaptations." + ], + "detailedDesign" : "ProviderRegistry factory registration, bounded admission, RPC proxy fail-closed behavior, generated provider service admission/dispatch, redaction wrappers, and conformance gates. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy provider runtime/toolkit; adapt SessionIdentity Zone name and route through `d2b-bus` instead of daemon embedding..", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reuse-004", + "kind" : "work-item", + "owner" : "ADR046-reuse-001 + ADR046-reuse-003; Provider owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-reuse-001", + "ADR046-reuse-003" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 13, + "validation" : "Adopt all `conformance.rs` and `runtime.rs` tests unchanged. Add v3 `SessionIdentity` zone-name gate. Add conformance-failure → `d2b_provider_reconcile_total` metric integration test.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-observability-otel/src/agent.rs` adapted; ComponentSessionDriver mock becomes shared Provider session fixture." + ], + "detailedDesign" : "Session-bound ProviderAgentProcess construction, bounded audit snapshot, closed-set ProviderAgentError paths, and full ComponentSessionDriver mock for hermetic Provider tests. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt provider-agent session wrapper and audit ring; replace gateway-runtime audit envelope with v3 `d2b-audit` records..", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reuse-005", + "kind" : "work-item", + "owner" : "ADR046-reuse-004 + ADR046-audit-003; Provider agent / observability-otel owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-audit-003", + "ADR046-reuse-004" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 14, + "validation" : "Adopt `provider_agent_v2.rs` mock harness unchanged as shared v3 Provider session fixture. Add v3 audit-bridge test: provider-agent session → `SessionConnect{transport_class=\"zone_link\"}` record emitted.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/routing.rs` adapted from `service_v2.rs`; RemoteNodeErrorKind stable code pattern becomes v3 BusErrorKind." + ], + "detailedDesign" : "Realm/Zone service ttrpc handler table, session authority constructors, host/gateway credential custody split, stable low-cardinality error codes, and binding/shortcut bounds. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt realm service routing to Zone service routing; preserve credential-custody, bounded routing, and stable error-code behavior..", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reuse-006", + "kind" : "work-item", + "owner" : "ADR046-reuse-001 + ADR046-bus-001; bus routing owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-bus-001", + "ADR046-reuse-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 12, + "validation" : "Adopt `authority_keeps_remote_credentials_in_gateway_guests` test renamed to `authority_keeps_remote_credentials_in_zone_link_sessions`; adapt `RealmId` → Zone name; adopt `authenticated_bootstrap_enrollment_route_and_shortcut_lifecycle` renamed with zone terminology.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/service_router.rs` and `packages/d2b-core-controller/src/provider_effects.rs`." + ], + "detailedDesign" : "Closed-set service.package route gates, provider lifecycle effect-adapter composition, closed-set error mapping, fixed local policy invariants, and bootstrap/enrolled credential binding shape. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt route-gate and ProviderLifecycleDispatch patterns; exclude daemon-embedded effect composition and route through v3 bus/provider processes..", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reuse-007", + "kind" : "work-item", + "owner" : "ADR046-reuse-004 + ADR046-reuse-006; core-controller routing owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-reuse-004", + "ADR046-reuse-006" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 14, + "validation" : "Port `local_daemon_policy_is_fixed_and_has_no_negotiation_or_fd_surface` invariant to v3 bus local policy test; port `every_generated_daemon_method_has_one_typed_adapter` to v3 bus method adapter completeness test.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/component_session_v2_vectors.rs` and `tests/noise_vectors.rs` copied verbatim." + ], + "detailedDesign" : "Pinned Noise KAT vectors, transcript/PSK mutation rejection, typed bootstrap admission fixture, and public-key corruption rejection remain the session wire security ground truth. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy the vector and conformance tests verbatim; add v3 constant gate for ComponentSession major/minor version..", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reuse-008", + "kind" : "work-item", + "owner" : "ADR046-reuse-001; contract-tests owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-reuse-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 12, + "validation" : "These tests are self-validating. Add one gate: assert `COMPONENT_SESSION_MAJOR = 2` and `COMPONENT_SESSION_MINOR = 0` constants are unchanged in v3 contract.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-telemetry/src/session_metrics_sink.rs`." + ], + "detailedDesign" : "Map every MetricEvent variant to the `d2b_session_*` inventory; convert MetricLabels closed sets to allowed v3 label strings; keep NoopMetrics for hermetic tests. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt MetricsSink bridge to v3 OTEL instruments and rename ADR45 endpoint-purpose label values to v3 closed-set labels..", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-reuse-009", + "kind" : "work-item", + "owner" : "ADR046-reuse-001 + ADR046-telem-001 + ADR046-telem-003; telemetry/session owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-reuse-001", + "ADR046-telem-001", + "ADR046-telem-003" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 12, + "validation" : "New test `packages/d2b-telemetry/tests/session_sink_bridge.rs`: drive `MetricEvent` variants through the sink; assert OTEL counter/gauge values; assert `MetricLabels` closed-set values map only to allowed label strings (no `DaemonLocal` string in v3 metric output).", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/zone_routing.rs`" + ], + "detailedDesign" : "Rename RealmPath → ZonePath, RealmId → ZoneLabelId, RouteId → ZoneRouteId, ControllerGenerationId → ZoneLinkControllerGeneration; preserve all bounds/validation/serde; add ZoneLink-specific advertisement envelope fields (v3 schema version and private allocated-capability field); preserve `RouteFailClosedReason` + add `zone-link-disconnected`, `hop-limit-exceeded`, `relay-denied`, `attachment-not-permitted-over-zone-link`; freeze v3 protobuf numbers separately from v2 Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-001", + "kind" : "work-item", + "owner" : "W0/W1a; zone routing contract owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 7, + "validation" : "Golden advertisement/path/failure vectors shared by Rust/Nix; property tests for NCA/loop/allocated-capability narrowing; replay-window tests; hop-count tests", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-zone-routing/src/engine.rs`" + ], + "detailedDesign" : "Adapt RouteTreeEngine to ZoneRouteEngine using ZonePath/ZoneRouteId/ZoneLinkControllerGeneration from ADR046-routing-001; preserve all NCA/loop/multi-parent/capability/replay/capacity logic; add `decide_route` enforcement of the fixed protocol hop budget; add hop-counter decrement in relay path; expose `ZoneRoutePath` in v3 types Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-002", + "kind" : "work-item", + "owner" : "ADR046-routing-001; zone route engine owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-001" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 8, + "validation" : "Copy exact `route_engine.rs` test suite adapted to ZonePath; add relay/hop-count/RBAC-narrowing/shortcut integration tests", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-zone-routing/src/resolver.rs` (ZoneEntrypointResolver)" + ], + "detailedDesign" : "Consume the canonical ZoneLink spec/status/intent types owned by ADR046-zone-control-002; ZoneLinkNamespaceAllocation is issued by the exact parent allocator selected in sealed Nix-compiled `parentZone` topology; ZoneEntrypointResolver uses longest-suffix match over ZonePath (adapted from `RealmEntrypointTable::resolve`) and is driven only by sealed `{ childZone, parentZone }` rows plus authenticated admitted route projections; no reciprocal parent-store resource or parent ZoneLink handler; fail closed on unknown topology, absent/stale projection, or unauthenticated route Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-003", + "kind" : "work-item", + "owner" : "ADR046-routing-001, ADR046-routing-002; Zone route resolver owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-001", + "ADR046-routing-002" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 9, + "validation" : "Longest-suffix match vectors over sealed topology; child-local ZoneLink spec validation; resolver rejects unknown/stale/withdrawn/unauthenticated route projections; parent-store fixture contains no ZoneLink row or handler", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/zone_links.rs`" + ], + "detailedDesign" : "Child-local ZoneLink handler in core-controller: consumes the exact six-field ZoneLink schema from ADR046-zone-control-002 and manages local ResourceSpec→allocator-bound session→advertisement lifecycle; session state machine (Pending/Established/Disconnected/Reconnecting/Revoked); Provider-internal reconnect backoff bounded by `spec.limits`; advertisement issuance/renewal/withdrawal using enrolled KK ComponentSession; child-store route cursor and bounded outbound intent queue; private allocator capability-scope changes; D088 `status.resource` writer; aggregate metrics use only closed semantic phase/reason/outcome labels and never `link_name_hash` or another ZoneLink/Zone/resource identity label; Nix-compiled `parentZone` selects the parent allocator, which alone owns privileged listeners, placement, and route namespace and creates no reciprocal resource. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-004", + "kind" : "work-item", + "owner" : "ADR046-routing-003; core-controller ZoneLink handler owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-003" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 10, + "validation" : "Session lifecycle tests; reconnect/disabled/revocation/allocator-policy-change; intent queue drain; cursor resync; advertisement renewal timing; fake-child tests; structural metric descriptor test asserts `vm`, `zone`, `zone_id`, `zone_uid`, and `link_name_hash` are absent and a ZoneLink-name canary never enters label values", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/zone_route.rs` (cross-Zone bus routing), `packages/d2b-bus/src/relay.rs` (per-hop relay handler)" + ], + "detailedDesign" : "Cross-Zone routing path in d2b-bus: ZoneEntrypointResolver consumes sealed topology plus authenticated route projections → ZoneRouteEngine::decide_route → admitted ComponentSession established by each next-hop child's local ZoneLink; hop-counter decrement and enforcement; independent target-verb plus canonical ZoneLink-scoped `relay` checks at each intermediate hop; idempotency key namespace (full 6-tuple) in ZoneLinkIdempotencyKey; pinned reverse path tracking; cancellation forwarding; watch cursor forwarding and revision-expired handling; no-FD/credential structural rejection at serialization boundary. No parent route step performs Resource API Get/List/Watch on ZoneLink Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-005", + "kind" : "work-item", + "owner" : "ADR046-routing-002, ADR046-routing-007 (from ComponentSession spec); d2b-bus owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-002", + "ADR046-routing-007" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 9, + "validation" : "End-to-end K0→K1→K2 resource call; relay-missing, target-verb-missing, wildcard/self-asserted relay, hop-limit, and FD-rejection tests; prove relay alone grants no CRUD/local lifecycle; idempotency namespace collision tests; cancellation delivery tests; watch resync tests", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-zone-routing/tests/route_engine_vectors.rs`, `packages/d2b-zone-routing/benches/route_decision.rs`" + ], + "detailedDesign" : "Copy exact advertisement/NCA/loop/capability/replay test vectors adapted to ZonePath; add K0/K1/K2 topology scenarios; add hop-count boundary tests; benchmark: p95 route decision for 1/10/100 active Zone tree entries <= 1 ms", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-006", + "kind" : "work-item", + "owner" : "ADR046-routing-002; benchmark owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-002" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 9, + "validation" : "All v3 baseline route_engine test cases must pass; p95 benchmark gate", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/session/`" + ], + "detailedDesign" : "Copy `d2b-session` crate wholesale into `d2b-bus/src/session/`; adapt `EndpointPurpose`/`EndpointRole`/`ServicePackage` closed-enum tags for v3 purposes; strip `GUEST_SESSION_CREDENTIAL_*` types; strip `serve_ttrpc_services` fixed-endpoint binding (replaced by allocator-issued FD bootstrap); adapt `SessionEngine` as ZoneLink session drive loop; keep all Noise profiles (Nn/Kk/IKpsk2), generation discovery, record/fragment/keepalive/credit/cancellation/attachment logic verbatim Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-007", + "kind" : "work-item", + "owner" : "W0; d2b-bus owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 7, + "validation" : "Port all `component_session.rs` tests; port `noise_vectors.rs`; add ZoneLink-specific KK enrollment test; add ZoneLink reconnect/revocation integration test", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/transport/unix.rs`, `packages/d2b-bus/src/transport/credit.rs`" + ], + "detailedDesign" : "Copy `UnixSeqpacketTransport`/`UnixStreamTransport`/credit modules verbatim; adapt `PeerIdentityPolicy` for v3 Zone principal model; adapt `InheritedSocketTransport` to receive allocator-issued FD directly (not SD_LISTEN_FDS); vsock paths adapted as transport-Provider-specific implementations (not hardcoded); `PidfdIdentityPolicy` adapted for v3 Process resource pidfd model Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-008", + "kind" : "work-item", + "owner" : "ADR046-routing-007; transport-provider owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-007" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 8, + "validation" : "Port all `unix_session.rs` tests; add allocator-issued FD handoff test; add inherited-socket no-SD-listen test", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/zone_session.rs`" + ], + "detailedDesign" : "Copy all protocol constants verbatim; copy `BoundedVec`, `BinaryError`, `ContractError`; copy `ComponentSessionPreface`, `LimitProfile`, `AttachmentDescriptor`/`AttachmentKind`/`AttachmentCreditClass`/`ChannelId`/`RecordHeader`/`FragmentHeader`/`SessionErrorCode`/`CloseReason`/`Remediation` verbatim; extend `ServicePackage` closed-enum with `ZoneV3`/`ResourceV3`/`ZoneLinkV3` variants at new tag values; extend `EndpointRole` with `ZoneController`/`ZoneRelay`/`ZoneBootstrap` variants; extend `EndpointPurpose`/`PurposeClass` with v3 Zone purposes; strip `GUEST_SESSION_CREDENTIAL_*` constants and types; re-freeze protobuf field numbers for v3 services independently from v2 assignments Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-009", + "kind" : "work-item", + "owner" : "ADR046-routing-007; contracts owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-007" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 8, + "validation" : "Updated `negotiate_offer`/`validate_exact` round-trip tests for v3 purposes; canonical encoding stability test; closed-enum exhaustiveness tests", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-client/`" + ], + "detailedDesign" : "Copy `Client` generic structure; rename `ServiceOwner::Realm(RealmId)` → `ServiceOwner::Zone(ZonePath)`, `ServiceOwner::Workload{realm,workload}` → `ServiceOwner::Guest{zone,guest}`, `ServiceOwner::LocalRoot` → `ServiceOwner::ZoneLocal`; rename `TargetInput` variants to match; add `TargetInput::ZoneService(ZonePath, ZoneServiceKind)` for cross-Zone service targeting; replace `ServiceKind` (25 ADR45 variants) with v3 service inventory (`Resource`, `Zone`, `ZoneLink`, `Provider`, plus retained guest/daemon variants); adapt `RouteTable` to route by `ZonePath`; replace `HostSocketConnector` uid-based trust with allocator-issued FD + KK static key pinning; keep `SessionFailure`/retry/cancellation/`MetadataInput`/`RetryPolicy` logic verbatim Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-010", + "kind" : "work-item", + "owner" : "ADR046-routing-007, ADR046-routing-009; resource-client owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-007", + "ADR046-routing-009" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 9, + "validation" : "Port `client.rs` tests; add ZonePath routing test; add cross-Zone K0→K1 end-to-end test; add retry/cancellation forwarding test", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/options-zones.nix` (new structural base), `nixos-modules/generated/resource-types.nix` (generated registry), `nixos-modules/generated/options-zones-.nix` (generated per ResourceType by `xtask gen-zone-nix-options`), `nixos-modules/assertions.nix` (new Zone assertions)" + ], + "detailedDesign" : "Declare compiler-only scalar `d2b.zones..parentZone` plus structural `d2b.zones..resources. = { type = ...; spec = {}; }` as specified in the \"Option schema\" section above. `parentZone` has no default, is required for every non-root Zone, forbidden on `local-root`, resolves to one declared Zone, and never enters a ResourceSpec. Wire `options-zones.nix` and all `generated/options-zones-*.nix` files into `nixos-modules/default.nix`. Add a new `xtask gen-zone-nix-options` command that reads `docs/reference/schemas/v3/.schema.json` for each ResourceType, derives `generated/resource-types.nix`, and emits a generated submodule overlaying typed spec options (types, bounds, enum constraints, defaults, docs) onto `d2b.zones..resources..spec`. The generated registry's standard subset must equal the canonical 19-type registry from `ADR-046-resource-object-model` exactly; installed signed Provider schemas may append qualified types. These generated modules are committed and kept in sync by `xtask gen-zone-nix-options && git diff --exit-code` wired into `make test-drift`. The ZoneLink module is generated from the exact six-field schema owned by ADR046-zone-control-002 and rejects any seventh field. Because the generated options carry field-level type constraints, field-level eval errors (wrong enum, out-of-range int, malformed ref) are caught without explicit assertions. Explicit assertions in `nixos-modules/assertions.nix` cover cross-resource invariants only: zone/resource key name regex, reserved names, `parentZone` required/forbidden/existence/self/cycle/16-name-depth constraints, child-local `childZoneName == zone`, at most one uplink resource per non-root Zone and none in local root, ref resolution, count limits, and transportSettings secret-key exclusion (listed in the eval-time assertions table). Conflicting parent scalar definitions fail through standard Nix module merging. Each new assertion must have a matching case in `tests/unit/nix/cases/zone-assertions.nix` (nix-unit auto-discovered). `d2b.realms` option namespace is NOT removed in this work item. Primary reuse disposition: `adapt`. Preserved source-plan detail: new module following same pattern.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-011", + "kind" : "work-item", + "owner" : "ADR046-routing-001; Nix module owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-001" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 8, + "validation" : "`nix-unit: zone-name-regex`, all five `nix-unit: zone-parent-*` vectors, `nix-unit: zone-link-credential-ref`, `nix-unit: zone-link-child-name`, `nix-unit: zone-link-one-uplink`, `nix-unit: zone-link-closed-spec`, `nix-unit: zone-link-limits`, `nix-unit: transport-settings-secret-key`; add `drift: standard-resource-type-registry` asserting the generated standard subset is exactly all 19 canonical types with no omission/addition/duplicate/reordering, plus `drift: zone-nix-options` (`xtask gen-zone-nix-options && git diff --exit-code`); run `make nix-unit-pin` after adding eval cases", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/zone-resources-json.nix` (new), private local-root allocator bootstrap compiler/sealer input (not a ResourceSpec or public bundle), `nixos-modules/bundle-artifacts.nix` (new row for per-Zone `resource-bundle.json`), `packages/xtask/src/main.rs` (`gen-zone-schemas` subcommand emitting `docs/reference/schemas/v3/.schema.json` for Zone and ZoneLink; `gen-zone-nix-options` subcommand emitting `nixos-modules/generated/options-zones-.nix`)" + ], + "detailedDesign" : "`zone-resources-json.nix` iterates `d2b.zones..resources.*` to produce the canonical sorted resource list: for each entry, render `{ apiVersion, type, metadata: { name, zone, ownerRef: , labels: , annotations: }, spec: }`. Separately canonicalize sorted `{ childZone, parentZone }` rows from the compiler-only topology and seal them into the private allocator bootstrap input; `parentZone` never enters a resource bundle or `Zone.spec`, and a topology digest change releases/reallocates affected edges independently of resource `generationId`. Per-Zone generation is strict: local root's generated bundle contains no ZoneLink; a non-root Zone's enabled uplink and referenced transport Provider appear together only in that child's bundle; no emitter copies either resource into the selected parent's bundle. The bundle JSON omits `managedBy` and `configurationGeneration`; the configuration service/core sets those fields when activating the validated bundle. Sort all resources by `(type, zone, name)`. Compute `generationId` as SHA-256 (lower hex) of the UTF-8 bytes of the sorted `resources` array JSON. Compute `integrity` as SHA-256 (base64url, no padding) of the full bundle JSON with integrity field zeroed. Install at `/etc/d2b/zones//resource-bundle.json` root:d2bd 0640. Canonical form: all object keys sorted lexicographically; order-significant arrays preserved; schema-declared set-like arrays sorted lexicographically; all optional fields emitted with defaults; no field renaming or restructuring. Build-time validation runs in a Nix derivation: (1) validate the complete parent map (non-root required, local-root forbidden, declared target, one scalar parent, not self, acyclic, max 16 names); (2) validate each resource against the committed JSON Schema, including the exact six-field ZoneLink schema from ADR046-zone-control-002; (3) validate `transportSettings` for each child-local ZoneLink against its same-Zone Provider's `transportSettingsSchema` — `transportProviderRef` is always explicit, never inferred or defaulted; (4) resolve every same-Zone `transportCredentials` ref; (5) verify `childZoneName == metadata.zone`, at most one uplink resource per non-root Zone, and no local-root uplink; (6) check for duplicate `(type, zone, name)` tuples. Private route capability policy is sealed in allocator bootstrap state and is not a ZoneLink ResourceSpec field. Providers MUST commit their `transportSettingsSchema` before any ZoneLink can reference them. Drift gates: `xtask gen-zone-schemas && git diff --exit-code` and `xtask gen-zone-nix-options && git diff --exit-code` both wired into `make test-drift`. Add `checks.${system}.zone-schema-drift` to `flake.nix`. Primary reuse disposition: `adapt`. Preserved source-plan detail: extend and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-012", + "kind" : "work-item", + "owner" : "ADR046-routing-011, ADR046-routing-001; bundle emitter owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-001", + "ADR046-routing-011" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 9, + "validation" : "`drift: zone-resource-schema`, `drift: zone-nix-options`, `build: zone-bundle-deterministic`, `build: parent-topology-sealed`, `build: child-local-zonelink-bundle` (K0 has no ZoneLink; K1 contains its self-matching ZoneLink and same-Zone transport Provider; neither is copied to K0), `build: zone-link-exact-six-fields`, `build: transport-settings-unknown-field`, `build: transport-credential-ref`, `build: missing-transport-provider`; run `make flake-matrix-pin` after adding flake checks", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/configuration.rs` (defined by ADR-046-core-controllers); shared bundle DTOs may live in `packages/d2b-core/`" + ], + "detailedDesign" : "Implement the configuration ownership and cleanup contract from the \"Configuration ownership and cleanup contract\" section. `configuration.rs` owns: (1) reading and integrity-verifying `/etc/d2b/zones//resource-bundle.json` on startup and SIGHUP; (2) diffing against active generation by `generationId` (no-op if unchanged); (3) queuing Create/UpdateSpec/Delete intents — core sets `configurationGeneration` and `managedBy` when applying Create/UpdateSpec; Delete targets only resources where BOTH `managedBy` equals the configuration service's value AND `configurationGeneration` matches the prior bundle — resources with `managedBy=controller` or `managedBy=api` are never seized; (4) setting `deletionRequestedAt` on pending-delete resources immediately and adding a Pending condition; (5) writing the prior bundle into the capped ring at `/var/lib/d2b/zones//configuration/prior/.json` (default retentionCount=3, range 1..16, no TTL; prune oldest when count would exceed limit); (6) enforcing boundary invariants (no diff-delete for absent `configurationGeneration`, `managedBy` collision guard, live controller-child teardown guard); (7) driving finalizer drain + controller-child cascade before completing a Delete; (8) on successful deletion: one store transaction writes the `Deleted` revision/change event and removes the resource row and all index entries; the authoritative audit record (`zone-resource-cleanup`) is appended from the committed revision with dedup/exactly-once recovery and is NOT part of the store transaction; (9) tracking `deletionRequestedAt`/`cleanupConfigGeneration`/`cleanupError`/`cleanupAttempt` per resource; (10) on rollback: clearing `deletionRequestedAt` and Pending condition for revived resources; (11) never pruning a prior bundle while a Delete intent from its `configurationGeneration` is in flight. OFD lock on the bundle file prevents concurrent activation races. Generation state persisted atomically at `/var/lib/d2b/zones//configuration/generation.json` (root:d2bd 0640). The `spec` object comparison for UpdateSpec detection uses the canonical JSON form so two identical specs always compare equal regardless of Nix rendering order. Resource phase transitions: Pending while Create/UpdateSpec in-flight; Degraded while cleanup pending; Ready when clean; Failed on permanent error. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-013", + "kind" : "work-item", + "owner" : "ADR046-routing-012, ADR046-routing-003; `d2b-core-controller` owner (ADR-046-core-controllers)", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-003", + "ADR046-routing-012" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 10, + "validation" : "`host-integration: cleanup-removed-zonelink`, `host-integration: rollback-restores-zonelink`, `host-integration: dynamic-child-not-deleted`, `host-integration: zonelink-no-reciprocal-row`; unit tests: deterministic generationId, no-op on same generationId, cross-ownership invariant enforcement, prior-bundle write/prune cycle, UpdateSpec canonical comparison, store-transaction-then-audit-append ordering, exactly-once audit dedup", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider/src/` (adapted in place)" + ], + "detailedDesign" : "Retain `ProviderRegistry`/`ProviderRegistryBuilder`/`ProviderRegistryManager` lifecycle verbatim; adapt `SessionIdentity` to carry `ZonePath` instead of `RealmId`; adapt `AdmissionOptions::peer_role` to v3 Zone principal + RBAC binding; adapt `ProviderDescriptor` schema version to v3; `RegistryLimits` unchanged; `RpcProviderProxy` field adaptations to match v3 session identity; `ProviderInstance` variants retain all 11 types Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-014", + "kind" : "work-item", + "owner" : "ADR046-routing-007; Provider resource owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-007" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 8, + "validation" : "Port inline registry lifecycle/drain/shutdown tests; add v3 ZonePath routing test; prove provider admission cannot self-assert relay and each forward requires relay plus the target verb", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-toolkit/src/` (adapted in place)" + ], + "detailedDesign" : "Retain `GeneratedProviderServiceServer` ttrpc dispatch verbatim; adapt `ProviderAgentProcess::from_registry` to receive ComponentSession FD from Zone allocator bootstrap instead of SD_LISTEN_FDS; adapt audit event types for v3 Zone principal; `ProviderAgentAdapter` (client-side proxy) adapted for v3 ZoneLink session; conformance kit extended for v3 Provider resource conformance checks; redaction helpers unchanged Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-015", + "kind" : "work-item", + "owner" : "ADR046-routing-014; Provider agent process owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-014" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 9, + "validation" : "Port `audit_capacity_is_closed_and_bounded`; add v3 bootstrap-via-allocator test; add conformance test for new Provider ResourceType schema", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-zone-routing/src/service.rs`" + ], + "detailedDesign" : "Rename `RealmServiceServer` → `ZoneServiceServer`; service wire name `d2b.realm.v2.RealmService` → `d2b.zone.v3.ZoneService`; rename methods (bootstrap→zone-bootstrap, enroll→zone-enroll, resolve_route→resolve-zone-route, authorize_shortcut→authorize-zone-shortcut, revoke_shortcut→revoke-zone-shortcut, report_shortcut_close→report-zone-shortcut-close, inspect→zone-inspect) and add list/watch topology-projection methods. The read-only projection starts from the sealed sorted `{ childZone, parentZone }` compiler input and joins only authenticated, admitted `ZoneRouteEngine` route/projection status. It exposes no ZoneLink resource name, UID, spec, status, Provider ref, fingerprint, transport setting, or handle. Replace `RealmSessionAuthority` with Zone principal + RBAC binding; replace `BootstrapBinding` with allocator-issued PSK binding associated with the child's local ZoneLink; replace `EnrollmentBinding` with the corresponding KK enrollment record; add independent target-verb plus canonical `relay` RBAC checks per forwarding hop; adapt shortcut model to ZonePath addressing; `RealmServiceLimits` defaults preserved; `MAX_DISPATCH_IN_FLIGHT=64`, `SHUTDOWN_TIMEOUT=5s` preserved; `CredentialCustody::GatewayGuest` excluded (all ZoneLink sessions are direct KK) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-zone-routing" + ], + "exitGate" : "ADR046-W2 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-routing-016", + "kind" : "work-item", + "owner" : "ADR046-routing-007, ADR046-routing-002, ADR046-routing-004, ADR046-routing-012; Zone service owner", + "parallelGroup" : "wi:ADR-046-zone-routing", + "prerequisites" : [ + "ADR-046-zone-routing", + "ADR046-routing-002", + "ADR046-routing-004", + "ADR046-routing-007", + "ADR046-routing-012" + ], + "specId" : "ADR-046-zone-routing", + "topologicalRank" : 11, + "validation" : "Bootstrap/enroll/resolve-route/shortcut integration tests against a child-local fake ZoneLink; topology list/inspect/watch golden vectors contain exact `{ childZone, parentZone }` rows plus authenticated status and no ZoneLink fields; stale/withdrawn/unauthenticated projection tests; parent-store no-row/no-handler test; relay-plus-target-verb RBAC tests; KK enrollment test; shortcut ZonePath addressing test; concurrent dispatch bound test (64 in-flight)", + "wave" : "W2" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/policy_telemetry_redaction.rs`" + ], + "detailedDesign" : "One policy test enumerating every forbidden metric-label/audit-field value from §21 and the content-secrecy table in §20 (store paths, `no_isolation`, credential bytes, raw paths/argv/PID/cgroup, CTAP/clipboard/terminal/notification content) and asserting, by static scan of instrumentation call sites plus a redaction-guard runtime test, that no `ADR046-*` Provider crate emits any of them Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-001", + "kind" : "work-item", + "owner" : "All ResourceType/Provider specs; owned by the security/telemetry integrator alongside `ADR046-telem-008`", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-telem-008" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Hermetic (`cargo test -p d2b-contract-tests policy_telemetry_redaction`); fails the build if a new Provider crate is added without a corresponding redaction test file under its `tests/`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-session/tests/noise_conformance.rs`, `packages/d2b-session/fuzz/fuzz_targets/{handshake_offer,record_frame}.rs`" + ], + "detailedDesign" : "Property/fuzz test suite over the three Noise profiles (§7): exact NN/KK/IKpsk2 vectors and rejection mutations (copied), plus new `cargo-fuzz` targets mutating the canonical handshake offer, preface, and encrypted record frame to assert no panic/UB and that every malformed input is a typed rejection (never a partial accept) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-002", + "kind" : "work-item", + "owner" : "`ADR046-session-001`/`ADR046-bus-001` (ComponentSession/d2b-bus implementation)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-bus-001", + "ADR046-session-001" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Hermetic vector tests plus fuzz corpus with a minimum 4-hour nightly run and zero crashes/hangs as acceptance", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-store/tests/rbac_property.rs`" + ], + "detailedDesign" : "Property test asserting, for a randomly generated Role/RoleBinding/request corpus: (1) no request whose payload sets a subject/role field ever changes the resolved `AuthenticatedSubjectContext.subjectRef`; (2) no non-core Role with a wildcard grant is ever admitted; (3) `scopeNarrowing` never widens beyond the referenced Role; (4) RoleBinding deletion never leaves an observable intermediate state under concurrent readers; (5) `relay` is accepted only as the canonical ZoneLink-scoped session verb with core-generated or explicit admin-policy provenance and exact target bounds; and (6) every forwarding hop independently requires relay plus the target verb Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-003", + "kind" : "work-item", + "owner" : "`ADR046-zone-control-004`/`ADR046-zone-control-005` (Role/RoleBinding implementation)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-zone-control-004", + "ADR046-zone-control-005" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 13, + "validation" : "Hermetic property test (`proptest`/`quickcheck`-style, minimum 10,000 cases per property)", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/fuzz/fuzz_targets/zonelink_frame.rs`, `packages/d2b-bus/tests/zonelink_structural_rejection.rs`" + ], + "detailedDesign" : "Fuzz + property suite asserting that no mutation of a ZoneLink-bound frame (attachment count, credential-shaped byte runs, path-shaped strings, PID-shaped integers) is ever forwarded — every such mutation is rejected at serialization with `attachment-not-permitted-over-zone-link` or the transport-specific equivalent, never silently dropped or partially forwarded Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (design copied from `ADR-046-zone-routing.md` structural-rejection sections).", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-004", + "kind" : "work-item", + "owner" : "`ADR046-routing-005`/`ADR046-bus-001` (ZoneLink/d2b-bus relay implementation)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-bus-001", + "ADR046-routing-005" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Fuzz corpus (`cargo fuzz run zonelink_frame -- -runs=1000000`, zero crashes); container test passes in `make test-integration`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/effectport_boundary_check.rs`, wired into `make test-policy`" + ], + "detailedDesign" : "For every crate under `packages/d2b-provider-*`, walk its `Cargo.toml` dependency graph and fail the build if it transitively depends on `d2b-priv-broker` or any crate exposing a raw broker client/DTO type; separately, grep-scan for direct syscalls forbidden per dossier (e.g. `socket(AF_VSOCK` in `transport-vsock`, `Command::new(\"systemctl\"` in `system-systemd`)", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-005", + "kind" : "work-item", + "owner" : "Every Provider dossier's own crate; owned by the Provider packaging integrator (`ADR-046-provider-model-and-packaging`)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Hermetic (`cargo xtask effectport-boundary-check`); a negative test intentionally adds a forbidden dependency to a scratch crate and asserts the check fails", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-minijail/tests/launchticket_toctou.rs`" + ], + "detailedDesign" : "Fault-injection test that issues a `LaunchTicket`, then mutates the referenced `CompiledSandboxPlan` digest (simulating a race between issue and exec) before the broker execs, and asserts the spawn fails closed rather than launching with the old plan; a companion test kills the broker mid-`clone3` and asserts no half-initialized process (missing cgroup placement, non-zero host capabilities) is ever observable by a concurrent reader", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-006", + "kind" : "work-item", + "owner" : "`ADR046-minijail-002`/`ADR046-minijail-003` (LaunchTicket/EffectPort implementation)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-minijail-002", + "ADR046-minijail-003" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 13, + "validation" : "Hermetic fault-injection test plus `make test-host-integration` NixOS/KVM test; acceptance is zero observable non-zero-capability or missing-cgroup-placement windows across 10,000 injected-fault iterations", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/quarantine_not_kill_matrix.rs`" + ], + "detailedDesign" : "One parameterized fault-injection matrix test, run once per adoption-capable Provider (`system-minijail`, `system-systemd`, `runtime-cloud-hypervisor`, `runtime-azure-container-apps`, `volume-local`), that restarts the controller with a deliberately ambiguous adoption candidate (duplicate InvocationID, mismatched marker inode, stale ACA operation handle) and asserts: (a) the resource transitions to `Degraded`/`Quarantined`, never `Deleted` or silently re-adopted; (b) no signal is sent to the ambiguous candidate process; (c) a `runtime-security-violation`-class audit record is emitted Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-007", + "kind" : "work-item", + "owner" : "`ADR046-minijail-005`, `ADR046-systemd-002`, `ADR046-aca-001`, `ADR046-volume-001` (every quarantine-on-ambiguity implementation)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-aca-001", + "ADR046-minijail-005", + "ADR046-systemd-002", + "ADR046-volume-001" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 15, + "validation" : "Matrix covers all five Providers listed; acceptance is 100% pass across all five with no signal sent to the ambiguous candidate in any case", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-core/tests/no_isolation_propagation.rs`" + ], + "detailedDesign" : "Integration test that creates a user-only `Host`, launches a Process on it, and asserts all three non-suppressible surfaces simultaneously: `status.isolationPosture == \"none\"` in JSON output; the unconditional stderr warning string is present with `--json` and non-JSON CLI invocation and cannot be suppressed by any combination of flags; the corresponding `ProcessEffect` audit record carries `no_isolation: true`; and a companion negative assertion that `no_isolation` never appears in any OTEL metric/span emitted during the same test run", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-008", + "kind" : "work-item", + "owner" : "`ADR046-exec-002` (Host `isolationPosture` propagation)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-exec-002" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Hermetic CLI/audit integration test; Nix eval test; acceptance is zero code paths reaching a live user-only-Host Process without all three surfaces firing", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-local/tests/marker_tamper_fault_injection.rs`" + ], + "detailedDesign" : "Fault-injection test that provisions a Volume, then out-of-band (as a simulated attacker with filesystem access) replaces the marker file, swaps the backing directory for a different inode on the same `st_dev`, and deletes the marker entirely — three separate scenarios — and asserts each transitions the Volume to `Failed` with `markerStatus: missing`/`replaced` respectively, never a silent re-provision, and that operator-only remediation is the only recovery path exercised", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-009", + "kind" : "work-item", + "owner" : "`ADR046-pstate-003` (Volume identity marker)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-pstate-003" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Hermetic + host/KVM fault-injection test; acceptance is 100% fail-closed across all three tamper scenarios", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/zero_secret_invariant.rs`" + ], + "detailedDesign" : "Static + dynamic gate: (1) static — every DTO type reachable from a `Credential`-adjacent module must implement a hand-written redacted `Debug` and must not derive `Debug`, enforced by a `#[forbid(clippy::derive_debug_ambient)]`-style custom lint or an `xtask` AST scan; (2) dynamic — a property test that generates random `Credential` delivery sessions and asserts the delivered token/`SignChallenge` byte sequence never appears, byte-for-byte, in any captured audit record, OTEL span, log line, or resource-store row taken during the same test run Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (design from `ADR-046-resources-credential.md` §1.1).", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-010", + "kind" : "work-item", + "owner" : "`ADR046-credential-001` through `ADR046-credential-007`", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-credential-001", + "ADR046-credential-007" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Hermetic; the dynamic test additionally runs as a canary-byte test (a unique random marker is embedded in the token and searched for across every observability surface)", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-{clipboard-wayland,shell-terminal,device-security-key,notification-desktop}/tests/stream_redaction.rs` (one per Provider, same shared test helper crate)" + ], + "detailedDesign" : "Shared canary-byte test helper: each Provider's test injects a unique random marker into its sensitive content path (clipboard bytes, terminal output, CTAP payload, notification body) and asserts the marker never appears in audit, OTEL, Debug output, or CLI error text captured during the test", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-011", + "kind" : "work-item", + "owner" : "`ADR046-notify-004`, clipboard/terminal/security-key Provider work items", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-notify-004" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 14, + "validation" : "Hermetic canary-byte test per Provider (4 Providers, shared helper crate); container test for the two D-Bus/Wayland-mediated cases", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-audit/tests/privileged_fail_closed.rs`" + ], + "detailedDesign" : "Fault-injection test that makes the audit sink's fsync fail (simulated ENOSPC/EIO) during a privileged `ResourceMutation`/`RBACChange`/`StateReset` write, and asserts the originating operation itself fails with `audit-unavailable` rather than completing with a lost audit record; a companion test floods `Standard`/`Best-effort` records past `DEFAULT_AUDIT_WRITES_PER_SECOND` and asserts privileged records are never dropped or delayed by the resulting backpressure", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-012", + "kind" : "work-item", + "owner" : "`ADR046-audit-002` (privileged audit durability)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-audit-002" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Hermetic fault-injection test; acceptance is zero privileged-class operations that complete despite a failed durable audit write", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/tests/dos_ceiling_fault_injection.rs`" + ], + "detailedDesign" : "Fault-injection/load test suite: (1) attachment-credit exhaustion at each of the six scopes (Packet/Request/Operation/Session/Process/Host), asserting typed rejection never a panic; (2) reconnect-storm exceeding `MAX_RECONNECT_ATTEMPTS`/`MAX_RECONNECT_WINDOW_MS`, asserting the session fails closed rather than looping; (3) ZoneLink hop-count/route-advertisement replay flood, asserting `hop-limit-exceeded`/`zone-advertisement-replay` rather than unbounded forwarding; (4) a stalled data stream under load, asserting control/cancellation traffic is never starved (priority-scheduling property) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-013", + "kind" : "work-item", + "owner" : "`ADR046-zone-control-009`/`ADR046-zone-control-010` (Quota/EmergencyPolicy), `ADR046-session-001` (session limits)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-session-001", + "ADR046-zone-control-009", + "ADR046-zone-control-010" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Hermetic fault-injection suite; container load test; acceptance is zero panics/unbounded-growth across all four scenarios", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b/src/commands/{doctor,support_bundle}.rs`" + ], + "detailedDesign" : "`d2b zone doctor` performs read-only status/audit-hash-chain checks with the redaction rules from §21 enforced on every field it prints; `d2b zone support-bundle` assembles a bounded archive of metadata+status (never spec bytes or `metadata.name`) and sets `bundle_completeness: \"partial\"` when any Provider in scope is quarantined, rather than omitting the gap silently Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (design from `ADR-046-telemetry-audit-and-support.md`).", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-014", + "kind" : "work-item", + "owner" : "`ADR046-doctor-001`/`ADR046-doctor-002` (doctor/support-bundle commands)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-doctor-001", + "ADR046-doctor-002" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 13, + "validation" : "Hermetic CLI test asserting no spec byte or `metadata.name` appears in a generated bundle; container test for the quarantined-Provider case", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/reset.rs`, `packages/d2b-core-controller/tests/reset_atomicity.rs`" + ], + "detailedDesign" : "Implements the `scope` (`zone`, `provider`, `host`, or `guest`) `StateReset` flow from §25: quiesce via `EmergencyPolicy`, revoke open Credential leases in scope, destroy Volumes in scope (key-shred first), commit the `StateReset` audit record durably, and only then report the reset complete. A crash-recovery path re-derives \"was this reset already committed?\" solely from the durable `StateReset` record, never from partial filesystem state Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (pattern only).", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-015", + "kind" : "work-item", + "owner" : "`ADR046-audit-001` (`StateReset` record), Volume/Credential lifecycle work items", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-audit-001" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Hermetic state-machine test; host/KVM crash-injection test at all four phases; acceptance is zero non-atomic outcomes across all injected crash points", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`tests/unit/gates/security-matrix-coverage.sh`" + ], + "detailedDesign" : "A drift-style gate that parses [Per-ResourceType threat matrix](#per-resourcetype-threat-matrix) and [Per-Provider-family threat matrix](#per-provider-family-threat-matrix), confirms every one of the 19 standard ResourceTypes and all 27 Provider dossiers under `docs/specs/providers/` has a row, and confirms every referenced dossier file actually contains a `## Security`-class section (by heading grep) — failing the gate if a new ResourceType/Provider is added without a corresponding row and dossier section Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (pattern from `tests/unit/gates/drift-check.sh`).", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-016", + "kind" : "work-item", + "owner" : "Documentation/CI integrator; depends on every Provider dossier's own `Security` section existing", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Hermetic shell-script gate; a negative test adds a scratch Provider dossier missing a Security section and asserts the gate fails", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`tests/integration/containers/malicious-child-zone.rs`" + ], + "detailedDesign" : "Container-based penetration test running a real parent Zone and a deliberately malicious child Zone container. Before attack injection, assert the one ZoneLink row/handler exists only in the child store/runtime and the parent has only sealed `{ childZone, parentZone }` topology plus authenticated allocator/route projection state. The child then attempts, over that link: FD smuggling, credential-shaped byte injection, cross-Zone `ownerRef` forgery, capability-ceiling widening claims, and route-advertisement replay. Every attempt must be rejected by the parent with the specific typed error named in §10, and none may create a parent ZoneLink row or reach the parent's resource store, Credential state, or Host substrate Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-017", + "kind" : "work-item", + "owner" : "`ADR046-routing-004`, gateway-custody Provider work items (`ADR046-aca-*`, `ADR046-azure-vm-*`, `ADR046-transport-relay-*`)", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-routing-004" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Container integration test; acceptance is zero successful attacks across all five attempted vectors", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`docs/reference/security-manual-validation-checklist.md` (new reference doc, out of scope for this spec's own file but named here as the required destination for the future implementation PR)" + ], + "detailedDesign" : "A checklist covering the scenarios that cannot be hermetically or even container-tested: (1) real Azure Container Apps/Azure VM credential rotation and revocation under `AzureEffectPort`, confirming zeroization on a real managed-identity/Entra token; (2) real TPM 2.0 hardware NVRAM persistence/tamper-marker behavior across a real host reboot; (3) real USBIP/security-key hardware mutual-exclusion enforcement with a physical FIDO2 device; (4) real Azure Relay listener/sender credential acquisition and relay-identity-not-local-auth verification against a live relay namespace Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (checklist pattern from `SECURITY.md`'s existing portability-roadmap manual milestones and `tests/README.md`'s manual hardware tier).", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-018", + "kind" : "work-item", + "owner" : "Manual validation owner; depends on `ADR046-azure-vm-*`, `ADR046-transport-relay-*`, `ADR046-aca-*`, device Provider work items reaching a testable state", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 12, + "validation" : "Checklist sign-off recorded in the release's validation evidence, not a CI gate (matches `D2b_LIVE=1` manual-tier precedent in `AGENTS.md`)", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/minijail_process_ownership.rs`; `tests/host-integration/minijail-cgroup-kill.nix`" + ], + "detailedDesign" : "Hermetic contract test proves only the broker that called `clone3` can produce the identity-bound `BrokerTerminalResult`; a non-parent poll-readable pidfd cannot be converted to status, while a verified duplicate holder can still request exact-main `pidfd_send_signal`. Host integration launches an owned descendant that calls `setsid(2)` plus an unrelated recycled-PGID decoy, performs graceful exact-main stop followed by anchored leaf `cgroup.kill`, and proves the owned leaf reaches `populated 0`, the broker reaps exactly once, the decoy survives, and rmdir/finalizer clearing wait for both proofs. Negative cases prove ambiguous adoption emits no signal/`cgroup.kill`, and Linux <5.14 or missing/unwritable `cgroup.kill` fails before spawn. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-security-and-threat-model" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-019", + "kind" : "work-item", + "owner" : "`ADR046-exec-003`/`ADR046-exec-007` and `ADR046-minijail-003` through `ADR046-minijail-005`; system-minijail/broker integration owner", + "parallelGroup" : "wi:ADR-046-security-and-threat-model", + "prerequisites" : [ + "ADR-046-security-and-threat-model", + "ADR046-exec-003", + "ADR046-exec-007", + "ADR046-minijail-003", + "ADR046-minijail-005" + ], + "specId" : "ADR-046-security-and-threat-model", + "topologicalRank" : 15, + "validation" : "Parent-only wait/reap, poll-readability-not-status, duplicate-holder signaling, setsid/PGID-reuse resistance, quarantine no-kill, exact-once reap, and Linux ≥5.14 platform-gate assertions all pass", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "Move to `packages/d2b-provider-device-security-key/src/session.rs` and `cid.rs`; adapt to Provider Process model (remove daemon Mutex wrapping, add async relay protocol)" + ], + "detailedDesign" : "Extract the baseline lease/session constants and CID mapping into provider-local modules. Preserve `LeaseId` stale-release protection, cancel-all-active-CIDs, `CEREMONY_TIMEOUT`, `QUEUE_WAIT_TIMEOUT`, and bounded fair queue semantics; remove daemon-global `Mutex` ownership; keep the authority relay's DeviceGrant/OFD lease for its lifetime. Ceremony rows remain high-churn session records, never Resources. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-001", + "kind" : "work-item", + "owner" : "ADR-046 provider-device-security-key session/relay owner; depends on ADR046-security-key-008 and the ComponentSession/Process contracts.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 12, + "validation" : "`session_state_machine.rs`, `session_ring.rs`, `cancel_propagation.rs`, `session_timeout.rs`, `fair_queue.rs`, and `cid_isolation.rs` verify queue/active/completed/timeout transitions, ring eviction, LeaseId stale-release denial, cancel-all-CIDs, fair timeout, and per-session CID isolation with no daemon-global lease state or ceremony Resource.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Move to `packages/d2b-provider-device-security-key/src/relay.rs`; replace daemon-internal Unix socket proxy with ComponentSession over the owned Service Endpoint" + ], + "detailedDesign" : "Extract the CTAPHID ceremony relay behavior into the provider relay binary. Preserve one-ceremony-at-a-time proxy semantics and CTAPHID cancel handling, but replace daemon-internal Unix socket proxying with the `d2b.security-key.v3` ComponentSession over the owned CTAPHID Endpoint and named `ctaphid` stream. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-002", + "kind" : "work-item", + "owner" : "ADR-046 provider-device-security-key relay extraction owner; depends on ADR046-security-key-008, ADR046-security-key-001, and the frozen ComponentSession/Endpoint contracts.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-001", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 13, + "validation" : "`host_relay_guest_frontend/` integration fixture, `device_grant_no_path.rs`, `descriptor_validation.rs`, and `cancel_propagation.rs` prove relay fd injection, ComponentSession transport, cancel propagation, and absence of daemon-internal socket proxying.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Adopt `main.rs` and `uhid.rs` as the v3 Process binary entry point; replace `framing.rs`/`vsock.rs` with ComponentSession client from `d2b-session-unix/src/vsock.rs`; wire as Process service in Provider crate" + ], + "detailedDesign" : "Retain UHID creation and frontend entry behavior, but run it as a Binding-owned v3 user-domain Process receiving a pre-opened `/dev/uhid` fd from the `Provider/system-core` Guest-substrate DeviceGrant. Delete raw frame/vsock protocol and use the ComponentSession client/named `ctaphid` stream. No virtual/projected Device exists. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-003", + "kind" : "work-item", + "owner" : "ADR-046 provider-device-security-key frontend extraction owner; depends on ADR046-security-key-008 and frozen Process/ComponentSession contracts.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 12, + "validation" : "`host_relay_guest_frontend/`, `device_grant_no_path.rs`, `descriptor_validation.rs`, and guest Nix migration tests prove UHID fd injection, no `/dev/uhid` path, ComponentSession client use, and no raw `framing.rs`/`vsock.rs` protocol.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Preserve revalidation logic; update `SecurityKeyOpenDevice` to use bundle device table `device_token` as sole open target (no iterative sysfs scan); add zone-field handling; remove sysfs fallback. **Core's LaunchTicket calls this internally; the Provider does not call it.**" + ], + "detailedDesign" : "Keep the FIDO usage-page and post-open revalidation logic, but make the trusted private bundle `device_token` the only open target. Add Zone-aware request handling, reject path/sysfs fallback inputs, and keep the operation internal to Core LaunchTicket DeviceGrant resolution rather than callable by the Provider controller.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-004", + "kind" : "work-item", + "owner" : "Core LaunchTicket/privileged broker reuse owner; depends on ADR-046-resources-device and ADR046-security-key-013 probe/device-token population.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-013" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 14, + "validation" : "`packages/d2b-priv-broker/tests/security_key_broker.rs` updates for bundle table lookup and zone-field round trip; `device_grant_no_path.rs` proves Provider code does not call the broker and sees no device path; audit tests prove path-free grant records.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Adapt to v3 Zone/ResourceRef identifiers; preserve serde shapes for zero downstream breakage where possible; remove `SecurityKeyApplyUdevRulesRequest` (ADR046-security-key-035)" + ], + "detailedDesign" : "Rebase wire DTOs onto v3 Zone/ResourceRef identifiers; consume the shared ADR046-provider-004 `security-key.d2bus.org` Service/Binding bases and define only strict `device-security-key.d2bus.org` Provider-extension DTOs; reject `spec.provider` on Core projections; place authority/import/attachment semantic observations only under `status.resource` and implementation observations only under `status.provider`; preserve opaque bounded ceremony records as non-Resource DTOs; add `zone` to `SecurityKeyOpenDeviceRequest`; drop the udev-rules request because UHID comes from the Guest-substrate DeviceGrant. No provider-named ResourceType alias is admitted.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-005", + "kind" : "work-item", + "owner" : "`d2b-contracts` security-key ceremony/effect DTO owner; depends on ADR046-provider-004, ADR-046-resource-object-model, ADR-046-resources-device, and ADR046-security-key-008.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-provider-004", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 12, + "validation" : "DTO serde round trips, exact provider-neutral ResourceType identity, provider-named alias rejection, canonical minimal base acceptance, Core projection `spec.provider` rejection, D088 `status.resource`/`status.provider` layering, base/Provider-extension field separation, unknown-field denial, zone-field round trip, path-redaction tests, and updated `usb_sk_contract.rs` assertions in the provider crate.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Move to `packages/d2b-provider-device-security-key/tests/`; update imports and v3 type names" + ], + "detailedDesign" : "Move the reusable semantic assertions for security-key DTO serde, unknown-field denial, and broker capability shape into the provider crate's hermetic `tests/` suite, updating imports and names to the v3 contract modules without weakening assertions. Primary reuse disposition: `adapt`. Preserved source-plan detail: move and adapt.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-006", + "kind" : "work-item", + "owner" : "Provider crate test owner; depends on ADR046-security-key-005 v3 DTOs and ADR046-security-key-008 provider crate layout.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-005", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 13, + "validation" : "Moved tests pass under the provider crate; contract assertions are retained; D094 disposition records moved/adapted coverage before old duplicate tests are deleted.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Move to `packages/d2b-provider-device-security-key/tests/`; update for v3 Process resource minijail profile; retain zero-capabilities assertion" + ], + "detailedDesign" : "Move the reusable minijail/sandbox assertions into the provider crate and retarget them from `ProcessRole::SecurityKeyFrontend` to the v3 Process resource templates and relay/controller minijail profiles. Preserve zero-capabilities and seccomp-class assertions while recognizing the frontend uses `Provider/system-systemd` hardening rather than a minijail profile. Primary reuse disposition: `adapt`. Preserved source-plan detail: move and adapt.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-007", + "kind" : "work-item", + "owner" : "Provider crate test/minijail adaptation owner; depends on ADR046-security-key-008 and the frozen Process sandbox contract.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 12, + "validation" : "Provider-crate tests retain zero-capability and seccomp assertions for relay/controller and assert no minijail profile is used for the frontend Process.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "New crate `packages/d2b-provider-device-security-key/` with `src/`, `tests/`, `integration/`, `README.md` (workspace policy requires all four)" + ], + "detailedDesign" : "New crate `packages/d2b-provider-device-security-key/` with `src/`, `tests/`, `integration/`, `README.md` (workspace policy requires all four)", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-008", + "kind" : "work-item", + "owner" : "ADR-046 provider-device-security-key crate owner; depends on provider-model/package workspace policy.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 11, + "validation" : "Workspace package-policy check rejects missing `src/`, `tests/`, `integration/`, or `README.md`; `cargo test -p d2b-provider-device-security-key --lib --tests` discovers the hermetic suite; README acceptance criteria from the provider crate standard layout are satisfied.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-security-key/src/controller.rs`" + ], + "detailedDesign" : "One controller implements standard reconcile for local physical Devices, authority/projection SecurityKeyServices, and SecurityKeyBindings. It observes Devices; realizes an authority Service as relay Process/Service-owned Endpoint; accepts projection Services only from Core/import after signed-factory admission; realizes each Binding as frontend Process/private Endpoint; enforces child-first finalizers and never creates an import or Device projection. Export/Import routing never treats an Endpoint as exported identity.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-009", + "kind" : "work-item", + "owner" : "Provider controller owner; depends on ADR046-security-key-013 probe, ADR046-security-key-016 templates, ADR046-security-key-025 effect port, ADR046-security-key-026 Service/Binding contracts, ADR046-security-key-027 status contract, and ADR-046-resource-reconciliation.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-013", + "ADR046-security-key-016", + "ADR046-security-key-025", + "ADR046-security-key-026", + "ADR046-security-key-027" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 15, + "validation" : "`controller_reconcile.rs`, `service_binding_projection.rs`, `mutual_exclusion.rs`, `status_binding.rs`, and deletion/finalizer tests cover authority/projection/Binding branches, signed-factory admission before projection reconcile, Service-owned Endpoint isolation from Export/Import identity, no Device projection, and no Volume API calls.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-security-key/src/relay.rs`" + ], + "detailedDesign" : "Authority Service-owned relay entry point: bounded authenticated Binding connections, one LeaseId-guarded fair ceremony queue, CID translation/cancel-all-CIDs, hidraw fd from Core DeviceGrant, CTAPHID named stream, and internal relay-control channel.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-010", + "kind" : "work-item", + "owner" : "Authority Service relay owner; depends on ADR046-security-key-001, ADR046-security-key-002, ADR046-security-key-011, ADR046-security-key-012, ADR046-security-key-014, ADR046-security-key-016, and ADR046-security-key-018.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-001", + "ADR046-security-key-002", + "ADR046-security-key-011", + "ADR046-security-key-012", + "ADR046-security-key-014", + "ADR046-security-key-016", + "ADR046-security-key-018" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 16, + "validation" : "`host_relay_guest_frontend/`, `fair_queue.rs`, `device_grant_no_path.rs`, `descriptor_validation.rs`, `cancel_propagation.rs`, and `cid_isolation.rs` prove one authority open, multi-Binding fair serialization, fd-only access, LeaseId cancel, and CID isolation.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-security-key/src/session.rs`" + ], + "detailedDesign" : "`SessionStateMachine` with Idle/Queued/Active/Completed/TimedOut, bounded FIFO queue, monotonic LeaseId stale-release guard, per-Binding session ring, timeout/cancel, and ring eviction. Ceremony rows are non-Resource records; DeviceGrant remains held for relay lifetime.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-011", + "kind" : "work-item", + "owner" : "Relay ceremony-session foundation owner; depends on ADR046-security-key-001 and ADR046-security-key-008.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-001", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 13, + "validation" : "`session_state_machine.rs`, `session_ring.rs`, `fair_queue.rs`, `session_timeout.rs`, and `cancel_propagation.rs` cover queue fairness, eviction, stale LeaseId rejection, timeout, and cancel.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-security-key/src/cid.rs`" + ], + "detailedDesign" : "CID translator: per-active-ceremony u32→u64 host-CID allocation, bimap, cancel-all-active-CIDs, and eviction on ceremony end", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-012", + "kind" : "work-item", + "owner" : "Relay CID-translation foundation owner; depends on ADR046-security-key-001 and ADR046-security-key-008.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-001", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 13, + "validation" : "`cid_isolation.rs` verifies per-session allocation, round trip, no sharing across relays, and eviction on session end.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-security-key/src/probe.rs`; Provider activation/Core private bundle device table population for label → `device_token`" + ], + "detailedDesign" : "hidraw probe: `probe.rs` — calls `SecurityKeyEffectPort::observe_inventory(&device_id, &policy_id)` with opaque types injected by Core; interprets `InventoryObservation`; never reads `/sys/class/hidraw/` directly; bundle device table population at activation time (Provider activation resolves label → `device_token` via Core; stored in private bundle)", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-013", + "kind" : "work-item", + "owner" : "Probe/effect-port and activation owner; depends on ADR046-security-key-025 effect port and Core private bundle device table support.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-025" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 13, + "validation" : "`controller_reconcile.rs` scheduled-observe tests, `descriptor_validation.rs` Debug-redaction capture, and path-safety tests prove Provider never reads sysfs and receives only opaque observations.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-security-key/src/descriptor.rs`" + ], + "detailedDesign" : "Declare relay↔controller service and relay↔Binding-frontend `d2b.security-key.v3` fingerprints, Noise profiles, canonical Service/Binding subject pairing, bounded encrypted-stream records, and descriptor validation; no ambient path or raw vsock CID.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-014", + "kind" : "work-item", + "owner" : "ComponentSession/security descriptor contract owner; depends on ADR046-security-key-008, ADR046-security-key-005, and ADR-046-componentsession-and-bus.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-005", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 13, + "validation" : "`descriptor_validation.rs` covers wrong service, wrong descriptor digest, wrong SO_PEERCRED uid, unenrolled key, oversized records, no ambient path, and redacted opaque IDs.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/minijail-profiles.nix` entries for relay and controller; provider descriptor sandbox templates for relay/controller/frontend" + ], + "detailedDesign" : "Minijail profiles for relay and controller only; frontend uses `Provider/system-systemd` hardening directives compiled from `SandboxSpec` (no minijail profile for frontend). Add relay and controller entries to `nixos-modules/minijail-profiles.nix`; `capabilityClasses: []`; `seccompClass: sk-relay` and `seccompClass: sk-controller`", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-015", + "kind" : "work-item", + "owner" : "Sandbox/minijail foundation owner; depends on ADR046-security-key-008, ADR046-security-key-007, and ADR-046-components-processes-and-sandbox.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-007", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 13, + "validation" : "`minijail_sk_frontend` successor tests, sandbox template tests, and zero-capability/seccomp assertions cover relay/controller minijail profiles and no frontend minijail profile.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Provider descriptor Process templates and owned CTAPHID `Endpoint` template for `Provider/device-security-key`" + ], + "detailedDesign" : "Templates: Provider controller; authority Service-owned relay Process/relay Endpoint; Binding-owned frontend Process/private Endpoint; projection Service local Endpoint. Frontend requires Guest/User and the system-core UHID DeviceGrant; no virtual Device template exists.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-016", + "kind" : "work-item", + "owner" : "Provider process/Endpoint-template owner; depends on ADR046-security-key-008, ADR046-security-key-015, and ADR046-security-key-026 Service/Binding ownership contracts.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-008", + "ADR046-security-key-015", + "ADR046-security-key-026" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 14, + "validation" : "`controller_reconcile.rs`, Process template golden tests, Endpoint resource tests, and frontend `userRef` admission tests prove templates and Endpoint shape.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Signed Provider descriptor JSON for `Provider/device-security-key` in the provider package" + ], + "detailedDesign" : "Signed descriptor: config; physical Device integration; implementation claim for the provider-neutral `security-key.d2bus.org` Service/Binding base schemas/fingerprints; strict `device-security-key.d2bus.org` spec/status extensions; authority/projection union and D097 descriptor; a D096 projection factory with exact `serviceType`, `projectionSchemaFingerprint`, and semantic `factoryFingerprint`; controller/relay/frontend/Endpoint templates; export/import adapter capability; ComponentSession services; empty ProviderStateSet; permission claims. Provider/adapter identity is signed separately and Service-owned Endpoints are not factory or Export fields.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-017", + "kind" : "work-item", + "owner" : "Provider package descriptor owner; depends on ADR046-security-key-008, ADR046-security-key-005, ADR046-security-key-014, ADR046-security-key-016, ADR046-security-key-026, ADR046-security-key-027, and ADR-046-provider-model-and-packaging.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-005", + "ADR046-security-key-008", + "ADR046-security-key-014", + "ADR046-security-key-016", + "ADR046-security-key-026", + "ADR046-security-key-027" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 15, + "validation" : "Descriptor schema validation, semantic-base versus Provider-extension fingerprints, exact projection-schema/factory fingerprint derivation and stability under Provider/adapter identity changes, exact type/no-alias tests, service inventory tests, permission claim tests, empty ProviderStateSet tests, and README/provider package conformance checks.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "v3 `SecurityKeyOpenDevice` broker op and Core LaunchTicket DeviceGrant resolution path" + ], + "detailedDesign" : "v3 `SecurityKeyOpenDevice` broker op update: add `zone` field; implement bundle device table `device_token` lookup as sole open path; remove iterative sysfs scan from broker; add post-open revalidation steps (fstat, HIDIOCGRAWINFO, HIDIOCGRDESC). This is an internal Core operation called by LaunchTicket; the Provider controller does not call it.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-018", + "kind" : "work-item", + "owner" : "Core LaunchTicket/broker owner; depends on ADR046-security-key-004, ADR046-security-key-005, ADR046-security-key-013, and ADR-046-resources-device.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-004", + "ADR046-security-key-005", + "ADR046-security-key-013" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 15, + "validation" : "Broker unit tests for zone field and token lookup, path-rejection tests, post-open revalidation tests, and provider tests proving no Provider broker call or sysfs path.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/` resource compiler/eval assertions for physical Device, authority Service, ResourceExport/Import, and consumer Binding" + ], + "detailedDesign" : "Compile the owner Device→Service→export and consumer import→projection-Service→Binding shape. Emit Export `resourceRef`, `serviceType`, `projectionSchemaFingerprint`, and `factoryFingerprint`, and matching Import `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`; the Import `exportKey` identifies the ResourceExport. Reject Export Endpoint/custom-key fields, authored projections, projection `spec.provider`, Device export/projection, cross-Zone refs, duplicate authorities/Bindings, paths, and any security-key/USB configuration that does not collide through the exact Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` tuple after trusted identity resolution.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-019", + "kind" : "work-item", + "owner" : "Nix resource compiler owner; depends on ADR046-security-key-017, ADR046-security-key-026, ADR046-zone-control-024, and ADR-046-nix-configuration.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-017", + "ADR046-security-key-026", + "ADR046-zone-control-024" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 16, + "validation" : "Nix eval tests for label resolution, `busClass=hidraw`, exclusive arbitration, exact canonical Export/Import field emission and fingerprint matching, rejection of obsolete Export `endpointRef`/`exportedType`/`baseSchemaFingerprint`/`exportKey` and Import `expectedType`/`expectedBaseSchemaFingerprint`/`projectionType`, Core-only projection without `spec.provider`, byte-identical USB/security-key physical backing tuple collision, Provider-private-class bypass rejection, prohibited fields, and providerRef resolution.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/components/security-key-guest.nix` migration gate `d2b.securityKey._legacySystemdUnit`" + ], + "detailedDesign" : "Guest Nix module migration gate: `d2b.securityKey._legacySystemdUnit` option, defaulting to false when Provider is installed; remove `d2b-sk-frontend.service` unit", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-020", + "kind" : "work-item", + "owner" : "Guest Nix migration owner; depends on ADR046-security-key-003, ADR046-security-key-016, and ADR046-security-key-026 Binding-owned frontend contract.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-003", + "ADR046-security-key-016", + "ADR046-security-key-026" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 15, + "validation" : "Nix eval tests show the legacy unit is absent by default with Provider installed, can be gated only during transition if required, and `uhid` module/binary wiring remains present.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Core `device-grant` audit and Provider controller Service/Binding ceremony lifecycle audit" + ], + "detailedDesign" : "Path-free authority-grant records from Core and bounded Service/Binding/session digests/outcomes from controller; no path, raw target identity, LeaseId, session content, or CTAP bytes. `resource_name_digest` is admitted only in the Core authority-grant audit after DeviceGrant authorization and is never copied to OTEL.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-021", + "kind" : "work-item", + "owner" : "Audit owner for Core device-grant and Service/Binding lifecycle; depends on ADR046-security-key-009, ADR046-security-key-018, and ADR-046-telemetry-audit-and-support.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-009", + "ADR046-security-key-018" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 16, + "validation" : "Audit tests assert path-free fields, bounded digests, no guest name/session content/CTAP bytes, grant emitted by Core not Provider controller, and lifecycle emitted by controller.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Provider/controller bounded telemetry emitter and observability-otel handoff for security-key metrics" + ], + "detailedDesign" : "OTEL metrics: `d2b_device_sk_session_total`, `d2b_device_sk_ceremony_duration_seconds`, `d2b_device_sk_relay_restarts_total` via bounded emitter ring; descriptors use only closed semantic labels and never Zone/resource-name-derived identity. Provider spans use only fixed operation/phase/outcome/error-class attributes. Neither metrics nor spans admit a resource name, UID, ref, digest (including `resource_name_digest`), session ID, or derived identity token, while `d2b.zone` and `d2b.provider` remain OTEL Resource attributes.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-022", + "kind" : "work-item", + "owner" : "Observability owner; depends on ADR046-security-key-010 relay, ADR046-security-key-009 controller, and ADR-046-telemetry-audit-and-support.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-009", + "ADR046-security-key-010" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 17, + "validation" : "`telemetry_identity_canaries.rs` and metric inventory tests structurally assert closed label/span-attribute sets; exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, resource-name-derived keys, and `resource_name_digest`; Device/Service/Binding/Guest/Zone name, UID, ref, and digest canary absence from metrics and spans; retained `d2b.zone` Resource attributes; bounded ring behavior; and correct session/ceremony/restart counters.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-security-key/README.md`" + ], + "detailedDesign" : "README: initial Provider identity, provider-neutral Service/Binding catalog, strict Provider-extension fields, physical Device, owner/export/import/projection/Binding chain, process ownership, RBAC, invariants, status/telemetry, no-alias rule, and commands", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-023", + "kind" : "work-item", + "owner" : "Provider documentation owner; depends on ADR046-security-key-008 through ADR046-security-key-022 and ADR046-security-key-024 through ADR046-security-key-029 for complete behavior.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-008", + "ADR046-security-key-009", + "ADR046-security-key-010", + "ADR046-security-key-011", + "ADR046-security-key-012", + "ADR046-security-key-013", + "ADR046-security-key-014", + "ADR046-security-key-015", + "ADR046-security-key-016", + "ADR046-security-key-017", + "ADR046-security-key-018", + "ADR046-security-key-019", + "ADR046-security-key-020", + "ADR046-security-key-021", + "ADR046-security-key-022", + "ADR046-security-key-024", + "ADR046-security-key-025", + "ADR046-security-key-026", + "ADR046-security-key-027", + "ADR046-security-key-028", + "ADR046-security-key-029" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 19, + "validation" : "README presence check from provider crate standard layout; documentation review verifies every listed section and command is present and matches the crate/package behavior.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Authority/projection Service Endpoint and Binding private Endpoint resolution, including transport-vsock and ZoneLink encrypted streams" + ], + "detailedDesign" : "Resolve each Binding only through its same-Zone Service Endpoint; enroll Noise KK for Service/Binding frontend; authority uses transport-vsock locally, projection uses per-import bounded encrypted stream with credits/backpressure/generation/deadline/cancel.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-024", + "kind" : "work-item", + "owner" : "Endpoint/ComponentSession integration owner; depends on ADR046-security-key-003, ADR046-security-key-010, ADR046-security-key-014, ADR046-security-key-016, ADR046-security-key-026, and ADR-046-componentsession-and-bus.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-003", + "ADR046-security-key-010", + "ADR046-security-key-014", + "ADR046-security-key-016", + "ADR046-security-key-026" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 17, + "validation" : "`host_relay_guest_frontend/` and `descriptor_validation.rs` verify Endpoint resolution, Noise KK enrollment, attachment opacity, and no raw vsock CID/port in status/spec.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`d2b-contracts` neutral `SecurityKeyEffectPort` trait/types; `packages/d2b-provider-device-security-key/src/effect_port.rs` re-export; Core adapter implementation in `d2b-provider` or `d2b-provider-toolkit`" + ], + "detailedDesign" : "Define/re-export the opaque redacting `SecurityKeyEffectPort` types in the neutral contract crate and implement the Core adapter; inject per physical Device into the Provider controller; relay and projection Service do not receive the port.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-025", + "kind" : "work-item", + "owner" : "`d2b-contracts` neutral effect-port foundation owner; depends on ADR046-security-key-008 and ADR-046-resources-device.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 12, + "validation" : "Unit tests assert Debug redaction, controller calls `observe_inventory` with injected IDs, relay has no port dependency, and fake Core adapter returns bounded `InventoryObservation`.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-security-key/src/{resource_type,provider_extension,admission}.rs`; controller contracts; system-core Guest UHID authority-subresource DeviceGrant (common base lives under ADR046-provider-004)" + ], + "detailedDesign" : "Bind the shared semantic authority/projection Service and Binding base versions/fingerprints from ADR046-provider-004, then define only the initial strict Provider extension and admission. The owner/Binding extension references the local physical Device/relay Endpoint and owns CTAPHID/fairness/frontend settings and observations. Projection is Core-owned by ResourceImport with `providerRef` plus semantic base/import fields, no `spec.provider`, and no Device/open; routing derives from the signed local descriptor, `providerRef`, and import record. Export admission binds the authority Service's `resourceRef` and `serviceType` to the signed projection-schema and factory fingerprints, never to its Endpoint. Binding is operator intent and the initial extension realizes its frontend Process/private Endpoint. Standard Device remains physical only; provider-named ResourceType aliases are rejected.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-026", + "kind" : "work-item", + "owner" : "Device-security-key Service/Binding implementation owner; depends on ADR046-provider-004, ADR046-security-key-008, ADR046-security-key-005, resource object/Device/D096/D097 contracts.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-provider-004", + "ADR046-security-key-005", + "ADR046-security-key-008" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 13, + "validation" : "Fast schema/lifecycle conformance consumes the ADR046-provider-004 fixtures, accepts canonical minimal base without `spec.provider`, includes a fake alternate security-key Provider, and proves Device→provider-neutral Service→export→import→projection Service→provider-neutral Binding→frontend, exact canonical Export/Import fields, no Endpoint export, projection `spec.provider` rejection, D088 status layering, strict base/Provider-extension separation, exact types with no aliases, strict ownership/finalizers, no Device projection, and no local hidraw open in consumer Zone.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Provider descriptor state declaration, controller/status logic, Process templates, and Nix principal provisioning for `Provider/device-security-key`" + ], + "detailedDesign" : "Empty ProviderStateSet and strict bounded status schemas: physical presence in Device `status.resource`; semantic authority/import aggregates in Service `status.resource`; attachment aggregates in Binding `status.resource`; initial physical-backing claim, relay, Endpoint, queue, and ceremony observations only in `status.provider`. No semantic field appears directly under `status`, and Core projections contain no `spec.provider`. Ceremony rows remain high-churn non-Resource session records; CTAP/fd/LeaseId/CID data stays transient. No Process has `/state`.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-027", + "kind" : "work-item", + "owner" : "Provider state/status contract owner; depends on ADR046-security-key-008, ADR046-security-key-026, and ADR-046-provider-state.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-008", + "ADR046-security-key-026" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 14, + "validation" : "`status_binding.rs` proves empty ProviderStateSet, no `/state` mounts, no Volume API calls, authority/import/attachment fields only under `status.resource`, implementation fields only under `status.provider`, no projection `spec.provider`, and no CTAP/fd/session secrets in status/log/audit/metrics.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-security-key/src/share_adapter.rs`" + ], + "detailedDesign" : "Signed adapters admit ResourceExport only when `resourceRef` names an authority SecurityKeyService, `serviceType` is `security-key.d2bus.org.SecurityKeyService`, and `projectionSchemaFingerprint` plus `factoryFingerprint` match the signed semantic factory. ResourceImport must supply the corresponding `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`; its `exportKey` identifies the ResourceExport. The Service's relay Endpoint stays a Service-owned implementation child and is never an Export field. Core invokes the factory to create one projection SecurityKeyService with `ownerRef: ResourceImport/`, `providerRef`, semantic base/import fields, and no `spec.provider`; route selection comes from the signed local descriptor and ResourceImport record. The semantic factory fingerprint binds factory metadata plus projection-protocol version only, while adapter identity is authenticated separately by the signed Provider descriptor. They never project Device or auto-create Binding. Route Binding ceremonies over bounded encrypted named streams to the single authority fair queue; no FD/USBIP/hidraw/ref crosses Zones. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (implement the signed security-key export/import adapter).", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-028", + "kind" : "work-item", + "owner" : "Cross-Zone adapter owner; depends on ADR046-security-key-024, ADR046-security-key-026, ADR046-security-key-029, ADR046-zone-control-019, and ADR046-zone-control-020.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-024", + "ADR046-security-key-026", + "ADR046-security-key-029", + "ADR046-zone-control-019", + "ADR046-zone-control-020" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 18, + "validation" : "Fast fake-stream conformance proves owner Service→export→import→projection Service→Binding→frontend; exact canonical Export/Import type and fingerprint fields; rejection of Export `endpointRef`, `exportedType`, `baseSchemaFingerprint`, and `exportKey` plus Import `expectedType`, `expectedBaseSchemaFingerprint`, and `projectionType`; rejection of projection `spec.provider`; semantic factory-fingerprint stability when signed adapter identity changes; separate signed-descriptor identity authentication; one fair LeaseId-guarded ceremony; ciphertext to intermediaries; no Device projection/local hidraw/FD/USBIP; revocation degradation; and audit metadata only.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-device-security-key/src/{authority,relay,streams}.rs`; D097 `AuthorityDescriptor` on authority SecurityKeyService" + ], + "detailedDesign" : "The provider-neutral authority Service, not Device/Endpoint/Process, is the stable D097 owner and carries the semantic opaque Host-scoped zero-or-one descriptor. The initial Provider extension references the local physical Device and relay Endpoint and supplies service-specific physical-key derivation, Service+relay ownerProof, and bounded-fairness details. After trusted USB identity resolution, Core additionally derives `physical-usb-backing/v1` and atomically claims the exact `(Host, physical-usb-backing, opaqueKeyDigest)` tuple used by every USB Provider before any open, withhold, bind, module, relay, or attachment effect; Provider-private claims cannot replace it. Preserve sole Core open with double-fstat/FIDO/HID validation, async fd I/O, per-session CidTranslator, LeaseId stale-release guard, cancel-all-CIDs, one ceremony, bounded FIFO wait, and Binding-owned UHID frontend. Ceremony rows are not Resources. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — relay becomes the D097 hidraw authority; transport moves to Endpoint/named-stream.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-029", + "kind" : "work-item", + "owner" : "D097 authority foundation owner; depends on ADR046-security-key-001, ADR046-security-key-002, ADR046-security-key-003, ADR046-security-key-004, ADR046-security-key-018, ADR046-security-key-026, ADR046-zone-control-024, and the D097 authority contract.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-001", + "ADR046-security-key-002", + "ADR046-security-key-003", + "ADR046-security-key-004", + "ADR046-security-key-018", + "ADR046-security-key-026", + "ADR046-zone-control-024" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 16, + "validation" : "Fast hermetic tests adapt the existing `CidTranslator`/lease/cancel/UHID/broker-revalidation suites: CID alloc/translate/release, `LeaseId` stale-release, cancel-all-CIDs on disconnect, one-ceremony + 120 s timeout, 15 s fair-wait `ERR_CHANNEL_BUSY`, UHID frame round-trip, broker double-`fstat`+FIDO+HID revalidation, byte-identical USB/security-key backing tuple derivation for one fake token, and `physical-usb-backing-conflict` before effects under alternate labels/private authority classes — all with fakes/`FakeEffectPort`, no real hidraw. Integration proves cross-Zone CTAP ceremony **serialization** over the encrypted named stream and the shared physical USB collision.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Removed from daemon; successor behavior lives in `packages/d2b-provider-device-security-key/src/relay.rs`, `session.rs`, and `cid.rs`" + ], + "detailedDesign" : "Remove target `packages/d2bd/src/security_key.rs` — `start_sk_accept_loop`, `SecurityKeyState`, `LeaseState`, `SkRegistry` after v3 relay Process is live and stable; keep behind feature gate only if needed during transition. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-030", + "kind" : "work-item", + "owner" : "Provider-device-security-key removal owner; depends on ADR046-security-key-001, ADR046-security-key-002, ADR046-security-key-010, ADR046-security-key-011, and ADR046-security-key-012 successor relay/session coverage.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-001", + "ADR046-security-key-002", + "ADR046-security-key-010", + "ADR046-security-key-011", + "ADR046-security-key-012" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 17, + "validation" : "Provider relay/session tests pass; daemon build has no references to removed symbols; no legacy security-key accept loop starts under d2bd.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Removed from daemon startup; successor launch path is ProviderDeployment/controller-created relay Process plus Endpoint/ComponentSession transport" + ], + "detailedDesign" : "Remove target `packages/d2bd/src/lib.rs` — `start_sk_accept_loop` call site and daemon-internal Unix socket proxy bind after ADR046-security-key-030. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-031", + "kind" : "work-item", + "owner" : "d2bd integration removal owner; depends on ADR046-security-key-030.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-030" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 18, + "validation" : "d2bd startup tests/build prove no `start_sk_accept_loop` call or security-key proxy bind remains; provider integration test proves CTAPHID flow through Endpoint/ComponentSession.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Removed from guest Nix module; successor is Binding-owned `Process/binding--sk-frontend`" + ], + "detailedDesign" : "Remove target `nixos-modules/components/security-key-guest.nix` — `d2b-sk-frontend.service` systemd unit declaration after ADR046-security-key-020 migration gate defaults to false. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-032", + "kind" : "work-item", + "owner" : "Guest Nix module removal owner; depends on ADR046-security-key-020, ADR046-security-key-003, and ADR046-security-key-026 Binding frontend/UHID contract.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-003", + "ADR046-security-key-020", + "ADR046-security-key-026" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 16, + "validation" : "Nix eval tests prove no static `d2b-sk-frontend.service` is emitted with Provider installed; frontend Process integration proves replacement lifecycle.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Removed from `packages/d2b-contract-tests/tests/`; successor tests live in `packages/d2b-provider-device-security-key/tests/`" + ], + "detailedDesign" : "Remove target `packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` and `packages/d2b-contract-tests/tests/usb_sk_contract.rs` after ADR046-security-key-006/ADR046-security-key-007 tests are in Provider crate and cover all prior assertions. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete after move/adapt.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-033", + "kind" : "work-item", + "owner" : "Test-suite migration/removal owner; depends on ADR046-security-key-006 and ADR046-security-key-007 provider-crate successor tests.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-006", + "ADR046-security-key-007" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 14, + "validation" : "Provider-crate tests pass with retained assertions; old contract-test paths are absent from manifests/CI; no duplicate old/new suite runs indefinitely.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Removed from `d2b-core/src/processes.rs`; successor frontend is a v3 Process resource owned by `Provider/device-security-key`" + ], + "detailedDesign" : "Remove target `ProcessRole::SecurityKeyFrontend` in `d2b-core/src/processes.rs` after relay and frontend are v3 Process resources; no other code reference expected. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-034", + "kind" : "work-item", + "owner" : "Core ProcessRole removal owner; depends on ADR046-security-key-016 Process resources, ADR046-security-key-015 sandbox templates, and system-minijail/system-systemd conformance.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-015", + "ADR046-security-key-016" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 15, + "validation" : "Workspace build proves no `ProcessRole::SecurityKeyFrontend` references; provider Process template tests prove the v3 replacement; process conformance passes.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Removed from contracts and broker; successor access is static guest Nix `uhid` module plus Core pre-opened `/dev/uhid` DeviceGrant for the frontend Process" + ], + "detailedDesign" : "Remove `SecurityKeyApplyUdevRules`, its DTO, and related broker code after the Binding-owned frontend and system-core UHID DeviceGrant are live. Guest Nix loads `uhid` but emits no security-key udev rule. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete.", + "entryContracts" : [ + "ADR-046-provider-device-security-key" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-security-key-035", + "kind" : "work-item", + "owner" : "Broker/contracts/Nix removal owner; depends on ADR046-security-key-005, ADR046-security-key-018, ADR046-security-key-020, and ADR046-security-key-026 Guest-substrate UHID replacement.", + "parallelGroup" : "wi:ADR-046-provider-device-security-key", + "prerequisites" : [ + "ADR-046-provider-device-security-key", + "ADR046-security-key-005", + "ADR046-security-key-018", + "ADR046-security-key-020", + "ADR046-security-key-026" + ], + "specId" : "ADR-046-provider-device-security-key", + "topologicalRank" : 16, + "validation" : "DTO unknown-field/capability tests prove `SecurityKeyApplyUdevRulesRequest` and op are absent; `device_grant_no_path.rs` proves frontend has UHID fd without udev/plugdev; broker build has no related code.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/component_session.rs`, `packages/d2b-session/`" + ], + "detailedDesign" : "Reversion canonical offer/records; retain NN/KK/IKpsk2; add canonical subject/authorization context hooks Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-componentsession-and-bus" + ], + "exitGate" : "ADR046-W1 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-session-001", + "kind" : "work-item", + "owner" : "W0 shared contract root", + "parallelGroup" : "wi:ADR-046-componentsession-and-bus", + "prerequisites" : [ + "ADR-046-componentsession-and-bus" + ], + "specId" : "ADR-046-componentsession-and-bus", + "topologicalRank" : 6, + "validation" : "Copied exact vectors/rejections plus subject/RBAC/revocation tests", + "wave" : "W1" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-session-unix/`, future enrolled transport adapter crates" + ], + "detailedDesign" : "Unix/socketpair/vsock owned transports, peer evidence, fd/pidfd/object validation, credits Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-componentsession-and-bus" + ], + "exitGate" : "ADR046-W1 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-session-002", + "kind" : "work-item", + "owner" : "ADR046-session-001; transport owner", + "parallelGroup" : "wi:ADR-046-componentsession-and-bus", + "prerequisites" : [ + "ADR-046-componentsession-and-bus", + "ADR046-session-001" + ], + "specId" : "ADR-046-componentsession-and-bus", + "topologicalRank" : 7, + "validation" : "Copied fd/peer/credit tests plus Host/Guest subject mapping", + "wave" : "W1" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/resources/{pool,session}.rs`" + ], + "detailedDesign" : "Implement `shell-terminal.d2bus.org.ShellPool` and `shell-terminal.d2bus.org.ShellSession` schemas with qualified names, common phases, and typed detail fields.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-001", + "kind" : "work-item", + "owner" : "Resource schemas area; owned by `d2b-provider-shell-terminal` resource modules.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/resource_schema.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/bin/d2b-shell-terminal-controller.rs`" + ], + "detailedDesign" : "Implement `d2b-shell-terminal-controller` with pool/session reconcile loops; assert ProviderStateSet is empty; publish bounded non-secret operational state to resource status and the core Operation ledger; no controller Provider state Volume or `/state` mount exists. Primary reuse disposition: `create`. Preserved source-plan detail: net-new controller; preserve status-first ProviderStateSet-empty rule.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-002", + "kind" : "work-item", + "owner" : "Controller binary area; owned by `d2b-provider-shell-terminal` controller and core Operation ledger integration.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/controller_reconcile.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/bin/d2b-shell-session-supervisor.rs`" + ], + "detailedDesign" : "Implement `d2b-shell-session-supervisor` as the sole PTY owner for Host and Guest pools. Primary reuse disposition: `adapt`. Preserved source-plan detail: reuse narrow mechanics only; move PTY authority into per-session supervisor and exclude legacy protocols/identities/state storage.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-003", + "kind" : "work-item", + "owner" : "Supervisor binary area; owned by `d2b-provider-shell-terminal` session supervisor runtime.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/supervisor_runtime.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/process_templates.rs`" + ], + "detailedDesign" : "Teach the Nix compiler and controller to emit the canonical controller and user-domain supervisor `Process` templates. Primary reuse disposition: `replace`. Preserved source-plan detail: replace incorrect draft templates with canonical controller and user-domain supervisor Process templates.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-004", + "kind" : "work-item", + "owner" : "Process templates area; owned by Nix compiler plus shell-terminal controller.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/process_templates.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/service/open_session.rs`" + ], + "detailedDesign" : "Create sessions from pools, freeze inherited fields, and return `supervisorGeneration` to callers.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-005", + "kind" : "work-item", + "owner" : "OpenSession lifecycle area; owned by controller service implementation.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/open_session.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/session/{pty,ring}.rs`" + ], + "detailedDesign" : "Implement the in-memory PTY owner model, bounded ring buffer, replay, and eviction counters; do not create any management worker or `EphemeralProcess`. Primary reuse disposition: `adapt`. Preserved source-plan detail: reuse ring semantics; keep bytes in supervisor memory and remove management worker/EphemeralProcess model.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-006", + "kind" : "work-item", + "owner" : "PTY and ring area; owned by per-session supervisor runtime.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/ring_buffer.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/session/adopt.rs`" + ], + "detailedDesign" : "Implement restart adoption, InvocationID plus cgroup verification, route registration, and stale-generation invalidation. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt identity-verification shape; do not reuse helper protocol, identities, or state storage assumptions.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-007", + "kind" : "work-item", + "owner" : "Adoption and routing area; owned by controller session adoption/routing module.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/adoption.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/host_rules.rs`" + ], + "detailedDesign" : "Emit Host `isolationPosture=none` warnings, same-UID verification, and relay denial for Host user-domain pools. Primary reuse disposition: `replace`. Preserved source-plan detail: replace unsafe-local helper shell policy with resource-backed Host pool warnings and same-UID checks.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-008", + "kind" : "work-item", + "owner" : "Host rules area; owned by shell-terminal Host policy module.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/host_rules.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/guest_rules.rs`" + ], + "detailedDesign" : "Require Guest `allowedDomains` to include `user`, require `defaultUserRef`, and place supervisors through the Guest user manager. Primary reuse disposition: `replace`. Preserved source-plan detail: replace guestd shell authority with Guest resource user-domain admission and supervisor placement.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-009", + "kind" : "work-item", + "owner" : "Guest rules area; owned by shell-terminal Guest policy module.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/guest_rules.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/authz.rs`" + ], + "detailedDesign" : "Gate all verbs on `Role/shell-admin` or Zone-admin superset and fail closed for relay-authenticated Host user-domain callers. Primary reuse disposition: `replace`. Preserved source-plan detail: replace legacy shell operation authorization with Role/shell-admin or Zone-admin service gates.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-010", + "kind" : "work-item", + "owner" : "RBAC and relay denial area; owned by shell-terminal authorization module.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/authz.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/{audit,telemetry}.rs`" + ], + "detailedDesign" : "Implement closed-label metrics, redacted spans, and audit events with no usernames, session names, paths, or terminal bytes. Primary reuse disposition: `create`. Preserved source-plan detail: net-new redacted observability.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-011", + "kind" : "work-item", + "owner" : "Audit and telemetry area; owned by shell-terminal audit/telemetry modules.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/redaction.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/migration.rs`" + ], + "detailedDesign" : "Delete superseded guestd shell runtime, unsafe-local helper shell supervisor, and public-wire `ShellOp` or `ShellOpResponse` shell protocol. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete superseded runtime/protocol surfaces after successor parity.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-012", + "kind" : "work-item", + "owner" : "Baseline removal area; owned by migration/removal implementation for shell-terminal.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/migration.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-shell-terminal/src/service/{controller,supervisor}.rs`" + ], + "detailedDesign" : "Define and implement `shell-terminal.v3` and `shell-session-supervisor.v1` ComponentSession services and the named `terminal` stream contract. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt service-shape ideas into v3 ComponentSession services and named terminal stream.", + "entryContracts" : [ + "ADR-046-provider-shell-terminal" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-sterm-013", + "kind" : "work-item", + "owner" : "Supervisor service area; owned by shell-terminal controller/supervisor service modules and ComponentSession contracts.", + "parallelGroup" : "wi:ADR-046-provider-shell-terminal", + "prerequisites" : [ + "ADR-046-provider-shell-terminal" + ], + "specId" : "ADR-046-provider-shell-terminal", + "topologicalRank" : 11, + "validation" : "`packages/d2b-provider-shell-terminal/tests/service_contract.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-store/src/lib.rs`, `packages/d2b-resource-store-redb/src/lib.rs`, `schema.rs`, `keys.rs`, `transaction.rs`" + ], + "detailedDesign" : "redb tables/encodings, fd backend, store identity, fair actor, MVCC reads, atomic indexes/revisions/operations/conflicts Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resource-store-redb" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-store-001", + "kind" : "work-item", + "owner" : "W0; store integrator", + "parallelGroup" : "wi:ADR-046-resource-store-redb", + "prerequisites" : [ + "ADR-046-resource-store-redb" + ], + "specId" : "ADR-046-resource-store-redb", + "topologicalRank" : 4, + "validation" : "Unit/property/fault tests and hard benchmark", + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-store-redb/src/revision_log.rs`, `packages/d2b-resource-api/src/watch.rs`" + ], + "detailedDesign" : "replay/live no-gap watch, cursors, owner hints, compaction floor, expired relist", + "entryContracts" : [ + "ADR-046-resource-store-redb" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-store-002", + "kind" : "work-item", + "owner" : "ADR046-store-001; watch/reconciliation integrator", + "parallelGroup" : "wi:ADR-046-resource-store-redb", + "prerequisites" : [ + "ADR-046-resource-store-redb", + "ADR046-store-001" + ], + "specId" : "ADR-046-resource-store-redb", + "topologicalRank" : 5, + "validation" : "deterministic watch/compaction/disconnect/fan-in tests", + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-store-redb/src/backup.rs`, `migration.rs`; generated v3 storage row" + ], + "detailedDesign" : "fd-backed provision/open, marker identity, logical backup, staged restore/upgrade, corruption quarantine", + "entryContracts" : [ + "ADR-046-resource-store-redb" + ], + "exitGate" : "ADR046-W0 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-store-003", + "kind" : "work-item", + "owner" : "ADR046-store-001; storage/broker integrator", + "parallelGroup" : "wi:ADR-046-resource-store-redb", + "prerequisites" : [ + "ADR-046-resource-store-redb", + "ADR046-store-001" + ], + "specId" : "ADR-046-resource-store-redb", + "topologicalRank" : 5, + "validation" : "marker replacement, crash publication, backup/restore/upgrade tests", + "wave" : "W0" + }, + { + "blockers" : [], + "destinations" : [ + "`docs/specs/ADR-046-spec-set.json`, `docs/specs/ADR-046-work-items.json`, `docs/specs/ADR-046-implementation-graph.json`, `docs/specs/ADR-046-implementation-graph.md` (generated, committed non-member artifacts; named in `docs/specs/README.md`)" + ], + "detailedDesign" : "New `cargo run -p xtask -- spec-registry` subcommand parsing every spec's metadata table + work-item tables with a Markdown-table parser, emitting the two manifest JSON files; the `implementation-graph` generator then maps every member spec and every work item exactly once to a wave and file-disjoint parallel group, emits typed edges (`spec-depends-on`, `work-item-depends-on`, `implements-spec`, `shared-contract`, `file-overlap-order`), computes topological rank, and renders the Mermaid/table human view; output is deterministic with sorted keys and no timestamps or host paths", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-001", + "kind" : "work-item", + "owner" : "`packages/xtask` owner; no dependency on any other streamline item", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 12, + "validation" : "`spec_registry::parse_metadata_table`, `spec_registry::acyclic_dependency_graph`, `xtask_spec_registry_regenerates_clean`, `spec_registry_json_schema_matches_doc`, `implementation_graph::every_spec_and_work_item_mapped_once`, `implementation_graph::acyclic_and_wave_monotonic`, `implementation_graph::parallel_groups_are_file_disjoint`, `implementation_graph_regenerates_clean`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`docs/specs/schemas/*.schema.json` (Tier A: hand-authored-once canonical source checked into the tree, matching the frozen D010/D032/D075/D076/D080 decisions exactly); `packages/d2b-core/src/resource_schema/*.rs` (Tier B: the eventual Rust source of truth once ResourceType implementation exists, at which point the Tier A JSON becomes generated from Rust instead of hand-authored)" + ], + "detailedDesign" : "Tier A: author the schema JSON once per primitive directly from the already-frozen decision-register entries (D010, D032, D075, D076, D080); Tier B: `xtask gen-spec-schemas` derives the same JSON from real Rust `#[derive(JsonSchema)]`-equivalent types once they land, replacing the hand-authored Tier A source without changing its consumers Primary reuse disposition: `create`. Preserved source-plan detail: net-new Tier A schema source; later replace source with generated output from real d2b-core ResourceType types without changing consumers.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-002", + "kind" : "work-item", + "owner" : "ADR046-streamline-001; `d2b-core` owner (Tier B only)", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 13, + "validation" : "`spec_schema_check::canonical_field_set`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/bin/spec_schema_check.rs`; wired as a Layer-1 policy lint at `packages/d2b-contract-tests/tests/policy_spec_schema.rs`" + ], + "detailedDesign" : "Reuse the CommonMark parser already available to the Rust toolchain (the same class of dependency `xtask`'s existing `gen-*` commands already use for structured generation) to walk fenced blocks; classify each block by its enclosing heading (`## Nix authoring and configuration cleanup` vs. any other heading) to apply the current-evidence exclusion without a manual per-block marker Primary reuse disposition: `create`. Preserved source-plan detail: net-new; reuse only the established xtask dispatch pattern and Rust Markdown-parser dependency class.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-003", + "kind" : "work-item", + "owner" : "ADR046-streamline-001, ADR046-streamline-002", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001", + "ADR046-streamline-002" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 14, + "validation" : "`spec_schema_check::extract_fenced_blocks`, `spec_schema_check::canonical_field_set`, `policy_spec_schema` (Type 4/5)", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`docs/specs/providers/TEMPLATE.md` (committed, non-normative reference); `packages/xtask/src/bin/new_provider_dossier.rs` (`cargo run -p xtask -- new-provider-dossier --name `)" + ], + "detailedDesign" : "Scaffold generator emits the template pre-filled with the requesting Provider's name, D080 qualification-grammar examples, and the canonical state-Volume snippet; does not attempt to author Provider-specific semantic sections (those remain the dossier author's normative content) Primary reuse disposition: `create`. Preserved source-plan detail: net-new scaffold; reuse canonical schema/snippet source from ADR046-streamline-002.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-004", + "kind" : "work-item", + "owner" : "ADR046-streamline-001, ADR046-streamline-002", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001", + "ADR046-streamline-002" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 14, + "validation" : "`xtask_new_provider_dossier_scaffold`; the emitted scaffold must independently pass `spec_schema_check`/`work_item_validator` on first generation", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/policy_spec_vocabulary.rs`, `policy_spec_ownership.rs`, `policy_spec_finalizer_phase.rs`, `policy_spec_source_policy.rs`" + ], + "detailedDesign" : "Four focused lint files (one invariant family per file, matching the existing `packages/d2b-contract-tests/tests/policy_*.rs` one-concern-per-file convention) each scanning the registry output plus raw Markdown text for the specific violation patterns named in F2/F4/F5/F7/F9 Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lints over generated registry and raw Markdown.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-005", + "kind" : "work-item", + "owner" : "ADR046-streamline-001", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 13, + "validation" : "`policy_spec_vocabulary`, `policy_spec_ownership`, `policy_spec_finalizer_phase`, `policy_spec_source_policy` (all Type 5)", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-store-redb/tests/provider_state_graph.rs` (or the eventual crate implementing Zone resource storage)" + ], + "detailedDesign" : "A graph-walk over the real controller-registration/resource-ownership index (not Markdown) asserting the D076/D086/D087 invariants; the doc-level half of this check (dossier text describing the invariant correctly) is covered now by ADR046-streamline-005's `policy_spec_ownership` Primary reuse disposition: `create`. Preserved source-plan detail: net-new future runtime graph checker.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-006", + "kind" : "work-item", + "owner" : "The Zone resource-store implementation work item (not yet filed; blocked on W0-W10 implementation request per D024); ADR046-streamline-001 for the doc-level invariant source", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 13, + "validation" : "New test asserting: zero `ProviderStateSet` rows in the store; every state Volume corresponds to a declared component namespace; no empty identity-only Volume; no bootstrap state Volume or bootstrap-storage mechanism exists", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/policy_effectport_boundary.rs`, `policy_worker_bus_boundary.rs`" + ], + "detailedDesign" : "Static source scan (import-graph/symbol-use analysis) over compiled Provider crates, mirroring the existing `policy_broker_dispositions.rs` pattern already proven against `packages/d2bd`/`packages/d2b-priv-broker` Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing policy-broker lint pattern to Provider/Worker boundary checks.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-007", + "kind" : "work-item", + "owner" : "The Provider-toolkit/EffectPort implementation work item (not yet filed; blocked on the W0-W10 implementation request); ADR046-streamline-001 for the doc-level invariant source", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 13, + "validation" : "New tests asserting zero direct-broker-import matches in any Provider crate; zero disallowed-capability matches in any Worker binary", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/policy_work_items.rs`" + ], + "detailedDesign" : "Consumes `docs/specs/ADR-046-work-items.json` (generated by ADR046-streamline-001); flags duplicate IDs, missing/placeholder fields, and dangling cross-references Primary reuse disposition: `create`. Preserved source-plan detail: net-new validator consuming ADR046-streamline-001 output.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-008", + "kind" : "work-item", + "owner" : "ADR046-streamline-001", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 13, + "validation" : "`work_item_validator::unique_ids`, `work_item_validator::required_fields`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`docs/specs/ADR-046-provider-catalog.md` (generated, committed)" + ], + "detailedDesign" : "`cargo run -p xtask -- gen-provider-catalog` reads the registry and decision-register D043-D049 rows and renders the table; drift-gated like the existing `gen-schemas`/`gen-migration-ledger` pattern Primary reuse disposition: `create`. Preserved source-plan detail: net-new generator consuming registry and decision-register rows.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-009", + "kind" : "work-item", + "owner" : "ADR046-streamline-001", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 13, + "validation" : "`provider_catalog::frozen_family_membership`, `provider_catalog_matches_frozen_families`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`tests/tools/reconcile-stale-base.sh` (reporting only) plus a documented `git town sync`/`git town` restack procedure this report feeds into, since this repository does not yet use Git Town and F1/F11 show plain rebase/cherry-pick was insufficient to prevent duplicate reconciliation" + ], + "detailedDesign" : "`git merge-base ` plus `git diff --name-only` intersection reporting; emits a JSON report (branch, divergence SHA, commits-behind count, file-overlap list) consumed by ADR046-streamline-013's ready/launched/blocker report Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new read-only git report.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-010", + "kind" : "work-item", + "owner" : "ADR046-streamline-001", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 13, + "validation" : "`reconcile_helper_reports_stale_base`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/bin/handoff_manifest.rs` (schema/validator only); manifest instances are per-round artifacts referenced from PR bodies, not committed to the tree (consistent with this repository's \"Screenshot and visual artifact hygiene\"-style external-evidence posture)" + ], + "detailedDesign" : "A small JSON schema (`assigned_files: [string]`, `commit_sha: string`, `test_result: {command, exit_code}`, `base_sha: string`) plus a validator rejecting incomplete records; no attribution field for any AI/tool/model, consistent with this repository's commit/PR-body attribution rule Primary reuse disposition: `create`. Preserved source-plan detail: net-new schema and validator.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-011", + "kind" : "work-item", + "owner" : "No prerequisite; `packages/xtask` schema/validator owner", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 12, + "validation" : "`handoff_manifest_rejects_incomplete_record`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`tests/tools/import-task-db-consistency.sh`" + ], + "detailedDesign" : "Reads the session database's todo table, resolves each `done` row's expected file set against `git log --name-only` for the current branch, and reports mismatches; read-only, no database or git mutation Primary reuse disposition: `create`. Preserved source-plan detail: net-new read-only importer/checker.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-012", + "kind" : "work-item", + "owner" : "ADR046-streamline-011 (shares the manifest schema for commit-SHA binding)", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-011" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 13, + "validation" : "New test seeding a synthetic task DB with one orphaned \"done\" row and confirming the importer flags it", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`tests/tools/anti-serialization-report.sh`" + ], + "detailedDesign" : "Cross-references the spec registry's dependency graph (ADR046-streamline-001) against the set of currently open `adr0046-*` branches and the reconcile helper's (ADR046-streamline-010) staleness report; emits a per-scope ready/launched/blocked classification Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new report combining registry, branch, and staleness data.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-013", + "kind" : "work-item", + "owner" : "ADR046-streamline-001, ADR046-streamline-010", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001", + "ADR046-streamline-010" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 14, + "validation" : "New test seeding a synthetic registry + branch list with one ready-but-unlaunched scope and confirming it is reported", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`tests/tools/run-layer.sh` extension (this repository already has `tests/tools/run-layer.sh` and `layer1-jobs.py` bounded-parallelism precedent) plus fake `EffectPort`/`ResourceClient` stub crates under `packages/d2b-provider-toolkit-fakes/`" + ], + "detailedDesign" : "Extend the existing `layer1-jobs.py` bounded-parallel-shard pattern with a per-Provider-crate shard definition; fake dependency crates implement the same `EffectPort`/`ResourceClient` trait surface with in-memory stand-ins Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt bounded-parallel shard pattern; add net-new fake EffectPort/ResourceClient crates.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-014", + "kind" : "work-item", + "owner" : "The Provider-toolkit implementation work item (blocked on the W0-W10 implementation request)", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 12, + "validation" : "New integration test proving a single Provider crate's test run succeeds with zero other Provider crates built", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "Shared `packages/xtask` regeneration-conflict-detection helper consumed by every `gen-*`/`spec-registry` subcommand" + ], + "detailedDesign" : "Each generator hashes its input set before writing; if a concurrent regeneration on a sibling branch (detected via the reconcile helper's file-overlap report) would produce a different hash for the same output path, the generator refuses to overwrite and reports the divergent input set Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing generator/drift-gate pattern with net-new input-hash conflict detection.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-015", + "kind" : "work-item", + "owner" : "ADR046-streamline-001, ADR046-streamline-009, ADR046-streamline-002 (Tier A part)", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001", + "ADR046-streamline-002", + "ADR046-streamline-009" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 14, + "validation" : "New test simulating two divergent input sets producing conflicting output for the same generated file and confirming the conflict is reported, not silently resolved", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/policy_no_leaked_decision_prefix.rs`; invoked as a required precondition script `tests/tools/pre-panel-gate.sh`" + ], + "detailedDesign" : "Scans registry output plus raw Markdown for `decision-required`, `TBD`, `TODO`, and any decision-ID-shaped token (`[A-Z]+-\\d+`) not matching the canonical `D\\d+` register numbering Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lint and precondition script.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-016", + "kind" : "work-item", + "owner" : "ADR046-streamline-001, ADR046-streamline-008", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001", + "ADR046-streamline-008" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 14, + "validation" : "`policy_no_leaked_decision_prefix`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`docs/specs/ADR-046-streamline-evidence-commands.md` (a follow-up artifact outside this task's file scope; tracked here as a required future addition, not authored by this spec)" + ], + "detailedDesign" : "A plain Markdown list of the exact commands used to produce each F1-F17 citation above (already reproduced verbatim in this spec's prose), organized by friction ID, so re-running them is copy-paste rather than re-derivation Primary reuse disposition: `adapt`. Preserved source-plan detail: extract/adapt the already-cited evidence commands into a net-new documentation artifact.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-017", + "kind" : "work-item", + "owner" : "No prerequisite; `docs/specs` evidence-command documentation owner", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 12, + "validation" : "Manual reviewer re-run of at least one command per friction ID during panel, confirmed to reproduce the cited count/SHA", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`tests/tools/worktree-disk-report.sh`" + ], + "detailedDesign" : "`git worktree list --porcelain` plus `du -sh` per worktree's `packages/target/`; output is a report only, never a `git worktree remove`/`rm -rf` invocation Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing disk-hygiene reporting concepts into a net-new non-mutating script.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-018", + "kind" : "work-item", + "owner" : "ADR046-streamline-010 (shares branch-staleness detection)", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-010" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 14, + "validation" : "New test confirming the reporting script's exit code and output never include a mutating command string, and confirming it correctly flags a fixture worktree with a real (non-symlink) `packages/target/`", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/bin/terminology_check.rs` (`cargo run -p xtask -- terminology-check`)" + ], + "detailedDesign" : "Parses every \"Current-code fit\" table's \"Current anchor\" cell for symbol/crate names, cross-references against the migration map's disposition rows, and flags any current-source citation absent from the map Primary reuse disposition: `create`. Preserved source-plan detail: net-new terminology checker over existing mapping specs.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-019", + "kind" : "work-item", + "owner" : "ADR046-streamline-001", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 13, + "validation" : "New test seeding a fixture spec citing a current-source symbol absent from the migration map and confirming it is flagged", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/policy_test_placement.rs`" + ], + "detailedDesign" : "Scans hermetic-tier Rust sources for banned API surfaces (`std::process::Command`, socket/container/DBus/systemd helpers, `#[ignore]`) and for `integration/`-only markers appearing outside `integration/` Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lint.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-020", + "kind" : "work-item", + "owner" : "ADR046-streamline-001", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 13, + "validation" : "Fixture crate with an intentional process-spawning hermetic test is rejected naming the file/line; a correct crate passes", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/policy_test_determinism.rs`" + ], + "detailedDesign" : "Scans hermetic sources for banned time/sleep APIs and asserts the deterministic fake-clock/RNG fixtures from the toolkit are used; classified crypto/property exceptions are allow-listed by explicit name with a declared per-test budget Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lint layered on ADR046-streamline-020.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-021", + "kind" : "work-item", + "owner" : "ADR046-streamline-020", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-020" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 14, + "validation" : "Fixture hermetic test using `thread::sleep` is rejected; a classified crypto test on the allow-list passes", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/test_runtime_ledger.rs` (shared with `ADR046-delivery-007`)" + ], + "detailedDesign" : "Parses per-test JSON timings, aggregates per test/crate/shard, compares against pinned budgets and the previous ledger, and fails on regression beyond the threshold Primary reuse disposition: `adapt`. Preserved source-plan detail: share/adapt ADR046-delivery-007 timing-ledger implementation for this gate.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-022", + "kind" : "work-item", + "owner" : "ADR046-delivery-007", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-delivery-007" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 14, + "validation" : "Synthetic timing regression fails the gate; ledger output is deterministic and machine-readable", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/bin/legacy_test_retirement.rs` (`cargo run -p xtask -- legacy-test-retirement`)" + ], + "detailedDesign" : "Reads the migration map's disposition rows, cross-references the live `tests/layer1-jobs.json`/gate manifests, and emits the delete set plus an absence assertion; never deletes automatically — it produces the gated checklist and the failing test that proves incomplete retirement Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new generator reading existing migration rows and gate manifests without mutating them.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-023", + "kind" : "work-item", + "owner" : "ADR046-streamline-008, ADR046-streamline-022", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-008", + "ADR046-streamline-022" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 15, + "validation" : "Fixture with a replaced behavior whose old selector still appears in `tests/layer1-jobs.json` fails; once removed, the absence assertion passes", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/bin/implementation_graph.rs` (`cargo run -p xtask -- implementation-graph`); folds the spec-set and test-runtime emitters into the one `xtask` dispatch" + ], + "detailedDesign" : "Deterministic (sorted keys, no timestamps/host paths); regenerated after any spec or work-item change and after the two manifests; a drift gate runs it and `git diff --exit-code`. Retains the duplicate-generator findings for the spec-set manifest and the test-runtime ledger and reconciles them to the single `xtask` path rather than leaving three independent emitters Primary reuse disposition: `create`. Preserved source-plan detail: net-new generator that consumes the two manifests and the wave topology; folds the duplicate spec-set/test-runtime emitters into one canonical path.", + "entryContracts" : [ + "ADR-046-streamline" + ], + "exitGate" : "ADR046-W7 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-streamline-024", + "kind" : "work-item", + "owner" : "ADR046-streamline-001", + "parallelGroup" : "wi:ADR-046-streamline", + "prerequisites" : [ + "ADR-046-streamline", + "ADR046-streamline-001" + ], + "specId" : "ADR-046-streamline", + "topologicalRank" : 13, + "validation" : "`implementation_graph::every_spec_and_work_item_mapped_once`; `implementation_graph::acyclic_and_wave_monotonic`; `implementation_graph::parallel_groups_are_file_disjoint`; `implementation_graph_regenerates_clean`; duplicate-generator reconciliation asserts one canonical emitter path for spec-set and test-runtime", + "wave" : "W7" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-core/src/manifest.rs`, `packages/d2b-provider-system-core/src/audit.rs`, and `packages/d2b-provider-system-core/tests/provider_boundary.rs`" + ], + "detailedDesign" : "Compile the system-core Provider manifest, empty closed config schema, Host/User component descriptors, and empty state-namespace declaration. The manifest binds both library handlers to the fixed `d2b-core-controller` bootstrap process without declaring either handler as a Process resource. The audit adapter emits one bounded, redacted `ResourceReconciled` record after each Host/User reconcile. The boundary rejects Provider config fields and proves that handler call paths neither write `Provider.status` nor emit `ProcessEffect`; core-controller infrastructure owns the former and `ADR046-exec-006`/`ADR046-exec-007` own the latter. `ADR046-pstate-012` remains the owner of generic optional-state admission; this item only declares system-core's empty state set. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt descriptor/audit patterns; implement the v3 Provider-specific boundary.", + "entryContracts" : [ + "ADR-046-provider-system-core" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-system-core-001", + "kind" : "work-item", + "owner" : "`ADR046-provider-001`, `ADR046-exec-003`, `ADR046-exec-004`, `ADR046-exec-005`, `ADR046-pstate-012`, `ADR046-telem-001`, and `ADR046-audit-001`; `Provider/system-core` owner", + "parallelGroup" : "wi:ADR-046-provider-system-core", + "prerequisites" : [ + "ADR-046-provider-system-core", + "ADR046-audit-001", + "ADR046-exec-003", + "ADR046-exec-004", + "ADR046-exec-005", + "ADR046-provider-001", + "ADR046-pstate-012", + "ADR046-telem-001" + ], + "specId" : "ADR-046-provider-system-core", + "topologicalRank" : 13, + "validation" : "`config_schema_empty_only`, `provider_status_not_written_by_handlers`, `provider_state_set_empty`, `host_no_process_effect_emitted`, `host_resource_reconciled_audit`, and `user_resource_reconciled_audit`; manifest golden vector proves no Process descriptor and no state namespace for either handler.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-systemd/src/controller.rs` (async reconcile loop), `src/launch.rs` (launch algorithm via effect port), `src/effect_port.rs` (`SystemdProcessEffectPort` trait + test double), `src/adoption.rs` (adoption algorithm), `src/sandbox.rs` (SandboxSpec → unit property compiler)" + ], + "detailedDesign" : "Full §6 launch algorithm (effect port integration); §7 EphemeralProcess; §8 restart/adoption (effect port `locate_by_identity`); §9 drain (effect port `stop`/`kill`); §10 sandbox compilation; §11 bus services; ProviderSupervisor LaunchTicket integration Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-system-systemd" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-systemd-001", + "kind" : "work-item", + "owner" : "`ADR046-process-002`; Process contracts/supervisor owner; effect port interface owner", + "parallelGroup" : "wi:ADR-046-provider-system-systemd", + "prerequisites" : [ + "ADR-046-provider-system-systemd", + "ADR046-process-002" + ], + "specId" : "ADR-046-provider-system-systemd", + "topologicalRank" : 11, + "validation" : "`tests/conformance.rs` (shared conformance kit); `tests/identity_binding.rs` (InvocationID/cgroup/MainPID/start-time golden vectors via mock effect port); `tests/adoption.rs` (quarantine/identity-mismatch cases); `tests/restart.rs` (backoff/maxRestarts); latency assertions (p95 ≤5 ms hint→handler, ≤20 ms commit→effect port `start` call)", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/` (Provider ResourceSpec emission for `system-systemd`); `packages/d2b-provider-system-systemd/` package derivation and catalog entry" + ], + "detailedDesign" : "§16 Nix configuration; `d2b.artifacts.system-systemd` catalog entry; Provider and Process ResourceSpec emission; eval/build validation rules; drift gate update (`xtask gen-nix-options` + `make test-drift`)", + "entryContracts" : [ + "ADR-046-provider-system-systemd" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-systemd-002", + "kind" : "work-item", + "owner" : "`ADR046-systemd-001`; Nix/package integrator", + "parallelGroup" : "wi:ADR-046-provider-system-systemd", + "prerequisites" : [ + "ADR-046-provider-system-systemd", + "ADR046-systemd-001" + ], + "specId" : "ADR-046-provider-system-systemd", + "topologicalRank" : 12, + "validation" : "`tests/unit/nix/cases/provider-system-systemd.nix` (eval-time validation); `tests/unit/gates/drift-check.sh` covers generated option schema", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-system-systemd/tests/conformance.rs`, `tests/fault.rs`, `tests/ephemeral.rs`, `tests/sandbox_compile.rs`; `integration/host_scenario.rs`, `integration/guest_scenario.rs`" + ], + "detailedDesign" : "Full §19 test/integration requirements Primary reuse disposition: `adapt`. Preserved source-plan detail: copy/adapt.", + "entryContracts" : [ + "ADR-046-provider-system-systemd" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-systemd-003", + "kind" : "work-item", + "owner" : "`ADR046-systemd-001`; conformance kit / test infrastructure", + "parallelGroup" : "wi:ADR-046-provider-system-systemd", + "prerequisites" : [ + "ADR-046-provider-system-systemd", + "ADR046-systemd-001" + ], + "specId" : "ADR-046-provider-system-systemd", + "topologicalRank" : 12, + "validation" : "All conformance vectors pass; all fault injection scenarios reach expected phase/condition; all §19 Host and Guest test scenarios pass", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-telemetry/src/{trace_context.rs,audit_hash.rs,emitter.rs,meter_registry.rs,metric_label_policy.rs,redaction_guard.rs}`" + ], + "detailedDesign" : "`d2b-telemetry` provides: (1) `TraceContext` / `AuditHash` / `AuditChainLink` extracted unchanged; (2) `BoundedEmitter`: `tracing`-subscriber layer that serializes span/metric events into compact frames and writes them over a private Unix datagram socket to the `observability-otel` Provider — no `opentelemetry_sdk` dependency; (3) the canonical closed `METRIC_LABEL_POLICY`, structural descriptor/data-point/exemplar validator, exact forbidden-key/suffix predicates, and non-serializable resource-identity canary matcher used by emitter defense in depth and the mandatory collector ingress gate; (4) `RedactionGuard` span wrapper that asserts the v3 resource attribute allowlist at span creation. No OTEL SDK in this crate. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract unchanged (`TraceContext`, `AuditHash`, `AuditChainLink`); adapt (`OperationId`/`CorrelationId` for v3 record contract); add bounded emitter.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-telem-001", + "kind" : "work-item", + "owner" : "W0/W1a; telemetry crate owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 10, + "validation" : "Unit test for `RedactionGuard` attribute gate; unit test for `BoundedEmitter` ring-full drop and FIFO drain; table tests for `METRIC_LABEL_POLICY` exact keys, suffixes, and `metadata.name`/UID/ResourceRef identity canaries; `policy_telemetry_redaction.rs::startup_tracing_avoids_host_path_fields` port; assert `config_source = \"realm-controllers\"` absent; assert no `opentelemetry_sdk` dependency in `d2b-telemetry` Cargo.toml", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-store-redb/src/metrics.rs`, `packages/d2b-resource-store-redb/src/tracing.rs`" + ], + "detailedDesign" : "Instrument the store actor, write/read/group-commit paths with the metric inventory from this spec via `d2b-telemetry` `BoundedEmitter`. Emit `d2b.store.*` spans. The p95 `d2b_store_write_duration_seconds` hard target (≤10 ms) feeds the benchmark fixture. No `vm` label; `resource_type` label only from closed catalog. No OTEL SDK in the store crate. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt bucket boundary constants (rename; remove `vm` labels); replace hand-rolled `Registry` with `d2b-telemetry` `BoundedEmitter` meter API.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-telem-002", + "kind" : "work-item", + "owner" : "ADR046-telem-001 + ADR046-store-001; store owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-store-001", + "ADR046-telem-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "p95 write ≤10 ms benchmark fixture; metric inventory policy test asserting no `vm` label; assert old `d2b_daemon_vm_state` shape absent", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-api/src/metrics.rs`, `packages/d2b-session/src/metrics.rs`, `packages/d2b-bus/src/metrics.rs`" + ], + "detailedDesign" : "Instrument resource API verb dispatch, watch delivery, bus route resolution, and session handshake/reconnect per the metric/span catalog in this spec. Propagate `TraceContext` from incoming bus request to store write transaction span as child context. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `TraceContext` protobuf codec for v3 resource API framing; adapt routing metrics patterns.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-telem-003", + "kind" : "work-item", + "owner" : "ADR046-telem-001 + ADR046-session-001 + ADR046-bus-001; session/bus owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-bus-001", + "ADR046-session-001", + "ADR046-telem-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "API request metric inventory test; session profile/outcome label cardinality gate; bus direction label gate; assert no `realm` field in span attributes", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/metrics.rs`, `packages/d2b-core-controller/src/tracing.rs`" + ], + "detailedDesign" : "Emit `d2b.controller.hint` span at the instant the post-commit dispatcher fires; emit `d2b.controller.reconcile` child span at handler entry. Interval = p95 ≤5 ms target. `handler` label from closed set; no resource name labels. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt bucket patterns; adapt trace-context-in-reconcile pattern from `allocator_engine.rs`.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-telem-004", + "kind" : "work-item", + "owner" : "ADR046-telem-001 + ADR046-core-001; core-controller owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-core-001", + "ADR046-telem-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "`hint_to_handler_latency` benchmark with p95 ≤5 ms assertion; closed `handler` label set gate", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-supervisor/src/metrics.rs`, `packages/d2b-provider-supervisor/src/tracing.rs`" + ], + "detailedDesign" : "`d2b_process_launch_duration_seconds`: start = instant Process controller receives commit-to-Ready hint; end = first OS spawn call (clone3 or systemd unit start). This implements p95 ≤20 ms. `provider` label replaces `vmm`/`RunnerRole` with the closed set `{minijail,systemd}`. No `vm` name label. A separate `d2b_process_ready_duration_seconds` histogram covers launch-attempt → readiness signal (not a hard target). Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt launch histogram bucket constants; rename `vm` label to no label (process identity in resource attributes); rename `vmm`/`RunnerRole` → `provider` closed enum.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-telem-005", + "kind" : "work-item", + "owner" : "ADR046-telem-001 + ADR046-process-001; Process Provider owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-process-001", + "ADR046-telem-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "`commit_to_launch_latency` benchmark with p95 ≤20 ms assertion; assert no `vm` label in process metrics; `vmm→provider` label rename gate", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-observability-otel/src/`, `nixos-modules/components/observability/` (adapted files)" + ], + "detailedDesign" : "`Provider/observability-otel` is an **ordinary optional non-bootstrap Process** (not counted toward the ≤64 MiB mandatory core aggregate). It owns: (1) per-Zone datagram receiver socket at `$ZONE_STATE/telemetry/emitter.sock` (drains frames from core emitters) and OTLP/gRPC Unix socket at `$ZONE_STATE/telemetry/otlp.sock`; (2) the full OTEL SDK with OTLP exporter — only this process links `opentelemetry_sdk`; (3) OTel Collector pipeline per Zone and per Host; (4) vsock OTLP forwarding to obs Zone (replaces socat-based `OtelHostBridgeArgvInputs`); (5) D096 import-stream ingest; (6) one structural `METRIC_LABEL_POLICY` gate shared by Unix emitter, OTLP Unix, OTLP/vsock, and import-stream metrics before aggregation, queueing, batching, retry, or export, with bounded non-echoing errors/quarantine/backpressure; (7) SigNoz stack Nix adapted from `stack.nix` with per-Zone `ingressSources` replacing per-VM `vmName`; (8) journald scrape (optional, disabled by default); (9) self-metrics endpoint. Trusted producer identity is stamped only into allow-listed OTEL Resource attributes; audit remains separate. Zone/controller startup does not wait for this Provider. If absent or unready, Zone health is `Degraded` (not `Failed`). Readiness: socket exists and first drain cycle completes successfully. `d2b.observability.host.identityName` option preserved; `vmName` in `ingressSources` populated from Zone name. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt Nix pipeline shape (replace per-VM `vmName` with per-Zone naming); adapt `OtelHostBridgeArgvInputs` vsock forwarding to native OTLP/gRPC-over-vsock; adapt readiness gate pattern (`OtelHostBridgeReadiness::Ready` → Provider phase `Ready`); adapt `ingressSources` per-VM → per-Zone.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-telem-006", + "kind" : "work-item", + "owner" : "ADR046-process-001 + ADR046-provider-001; `observability-otel` Provider owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-process-001", + "ADR046-provider-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 10, + "validation" : "`emitter_socket_receive`, `emitter_ring_drains_on_socket_available`, `emitter_ring_drop_on_overflow`, table-driven `ingress_metric_policy` across all four ingress adapters, `no_vm_label_in_metrics`, and `zone_startup_proceeds_without_provider` tests; adapted `policy_observability.rs` tests (retain `loki_native_otel_resource_attributes` and SigNoz-only backend assertions); adapted `minijail_relay_otel.rs` shape test for Provider-managed runner", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-observability-otel/src/nix/journald.nix` (new Nix fragment)" + ], + "detailedDesign" : "`d2b.zones..observability.journald.enable = false` (default). When enabled: journald receiver follows `z-/*` and `s-/*` cgroup filters. Collector applies redaction: drops `MESSAGE` credential/path patterns, `_CMDLINE`, `_EXE`, `INVOCATION_ID`. Current `scrapeJournal` host option is preserved unchanged. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt journald receiver config for per-Zone cgroup filter (`z-/*`, `s-/*`).", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-telem-007", + "kind" : "work-item", + "owner" : "ADR046-telem-006; Nix/observability owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-telem-006" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "Nix eval test: filter expression set when enabled; test that `_CMDLINE` and `INVOCATION_ID` appear in drop list", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contract-tests/tests/policy_telemetry_redaction.rs` (new); updated `policy_observability.rs`; updated `policy_metrics.rs`" + ], + "detailedDesign" : "(1) Extend `loki_native_otel_resource_attributes` allowlist to include `d2b.zone`, `d2b.provider`, `d2b.component`, `service.version`. (2) Add redaction lint: scan all v3 instrumentation call sites for `realm`, `workload_id`, `node_id`, `vm` (as label key), `path`, `socket`, `argv`, `pid`, `exe`. (3) Add structural metric-label policy lint: parse every v3 `MetricDescriptor`, require each label key and value domain to exist in the closed `METRIC_LABEL_POLICY`, reject exact keys `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `network`, `network_name`, and `link_name_hash`, reject resource-name-derived key suffixes `*_name`, `*_name_hash`, `*_name_digest`, and `*_uid`, and prove `metadata.name`, UID, ResourceRef, and resource-identity canaries never enter label values. Fixed semantic labels remain allowed only with closed domains. (4) Prove the observability Provider's Unix-emitter, OTLP-Unix, OTLP/vsock, and import-stream adapters all invoke that validator before queue/batch/export; assert whole-frame rejection, bounded non-echoing error classes/quarantine, and policy-before-capacity backpressure. (5) Assert the `d2b.zone` Resource attribute remains present and audit is unchanged. (6) Add bucket boundary gates for 5 ms and 20 ms. (7) Retain: `startup_tracing_avoids_host_path_fields`; SigNoz-only backend assertion; `tempo_guest_collector_shape`; `config_source = \"realm-controllers\"` absence gate. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt and extend; keep existing tests; add new policy gates.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-telem-008", + "kind" : "work-item", + "owner" : "ADR046-telem-001; policy/contract-tests owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-telem-001" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "These tests are their own validation artifact", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/resources.nix` (uniform `d2b.zones..resources` schema-aware option; `spec.*` option types generated from `ResourceTypeSchema` for each `type`); `nixos-modules/resources-bundle.nix` (ADR-only: sorted integrity-pinned bundle derivation)" + ], + "detailedDesign" : "(1) Implement `d2b.zones..resources = lib.mkOption { type = lib.types.attrsOf (schemaAwareResourceSubmodule); }` where the submodule, given `config.type`, loads the registered `ResourceTypeSchema` and generates `spec.*` option types from it. For `type = \"Provider\"`, `spec.config.*` options are generated from the signed Provider schema for the package identified by `spec.artifactId` (see ADR-046-provider-model-and-packaging). No second bespoke vocabulary; `spec` fields mirror the canonical JSON fields exactly. (2) `resources-bundle.nix` derivation: serialize each resource to canonical sorted JSON (keys alphabetically sorted at every level); sort resources by `(type, name)`; compute generation digest; emit `zone-resources-.json` as Nix store output. Publication handler sets `metadata.managedBy = \"configuration\"` and `metadata.configurationGeneration` on activation — these fields are NOT authored in Nix. (3) `status`, UID, generation, revision, and timestamps are absent from Nix authoring; core fills them. Primary reuse disposition: `adapt`. Preserved source-plan detail: Implement uniform `d2b.zones..resources. = { type; spec; }` option with schema-driven `spec.*` generated option types; adapt option submodule pattern from `options-realms.nix`; adapt pipeline shape from `{host,stack,guest}.nix`; emit canonical sorted ResourceSpec JSON from `resources-bundle.nix`.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-telem-009", + "kind" : "work-item", + "owner" : "W0/W1a; Nix integrator (adapts `nixos-modules/options-observability.nix` and `options-realms.nix`)", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 10, + "validation" : "`eval_rejects_unknown_type`, `eval_rejects_invalid_emitter_ring_size`, `eval_rejects_unknown_provider_settings`, `eval_rejects_inline_secret_in_settings`, `eval_rejects_unresolved_credential_ref`, `eval_rejects_duplicate_resource_name` nix-unit cases; `bundle_is_sorted_canonically`, `bundle_digest_is_deterministic`, `bundle_contains_no_secret_values`, `bundle_schema_validates_against_provider_schema` contract tests", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/resources-bundle.nix` (build-time validation step 4 in the `resources-bundle` derivation); `packages/d2b-core-controller/src/configuration.rs` (runtime activation checks)" + ], + "detailedDesign" : "Build-time: (1) For each `Provider` resource, fetch the `resourceTypeSchema` output from the package; validate `settings` JSON against the JSON Schema; fail the build on schema mismatch or unknown fields. (2) Assert no resource spec field contains a bare secret/path/argv (forbidden-field pattern from `startup_tracing_avoids_host_path_fields`). Runtime: (3) Core-controller re-validates Provider package identity (per ADR-046-provider-model-and-packaging) against the installed package; resolves Credential refs; checks conflict/bounds; rejects with closed-enum `generation-rejected` reason on any failure; no partial activation. (4) Provider schema mismatch between the bundle's schema and the installed Provider's live schema → reject, emit `generation-rejected{reason=\"package-identity-mismatch\"}`. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt `startup_tracing_avoids_host_path_fields` forbidden-field pattern for bundle schema gate; adapt `ProviderBindingV2` non-exhaustive contract for Provider-specific settings schema fingerprint.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-telem-010", + "kind" : "work-item", + "owner" : "ADR046-telem-009 + ADR046-telem-006 + ADR046-store-001; schema/validation owner", + "parallelGroup" : "wi:ADR-046-telemetry-audit-and-support", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-store-001", + "ADR046-telem-006", + "ADR046-telem-009" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 11, + "validation" : "`bundle_schema_validates_against_provider_schema` bundle contract test; `generation_rejected_emits_audit_record` cleanup contract test with a `schema-validation-failed` reason; add a nix-unit case `eval_rejects_unknown_fields_against_signed_schema` that runs the bundle derivation with a schema mismatch and asserts build failure", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/{configuration.rs, ownership.rs}`" + ], + "detailedDesign" : "(1) On new generation activation, every stored `managedBy=configuration` resource absent from the new configured set receives `deletionRequestedAt` plus `deletion-pending`; controller/API-managed resources are untouched. (2) Activation returns after durable intent queueing and does not wait for cleanup. (3) The ownership handler drives child-before-parent finalizers. (4) When finalizers clear, one atomic store transaction writes the `Deleted` revision/change event and removes the row and indexes. After commit, the audit subsystem appends `ResourceMutation{event=\"deleted\", trigger=\"config-cleanup\"}` from that revision using a dedup/exactly-once recovery key; audit append is not part of the store transaction. (5) Stall detection sets `cleanup-stalled` without force-removing finalizers. (6) Prior generations use count retention, default 3 and range 1..16, with no TTL. (7) Core sets `managedBy`/`configurationGeneration` in persisted resources; input bundles omit both. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt hash-chain append from `daemon_audit.rs` for `ResourceMutation{trigger=\"config-cleanup\"}` records; adapt prior-generation retention window from `manifest.nix` pattern.", + "entryContracts" : [ + "ADR-046-telemetry-audit-and-support" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-telem-011", + "kind" : "work-item", + "owner" : "ADR046-telem-009 + ADR046-telem-010 + ADR046-audit-001 + ADR046-store-001; core-controller owner", + "parallelGroup" : "wi:core-config-hub:w5", + "prerequisites" : [ + "ADR-046-telemetry-audit-and-support", + "ADR046-audit-001", + "ADR046-core-001", + "ADR046-store-001", + "ADR046-telem-009", + "ADR046-telem-010" + ], + "specId" : "ADR-046-telemetry-audit-and-support", + "topologicalRank" : 12, + "validation" : "All tests in \"Configuration-owned cleanup contract tests\" subsection; additionally: `managedby_configuration_set_on_activated_resources`, `controller_created_resources_have_managedby_controller`, `absent_resource_receives_delete_on_new_generation`, `deletion_sets_deletionrequestedat_not_phase`, `final_deletion_is_atomic`, `cleanup_does_not_touch_controller_children`, `pending_cleanup_condition_set_on_zone`, `zone_is_degraded_not_failed_during_cleanup`, `pending_cleanup_cleared_after_deletion_completes`, `prior_generation_retained_count_based`, `rollback_schedules_delete_for_new_generation_resources`, `audit_segments_preserved_on_provider_delete`, `cleanup_stall_condition_set`, `generation_rejected_emits_audit_record`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-azure-relay/src/relay_transport.rs`" + ], + "detailedDesign" : "Adapt `RelayStream` as relay transport service process; expose named opaque byte stream on the `transport-service` Unix endpoint; add 2-byte length-prefixed framing; preserve credential redaction; TLS/WebSocket state stays in-process — only Noise record bytes traverse the named stream; register named stream with d2b-bus as `TransportHandle`; transport descriptor: `attachment_support: false`, `locality: Remote`, `atomic: false`; expose `OpenTransport`/`CloseTransport`/`ObserveTransport` interface to core; long-lived service process multiplexes sessions internally Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-transport-azure-relay" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-relay-001", + "kind" : "work-item", + "owner" : "W0 shared contract root; ComponentSession transport adapter owner", + "parallelGroup" : "wi:ADR-046-provider-transport-azure-relay", + "prerequisites" : [ + "ADR-046-provider-transport-azure-relay" + ], + "specId" : "ADR-046-provider-transport-azure-relay", + "topologicalRank" : 11, + "validation" : "`tests/fake_relay_transport.rs`: connect/accept, framing, credential redaction, named stream roundtrip; `tests/listener_sender_conformance.rs`: named stream contract; Noise KK binding; relay identity exclusion", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-azure-relay/src/credential_client.rs`" + ], + "detailedDesign" : "Async Credential KK session client for service components (service processes have d2b-bus access, enabling Credential KK; workers do not); child core validates the same-Zone refs in `spec.transportCredentials`, selects the unique listener or sender ref by Credential audience, and supplies it to the child-local Provider service inside its gateway Guest; raw credential bytes are held in zeroizing memory inside the gateway Guest, presented to Azure Relay, then immediately zeroized; no Credential ref or byte crosses a Zone or Guest boundary, and byte delivery between the Credential Provider and consuming service remains inside the protected KK session; the parent allocator receives neither refs nor credentials and keeps only sealed route state; redacted Debug; no credential bytes in logs/audit/OTEL; the child Zone's core ProviderDeployment creates a private persistent Volume (per ADR-046-provider-state) for each component before its Process starts — the transport Provider does not own or create these Volumes; `Provider/volume-local` reconciles them; `migrationPolicy: none` means no migration worker is ever spawned; no relay auth token, WebSocket handle, session key, or credential byte is written to that Volume; all relay session state remains transient in-process memory", + "entryContracts" : [ + "ADR-046-provider-transport-azure-relay" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-relay-002", + "kind" : "work-item", + "owner" : "ADR046-transport-relay-001; Credential KK session; ComponentSession/d2b-bus owner", + "parallelGroup" : "wi:ADR-046-provider-transport-azure-relay", + "prerequisites" : [ + "ADR-046-provider-transport-azure-relay", + "ADR046-transport-relay-001" + ], + "specId" : "ADR-046-provider-transport-azure-relay", + "topologicalRank" : 12, + "validation" : "`tests/credential_redaction.rs`: credential bytes never reach any Debug/log/audit/OTEL path; `src/tests/integration/credential_delivery.rs`: end-to-end credential delivery using injected fake Credential effect port", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-azure-relay/src/reconnect.rs`" + ], + "detailedDesign" : "Relay service responds to `CloseTransport`+`OpenTransport` cycle from core; core owns reconnect policy and backoff scheduling; relay service tears down the current WebSocket when core calls `CloseTransport` and establishes a new WebSocket connection when core calls `OpenTransport`; relay service does not maintain a backoff state machine or independently retry — it starts a new WebSocket on demand and emits the connect result via `ObserveTransport`; listener and sender are long-lived service processes that do not re-spawn on reconnect", + "entryContracts" : [ + "ADR-046-provider-transport-azure-relay" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-relay-003", + "kind" : "work-item", + "owner" : "ADR046-transport-relay-001; reconnect contract; child Zone's ZoneLink handler", + "parallelGroup" : "wi:ADR-046-provider-transport-azure-relay", + "prerequisites" : [ + "ADR-046-provider-transport-azure-relay", + "ADR046-transport-relay-001" + ], + "specId" : "ADR-046-provider-transport-azure-relay", + "topologicalRank" : 12, + "validation" : "`tests/reconnect_backoff.rs`: relay responds to CloseTransport/OpenTransport cycle; WebSocket starts on demand; ObserveTransport reports connect result; `src/tests/integration/reconnect_scenario.rs`: full reconnect cycle including Credential re-acquisition", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-azure-relay/src/transport_settings.rs`; `docs/reference/schemas/v3/providers/transport-azure-relay.transport-settings.json`" + ], + "detailedDesign" : "`AzureRelayTransportSettings` Rust struct with serde for only `relayNamespaceId` and `relayEntityId`; validation against committed JSON Schema; reject secret-shaped fields/values; generate and admit the exact six-field ZoneLink base; reject legacy provider envelopes and allocator-private fingerprint/capability fields; resolve `spec.transportProviderRef` before schema validation; validate exactly two same-Zone `spec.transportCredentials` refs with one `azure-relay-listen` and one `azure-relay-send` audience; enforce `disabled`/`limits` in child core; xtask `gen-provider-transport-schemas` integration", + "entryContracts" : [ + "ADR-046-provider-transport-azure-relay" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-relay-004", + "kind" : "work-item", + "owner" : "ADR046-transport-relay-001; transport settings schema; Nix configuration owner", + "parallelGroup" : "wi:ADR-046-provider-transport-azure-relay", + "prerequisites" : [ + "ADR-046-provider-transport-azure-relay", + "ADR046-transport-relay-001" + ], + "specId" : "ADR-046-provider-transport-azure-relay", + "topologicalRank" : 12, + "validation" : "`tests/transport_settings_schema.rs`: valid/invalid schema vectors; `tests/transport_credentials.rs`: exact canonical ZoneLink field set, same-Zone ref/count/audience/scope checks, and rejection of credential refs inside `transportSettings`; eval-time Nix assertion coverage from `nix-unit: transport-settings-secret-key` test (see zone-routing spec)", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-azure-relay/src/backpressure.rs`" + ], + "detailedDesign" : "Outbound WebSocket send buffer bounded at `MAX_AGGREGATE_NAMED_STREAM_QUEUE_BYTES`; relay WebSocket write backpressure propagates to `FairScheduler` credit; `d2b_relay_transport_backpressure_events_total` counter emitted; no unbounded memory growth under slow relay", + "entryContracts" : [ + "ADR-046-provider-transport-azure-relay" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-relay-005", + "kind" : "work-item", + "owner" : "ADR046-transport-relay-001; backpressure/credit contract", + "parallelGroup" : "wi:ADR-046-provider-transport-azure-relay", + "prerequisites" : [ + "ADR-046-provider-transport-azure-relay", + "ADR046-transport-relay-001" + ], + "specId" : "ADR-046-provider-transport-azure-relay", + "topologicalRank" : 12, + "validation" : "`tests/backpressure_credit.rs`: slow relay writer saturates outbound queue; named-stream credit stalls before unbounded growth; source Zone never buffers beyond aggregate limit", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-azure-relay/src/{metrics.rs, audit.rs}`" + ], + "detailedDesign" : "Emit all OTEL metrics and audit records listed in §OTEL and §Audit; closed semantic label sets with no Zone/resource-name-derived keys; retain Zone identity only in the `d2b.zone` OTEL resource attribute; never label secret bytes; provider audit covers **carriage authentication and health observations only** — Azure auth events, WebSocket lifecycle, credential acquisition outcomes — and is **separate from resource audit** (resource lifecycle events are owned by core); audit records appended through the Zone runtime audit log interface (no atomicity guarantee with Zone resource state in redb; best-effort delivery per the Zone's audit provider configuration); OTEL via lightweight emitter ring (no direct OTEL SDK dependency in Provider)", + "entryContracts" : [ + "ADR-046-provider-transport-azure-relay" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-relay-006", + "kind" : "work-item", + "owner" : "ADR046-transport-relay-001 through ADR046-transport-relay-005; telemetry/audit owner", + "parallelGroup" : "wi:ADR-046-provider-transport-azure-relay", + "prerequisites" : [ + "ADR-046-provider-transport-azure-relay", + "ADR046-transport-relay-001", + "ADR046-transport-relay-002", + "ADR046-transport-relay-003", + "ADR046-transport-relay-004", + "ADR046-transport-relay-005" + ], + "specId" : "ADR-046-provider-transport-azure-relay", + "topologicalRank" : 13, + "validation" : "`tests/credential_redaction.rs` extended to cover audit/OTEL paths; `tests/metric_labels.rs` structurally asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys and that a Zone-name canary never enters label values; `tests/fake_relay_transport.rs` asserts audit record fields against schema", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-azure-relay/src/tests/integration/README`" + ], + "detailedDesign" : "Required content: fake relay server setup and teardown using the injected fake Relay effect port; how to run hermetic integration tests without a live Azure service; how to configure the injected fake Credential effect port for credential delivery tests; the fixture declares compiler-only `k2.parentZone = \"local-root\"` and puts the exact-shape ZoneLink, selected Provider, Network, Credentials, Process, and Endpoint resources only in K2; local-root's store is asserted to contain no reciprocal ZoneLink or Provider; how to run with a real Azure namespace (requires same-child-Zone `Credential` resources listed in `spec.transportCredentials`, not environment-variable credential paths); integration test scenarios and expected outcomes; CI/local execution instructions", + "entryContracts" : [ + "ADR-046-provider-transport-azure-relay" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-relay-007", + "kind" : "work-item", + "owner" : "Provider crate owner; integration test owner", + "parallelGroup" : "wi:ADR-046-provider-transport-azure-relay", + "prerequisites" : [ + "ADR-046-provider-transport-azure-relay" + ], + "specId" : "ADR-046-provider-transport-azure-relay", + "topologicalRank" : 11, + "validation" : "File must be present; workspace policy gate enforces `src/tests/integration/README`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-unix/src/credit.rs` (imports `MAX_PACKET_ATTACHMENTS=32`, `RESERVED_CONTROL_FDS=64`, credit-class constants from v3 contract); `src/portal.rs` (imports `MAX_PACKET_ATTACHMENTS` for portal validation)" + ], + "detailedDesign" : "Import credit scope capacities and headroom from `v3_zone_session.rs`; add `MAX_OPEN_TRANSPORTS: usize = 256` local constant for handle table bound. Primary reuse disposition: `adapt`. Preserved source-plan detail: Dependency on ADR046-session-001 output.", + "entryContracts" : [ + "ADR-046-provider-transport-unix" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-unix-001", + "kind" : "work-item", + "owner" : "ADR046-session-001 (v3 ComponentSession contracts); W0 shared contract root", + "parallelGroup" : "wi:ADR-046-provider-transport-unix", + "prerequisites" : [ + "ADR-046-provider-transport-unix", + "ADR046-session-001" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 11, + "validation" : "`tests/credit.rs::ancillary_capacity_is_derived_from_closed_hard_bounds` passes against v3 constants", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-unix/src/{seqpacket,identity,socket}.rs`" + ], + "detailedDesign" : "Copy transport structs verbatim; adapt `PeerIdentityPolicy` to report `PeerCredentials` upward to ComponentSession for subject mapping (not for direct resource lookup — that is core's responsibility); maintain `SO_PASSCRED` setup and first-packet credential extraction as documented; CLOEXEC enforcement uses `rustix` syscall wrappers over `libc` where available Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-provider-transport-unix" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-unix-002", + "kind" : "work-item", + "owner" : "ADR046-transport-unix-001; d2b-bus transport layer (ADR046-bus-001)", + "parallelGroup" : "wi:ADR-046-provider-transport-unix", + "prerequisites" : [ + "ADR-046-provider-transport-unix", + "ADR046-bus-001", + "ADR046-transport-unix-001" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 12, + "validation" : "Copy all 12 test functions; add `peercred_reported_to_componentsession_not_resolved_to_subject_here`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-unix/src/{stream,socket}.rs`" + ], + "detailedDesign" : "Copy verbatim; add `attachment_support: false` in `TransportDescriptor` (stream never carries SCM_RIGHTS regardless of route class); `admission.rs::validate_route_class` rejects `attachments_enabled=true` for stream Primary reuse disposition: `adapt`. Preserved source-plan detail: copy unchanged.", + "entryContracts" : [ + "ADR-046-provider-transport-unix" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-unix-003", + "kind" : "work-item", + "owner" : "ADR046-transport-unix-001", + "parallelGroup" : "wi:ADR-046-provider-transport-unix", + "prerequisites" : [ + "ADR-046-provider-transport-unix", + "ADR046-transport-unix-001" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 12, + "validation" : "`tests/portal.rs::stream_open_transport_forces_no_attachments`; `tests/identity.rs::stream_transport_reassembles_partial_and_coalesced_records`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-unix/src/credit.rs`" + ], + "detailedDesign" : "Copy all five types verbatim; import scope-capacity constants from v3 contract; add `#[derive(Debug)]` with redacted Display (no raw counts in Debug output) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy unchanged.", + "entryContracts" : [ + "ADR-046-provider-transport-unix" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-unix-004", + "kind" : "work-item", + "owner" : "ADR046-transport-unix-001", + "parallelGroup" : "wi:ADR-046-provider-transport-unix", + "prerequisites" : [ + "ADR-046-provider-transport-unix", + "ADR046-transport-unix-001" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 12, + "validation" : "Copy all 4 credit test functions; add `credit_released_on_attachment_close` and `emergency_headroom_constant_across_fd_counts`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-unix/src/descriptor.rs`" + ], + "detailedDesign" : "Copy verbatim; adapt `DescriptorPolicy::verify` to produce `AcceptedAttachment` carrying `ObjectIdentity` binding for v3 ComponentSession attachment descriptor model; `pid` not stored beyond liveness check Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "entryContracts" : [ + "ADR-046-provider-transport-unix" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-unix-005", + "kind" : "work-item", + "owner" : "ADR046-transport-unix-002", + "parallelGroup" : "wi:ADR-046-provider-transport-unix", + "prerequisites" : [ + "ADR-046-provider-transport-unix", + "ADR046-transport-unix-002" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 13, + "validation" : "Copy `pidfd_identity_requires_live_launch_evidence_and_rejects_unrelated_process` and `duplicate_kernel_objects_are_rejected_and_cleaned_up`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-unix/src/admission.rs`" + ], + "detailedDesign" : "`validate_route_class(route_class, socket_kind, attachments_enabled, received_fd)` calls `getsockopt(SO_TYPE)` (blocking adapter) on `received_fd`: `SOCK_SEQPACKET` must match `\"seqpacket\"`, `SOCK_STREAM` must match `\"stream\"`, any other type fails `invalid-socket-fd`; if `route_class == RouteClass::ZoneLink && attachments_enabled == true` fail `attachment-policy-conflict` with detail `cross-zone-attachments-forbidden`; if `socket_kind == \"stream\" && attachments_enabled == true` fail `attachment-policy-conflict`; no Noise profile enforcement (that is ComponentSession's responsibility); returns `Ok(RouteAdmission { route_class, socket_kind, attachments_enabled })`", + "entryContracts" : [ + "ADR-046-provider-transport-unix" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-unix-006", + "kind" : "work-item", + "owner" : "ADR046-transport-unix-002", + "parallelGroup" : "wi:ADR-046-provider-transport-unix", + "prerequisites" : [ + "ADR-046-provider-transport-unix", + "ADR046-transport-unix-002" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 13, + "validation" : "`tests/admission.rs::seqpacket_fd_passes_seqpacket_kind`; `stream_fd_passes_stream_kind`; `seqpacket_fd_rejects_stream_kind_declaration`; `zone_link_with_attachments_enabled_fails`; `local_portal_seqpacket_with_attachments_accepted`; `stream_with_attachments_enabled_rejected`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-unix/src/{portal,service}.rs`" + ], + "detailedDesign" : "`portal.rs`: `PortalHandler` struct owns a bounded `HashMap` (capacity `MAX_OPEN_TRANSPORTS=256`); `open_transport(req, attachment_fd)` validates via `admission.rs`, dups FD, allocates handle, stores `MonitorState { dup_fd, observation_senders: Vec }`; `close_transport(handle)` closes dup FD, half-closes all observation senders, removes entry; `observe_transport(handle)` registers a new `NamedStreamSender` and spawns an async epoll-watcher task on the dup FD; `TransportHandle` is a `[u8; 16]` random token; redacted in all Debug impls; `service.rs` is the binary entry: accepts the allocator-issued portal endpoint FD at launch, runs `GeneratedTransportServiceServer` over it, dispatches to `PortalHandler` Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt dispatch pattern; implement portal methods as new.", + "entryContracts" : [ + "ADR-046-provider-transport-unix" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-unix-007", + "kind" : "work-item", + "owner" : "ADR046-transport-unix-002 through 006; ADR046-bus-001 (d2b-bus ComponentSession method dispatch); ADR046-session-001 (named-stream protocol)", + "parallelGroup" : "wi:ADR-046-provider-transport-unix", + "prerequisites" : [ + "ADR-046-provider-transport-unix", + "ADR046-bus-001", + "ADR046-session-001", + "ADR046-transport-unix-002", + "ADR046-transport-unix-003", + "ADR046-transport-unix-004", + "ADR046-transport-unix-005", + "ADR046-transport-unix-006" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 14, + "validation" : "`tests/portal.rs::open_transport_zone_link_validates_and_returns_ownedtransport`; `open_transport_uses_child_bootstrap_endpoint_only`; `parent_endpoint_never_enters_provider_portal`; `open_transport_local_portal_seqpacket_with_attachments_accepted`; `open_transport_zone_link_attachments_enabled_rejected`; `close_transport_is_idempotent_after_handle_removed`; `observe_transport_delivers_pollhup_as_peer_disconnected`; `handle_table_rejects_at_max_capacity`; `restart_clears_all_handles`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-unix/` crate Cargo.toml binary target `d2b-transport-unix-service`; Provider component descriptor JSON committed at `packages/d2b-provider-transport-unix/descriptor/unix-transport-service.json`; Nix package derivation at `packages/d2b-provider-transport-unix/`" + ], + "detailedDesign" : "Component descriptor declares: `processClass=service`, `template=unix-transport-service`, `stateNamespaces=[]` (no Provider state Volume; bounded non-secret operational state in status/core ledger, D087), `sandbox.capabilityClasses=[]`, `sandbox.namespaceClasses=[mount]`, `sandbox.seccompClass=strict`, `budget.memory.limit=\"16Mi\"`, `budget.cpu.limit=\"200m\"`, `budget.fds.limit=512`, `endpoints=[{name:portal,transport:unix,purpose:transport-unix-portal}]`, `readiness={class:provider-defined,initialDelay:\"0s\",timeout:\"5s\",failureThreshold:1,successThreshold:1}`, `restartPolicy={class:always,backoffBase:\"2s\",backoffMax:\"60s\",backoffMultiplier:2.0,maxRestarts:10,resetAfter:\"1h\"}`; Provider package bundles descriptor digest; core ProviderDeployment creates the Process with empty `mounts` when `Provider/transport-unix` is installed Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt; no direct symbol copy.", + "entryContracts" : [ + "ADR-046-provider-transport-unix" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-unix-008", + "kind" : "work-item", + "owner" : "ADR046-transport-unix-007; ADR046-provider-003 (system Provider framework); Provider/system-minijail (ADR046-provider-003)", + "parallelGroup" : "wi:ADR-046-provider-transport-unix", + "prerequisites" : [ + "ADR-046-provider-transport-unix", + "ADR046-provider-003", + "ADR046-transport-unix-007" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 15, + "validation" : "`tests/conformance.rs::process_resource_matches_component_descriptor`; `tests/conformance.rs::provider_state_set_is_empty`; `tests/conformance.rs::no_state_volume_mount`; sandbox policy tests against minijail conformance kit", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`docs/reference/schemas/v3/providers/transport-unix.transport-binding.json`; `nixos-modules/assertions.nix` (assertion additions); generated `nixos-modules/generated/options-zones-ZoneLink.nix` `transportSettings` submodule" + ], + "detailedDesign" : "Commit the JSON Schema; run `xtask gen-zone-schemas` and `xtask gen-zone-nix-options` to regenerate committed files; generate the exact six-field ZoneLink base; reject legacy provider envelopes and allocator-private fingerprint/capability fields; add assertions for stream+attachments conflict and sensitive key names; require an empty `transportCredentials` list; reuse the common topology assertions to require the ZoneLink and selected Provider in the same child Zone, self-matching `childZoneName`, compiler-only non-root `parentZone`, and no reciprocal parent resource; `xtask gen-zone-resources` adds Provider-selected `transportSettings` validation", + "entryContracts" : [ + "ADR-046-provider-transport-unix" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-unix-009", + "kind" : "work-item", + "owner" : "ADR046-transport-unix-001; Nix/build integrator", + "parallelGroup" : "wi:ADR-046-provider-transport-unix", + "prerequisites" : [ + "ADR-046-provider-transport-unix", + "ADR046-gpu-007", + "ADR046-transport-unix-001" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 13, + "validation" : "All eval/build tests in the Nix section, including `transport-unix-exact-zonelink-spec`, `transport-unix-child-local-topology`, legacy-field rejection, empty-credential enforcement, and a generated-bundle assertion that the parent store has no reciprocal Provider/ZoneLink row", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-unix/src/{audit,metrics}.rs`" + ], + "detailedDesign" : "`AuditRecordKind` enum with 6 event kinds from Audit section; `AuditRecord` carries only the fields listed (no uid/gid/pid/path/handle/ZoneLink name); emit via Zone runtime `emit_audit_record()` interface; `MetricCounter`/`MetricHistogram` with closed label types per Metrics section; emit via bounded in-process ring to OTEL Provider datagram socket; `tracing::instrument` spans on `PortalHandler` methods with the 3 permitted span attributes only", + "entryContracts" : [ + "ADR-046-provider-transport-unix" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-unix-010", + "kind" : "work-item", + "owner" : "ADR046-transport-unix-007; ADR-046-telemetry-audit-and-support", + "parallelGroup" : "wi:ADR-046-provider-transport-unix", + "prerequisites" : [ + "ADR-046-provider-transport-unix", + "ADR046-transport-unix-007" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 15, + "validation" : "`tests/conformance.rs::audit_records_contain_no_pid_uid_or_handle`; `tests/conformance.rs::metric_labels_are_closed_set`; `tests/conformance.rs::span_attributes_contain_no_sensitive_fields`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-unix/integration/` and `integration/README.md`" + ], + "detailedDesign" : "Four scenarios: `transport_open.rs` (fixture models compiler-only `k1.parentZone = \"local-root\"`; fake selected allocator retains one socketpair endpoint as sealed route state and injects the other into K1 bootstrap; K1 core passes only that child endpoint to its same-Zone Provider → OwnedTransport out; verify socket kind, CLOEXEC, SO_PASSCRED, K1-local Provider/ZoneLink, and absent local-root reciprocal row; p95 latency assertion ≤2 ms); `fd_transfer.rs` (within-Zone local-portal seqpacket `SCM_RIGHTS` transfer, credit accounting, scavenge on error injection; ZoneLink route rejects the same packet); `reconnect.rs` (CloseTransport + re-OpenTransport with a fresh sealed child endpoint, verify previous handle is unknown and monitoring dup closed); `observation_stream.rs` (ObserveTransport stream receives `PEER_DISCONNECTED` when peer closes within 5 ms p95). `integration/README.md` documents prerequisites (no KVM required; all scenarios use in-process socketpairs and fake Zone API endpoint stub), invocation (`cargo test -p d2b-provider-transport-unix --test integration`), environment variables, and expected output", + "entryContracts" : [ + "ADR-046-provider-transport-unix" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-transport-unix-011", + "kind" : "work-item", + "owner" : "ADR046-transport-unix-007 through 010; test orchestration owner", + "parallelGroup" : "wi:ADR-046-provider-transport-unix", + "prerequisites" : [ + "ADR-046-provider-transport-unix", + "ADR046-transport-unix-007", + "ADR046-transport-unix-008", + "ADR046-transport-unix-009", + "ADR046-transport-unix-010" + ], + "specId" : "ADR-046-provider-transport-unix", + "topologicalRank" : 16, + "validation" : "All four scenarios pass in CI; latency assertions enforced using monotonic timestamps; scavenge correctness verified by open-FD count before/after error injection", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-contracts/src/usbip_effect_port.rs" + ], + "detailedDesign" : "Define UsbipEffectPort and UsbipGuestEffectPort in d2b-contracts with DeviceUid, NetworkUid, UsbBindingUid, LeaseToken, FirewallToken, FirewallGenerationFence, FirewallObservation, KernelModuleClass, DeviceProbeResult, and UsbipEffectError; export traits/types only with no implementation. `apply_firewall` and `release_firewall` both accept expected Network/Service generations; release also accepts NetworkUid and borrows the token so the controller can retain it until confirmed effect. Keep firewall apply/observe/release Network/busid-scoped, attach/detach Binding-addressed, and all fd/path/busid values private. `TransientDetail` derives `Clone, PartialEq, Eq` while retaining manual redacted Debug/Display so `UsbipEffectError`'s derives compile without disclosure. Primary reuse disposition: `create`. Preserved source-plan detail: net-new trait definition.", + "entryContracts" : [ + "ADR-046-provider-device-usbip" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-usbip-001", + "kind" : "work-item", + "owner" : "d2b-contracts crate shape stabilised by shared root contract; d2b-contracts owner", + "parallelGroup" : "wi:ADR-046-provider-device-usbip", + "prerequisites" : [ + "ADR-046-provider-device-usbip" + ], + "specId" : "ADR-046-provider-device-usbip", + "topologicalRank" : 11, + "validation" : "d2b-contracts tests for trait object safety, generation-fenced firewall apply/observe/release signatures (release requires NetworkUid and token), `FirewallGenerationMismatch`, `UsbipEffectError: Clone + PartialEq + Eq`, `TransientDetail` clone/equality, redacted Debug/Display behavior, and no implementation leakage.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-core/src/device_usbip_adapter.rs" + ], + "detailedDesign" : "Implement UsbipEffectPort in the core adapter: signed-bundle busid lookup, same-Zone validation, trusted physical-USB identity resolution, mandatory Core-derived `physical-usb-backing/v1` digest and exact `(Host, physical-usb-backing, opaqueKeyDigest)` claim shared with every security-key/USB Provider, exclusive OFD claim, sole ownership of all USBIP TCP/3240 and exact per-Network/per-busid effects through the existing closed `UsbipBindFirewallRule` request with action enum `Ensure|Remove`, ownership-scoped observe/release, anti-spoof probe, one shared Host module/backend authority, one Core-derived D097 relay Endpoint/firewall authority per Network, D097 authority-index preflight/adoption, and post-effect audit. `Ensure` applies; `Remove` uses the same request, binds expected Network/Service generations, validates exact ownership, succeeds idempotently on validated absence, and fails closed on a foreign marker. No separate release op exists. Complete the shared claim before any open, withhold, bind, module, relay, firewall, or attachment effect; never expose raw busid, identity digest, path, fd, bind address, nftables body, audit structs, or broker wire types. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt into framework-internal adapter.", + "entryContracts" : [ + "ADR-046-provider-device-usbip" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-usbip-002", + "kind" : "work-item", + "owner" : "ADR046-usbip-001, ADR046-zone-control-024; UsbipBindFirewallRule broker op; d2b-host usbip argv support; framework core adapter owner", + "parallelGroup" : "wi:ADR-046-provider-device-usbip", + "prerequisites" : [ + "ADR-046-provider-device-usbip", + "ADR046-usbip-001", + "ADR046-zone-control-024" + ], + "specId" : "ADR-046-provider-device-usbip", + "topologicalRank" : 14, + "validation" : "Fast packages/d2b-core/tests/device_usbip_adapter.rs covers same-Zone gate, exact shared physical backing tuple derivation, byte-identical keys for USB/security-key views of one fake token, Provider-private-class/digest bypass rejection, `physical-usb-backing-conflict` before effects, separate USBIP module/relay authorities, one-module/one-relay reuse, exact closed `Ensure|Remove` decoding and same-request broker mapping, unknown-action rejection, expected Network/Service generation binding, exact Network/busid firewall scoping, validated-absence `Remove`, foreign-marker failure without mutation, ownership-scoped drift/status, transient retry with retained token/authority, independent per-busid release, no network-local dependency, anti-spoof, redaction, and no digest/busid/path/fd exposure.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-usbip/" + ], + "detailedDesign" : "Create the required crate layout; bind the shared D098 `UsbService`/`UsbBinding` base versions/fingerprints from ADR046-provider-004 and implement only strict USBIP Provider extensions; sign/register extension schemas and advertise explicit export only for authority `UsbService` resources implemented by this Provider; implement validation.rs and compile-checked EffectPort injection. Declare the controller user/User resource in Nix activation. Primary reuse disposition: `create`. Preserved source-plan detail: net-new crate skeleton with contract reuse.", + "entryContracts" : [ + "ADR-046-provider-device-usbip" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-usbip-003", + "kind" : "work-item", + "owner" : "ADR046-usbip-001, ADR046-provider-004; Provider model crate structure; device-usbip provider owner", + "parallelGroup" : "wi:ADR-046-provider-device-usbip", + "prerequisites" : [ + "ADR-046-provider-device-usbip", + "ADR046-provider-004", + "ADR046-usbip-001" + ], + "specId" : "ADR-046-provider-device-usbip", + "topologicalRank" : 12, + "validation" : "make test-policy passes; Cargo.toml has no d2b-priv-broker dependency; fast schema/manifest tests consume the common fixtures, accept canonical minimal base without `spec.provider`, prove a fake direct-local Provider can implement the same base, and cover Service-only exportability, Binding non-exportability, Core projection ownerRef/base fields with explicit `spec.provider` rejection, D088 status layering, semantic factory-fingerprint stability across Provider/adapter identity changes, strict refs, and trait injection.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-usbip/src/{controller,reconcile,export_import}.rs" + ], + "detailedDesign" : "Reconcile provider-neutral authority/projection `UsbService` and per-Guest `UsbBinding` resources through strict USBIP Provider extensions, consuming UsbipEffectPort, the exact shared Host-global physical USB tuple, USBIP-private D097 authorities, and signed D096 ExportAdapter/ImportAdapter. Enforce same-Zone base and provider refs; ResourceExport authority-Service-only target; same-type ResourceImport-owned projection with `providerRef`, semantic base/import fields, no `spec.provider`, and no physical fields/effects; route selection from the signed local descriptor/import record; semantic factory fingerprint independent of separately authenticated adapter identity; exclusive fair Binding admission; encrypted bounded named-stream control/data; D088 layered status; Service/Binding finalizers; restart adoption; declared/explicit modes; no session/transfer resources. Firewall reconcile maps apply to `UsbipBindFirewallRule` action `Ensure`; finalization maps release to action `Remove` on the same request with expected Network/Service generations. Finalization retains firewall token/status and relay authority until confirmed effect, retries transient failure, refreshes on generation mismatch, and blocks fail-closed on a foreign marker. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt step machine into Provider reconcile loop.", + "entryContracts" : [ + "ADR-046-provider-device-usbip" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-usbip-004", + "kind" : "work-item", + "owner" : "ADR046-usbip-001, ADR046-usbip-002, ADR046-usbip-003, and ADR046-zone-control-024; device-usbip controller owner", + "parallelGroup" : "wi:ADR-046-provider-device-usbip", + "prerequisites" : [ + "ADR-046-provider-device-usbip", + "ADR046-usbip-001", + "ADR046-usbip-002", + "ADR046-usbip-003", + "ADR046-zone-control-024" + ], + "specId" : "ADR-046-provider-device-usbip", + "topologicalRank" : 15, + "validation" : "Fast tests/controller_state_machine.rs, service_binding_schema.rs, export_import.rs, authority_conflict.rs, async_loop.rs, finalizer.rs, and wrong_zone.rs cover authority/projection/Binding lifecycle, Service-only export, projection `spec.provider` rejection, semantic fingerprint stability under adapter identity changes plus separate descriptor authentication, D088 `status.resource`/`status.provider` placement, shared physical tuple collision before effects, encrypted fake streams, no physical projection effect, exclusivity, restart, WrongZone degradation, exact `Ensure|Remove` mapping, and firewall status/token/authority retention until `Remove` confirmation.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-usbip/src/reconcile.rs" + ], + "detailedDesign" : "Create/adopt exactly one Host backend Process authority, exactly one D097 Network relay Process/Endpoint/firewall authority bound to TCP 3240 with a Core-derived Network/policy key, and one Binding-owned Guest proxy/private Endpoint per attached Binding. Restrict relay resolution to the Core adapter and exact active-lease Binding proxies; deliver their connected streams by LaunchTicket. Use canonical system-minijail specs, signed templates, bounded budgets/readiness/restart, no argv/path/address/fd fields; attach/detach remains a one-shot EffectPort operation, not a second Process. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt into singleton Host backend, per-Network relay, and per-Binding Guest proxy management.", + "entryContracts" : [ + "ADR-046-provider-device-usbip" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-usbip-005", + "kind" : "work-item", + "owner" : "ADR046-usbip-003; Process ResourceType schema; device-usbip process lifecycle owner", + "parallelGroup" : "wi:ADR-046-provider-device-usbip", + "prerequisites" : [ + "ADR-046-provider-device-usbip", + "ADR046-usbip-003" + ], + "specId" : "ADR-046-provider-device-usbip", + "topologicalRank" : 13, + "validation" : "Fast Process/Endpoint shape tests prove one backend per Host, one Core-derived multiplexed TCP 3240 Endpoint/firewall authority per Network, deterministic duplicate conflict, adapter/exact-Binding-only resolution, LaunchTicket-connected stream, no generic Network reader access, Binding ownership/private Guest policy, no per-Device listener, no raw address/argv/path/fd, and readiness before bind/attach.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-usbip/src/status.rs" + ], + "detailedDesign" : "Define provider-neutral `UsbService` `status.resource` with whole-device availability/access counts, authority-only `physical-usb-backing` claim state, and projection-only common import state; define `UsbBinding.status.resource` with generic attachment phase/queue/generation/timestamps. Define separate strict `status.provider` USBIP details: Device probe, owner backend/relay, USBIP-owned `firewallState`/opaque digest/observed generation, imported-route observations, and Binding proxy/private Endpoint/subphase. During `UsbipBindFirewallRule` action `Remove`, strict status remains `firewallState: releasing`; the token and relay authority remain held until confirmed success, including validated absence. No common access/import/attachment field appears directly under `status`; no USBIP module, Network, Endpoint, proxy, server/client, port, firewall, or busid field enters base status; no raw backing digest, busid, path, fd, address, session/transfer ID, remote identity, or payload appears anywhere. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt state fields to typed status.provider.details.", + "entryContracts" : [ + "ADR-046-provider-device-usbip" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-usbip-006", + "kind" : "work-item", + "owner" : "ADR046-usbip-003; Device provider details plus `UsbService` and `UsbBinding` base/provider status schema owner", + "parallelGroup" : "wi:ADR-046-provider-device-usbip", + "prerequisites" : [ + "ADR-046-provider-device-usbip", + "ADR046-usbip-003" + ], + "specId" : "ADR-046-provider-device-usbip", + "topologicalRank" : 13, + "validation" : "Fast tests/status_serde.rs covers generic base status plus three strict USBIP detail schemas, exact `status.resource` placement for access/import/attachment/backing claim, exact `status.provider` placement for relay/firewall implementation observations, drift transition and ownership, `releasing` retention until `Remove` confirmation, rejection of semantic fields directly under `status`, mode-dependent omissions, bounded counts/refs, unknown-field denial, and a deny corpus proving USBIP-only fields are rejected from base and Network status.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2b-provider-device-usbip/{src,tests,integration/README.md}; tests/host-integration/usbip-service.nix; tests/host-integration/hardware/usbip-service.sh" + ], + "detailedDesign" : "Put provider-neutral Service/Binding base-schema separation, strict USBIP extensions, projection `spec.provider` rejection, D088 layered status, semantic factory-fingerprint independence from Provider/adapter identity, exact shared physical backing tuple/conflict, one Core-derived per-Network relay Endpoint/firewall authority, exact per-busid firewall ownership/drift/release through one `UsbipBindFirewallRule` request with closed `Ensure|Remove`, generation fencing, idempotent validated-absence `Remove`, foreign-marker fail-closed behavior, and status/token/authority retention until confirmation, arbitration, same-type export/import, encrypted fake-stream, and least-privilege process/Endpoint shape coverage in fast Layer-1 Rust tests. Include a fake direct-local Provider proving the same base contract has no USBIP dependency. Reserve runNixOSTest for real Linux usbip_host/vhci_hcd, usbipd, namespaces/nftables, TCP 3240, zero network-local carve-out, and Guest checks; reserve the hardware script for an approved physical device. Use existing Make gates only. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing network-scoping assertion and add new scenarios.", + "entryContracts" : [ + "ADR-046-provider-device-usbip" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-usbip-007", + "kind" : "work-item", + "owner" : "ADR046-usbip-004 and ADR046-usbip-005; device-usbip integration owner", + "parallelGroup" : "wi:ADR-046-provider-device-usbip", + "prerequisites" : [ + "ADR-046-provider-device-usbip", + "ADR046-usbip-004", + "ADR046-usbip-005" + ], + "specId" : "ADR-046-provider-device-usbip", + "topologicalRank" : 16, + "validation" : "`make test-host-integration` runs the non-hardware real-kernel case on a capable host; `make test-hardware` runs the explicit manual device case. No Layer-1 test opens a device, loads a module, creates a namespace, or listens on a socket.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "nixos-modules/components/usbip.nix, nixos-modules/options-zones.nix, nixos-modules/assertions.nix" + ], + "detailedDesign" : "Add Provider config; remove the old per-VM option; emit provider-neutral authority `UsbService`, per-Guest `UsbBinding`, strict USBIP Provider envelopes, and optional ResourceExport/ResourceImport authoring shapes; imported same-type projection Services remain Core-created with no `spec.provider`. Assert same-Zone base/provider refs, projection ownerRef/forbidden implementation and physical fields, Service-only export target, one Host backend/Network relay, exact Core-derived Host-global `(Host, physical-usb-backing, opaqueKeyDigest)` exclusion shared by USB/security-key, Provider-private-class bypass rejection, one `UsbipBindFirewallRule` contract with exact actions `Ensure|Remove` and no separate release operation, and retain guest vhci_hcd/tools. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt guest module, remove host-side option surface, and extend eval assertions.", + "entryContracts" : [ + "ADR-046-provider-device-usbip" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-usbip-008", + "kind" : "work-item", + "owner" : "ADR046-usbip-003, ADR046-zone-control-024; ADR-046-nix-configuration; Nix integrator", + "parallelGroup" : "wi:ADR-046-provider-device-usbip", + "prerequisites" : [ + "ADR-046-provider-device-usbip", + "ADR046-qemu-media-017", + "ADR046-usbip-003", + "ADR046-zone-control-024" + ], + "specId" : "ADR-046-provider-device-usbip", + "topologicalRank" : 15, + "validation" : "Fast tests/unit/nix/cases/usbip-*.nix cover schema shape, Core projection `spec.provider` rejection, D088 layered status fixtures including USBIP-owned firewall state and `releasing` retention, all reference/owner/export assertions, byte-identical USB/security-key tuple collision and private-class bypass rejection, one Core-derived 3240 Endpoint/firewall authority per Network, exact `Ensure|Remove` broker action surface with unknown values rejected and no second op, least-privilege consumer policy, absence of network-local 3240 rules, old-option removal, and guest module retention.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "packages/d2bd/src/, nixos-modules/network.nix, packages/d2b-core/src/processes.rs" + ], + "detailedDesign" : "Remove daemon-coupled USBIP after Provider tests and integration tests pass: delete per-env autostart, state machine, and reconcile state modules after migration; remove USBIP firewall block from network.nix; remove ProcessRole::Usbip; run Layer-1 gates and confirm no d2bd or network.nix references remain outside the adapter and contracts. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete after Provider replacement reaches parity.", + "entryContracts" : [ + "ADR-046-provider-device-usbip" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-usbip-009", + "kind" : "work-item", + "owner" : "ADR046-usbip-004 and ADR046-usbip-008; Provider fully wired and validated; daemon cleanup owner", + "parallelGroup" : "wi:ADR-046-provider-device-usbip", + "prerequisites" : [ + "ADR-046-provider-device-usbip", + "ADR046-usbip-004", + "ADR046-usbip-008" + ], + "specId" : "ADR-046-provider-device-usbip", + "topologicalRank" : 16, + "validation" : "make test-unit and make test-flake plus grep or contract checks for removed symbols and no residual d2bd/network.nix USBIP lifecycle references.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/user_session_authority.rs` (or a core/user-agent per-session agent Process under `Provider/system-systemd`); `AuthorityDescriptor` on the session authority" + ], + "detailedDesign" : "Name and implement the **fixed user-session authority** (D097 desktop/session): `authorityScope: seat` bound to `(Host, User, login-session/seat)`, opaque `authorityKey` (never a raw socket path/XDG_RUNTIME_DIR/DISPLAY/seat name), `cardinality: exactly-one` per `(Host, User, login-session)`, `arbitration: exclusive`, owner = a core/user-agent per-user-session agent Process (NOT a new Provider), adoption by `ownerProof` (agent Process identity + login-session id), `exportability: forbidden`. It is the sole opener of the compositor/PipeWire/session-bus FDs and hands them to desktop Providers only via the EffectPort/LaunchTicket. Core's authority index rejects a duplicate session authority (or a duplicate same-user display portal, clipboard host, notification sink, audio mediator, systemd user manager, Secret Service, or seat-input claimant) with `duplicateConflict` before any FD open; multi-user/seat is admitted only up to the declared per-Host limit. Guest-stop invalidates every session authority/lease bound to that Guest across display/audio/notification/credential/shell in one dependency-aware cascade (D091), with no stale FD surviving. Host input (`wl_seat`/pointer constraints) is an `at-most-one`-per-seat authority under this session authority; pointer-constraint enforcement is a declared boundary until an interaction Provider implements it. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (name and implement the shared user-session authority).", + "entryContracts" : [ + "ADR-046-resources-host-guest-process-user" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-user-session-001", + "kind" : "work-item", + "owner" : "ADR046-zone-control-019 (authority index); `Provider/system-systemd` (user manager) + core/user-agent owner", + "parallelGroup" : "wi:ADR-046-resources-host-guest-process-user", + "prerequisites" : [ + "ADR-046-resources-host-guest-process-user", + "ADR046-zone-control-019" + ], + "specId" : "ADR-046-resources-host-guest-process-user", + "topologicalRank" : 12, + "validation" : "Single session authority per `(Host, User, session)`; duplicate same-user session authority / desktop service rejected with `duplicateConflict`; multi-seat declared-limit enforcement; Guest-stop invalidates all bound desktop/audio/notification/credential/shell authorities and leases (no stale compositor/PipeWire/session-bus FD); seat-input second claimant rejected; adoption by `ownerProof` and quarantine on ambiguity; hermetic with fakes", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`d2b-contracts/src/v3/volume_layout.rs` (LayoutEntry, EntryType, all policy enums, AclGrant, Invariant, SensitivityClass); `d2b-contracts/src/v3/volume_spec.rs` (VolumeSpec, ViewSpec, Attachment, QuotaSpec, SourceKind, `SourcePolicyId` opaque newtype); `d2b-contracts/src/v3/effect_port.rs` (`VolumeEffectPort` trait, opaque ID newtypes `VolumeId`/`LayoutEntryId`/`UserId`/`ViewId`/`SealingPolicyId` each with custom redacted Debug, `VolumeId` and `SealingPolicyId` with the exact `Clone`/serde wire derives and bounded `TryFrom` deserialization required by the canonical request, `VolumeMountToken`, and canonical `RotateSealingKeyRequest`/`Result`/`Error` types)" + ], + "detailedDesign" : "All LayoutEntry fields as documented in this dossier; enum value names preserved from `StoragePathKind`/policy enums with renames where noted; `User/` ACL principal (no numeric UID); `sourcePolicyId` opaque newtype replaces raw `hostPath` in `SourceKind::LocalPath` and `SourceKind::BlockImage`; deny-unknown sealing-rotation request contains only opaque Volume/policy/operation IDs and generation/revision preconditions, with no key bytes/path/handle; its crate-private `VolumeId` and `SealingPolicyId` strings serialize canonically and deserialize only when non-empty, ASCII-graphic, and at most 128 bytes, while retaining manual redacted Debug Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-001", + "kind" : "work-item", + "owner" : "ADR046-primitives-001; v3 contracts owner", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-primitives-001" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 11, + "validation" : "Schema golden vectors; round-trip serde; ACL principal validation rejects numeric forms; `sourcePolicyId` present; no `hostPath` field in any volume_spec contract; compile-time assertions for the exact `Clone + Serialize + DeserializeOwned` ID bounds and `Serialize + DeserializeOwned` request bounds; sealing-rotation request canonical-string/round-trip and deny-unknown tests; exact redacted-Debug assertions; deserialization rejects empty, over-128-byte, non-ASCII, whitespace, and control-byte IDs without echoing input; compile-time trait conformance includes `rotate_sealing_key`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Full `packages/d2b-provider-volume-local/` scaffold per §Crate layout: `src/`, `tests/`, `integration/`, `README.md`; crate `Cargo.toml` depends only on `d2b-contracts`, `d2b-provider`, `d2b-provider-toolkit`" + ], + "detailedDesign" : "`AnchoredDir`, `AnchoredResource`, `LeafName`, `RelativePath`; adapted `AtomicFilesystem`/`StateEnvelope`; adapted `LockGuard`/`LockSet`/`OfdTransfer`; marker write/verify/check; `src/effect_port.rs` re-exports `VolumeEffectPort` trait from `d2b-contracts::v3::effect_port` and provides Provider-side opaque ID construction helpers (no adapter implementation; adapter lives in host runtime); `sourcePolicyId` validation against declared policy list; no `openat2`/`setfacl`/`fallocate`/numeric-UID call sites in Provider crate Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged (`path.rs`); adapt (`atomic.rs`, `lock.rs`); adapt swtpm_dir marker algorithm.", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-002", + "kind" : "work-item", + "owner" : "ADR046-vl-001; volume-local Provider owner", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-vl-001" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 12, + "validation" : "All `tests/marker.rs`, `tests/state.rs` scenarios; all `integration/provision.rs` scenarios; `cargo deny check` verifies no `d2b-priv-broker`/`d2bd` dependency", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/controller.rs`, `src/layout.rs`, `src/acl.rs`, `src/source.rs`" + ], + "detailedDesign" : "Async reconcile loop; topological LayoutEntry evaluation; `VolumeEffectPort` semantic op dispatch (no direct broker connection; no `openat2`/`setfacl` call sites); ACL reconciliation cycle via effect port; drift detection; status write with expected revision; `sourcePolicyId` validation against declared `sourcePolicies`; controller watch remains responsive while per-resource effect calls run concurrently; **single watch scope** `providerRef: Provider/volume-local` — physical state reconciliation for all served Volumes (layout/ACL/quota/marker); ProviderDeployment creates/deletes Volume instances; volume-local does not issue create/delete API calls; Nix-preprovisioned `User/` layout principals; no cross-component Volume sharing; each component consumes only its declared view; empty-payload stateNamespace Volumes use `migrationPolicy: none` — no migration EphemeralProcess dispatched", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-003", + "kind" : "work-item", + "owner" : "ADR046-vl-002; ADR046-reconcile-001; d2b-bus/ComponentSession owner", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-reconcile-001", + "ADR046-vl-002" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 13, + "validation" : "`tests/layout_provision.rs`, `tests/layout_repair.rs`, `tests/layout_adopt.rs`, `tests/acl.rs`, `tests/view_rights.rs`, `tests/source.rs`, `integration/provision.rs`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/store_view.rs`; `tests/store_view.rs`; `integration/store_view.rs`" + ], + "detailedDesign" : "Store-view LayoutEntry matrix (see §Same-filesystem hardlink farm); private-NS sync via `run_store_sync` effect op; `StoreSyncComplete` semantic operation; OFD lock semantics; `gcroots/` and `state/` at store-view root; spec-correction enforcement", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-004", + "kind" : "work-item", + "owner" : "ADR046-vl-003; runtime-cloud-hypervisor Provider owner", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-vl-003" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 14, + "validation" : "`tests/store_view.rs` all invariants; `integration/store_view.rs` same-filesystem boundary; private-NS sync with concurrent reader", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/swtpm_volume.rs`; `tests/swtpm_volume.rs`; `integration/swtpm_marker.rs`" + ], + "detailedDesign" : "TPM LayoutEntry matrix; `create-if-never-provisioned` + fail-closed repair; broker-maintained provisioning marker; ancestor traverse ACL; `previously-provisioned-swtpm-state-missing` fail-closed detection; `secret-adjacent` sensitivity enforcement", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-005", + "kind" : "work-item", + "owner" : "ADR046-vl-002; device-tpm Provider owner", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-vl-002" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 13, + "validation" : "`tests/swtpm_volume.rs` all scenarios; `integration/swtpm_marker.rs` real broker-maintained marker", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/source.rs` (block-image and tmpfs branches); `tests/source.rs`; `integration/block_image.rs`" + ], + "detailedDesign" : "`block-image`: image file create/verify via `provision_block_image` effect op; `fallocate` performed by adapter when `preallocate: true`; FD transfer to Guest runtime via LaunchTicket via `open_volume_mount_token` effect op; `tmpfs`: `mount_tmpfs`/`umount_tmpfs` effect ops; `size=` and `nr_inodes=` derived from quota fields; cleanup via `umount_tmpfs` op", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-006", + "kind" : "work-item", + "owner" : "ADR046-vl-003", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-vl-003" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 14, + "validation" : "`tests/source.rs` allowlist pass/fail; block-image/tmpfs eval constraints; `integration/block_image.rs` real image lifecycle", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/{migration,snapshot,sealing}.rs`; `tests/{migration_unit,snapshot_unit,sealing_unit}.rs`; `integration/{migration,snapshot,sealing}.rs`" + ], + "detailedDesign" : "Schema migration (see §Schema migration) via `volume-migration-worker` EphemeralProcess; Snapshot create/list/expire (see §Snapshots) via `volume-snapshot-worker` EphemeralProcess; controller reports `stateSchemaPhase` and `snapshots` in Volume status. Sealing lifecycle uses the status-first `rotation-pending` transition and only canonical `VolumeEffectPort::rotate_sealing_key`; it persists/resumes the core Operation-ledger fingerprint, classifies exact typed errors, and has no key lease, direct rewrite, generic broker call, or Provider-owned EphemeralProcess worker.", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-007", + "kind" : "work-item", + "owner" : "ADR046-vl-002; ADR046-vl-003; ADR046-pstate-004 through ADR046-pstate-006", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-pstate-004", + "ADR046-pstate-005", + "ADR046-pstate-006", + "ADR046-vl-002", + "ADR046-vl-003" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 14, + "validation" : "All `tests/migration_unit.rs`, `tests/snapshot_unit.rs`, `tests/sealing_unit.rs`, `integration/migration.rs`, `integration/snapshot.rs`, and `integration/sealing.rs` scenarios, including restart/idempotency and status-before-effect assertions", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/relocation.rs`; `tests/relocation_unit.rs`; `integration/relocation.rs`" + ], + "detailedDesign" : "As documented in §Relocation, §Retention, §Incident hold, §Unclaimed Volume GC, §Destruction", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-008", + "kind" : "work-item", + "owner" : "ADR046-vl-003; ADR046-vl-007", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-vl-003", + "ADR046-vl-007" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 15, + "validation" : "All `tests/relocation_unit.rs`, `integration/relocation.rs` scenarios; destruction ordering under fault injection", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`src/audit.rs`; `src/otel.rs`; `src/error.rs`; `tests/audit_unit.rs`; `integration/audit.rs`" + ], + "detailedDesign" : "Event types and Zone audit emission per §Audit events, including controller rotation start/failure/commit and exactly-once broker `RotateSealingKey` success; OTEL metric definitions per §OTEL metrics with closed semantic labels and no Zone/resource-name-derived dimensions; Zone identity remains in `d2b.zone` resource attributes; error catalog per §Error catalog; no-path/no-key invariant enforced in all outputs", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-009", + "kind" : "work-item", + "owner" : "ADR046-vl-001; Zone audit stream owner; `observability-otel` Provider owner", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-vl-001" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 12, + "validation" : "`tests/audit_unit.rs` golden records and structural metric descriptor assertions for exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus resource-name canary absence; rotation audit exact-once/digest-only vectors; `tests/error_messages.rs` bounded messages; `integration/audit.rs` live stream", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/zone-resources.nix` (per §ADR046-pstate-010); `root-config.schema.json` in the Provider package" + ], + "detailedDesign" : "`sourcePolicies` in Provider root config (opaque IDs; no raw host paths); path prefix injection by resource compiler into private bundle (never into ResourceSpec or operator-authored Nix); `controllerExecutionRef` in Provider config; all eval-time validation rules per §Nix configuration including `sourcePolicyId` validation; artifact catalog entry; Provider and Volume resource authoring shapes", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-010", + "kind" : "work-item", + "owner" : "ADR046-vl-001; ADR046-pstate-010; NixOS module owner", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-pstate-010", + "ADR046-vl-001" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 12, + "validation" : "All Nix eval-time validation rules; `contentId` determinism; credential-ref guard; unknown Provider config key → build fail", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/xtask/src/provider_crate_policy.rs`; `tests/unit/gates/provider-crate-layout-check.sh`" + ], + "detailedDesign" : "`cargo xtask check-provider-crate-layout` gate asserts `src/`, `tests/`, `integration/` (with at least one `.rs` file and a `README.md`), and `README.md` for every `packages/d2b-provider-*` workspace member; fails closed with typed `missing-provider-crate-path` error Primary reuse disposition: `adapt`. Preserved source-plan detail: extend (per ADR046-pstate-011).", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-011", + "kind" : "work-item", + "owner" : "ADR046-vl-002; ADR046-pstate-011; workspace policy owner", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-pstate-011", + "ADR046-vl-002" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 13, + "validation" : "Gate detects each missing path; idempotent across re-runs; existing non-provider `d2b-*` crates not flagged", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-host/src/volume_effect_adapter.rs` (or the equivalent host-runtime crate designated by the Zone broker owner), implementing the `VolumeEffectPort` trait defined in `d2b-contracts`; planned `d2b-priv-broker/src/ops/rotate_sealing_key.rs` closed operation" + ], + "detailedDesign" : "Adapter holds trusted FD table keyed by `VolumeId`; resolves `SourcePolicyId` to host path prefix from private bundle; calls `openat2(RESOLVE_BENEATH)` anchored at retained FD for all FS ops; calls `setfacl`/`acl_set_fd`, `mount`/`umount`, `fallocate` from within adapter only; authorizes `volume.rotate-sealing-key`, verifies committed proof, recomputes the canonical idempotency key, and maps `rotate_sealing_key` one-to-one to the closed broker `RotateSealingKey` operation. Broker independently resolves opaque `VolumeId`/`SealingPolicyId`, checks policy/generation/preconditions, performs journaled atomic rewrap and roll-forward recovery, and durably emits exactly one success audit before returning. Neither boundary accepts key bytes, credential bytes, key handles, or paths. Other blocking filesystem calls run in the bounded blocking-thread pool Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt into adapter.", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-012", + "kind" : "work-item", + "owner" : "ADR046-vl-001; ADR046-vl-002; Zone broker/core owner", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-vl-001", + "ADR046-vl-002" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 13, + "validation" : "Adapter hermetic tests: each effect op called with mock FD table and bundle; rotation authorization, policy binding, all generation/revision preconditions, canonical idempotency vectors, byte-identical duplicate/retry, different-payload conflict, typed retry classification, and no key/path/handle in wire/Debug/error/audit; broker crash injection at every journal boundary with old-or-target visibility, roll-forward, and exactly-once success audit; anchored-path rejection for RESOLVE_BENEATH violations; `cargo deny check` verifies adapter exposes neither raw paths nor broker implementation to Provider crate; `integration/{provision,sealing}.rs` exercise full adapter paths", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Zone core ProviderDeployment controller-start path (outside `d2b-provider-volume-local`)" + ], + "detailedDesign" : "The volume-local controller declares no Provider state Volume, so there is no bootstrap Volume, no `BootstrapProviderStateVolume` broker op, no pre-provisioned controller Volume, and no bootstrap-storage exception (D086, superseded by D087). On first install and on every daemon restart, core ProviderDeployment starts the volume-local controller Process directly; the controller reaches `Ready` from its own resource `status`, the core Operation ledger, and a resource-store relist. Once Ready, it reconciles every Volume carrying `providerRef: Provider/volume-local` (operator-created Volumes and other Providers' declared state Volumes) as they appear in its `providerRef` watch, re-verifying identity markers against external reality, never creating them itself. A Guest bootstraps its own Guest-local volume-local instance from Guest-local primitives only.", + "entryContracts" : [ + "ADR-046-provider-volume-local" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vl-013", + "kind" : "work-item", + "owner" : "ADR046-vl-001; ADR046-vl-012; Zone broker/core owner", + "parallelGroup" : "wi:ADR-046-provider-volume-local", + "prerequisites" : [ + "ADR-046-provider-volume-local", + "ADR046-vl-001", + "ADR046-vl-012" + ], + "specId" : "ADR-046-provider-volume-local", + "topologicalRank" : 14, + "validation" : "`integration/provider_state.rs`: controller starts and reaches Ready with no state Volume; served Volumes reconciled and markers re-verified after restart; no bootstrap Volume and no bootstrap Provider Process in the resource list", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/volume.rs`, `volume_layout.rs`, `volume_attachment.rs`" + ], + "detailedDesign" : "Complete Volume ResourceSpec, LayoutEntry, all policy enums (values preserved from baseline), AclGrant, ViewSpec, AttachmentSpec, quota placeholder, strict serde unknown-field rejection, canonicalization, bounds Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-volume" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-volume-001", + "kind" : "work-item", + "owner" : "W0 shared contract root; `d2b-contracts`", + "parallelGroup" : "wi:ADR-046-resources-volume", + "prerequisites" : [ + "ADR-046-resources-volume" + ], + "specId" : "ADR-046-resources-volume", + "topologicalRank" : 10, + "validation" : "Golden JSON spec vectors; serde unknown-field; path anchor/depth/traversal validators; ACL grant bounds; policy enum coverage", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-local/src/` (layout engine, store_view.rs, swtpm_volume.rs, broker op adapters); `packages/d2b-provider-volume-local/tests/` (hermetic layout/store-view/swtpm tests); `packages/d2b-provider-volume-local/integration/` (container Host-path and store-view FS fixtures); `packages/d2b-provider-volume-local/README.md`" + ], + "detailedDesign" : "volume-local controller: layout engine (provision/repair/cleanup/adopt per policy), store-view mode (hardlink farm from `hardlink_farm.rs`, private-NS sync, zero-length marker, `gcroots/` and `state/` at store-view root, `sync.lock` OFD), swtpm volume hardening (provisionIfNeverProvisioned + marker + fail-closed repair as in `swtpm_dir.rs`), path-free broker audit ops, storage lifecycle report, opaque BundleOpId contract preserved from `storage_contract.rs` Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-volume" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-volume-002", + "kind" : "work-item", + "owner" : "ADR046-volume-001; volume-local Provider owner", + "parallelGroup" : "wi:ADR-046-resources-volume", + "prerequisites" : [ + "ADR-046-resources-volume", + "ADR046-volume-001" + ], + "specId" : "ADR-046-resources-volume", + "topologicalRank" : 11, + "validation" : "`tests/unit/nix/cases/per-vm-state-ownership.nix` adapted to Volume LayoutEntry matrix; `tests/unit/smoke/smoke-eval-tpm.nix` migrated to TPM Volume invariant; `d2b-contract-tests/tests/storage_sync_contracts.rs` parity tests adapted; new: store-view same-filesystem, zero-length marker existence, sync.lock preserve-OFD, gcroots at store-view root (not meta/), state/ dir existence, swtpm fail-closed-on-missing-after-provision, anchored-path validators", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-virtiofs/src/` (controller, virtiofsd_argv.rs); `packages/d2b-provider-volume-virtiofs/tests/` (hermetic argv/lifecycle/ADR-0021 tests); `packages/d2b-provider-volume-virtiofs/integration/` (virtiofsd launch and guest-mount fixtures); `packages/d2b-provider-volume-virtiofs/README.md`" + ], + "detailedDesign" : "volume-virtiofs controller: attachment lifecycle, owned virtiofsd Process create/update/delete, argv generation (reuse current 14 tests), ADR 0021 invariant (`capabilityClasses: []`, `startRoot: false`, `sandbox: chroot`, user-NS via `userNamespace.mappingClass: process-principal-root`), per-attachment export socket readiness check (`unix-socket-exists` readiness kind; current v2 socket path: `/run/d2b/vms//-virtiofs-.sock`; v3: stable hash-derived private path under Zone runtime directory, never exposed in spec/status/API), guest-mount status observation, finalizer drain Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-resources-volume" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-volume-003", + "kind" : "work-item", + "owner" : "ADR046-volume-001; volume-virtiofs Provider owner", + "parallelGroup" : "wi:ADR-046-resources-volume", + "prerequisites" : [ + "ADR-046-resources-volume", + "ADR046-volume-001" + ], + "specId" : "ADR-046-resources-volume", + "topologicalRank" : 11, + "validation" : "Migrated `virtiofsd_argv` unit tests (14 tests); `tests/tools/gen-migration-ledger.sh` virtiofsd-argv-shape gate adapted; `minijail-validator-virtiofsd` gate adapted to Process sandbox spec; new: attachment lifecycle (create/ready/delete), ADR 0021 invariant rejection test, multi-attachment isolation, readOnly flag per access mode, store-view shared-dir = store-view/live (never /nix/store)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/resources-volume.nix`, `nixos-modules/options-volumes.nix`" + ], + "detailedDesign" : "Nix resource compiler for Volume/LayoutEntry/View/Attachment from d2b.zones config; strict schema validation; emit canonical JSON per Volume; generate store-view Volume per Guest (from current `d2b.vms.` → future flat `d2b.zones..resources.` with `type = \"Guest\"`) with hardlink-farm layout (gcroots/, state/ at root per `hardlink_farm.rs`); generate swtpm Volume for TPM-enabled Guests; emit volume-virtiofs attachment spec per virtiofs share; migration: store-view stateDir root configuration", + "entryContracts" : [ + "ADR-046-resources-volume" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-volume-004", + "kind" : "work-item", + "owner" : "ADR046-volume-001; Nix integrator", + "parallelGroup" : "wi:ADR-046-resources-volume", + "prerequisites" : [ + "ADR-046-resources-volume", + "ADR046-volume-001" + ], + "specId" : "ADR-046-resources-volume", + "topologicalRank" : 11, + "validation" : "nix-unit cases for store-view Volume output (gcroots at root), TPM Volume spec, virtiofs attachment spec, anchored-path rejection; render parity with current storage.json path rows; canonical JSON golden vector; Provider schema validation rejection; symlink target validation; bundle digest coverage", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-local/src/` (block-image, quota, snapshots, tmpfs, ACL reconciliation); `packages/d2b-provider-volume-local/tests/` (hermetic quota/tmpfs/ACL/block-image/snapshot tests); `packages/d2b-provider-volume-local/integration/` (block-image virtio-blk, FS-without-quota fixture, tmpfs memory-budget); `packages/d2b-provider-volume-virtiofs/src/` (single-writer enforcement, shared-write, private socket path contract); `packages/d2b-provider-volume-virtiofs/tests/` (single-writer rejection, shared-write capability gate, socket-path invariant); `packages/d2b-provider-volume-virtiofs/integration/` (shared-write cross-Guest fixture)" + ], + "detailedDesign" : "(1) **block-image SourceKind**: add `SourceKind::BlockImage` to volume-local; manage raw/qcow2 image file lifecycle; emit virtio-blk attachment spec consumed by Guest Provider; `quota.maxBytes` required; add store-overlay.img migration path for current `DiskInit` plan-op. (2) **Quota hard enforcement**: implement `enforcement: hard` capability check in volume-local at Volume creation time; query backing FS for quota/limits support; test with no-project-quota fixture; enforce `maxBytes`/`maxInodes` via xfs project quota or ext4 per-dir quota where available. (3) **Volume snapshots/migrations**: design and implement EphemeralProcess templates in volume-local catalog for snapshot (copy-on-write or rsync capture) and content migration (atomic rename + sync); no CLI-only path; all operations surface through resource API. (4) **Single-writer enforcement**: volume-virtiofs controller tracks active `read-write` attachment count; rejects a second `read-write` attachment while one is active (returns `ResourceConflict` error); `shared-write` mode accepted only if Provider declares `supportsSharedWrite: true`. (5) **tmpfs source**: implement tmpfs mount/unmount lifecycle in volume-local; `maxBytes` → `size=`, `maxInodes` → `nr_inodes=` mount options; charge memory against Host/Guest budget; cleanup unmounts on Volume deletion or restart. (6) **Bounds enforcement**: enforce max 1024 layout entries, 64 Views, 64 attachments at schema validation layer; add corresponding row to API request-size limit table in `ADR-046-resource-api-and-authorization`. (7) **File/symlink first-class lifecycle**: implement independent `createPolicy`/`repairPolicy`/`cleanupPolicy` for `file` and `symlink` entries; implement `target` field validation (relative, no `..`, must resolve within Volume root); `symlink` create writes the target link. (8) **ACL principal ResourceRef**: remove bare `{type,ref}` struct from AclGrant; implement `User/` ResourceRef resolution with User resource watch and re-reconcile on User revision change. (9) **Continuous ACL reconciliation**: implement `foreignChildPolicy: preserve|fail` in broker reconcile loop; re-apply `accessAcl`/`defaultAcl` to all existing entries and children on every repair cycle; emit `ForeignAclViolation` condition when `foreignChildPolicy: fail` and unexpected entries found. (10) **virtiofsd socket path contract**: implement stable hash-derived private socket path in volume-virtiofs (deterministic hash of Zone name + Volume name + attachment executionRef); assert path never appears in public status, spec, audit, or CLI output; validate with a dedicated security invariant test.", + "entryContracts" : [ + "ADR-046-resources-volume" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-volume-005", + "kind" : "work-item", + "owner" : "ADR046-volume-002, ADR046-volume-003; respective Provider owners", + "parallelGroup" : "wi:ADR-046-resources-volume", + "prerequisites" : [ + "ADR-046-resources-volume", + "ADR046-volume-002", + "ADR046-volume-003" + ], + "specId" : "ADR-046-resources-volume", + "topologicalRank" : 12, + "validation" : "(1) `VirtioblkArgvInput` unit tests; block-image integration fixture. (2) Quota-enforcement fixture with FS-without-quota; hard-enforcement failure test. (3) EphemeralProcess snapshot lifecycle test; content-migration parity test. (4) Single-writer rejection test; shared-write capability gate test. (5) tmpfs mount/unmount lifecycle test; memory-budget accounting assertion. (6) Schema bound rejection tests (1025 entries, 65 views, 65 attachments). (7) File/symlink independent lifecycle tests; target validation (absolute rejected, `..` rejected, escape rejected). (8) ACL principal ResourceRef validation; numeric form rejected; User revision trigger test. (9) foreignChildPolicy preserve/fail tests; continuous repair cycle test. (10) Socket path invariant test; no-status-leak assertion.", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/resources-volume.nix` (Nix eval-time schema validation, canonical JSON emission, bundle digest); `packages/d2b-core-controller/src/configuration.rs` (config-publication handler cleanup logic); `packages/d2b-contracts/src/v3/zone_bundle.rs` (bundle index schema)" + ], + "detailedDesign" : "**Nix eval/build validation**: implement all 15 validation steps in §Nix eval/build validation as Nix assertions; abort build on any failure with structured error (Volume name + field path + error class); provider-specific settings schema (`root-config.schema.json`, `attachment.schema.json`) read from the private artifact catalog entry for each Provider's `artifactId` by the resource compiler; validate against `lib.evalModules`-compatible schema; emit canonical sorted JSON with all defaults materialized; compute SHA-256 per resource; emit Zone resource bundle with `generationId` and `bundleDigest`. **Config-publication handler cleanup**: on new bundle activation, diff resources with `metadata.managedBy = \"configuration\"` between new and prior bundle; issue async Delete for resources absent from new bundle; mark deleted resources with `ConfigurationRemoved` condition; track pending-cleanup set in Zone status; Zone status is `Degraded/PendingCleanup` while prior-generation deletions are in progress; activation is immediate but Zone readiness reflects cleanup completion. **Config-owned vs controller-created distinction**: `metadata.managedBy = \"configuration\"` is the authoritative marker set by core at activation; the bundle index carries only digests; controller-created resources have `metadata.managedBy = \"controller\"` and are never touched by the configuration cleanup pass. **Prior generation retention**: retain `priorGenerationCount` prior generations (default 3, range 1..16); no time-based TTL; when count is exceeded prune oldest generation from the store with a tamper-evident audit record. **Generation reactivation**: re-activate any retained prior bundle via `ActivateGeneration` operation; cancel in-flight Deletes for resources being reinstated; issue Deletes for resources added by the aborted new generation.", + "entryContracts" : [ + "ADR-046-resources-volume" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-volume-006", + "kind" : "work-item", + "owner" : "ADR046-volume-001, ADR046-volume-004; Nix integrator and core-controller config-publication owner", + "parallelGroup" : "wi:ADR-046-resources-volume", + "prerequisites" : [ + "ADR-046-resources-volume", + "ADR046-volume-001", + "ADR046-volume-004" + ], + "specId" : "ADR-046-resources-volume", + "topologicalRank" : 12, + "validation" : "Tests per §Cleanup contract — Tests for removed-resource cleanup table (10 tests); nix-unit: `volume_canonical_json_golden_vector`, `volume_bundle_digest_covers_all_resources`, `provider_schema_validation_rejects_unknown_fields`, `symlink_target_escape_rejected_at_eval`, `tmpfs_without_quota_rejected_at_eval`, `layout_bounds_1025_entries_rejected`, `attachment_bounds_65_rejected`, `conflicting_host_paths_rejected`; integration: cleanup audit redaction, generation reactivation, prior-generation pruning", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-vsock/src/effect_port.rs`; test fake in `tests/effect_port_mock.rs`; redaction checks in `tests/redaction.rs`." + ], + "detailedDesign" : "Implement `VsockEffectPort` trait and `OpaqueEndpointId`/`OpaqueBindingId` newtypes. Define `VsockEffectPort` async trait and opaque ID newtypes in `effect_port.rs`; implement `FakeVsockEffectPort` for tests; `redaction.rs` asserts no raw `u32` in any `Debug`/`Display` output of opaque types; no real vsock socket opened. Primary reuse disposition: `create`. Preserved source-plan detail: net-new trait/newtypes with redaction tests; no real vsock socket opened.", + "entryContracts" : [ + "ADR-046-provider-transport-vsock" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vsock-001", + "kind" : "work-item", + "owner" : "Title: Implement `VsockEffectPort` trait and `OpaqueEndpointId`/`OpaqueBindingId` newtypes; Phase 1; Priority P0; Depends on ADR046-bus-001 (OwnedTransport in d2b-session); Owner crate `d2b-provider-transport-vsock`.", + "parallelGroup" : "wi:ADR-046-provider-transport-vsock", + "prerequisites" : [ + "ADR-046-provider-transport-vsock", + "ADR046-bus-001" + ], + "specId" : "ADR-046-provider-transport-vsock", + "topologicalRank" : 11, + "validation" : "Proof type: hermetic unit + redaction test; `tests/effect_port_mock.rs` and `tests/redaction.rs`.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-vsock/src/framing.rs` and `src/bridge.rs`; tests in `packages/d2b-provider-transport-vsock/tests/framing.rs`." + ], + "detailedDesign" : "Implement framing utilities and bridge task in Provider crate. Copy `FramedVsockTransport` framing-only code (length-prefix encode/decode, bounded allocation, EOF/reset) from main `a1cc0b2d` → `framing.rs`; implement bridge task pumping bytes between an opaque `AsyncRead+AsyncWrite` stream from `VsockEffectPort::open` and the named ComponentSession stream; hermetic tests using `FakeVsockEffectPort` (no real socket). Primary reuse disposition: `adapt`. Preserved source-plan detail: copy/adapt framing-only code; exclude raw AF_VSOCK socket calls and ADR 0045 endpoint-role assumptions.", + "entryContracts" : [ + "ADR-046-provider-transport-vsock" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vsock-002", + "kind" : "work-item", + "owner" : "Title: Implement framing utilities and bridge task in Provider crate; Phase 1; Priority P0; Depends on ADR046-vsock-001; Owner crate `d2b-provider-transport-vsock`.", + "parallelGroup" : "wi:ADR-046-provider-transport-vsock", + "prerequisites" : [ + "ADR-046-provider-transport-vsock", + "ADR046-vsock-001" + ], + "specId" : "ADR-046-provider-transport-vsock", + "topologicalRank" : 12, + "validation" : "Proof type: hermetic framing tests; `tests/framing.rs` covers partial/coalesced records, oversized frames, EOF/reset classification, and no real socket.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-vsock/src/service.rs`; tests `tests/open_close.rs`, `tests/observe.rs`, and conformance kit." + ], + "detailedDesign" : "Implement `VsockTransportService` (OpenTransport/CloseTransport/ObserveTransport). Child core resolves the exact six-field ZoneLink spec, validates `transportSettings` through the Provider selected by `transportProviderRef`, rejects non-empty `transportCredentials`, and passes only derived opaque IDs/deadline to `service.rs`; `open_close.rs`, `observe.rs`, and topology tests cover the service and reject legacy ZoneLink provider/fingerprint/capability fields; conformance kit passes. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new service implementation over ComponentSession and fake effect port tests.", + "entryContracts" : [ + "ADR-046-provider-transport-vsock" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vsock-003", + "kind" : "work-item", + "owner" : "Title: Implement `VsockTransportService` (OpenTransport/CloseTransport/ObserveTransport); Phase 1; Priority P0; Depends on ADR046-vsock-002 and ADR046-bus-001; Owner crate `d2b-provider-transport-vsock`.", + "parallelGroup" : "wi:ADR-046-provider-transport-vsock", + "prerequisites" : [ + "ADR-046-provider-transport-vsock", + "ADR046-bus-001", + "ADR046-vsock-002" + ], + "specId" : "ADR-046-provider-transport-vsock", + "topologicalRank" : 13, + "validation" : "Proof type: service round-trip plus exact-shape tests; `tests/open_close.rs`, `tests/observe.rs`, `tests/topology.rs::{canonical_zonelink_spec_fields_are_exact,legacy_zonelink_provider_fields_are_rejected,transport_credentials_must_be_empty}`, and provider conformance tests.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`d2b-core-controller` child Zone runtime `LiveVsockEffectPort`; child-local Provider receives it by dependency injection at startup." + ], + "detailedDesign" : "Implement `LiveVsockEffectPort` in the child Zone runtime. It consumes the child's sealed binding for the allocator selected by compiler-only `parentZone`, resolves opaque endpoint/binding IDs only inside the effect adapter, opens the child AF_VSOCK endpoint, and injects an opaque stream into the same-Zone Provider service; the selected parent keeps its peer binding and port registry only as sealed allocator/route state; no raw CID/port is exposed to the Provider and no FD or ResourceRef crosses Zones. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new core adapter; keep raw AF_VSOCK syscalls outside Provider crate.", + "entryContracts" : [ + "ADR-046-provider-transport-vsock" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vsock-004", + "kind" : "work-item", + "owner" : "Title: Implement `LiveVsockEffectPort` in child Zone runtime; Phase 2; Priority P0; Depends on ADR046-vsock-001 and the Zone allocator (`ADR-046-resources-zone-control`); Owner crate `d2b-core-controller`.", + "parallelGroup" : "wi:ADR-046-provider-transport-vsock", + "prerequisites" : [ + "ADR-046-provider-transport-vsock", + "ADR046-vsock-001" + ], + "specId" : "ADR-046-provider-transport-vsock", + "topologicalRank" : 12, + "validation" : "Proof type: integration test; `integration/host_guest.rs` exercises live open/close byte round-trip with the injected effect and proves the selected parent has only sealed allocator/route state, with no parent-store Provider/ZoneLink row.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "ProviderDeployment Volume creation/deletion path plus `packages/d2b-provider-transport-vsock/tests/state_volume.rs`." + ], + "detailedDesign" : "Child core ProviderDeployment creates/deletes the service component state Volume in the same child Zone. It creates `Volume/transport-vsock--service--empty-state--*` before the component Process and deletes it after the Process finalizer; the transport-vsock service does not own Volume, add Volume to exported ResourceTypes, or create its prerequisite; Volume spec: empty schema, `kind: state`, `persistenceClass: persistent`, `migrationPolicy: none`, `User/d2b-transport-vsock` owner, minimal nonzero `quota.maxBytes`/`quota.maxInodes` with `enforcement: hard`, `private` sensitivity, `broker-maintained` identity marker; `state_volume.rs` tests the canonical schema; installation/removal tests verify marker lifecycle; no operator-authored or parent-store Volume exists; component receives only its child-local dirfd view. Primary reuse disposition: `create`. Preserved source-plan detail: net-new ProviderDeployment/Volume integration and tests.", + "entryContracts" : [ + "ADR-046-provider-transport-vsock" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vsock-005", + "kind" : "work-item", + "owner" : "Title: Child core ProviderDeployment creates/deletes service component state Volume; Phase 1; Priority P0; Depends on the volume-local Provider (`ADR-046-provider-volume-local`); Owner crate `d2b-provider-transport-vsock`.", + "parallelGroup" : "wi:ADR-046-provider-transport-vsock", + "prerequisites" : [ + "ADR-046-provider-transport-vsock" + ], + "specId" : "ADR-046-provider-transport-vsock", + "topologicalRank" : 11, + "validation" : "Proof type: unit + integration test; `tests/state_volume.rs` and Provider install/remove integration tests verify schema, user refs, marker lifecycle, and no ComponentPrincipal.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-transport-vsock/integration/host_guest.rs` and `integration/no_fd_transfer.rs`." + ], + "detailedDesign" : "Integration test: fixture declares compiler-only `k1.parentZone = \"local-root\"`, puts the selected Provider and an exact six-field ZoneLink (`transportProviderRef`, validated `transportSettings`, empty `transportCredentials`, `disabled`, and `limits`, plus self-matching `childZoneName`) only in K1, and gives local-root only sealed allocator/route state; `integration/host_guest.rs` opens a real Linux vsock path through K1's `LiveVsockEffectPort`, then exercises `OpenTransport` + byte round-trip + `CloseTransport` and validates bridge throughput ≥ 512 MiB/s; `no_fd_transfer.rs` structurally rejects attachment packets and asserts no FD/ResourceRef crosses the Zone boundary. Primary reuse disposition: `create`. Preserved source-plan detail: net-new integration coverage with no FD transfer over vsock.", + "entryContracts" : [ + "ADR-046-provider-transport-vsock" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vsock-006", + "kind" : "work-item", + "owner" : "Title: Integration test: real vsock socketpair + full ZoneLink open/close; Phase 2; Priority P1; Depends on ADR046-vsock-003 and ADR046-vsock-004; Owner crate `d2b-provider-transport-vsock`.", + "parallelGroup" : "wi:ADR-046-provider-transport-vsock", + "prerequisites" : [ + "ADR-046-provider-transport-vsock", + "ADR046-vsock-003", + "ADR046-vsock-004" + ], + "specId" : "ADR-046-provider-transport-vsock", + "topologicalRank" : 14, + "validation" : "Proof type: integration test; `make test-integration` runs `host_guest.rs` and `no_fd_transfer.rs`.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "Remove legacy paths from `d2b-host` and `d2bd`; replacement lives in `observability-otel` Provider native vsock relay and Guest resource lifecycle/bootstrap." + ], + "detailedDesign" : "Delete legacy socat OTLP relay and CONNECT-proxy guest-control vsock. Remove `vsock_relay_argv.rs` socat path after `observability-otel` Provider native vsock relay passes parity; remove `guest_control_vsock.rs` CONNECT-proxy after Guest resource lifecycle + guestd vsock bootstrap reach parity; no raw CID or socat vsock path remains. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete after replacement parity; preserve reserved guest-control/OTLP port exclusions until replacements own them.", + "entryContracts" : [ + "ADR-046-provider-transport-vsock" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vsock-007", + "kind" : "work-item", + "owner" : "Title: Delete legacy socat OTLP relay and CONNECT-proxy guest-control vsock; Phase 3; Priority P2; Depends on the observability-otel Provider (`ADR-046-provider-observability-otel`) and the Guest resource lifecycle (`ADR-046-resources-host-guest-process-user`); Owner crates `d2b-host`, `d2bd`.", + "parallelGroup" : "wi:ADR-046-provider-transport-vsock", + "prerequisites" : [ + "ADR-046-provider-transport-vsock" + ], + "specId" : "ADR-046-provider-transport-vsock", + "topologicalRank" : 11, + "validation" : "Proof type: deletion + parity test; parity tests for observability-otel and Guest lifecycle plus redaction checks that no raw CID/socat vsock path remains.", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-virtiofs/src/virtiofsd_argv.rs`; `packages/d2b-provider-volume-virtiofs/tests/argv_golden.rs`" + ], + "detailedDesign" : "Create crate skeleton with mandatory `src/`, `tests/`, `integration/`, `README.md`. Extract `VirtiofsdArgvInput` and `generate_virtiofsd_argv` with these changes: (1) replace `extra_args: Vec` with nothing (removed); (2) replace `socket_path: String` with `socket_path: SocketPath` newtype backed by `socket_path.rs`; (3) add `shared_dir_fd: i32` replacing `shared_dir: String` (FD-based); (4) replace `socket_group: Option` with `socket_group: Option`. Implement `socket_path.rs`: private path using SHA-256 of `\\x00\\x00`, truncated 8 hex chars, formatted as `/vms//vol-.vfd.sock`. Assert path length ≤ 108 bytes. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-volume-virtiofs" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vvfs-001", + "kind" : "work-item", + "owner" : "ADR046-volume-001 (Volume contract types); ADR046-vvfs-export-001 (Export type); W1; volume-virtiofs Provider owner", + "parallelGroup" : "wi:ADR-046-provider-volume-virtiofs", + "prerequisites" : [ + "ADR-046-provider-volume-virtiofs", + "ADR046-volume-001", + "ADR046-vvfs-export-001" + ], + "specId" : "ADR-046-provider-volume-virtiofs", + "topologicalRank" : 12, + "validation" : "`tests/argv_golden.rs`: 14 migrated tests + `no_extra_args_ever_emitted`, `socket_path_is_not_in_args`, `shared_dir_is_fd_path`, `path_length_within_sunpath_limit`; `tests/socket_path_privacy.rs`: `socket_path_not_in_export_status`, `socket_path_not_in_volume_status`, `socket_path_not_in_audit_record`; `tests/schema_conformance.rs`: `process_spec_readiness_class_is_provider_defined`, `process_spec_readiness_has_no_kind_or_period_fields`, `process_spec_budget_cpu_request_limit_nested`, `process_spec_budget_memory_request_limit_nested`, `process_spec_budget_pids_limit_present`, `process_spec_budget_fds_limit_present`, `process_spec_sandbox_no_new_privileges_true`, `process_spec_sandbox_read_only_root_true`, `process_spec_no_host_uid_gid_in_spec`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-virtiofs/src/user_ns.rs` (conformance kit); `packages/d2b-provider-volume-virtiofs/tests/adr021_invariant.rs`" + ], + "detailedDesign" : "`user_ns.rs` contains only the conformance check and template descriptor assertion: verify that the virtiofsd-worker Process template declares `capabilityClasses: []`, `startRoot: false`, `noNewPrivileges: true`, `readOnlyRoot: true`, and `sandbox.userNamespace.mappingClass: process-principal-root`. `hostUid`/`hostGid` are NOT set by the controller — system-minijail resolves the mapping from the `User/vol--vfd` principal when building the LaunchTicket via the effect port. The conformance check rejects any template mutation that adds host capability classes, sets `startRoot: true`, disables `noNewPrivileges`, or disables `readOnlyRoot`. The user-NS pre-establishment code itself remains in `d2b-priv-broker/src/sys.rs` and is invoked via the system-minijail effect port. Primary reuse disposition: `extract`. Preserved source-plan detail: extract conformance kit only; pre-establishment code stays in broker.", + "entryContracts" : [ + "ADR-046-provider-volume-virtiofs" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vvfs-002", + "kind" : "work-item", + "owner" : "ADR046-vvfs-001; ADR046-volume-001; W1; broker/spawn owner", + "parallelGroup" : "wi:ADR-046-provider-volume-virtiofs", + "prerequisites" : [ + "ADR-046-provider-volume-virtiofs", + "ADR046-volume-001", + "ADR046-vvfs-001" + ], + "specId" : "ADR-046-provider-volume-virtiofs", + "topologicalRank" : 13, + "validation" : "`tests/adr021_invariant.rs`: `virtiofsd_capability_classes_must_be_empty`, `virtiofsd_start_root_must_be_false`, `virtiofsd_no_new_privileges_must_be_true`, `virtiofsd_read_only_root_must_be_true`, `process_spec_has_no_host_uid_gid`, `sandbox_namespace_never_emitted`, `user_ns_single_entry_single_uid_mapping`, `uid_map_write_ordering_uid_setgroups_gid`, `child_setuid_in_ns_not_host_uid`, `clone_newns_not_in_clone3_flags`, `child_exits_user_ns_sync_on_pipe_eof`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-virtiofs/src/controller.rs`; `packages/d2b-provider-volume-virtiofs/src/export.rs`" + ], + "detailedDesign" : "Implement volume-virtiofs-controller reconcile loop using toolkit ResourceClient. Watch selector: `virtiofs.d2bus.org.Export` resources (all in zone), owned Process resources, owned User resources, Volume resources (read-only for view/vcpu resolution), Guest resources (read-only for vcpu count). On `spec-generation-changed` for an Export: (1) resolve View from Volume; (2) check store-view marker if applicable; (3) resolve threadPoolSize from Guest vcpus; (4) ensure User/vol--vfd; (5) diff against current Process; (6) emit Create/UpdateSpec. On `owned-resource-changed` for a Process: update Export status. On `deletionRequestedAt` for Export: two-phase teardown (§6.2). Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-volume-virtiofs" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vvfs-003", + "kind" : "work-item", + "owner" : "ADR046-vvfs-001, ADR046-vvfs-002, ADR046-vvfs-export-001; ADR046-volume-001; W2; volume-virtiofs controller owner", + "parallelGroup" : "wi:ADR-046-provider-volume-virtiofs", + "prerequisites" : [ + "ADR-046-provider-volume-virtiofs", + "ADR046-volume-001", + "ADR046-vvfs-001", + "ADR046-vvfs-002", + "ADR046-vvfs-export-001" + ], + "specId" : "ADR-046-provider-volume-virtiofs", + "topologicalRank" : 14, + "validation" : "`tests/export_lifecycle.rs`: `export_create_spawns_virtiofsd_process`, `export_ready_when_socket_present`, `export_delete_terminates_virtiofsd`, `export_delete_waits_for_guest_mount_absent`, `export_delete_with_guest_unreachable_holds_finalizer_degraded`, `export_proof_of_ns_death_clears_finalizer`; `tests/multi_attachment.rs`: `two_guests_get_separate_exports_and_processes`, `process_failure_does_not_affect_sibling_export`; `tests/schema_conformance.rs`: `provider_state_set_volume_created_on_install`, `provider_state_set_volume_owner_ref_is_provider`, `provider_state_set_volume_layout_principal_is_user_not_component_principal`, `provider_state_set_no_cross_component_volume_sharing`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-virtiofs/src/readiness.rs`; `packages/d2b-provider-volume-virtiofs/integration/guest_mount_readiness/`" + ], + "detailedDesign" : "`unix-socket-exists` readiness: check file existence at the private socket path via a bounded blocking adapter (e.g., `tokio::task::spawn_blocking` wrapping `fstatat(2)` relative to the zone runtime `OwnedFd`, or an async-safe fd-relative equivalent); no blocking syscall on the async executor thread. Probe period 1 s; timeout 30 s. On socket present → set `Export.status.exportReady: true`. Guest-mount readiness: send `VirtioFsMountReady?` probe to guest-control health endpoint over vsock. Response `MountReady` sets `guestMountReady: true`. Response `MountAbsent` or timeout sets `guestMountReady: false`. The vsock health probe is async-native. If Guest is down, set Export `phase: Unknown`. All readiness probes (unix-socket-exists, guest-mount health) use bounded blocking adapters or async-safe fd-relative equivalents; no blocking I/O on the reconcile executor thread. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "entryContracts" : [ + "ADR-046-provider-volume-virtiofs" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vvfs-004", + "kind" : "work-item", + "owner" : "ADR046-vvfs-003; guest-control integration owner; W2", + "parallelGroup" : "wi:ADR-046-provider-volume-virtiofs", + "prerequisites" : [ + "ADR-046-provider-volume-virtiofs", + "ADR046-vvfs-003" + ], + "specId" : "ADR-046-provider-volume-virtiofs", + "topologicalRank" : 15, + "validation" : "`tests/export_lifecycle.rs` (extended); `integration/guest_mount_readiness/`: virtiofsd launches, socket appears, guest-control probe returns MountReady, guestMountReady flips to true; probe returns MountAbsent on umount", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-virtiofs/src/controller.rs` (pre-launch prerequisite check); `packages/d2b-provider-volume-virtiofs/integration/store_view_readonly/`" + ], + "detailedDesign" : "Before issuing Process Create for a store-view virtiofs Export, check that `live/.d2b-marker-` exists (zero-length, correct mode) via a bounded blocking adapter (e.g., `tokio::task::spawn_blocking` wrapping `fstatat(2)` relative to the zone runtime directory, or an async-safe fd-relative equivalent); no blocking syscall on the async executor thread directly. If absent, requeue with exponential backoff. Assert `--shared-dir` resolves to `store-view/live` (the `ro-store` View path), never to `/nix/store`. Validate in `integration/store_view_readonly/` that virtiofsd serves only paths under `store-view/live`.", + "entryContracts" : [ + "ADR-046-provider-volume-virtiofs" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vvfs-005", + "kind" : "work-item", + "owner" : "ADR046-vvfs-003, ADR046-vvfs-004; ADR046-volume-002 (store-view Volume); W3", + "parallelGroup" : "wi:ADR-046-provider-volume-virtiofs", + "prerequisites" : [ + "ADR-046-provider-volume-virtiofs", + "ADR046-volume-002", + "ADR046-vvfs-003", + "ADR046-vvfs-004" + ], + "specId" : "ADR-046-provider-volume-virtiofs", + "topologicalRank" : 16, + "validation" : "`integration/store_view_readonly/`: mount from guest reads closure paths; no host-store path escapes; `tests/argv_golden.rs`: `store_view_shared_dir_is_live_not_nix_store`; `tests/export_lifecycle.rs`: `store_view_launch_waits_for_marker`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/resources-volume.nix` (store-view and user Volume attachment emission); `nixos-modules/options-volumes.nix` (optional user-facing volume/attachment options)" + ], + "detailedDesign" : "Extend the Nix resource compiler to: (1) auto-emit a store-view Volume (with `ro-store` and `meta` Views, virtiofs ro-store attachment) per Guest that has a VM runtime Provider; (2) emit virtiofs attachment entries for explicitly configured user Volumes; (3) emit `User/vol--vfd` resources for each Volume with virtiofs attachments; (4) emit `Provider/volume-virtiofs` as a Provider resource when any virtiofs attachment is configured. volume-local creates Export resources at runtime (not in Nix bundle); no `virtiofs.d2bus.org.Export` resources appear in the Nix-emitted bundle. All eval validation steps (§16.5) apply.", + "entryContracts" : [ + "ADR-046-provider-volume-virtiofs" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vvfs-006", + "kind" : "work-item", + "owner" : "ADR046-vvfs-001, ADR046-volume-004; Nix integrator; W3", + "parallelGroup" : "wi:ADR-046-provider-volume-virtiofs", + "prerequisites" : [ + "ADR-046-provider-volume-virtiofs", + "ADR046-volume-004", + "ADR046-vvfs-001" + ], + "specId" : "ADR-046-provider-volume-virtiofs", + "topologicalRank" : 13, + "validation" : "nix-unit: `store_view_volume_auto_emitted_per_guest`, `volume_virtiofs_attachment_canonical_json`, `virtiofs_provider_emitted_when_attachment_configured`, `vfd_user_emitted_per_volume`, `second_read_write_attachment_rejected_at_eval`, `transport_virtiofs_requires_provider_installed`; drift-check gate for `nixos-modules/processes-json.nix` virtiofsdRunner removal", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider-volume-virtiofs/src/export.rs`; `packages/d2b-contracts/src/v3/virtiofs_export.rs`" + ], + "detailedDesign" : "Declare `virtiofs.d2bus.org.Export` ResourceType in `d2b-contracts`. Base fields: `providerRef`, `volumeRef`, `executionRef`, `view`, `access`, `mountPath`; virtiofs tunables live under `spec.provider.settings` (as in §4.2). Status fields: top-level `phase`/`conditions`, `status.resource.exportReady`, `status.resource.guestMountReady`, and `status.provider.details.workerProcessRef`. Strict serde `deny_unknown_fields`. Implement the conformance test fixture that validates schema fingerprint stability. The Export spec JSON schema and provider status extension schema are signed and included in the Provider package.", + "entryContracts" : [ + "ADR-046-provider-volume-virtiofs" + ], + "exitGate" : "ADR046-W6 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-vvfs-export-001", + "kind" : "work-item", + "owner" : "ADR046-volume-001; W1; volume-virtiofs Provider owner", + "parallelGroup" : "wi:ADR-046-provider-volume-virtiofs", + "prerequisites" : [ + "ADR-046-provider-volume-virtiofs", + "ADR046-volume-001" + ], + "specId" : "ADR-046-provider-volume-virtiofs", + "topologicalRank" : 11, + "validation" : "`tests/schema_conformance.rs`: `export_schema_canonical_json_stable`, `export_spec_denied_unknown_fields`, `export_status_exportready_is_boolean_not_path`, `export_owner_must_be_volume`", + "wave" : "W6" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/{services,state,identity,provider}.rs` (new v3 namespace); generated stubs regenerated from v3 proto files in `packages/d2b-contracts/proto/v3/`" + ], + "detailedDesign" : "**Selected**: `MethodSpec`/`ServiceSpec`/fingerprinting infrastructure → v3 Zone API service schema fingerprinting; `StrictWireMessage`/`decode_strict`/`encode_strict`/`admit_metadata` → v3 wire decode/admit for all resource API requests; `CanonicalPayloadVerifier` payload-digest binding → v3 resource store integrity checks; audit chain types `AuditRecord`/`AuditSegment`/`AuditCheckpoint`/`detect_audit_gap` + `MAX_AUDIT_RETENTION_DAYS=14`/`MAX_AUDIT_RECORD_BYTES=8192`/`MAX_AUDIT_SEGMENT_BYTES=64MiB` → v3 Zone audit (§13.3); lock types `LockSpec`/`SyncInventory`/`LeaseRecord` + `MAX_LOCKS=1024`/`MAX_LOCK_DEADLINE_MS=300_000` → v3 resource store lock layer; bounded opaque ID pattern `[a-z][a-z0-9-]{0,63}` → v3 `OperationId`/`HandleId`/`PlanId`/`LeaseId`; `Fingerprint` 64-hex-char → v3 Provider `spec.configFingerprint`; `ProviderContractError` 34 variants → v3 Provider operation error taxonomy; canonical name constraint `^[a-z][a-z0-9-]*$` max 63 bytes → same as `ids.rs::is_label()` shared validator (§16.2). **Excluded ADR45 assumptions**: `RealmLabel`/`WorkloadName`/`RealmPath` identity types: ADR45 workload/realm address format; replaced by `metadata.name` + Zone `ResourceRef`. `ProviderType::ALL` fixed 11-type closed enum: v3 Provider type is an open string field in the Provider resource spec. `STATE_SCHEMA_VERSION=2`/`STATE_SCHEMA_GENERATION=1`: v3 store schema uses redb table versioning, not a JSON schema version field. v2 service fingerprint tests reference ADR45-specific proto files; v3 fingerprints use different proto inputs but the same `service_schema_fingerprint` seeding mechanism. Generated stubs in `generated_v2_services/` are v2-specific and excluded; v3 uses regenerated stubs from `proto/v3/`.", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-wire-001", + "kind" : "work-item", + "owner" : "ADR046-zone-control-013", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-013" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 11, + "validation" : "`v3-canonical-name-matches-ids-is-label-regex`, `v3-service-fingerprint-changes-on-method-mutation` (behavior ported from `public_endpoint_fingerprint_binds_daemon_and_guest_method_descriptors`), `v3-audit-gap-detection-covers-missing-segment`, `v3-strict-wire-rejects-unknown-fields`, `v3-state-envelope-digest-mismatch-rejected`, `v3-canonical-payload-verifier-binding-holds-under-mutation`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/zone.rs`; `packages/d2b-core-controller/src/zone.rs`" + ], + "detailedDesign" : "Zone ResourceType schema with `spec = {}`; self-resource enforcement; store_meta binding checks; phase/conditions; cardinality-1 enforcement; `zone-drain` finalizer; canonical JSON schema. Nix Zone options include compiler-only `parentZone`: required for every non-root Zone, forbidden on `local-root`, and never emitted into `Zone.spec` Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`RealmId`→`ResourceName` validators, `RealmPath` depth bound→ZoneLink depth, `RealmControllerPlacement`→Provider component placement); new (Zone resource schema/store table/handler).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-001", + "kind" : "work-item", + "owner" : "ADR046-object-001 (resource envelope); ADR046-store-001 (redb store); ADR046-identities-001 (types)", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-identities-001", + "ADR046-object-001", + "ADR046-store-001" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 10, + "validation" : "`zone-self-resource-enforced`, `zone-uid-mismatch-quarantine`, `zone-name-mismatch-rejected`, `zone-cardinality-one`, `zone-cross-zone-ref-rejected`, `zone-owner-rejected`, `zone-deletion-only-on-drain`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/zone_link.rs`; `packages/d2b-core-controller/src/zone_link.rs`" + ], + "detailedDesign" : "Child-local ZoneLink schema with self-matching `spec.childZoneName` plus same-Zone `spec.transportProviderRef`, `spec.transportSettings`, and `spec.transportCredentials`; at most one uplink resource per non-root Zone and none in local root; child-store cursor/intent state; reconnect loop with exponential backoff; local intent queue (max 256 entries); local child UID change detection; drain finalizer; no reciprocal parent-store resource; compiler-only `parentZone` selects the one allocator owner, while that allocator owns privileged listener/placement/route allocation effects and exposes only a sealed bootstrap/allocation interface Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`SecurePeerSession` Noise model → ComponentSession Noise KK; `SessionLifecycle`/`SessionPhase` → ZoneLink session reconnect loop and connection detail fields; `Connecting`/`Established` current evidence phases drive `status.connected` and `status.phase` transitions to `Pending`/`Ready`, not direct phase values; `RouteTreeEngine.decide_route()` → cursor tracking; `RealmIdentityStore` enrollment → ZoneLink child key-pin).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-002", + "kind" : "work-item", + "owner" : "ADR046-zone-control-001; ADR046-zone-control-018 (ComponentSession)", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-001", + "ADR046-zone-control-018" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 13, + "validation" : "`zonelink-reconnect-child-uid-change`, `zonelink-disconnect-unknown-phase`, `zonelink-intent-queue-limit`, `zonelink-disabled-no-reconnect`, `zonelink-child-auth-denied-failed`, `zonelink-drain-closes-session`, `zonelink-child-name-matches-store`, `zonelink-one-child-local-uplink`, `zonelink-parent-bootstrap-binding`, `zonelink-parent-has-no-reciprocal-row`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/provider.rs`; `packages/d2b-core-controller/src/provider_lifecycle.rs`; `packages/d2b-core-controller/src/api_catalog.rs`" + ], + "detailedDesign" : "Provider resource schema with all spec fields from §4.3, including resolved component bounds (max 8 controllers, 8 services, 32 worker templates, 16 ResourceTypes per controller); trust/conformance/config validation; component descriptor validation; dependency alias resolution; API binding with permission intersection; lifecycle policies; Nix Provider installation options; Provider crate layout enforcement per §4.8 (see ADR046-pkg-001) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`workload_lists_and_advertises`/`display_fails_closed_when_unsupported` conformance behavior; `RuntimeCapabilitySet`/`WorkloadCapabilitySet`/`NodeCapabilitySet` → Provider component `supportedCapabilities`; `ProviderError`/`RetryHint` → Provider lifecycle error schema; `ProviderId` → Provider `metadata.name` validator; `ProcessRole` variants → Provider component type identifiers).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-003", + "kind" : "work-item", + "owner" : "ADR046-zone-control-001; ADR046-api-001; ADR046-zone-control-017", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-api-001", + "ADR046-zone-control-001", + "ADR046-zone-control-017" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 12, + "validation" : "All §15.3 Provider tests including the resolved bounds checks", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/role.rs`; `packages/d2b-core-controller/src/authz.rs`" + ], + "detailedDesign" : "Role resource schema with rule schema from §5.3 and resolved bounds (32 rules, 16 resourceTypes per rule, 64 resourceNames, 32 executionRefs); separate closed resource/session verb enums including `relay`; core-generated ZoneLink ownership, exact adjacent-Zone enrollment selector, exact target bounds, and explicit admin-policy exception admission; explicit wildcard enforcement; index builder; phase/conditions; `role-binding-drain` finalizer; generated Nix Role option help; audit/OTEL instrumentation Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`verb_requires_admin()` verb table → Role verb enum; `DaemonAccessDecision` error types → Role admission errors; `MappedDaemonAccessPrincipal` → subject identity model).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-004", + "kind" : "work-item", + "owner" : "ADR046-zone-control-001; ADR046-api-002", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-api-002", + "ADR046-zone-control-001" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 11, + "validation" : "All §15.4 Role tests including closed-enum, relay origin/scope/target-verb, and resolved-bounds checks", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/role_binding.rs`; `packages/d2b-core-controller/src/authz.rs` (shared with Role handler)" + ], + "detailedDesign" : "RoleBinding resource schema with no expiry field and max 128 subjects; subject resolution and UID binding; external principal selector; scope narrowing intersection; revocation; immediate deletion with cache invalidation; Nix RoleBinding options Primary reuse disposition: `adapt`. Preserved source-plan detail: new (RoleBinding resource schema/store table/handler); adapt (`DaemonAccessAdmissionSource` identity fields → subject selector; `map_remote_daemon_access()` logic → subject UID-binding behavior).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-005", + "kind" : "work-item", + "owner" : "ADR046-zone-control-004", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-004" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 12, + "validation" : "All §15.5 RoleBinding tests, including the 128-subject admission bound and no-expiry lifecycle model", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-api/src/bootstrap_authz.rs`; Zone runtime startup path" + ], + "detailedDesign" : "Compiled bootstrap authorization as described in §9; exact subjects (system-core, system-minijail); closed verb table; non-configurable enforcement; atomic supersession after stored RBAC publishes; out-of-band reset path Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`authorize_peer()`/`verb_requires_admin()` verb table → bootstrap constant policy verb table; `PeerRole` two-variant model → system-core/system-minijail bootstrap subjects; `map_local_unix_daemon_access()` SO_PEERCRED mapping → bootstrap subject derivation; `LoadedRealmControllersConfig` startup path → Zone runtime startup bootstrap init sequence).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-006", + "kind" : "work-item", + "owner" : "ADR046-zone-control-004, ADR046-zone-control-005; Zone runtime", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-004", + "ADR046-zone-control-005" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 13, + "validation" : "All §15.6 bootstrap tests", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/options-zones.nix`; `nixos-modules/resources-zone-control.nix`; extended `nixos-modules/index.nix`" + ], + "detailedDesign" : "`d2b.zones..*` Nix options for Zone/ZoneLink/Provider/Role/RoleBinding/Quota/EmergencyPolicy authoring; compiler-only scalar `parentZone` required on non-root Zones and forbidden on `local-root`; Nix eval-time validation of parent existence, one resolved parent, self/cycle rejection, 16-name ancestry depth, ResourceRefs, separate resource/session verb enums (including session-only `relay`), relay origin/scope restrictions, digest format, subject types, child-local ZoneLink self-name/one-uplink/local transport-ref constraints, and the no-expiry RoleBinding model; generated help carries the relay semantics; canonical JSON serialization; generation-bound resource bundle output; the compiler seals the validated parent map into allocator bootstrap topology without emitting `parentZone` into `Zone.spec` or reciprocal resources Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (`providerType` submodule → Provider option submodule; label regex `^[a-z][a-z0-9-]*$` retained unchanged; `placement` option → Provider component placement; `d2b.realms.*` option namespace → `d2b.zones.*` option namespace).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-007", + "kind" : "work-item", + "owner" : "ADR046-identities-002; ADR046-zone-control-001; ADR046-zone-control-002; ADR046-zone-control-003; ADR046-zone-control-004; ADR046-zone-control-005; ADR046-zone-control-009; ADR046-zone-control-010", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-identities-002", + "ADR046-zone-control-001", + "ADR046-zone-control-002", + "ADR046-zone-control-003", + "ADR046-zone-control-004", + "ADR046-zone-control-005", + "ADR046-zone-control-009", + "ADR046-zone-control-010" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 14, + "validation" : "nix-unit vectors for each Zone control type schema; closed resource/session verb and relay restriction vectors; cross-field constraint tests; rendered JSON contract tests (`make test-drift`)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/host.rs` (Host resource schema, user-domain variant); `packages/d2b-core-controller/src/host_user.rs` (reconciler owned by Provider/system-core); Nix Host authoring via `d2b.zones..resources. = { type = \"Host\"; spec = { ... }; };` (validated per ResourceTypeSchema; no separate `options-zones-hosts.nix` submodule)" + ], + "detailedDesign" : "Host ResourceType schema for user-domain variant: `spec.defaultDomain=user`, `spec.allowedDomains=[user]`, `spec.defaultUserRef=User/`, `spec.shellPolicy` (adapted from `UnsafeLocalShellPolicy`), `spec.launcherItems` (adapted from `UnsafeLocalLauncherItem`). No-isolation posture recorded in `status.observedPosture`. A dedicated Host admission gate blocks unsafe-local Host creation without opt-in. Mandatory no-isolation warning in Host `status.conditions[0].message` and CLI/UI output for all Host commands. No-isolation posture included in audit record body (`isolation=no-isolation`); it is never emitted as an OTEL metric label value or span attribute. Child processes use standard Process Providers — no Provider resource with name or kind `unsafe-local` is created. Cardinality bounds: max 256 user-domain Hosts per Zone, max 64 launcher items per Host, max 64 shell sessions per Host. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (`WorkloadExecutionPosture` unsafe-local posture tuple → Host `status.observedPosture`; posture details in audit record body, not OTEL labels; `UnsafeLocalShellPolicy.{defaultName,maxSessions}` → Host `spec.shellPolicy`; `HelperRegistry.dispatch_launch` → Zone runtime Host process launch broker; `policy.allowUnsafeLocal` → dedicated Host admission gate); new (Host ResourceType user-domain schema with `defaultDomain`, `allowedDomains`, `defaultUserRef`, `shellPolicy`, cardinality bounds).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-008", + "kind" : "work-item", + "owner" : "ADR046-zone-control-001 (Zone resource); ADR046-zone-control-003 (Provider/system-core installed)", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-001", + "ADR046-zone-control-003" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 13, + "validation" : "`host-user-domain-no-isolation-warning-required`, `host-user-only-disallows-system-domain`, `host-allowUnsafeLocal-gates-creation`, `host-defaultUserRef-user-type-required`, `host-shell-policy-max-sessions-bound`, `host-launcher-item-max-count-bound`, `host-audit-body-isolation-label-present`, `host-otel-no-posture-label`, `host-cli-no-isolation-warning-present`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/quota.rs`; `packages/d2b-core-controller/src/quota.rs`; `packages/d2b-resource-api/src/quota_gate.rs`" + ], + "detailedDesign" : "Quota resource schema with all spec/status fields from §7; ceiling bounds enforcement at admission (hard policy: reject over-quota with `quota-exceeded`; soft policy: warn); usage index built from resource scan with `quotaRef` field; per-ResourceType ceiling in `perTypeCeilings`; `core.quota-drain` finalizer that blocks Quota deletion until all dependent resources with `spec.quotaRef` pointing to this Quota are reassigned or deleted by authorized owners/operators — the controller never issues spec-updates to clear `quotaRef` on other resources; `dependentCount` status field updated from dependency index; audit event `quota-check` per admission; Nix Quota options per §7.7", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-009", + "kind" : "work-item", + "owner" : "ADR046-zone-control-001; Zone store (ADR046-store-001); Quota handler owner", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-store-001", + "ADR046-zone-control-001" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 11, + "validation" : "`quota-ceiling-hard-reject`, `quota-ceiling-soft-warn`, `quota-ceiling-pertype`, `quota-drain-blocks-on-dependents`, `quota-over-quota-status`, `quota-nix-eval-bounds`, `quota-nix-build-pertype-unknown-type`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/emergency_policy.rs`; `packages/d2b-core-controller/src/emergency_policy.rs`; `packages/d2b-resource-api/src/emergency_gate.rs`" + ], + "detailedDesign" : "EmergencyPolicy resource schema from §8; union semantics: multiple `enabled=true` policies allowed simultaneously; effective scope = OR of all enabled policies' scope flags; effective `drainDeadlineSeconds` = minimum across enabled policies; scope flag evaluation: `stopNewAdmissions` signal to API admission gate, `disconnectZoneLinks` graceful signal to ZoneLink handler, `stopProviderProcesses` suppresses Process launch and sends stop signal to running Provider component Processes (does NOT set `deletionRequestedAt` on Process resources; reconciliation resumes on deactivation), `drainOngoingOperations` deadline drain; `core.emergency-drain` finalizer for enabled policies; audit events `emergency-policy-activated` / `emergency-policy-deactivated`; `reason` field stored in spec and included in audit record body (never in OTEL metric label values, structured log labels, or status fields); Nix EmergencyPolicy options per §8.7", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-010", + "kind" : "work-item", + "owner" : "ADR046-zone-control-001; Zone store (ADR046-store-001); EmergencyPolicy handler owner", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-store-001", + "ADR046-zone-control-001" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 11, + "validation" : "`emergency-policy-activates-gate`, `emergency-policy-disconnects-zonelinks`, `emergency-policy-union-most-restrictive`, `emergency-policy-multi-enabled-combined-scope`, `emergency-policy-stop-processes-no-delete`, `emergency-policy-deactivation-restores-gate`, `emergency-policy-drain-finalizer`, `emergency-nix-eval-drain-deadline-bound-tightest`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus/src/{lifecycle,engine,driver,streams,transport,error}.rs` (new crate `d2b-bus`)" + ], + "detailedDesign" : "**Selected**: full `SessionLifecycle` FSM including keepalive ping-nonce/timeout close (`poll_keepalive` lines 81–124), nonce-exhaustion close (`NonceExhausted` remediation `ReplaceGeneration`), reconnect attempt counting + window expiry (`begin_reconnect` lines 147–174), generation increment on reconnect; `ComponentSessionDriver` 18-method trait as the d2b-bus driver contract; `SessionEngine` full frame encode/decode, keepalive, named-stream credit/half-close/reset; `NamedStreamMux` credit model with send/receive credit, phase transitions, terminal removal; `DriverQueues` backpressure (`QueueBackpressure`) on both deliver and receive paths; all `SessionError`/`From<>` mapping chains. **Excluded ADR45 assumptions**: `establish_initiator_with_generation_discovery()` (lines 102–123): ADR45 initiator-probes-server for current generation; in v3 the generation lives in the Zone resource store — use `establish_initiator()` directly. OTEL labels in `metrics.rs` reference ADR45 realm/workload dimensions — replace with Zone/ZoneLink labels per §13.2. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-011", + "kind" : "work-item", + "owner" : "ADR046-identities-001; ADR046-store-001", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-identities-001", + "ADR046-store-001" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 10, + "validation" : "`session-lifecycle-reconnect-attempts-exhausted`, `session-lifecycle-keepalive-timeout-closes`, `session-lifecycle-nonce-exhausted-close-with-replace-generation-remediation`, `session-driver-cancelled-receiver-restores-event` (ported from test at line 707), `named-stream-credit-half-close-then-remote-close-transitions-closed`, `named-stream-reset-cancels-pending-send`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-bus-unix/src/{adapter,socket,pidfd,credit,descriptor,error,systemd}.rs` (new crate `d2b-bus-unix`)" + ], + "detailedDesign" : "**Selected**: `UnixSeqpacketTransport` seqpacket adapter implementing `OwnedTransport`; `PeerIdentityPolicy` SO_PEERCRED verification (pathname + inherited socketpair); `prearmed_seqpacket_pair()` for bootstrap inherited-socketpair path; `CreditPool`/`CreditScopeSet` six-scope credit model with per-packet/request/operation/session/process/host reservation; `ProcessCreditLimit::derive()` from `RLIMIT_NOFILE` minus `RESERVED_CONTROL_FDS`; `ProcPidfdIdentityVerifier` double-read race guard on `/proc/self/fdinfo/` with executable + cgroup digest callbacks; sealed memfd four-seal enforcement; `AncillaryCapacity` derived from `AttachmentPolicy`; `UnixSessionError` 21-variant Display strings preserved verbatim for audit log compatibility; `consume_peer_credentials()` strips-and-verifies SCM_CREDENTIALS exactly once on first packet. **Excluded ADR45 assumptions**: `vsock.rs` (`FramedVsockTransport`, `NativeVsockListener`, `NativeVsockTransport`): ADR45 guest-control vsock transport; v3 ZoneLink uses Unix sockets for host-local; vsock for Guest connections is separate work. Specific socket paths (`PUBLIC_SOCKET_PATH`, `BROKER_SOCKET_PATH`) are replaced by Zone-resource-managed paths. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-012", + "kind" : "work-item", + "owner" : "ADR046-zone-control-011", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-011" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 11, + "validation" : "`seqpacket-inherited-missing-credentials-rejected` (ported from `inherited_first_packet_requires_credentials`), `seqpacket-inherited-wrong-credentials-rejected` (ported from `inherited_first_packet_rejects_wrong_credentials`), `pidfd-double-read-race-guard-detects-pid-reuse`, `sealed-memfd-partial-seal-rejected`, `credit-scope-six-levels-ordered`, `ancillary-capacity-from-disabled-policy-is-zero`, `raw-control-unknown-header-rejected` (ported from `raw_control_scanner_rejects_unknown_and_partial_headers`), `raw-control-valid-rights-parsed` (ported from `raw_control_scanner_accepts_exact_rights_shape`)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/component_session.rs` (new v3 namespace in existing contracts crate)" + ], + "detailedDesign" : "**Selected**: all numeric constants verbatim; all 21 `HandshakeRejectReason` tags; `ComponentSessionPreface::parse/encode` with exact magic/length layout; `HandshakeOffer` 11-field shape; `AttachmentPolicy::validate` transport constraints (packet-atomic requires seqpacket or inherited-socketpair); `LimitProfile` 15-field profile; `NoiseHandshake` for Nn/Kk/IkPsk2 profiles; `EstablishedHandshake::transcript_hash`; `BoundedVec`. **Excluded ADR45 assumptions**: `EndpointPurpose` 19 tags and `ServicePackage` 15 tags encode ADR45-specific service families — v3 will append new tags for Zone API endpoints without renumbering existing ones. `IdentityEvidenceRequirement` + `GuestSessionCredentialV1` / `GuestBootstrapPsk` / `GUEST_SESSION_CREDENTIAL_MAGIC`: ADR45 guest bootstrap credential formats, excluded until v3 Guest bootstrap work item. Generation-discovery protocol (`encode_generation_discovery_request` lines 138–149): ADR45 initiator probes server for current generation; v3 generation lives in Zone resource store — generation-discovery excluded from initial d2b-bus copy. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-013", + "kind" : "work-item", + "owner" : "ADR046-identities-001", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-identities-001" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 10, + "validation" : "`preface-magic-exact-16-bytes`, `preface-offer-len-zero-rejected`, `preface-offer-len-over-max-rejected`, `preface-offer-len-canonical-accepted`, `handshake-21-reject-reasons-all-covered`, `attachment-policy-packet-atomic-requires-seqpacket-or-inherited`, `limit-profile-local-default-all-fields-positive`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`nixos-modules/options-zones.nix`, `nixos-modules/generated/resource-types.nix`, `nixos-modules/generated/options-zones-.nix`, `nixos-modules/resource-type-validators.nix`" + ], + "detailedDesign" : "Consume the generator and registry established by ADR046-routing-011 to declare the unified `d2b.zones..resources.` option tree plus the Zone-level compiler-only `parentZone` scalar. The generated standard registry is exactly the canonical 19 types (`Zone`, `ZoneLink`, `Provider`, `Host`, `Guest`, `Process`, `EphemeralProcess`, `Network`, `Volume`, `Credential`, `Device`, `Endpoint`, `User`, `Role`, `RoleBinding`, `Quota`, `EmergencyPolicy`, `ResourceExport`, `ResourceImport`), and every standard type has a strict generated `spec` submodule carrying the schema's Nix types, defaults, bounds, and documentation. Installed Provider artifacts may append only signed qualified ResourceTypes whose strict schema has been verified and generated into the evaluated option set. `type` is selected from that closed combined registry: an unknown standard string, an unqualified extension, or a qualified type without an installed verified schema fails evaluation; there is no unrestricted string or free-form `spec` fallback. Require `parentZone` on every non-root Zone and forbid it on `local-root`; resolve it against declared Zone keys, reject self-parent/conflicting module definitions/cycles, and cap each ancestry path at 16 Zone names. Compile the validated map into sealed allocator bootstrap topology; never emit it into the resource bundle or `Zone.spec`. Declare the global `d2b.artifacts.` catalog with `package` (types.package, required) and `type` (closed enum, required). Provider `spec.artifactId` is a plain catalog ID; the derivation is not a `spec` field. Implement the Phase 1 cross-resource assertions (§14.10 Phase 1 table); retain `credentialRef`, `resourceRef`, and `closedEnum` helpers; reject operator-authored `Zone`; and enforce child-local ZoneLink topology. The runtime creates `Zone/`, `Provider/system-core`, and `Provider/system-minijail` with `managedBy=controller`; none is emitted or inferred from the configuration bundle. `allowUnsafeLocal` maps to the dedicated Host admission gate. Provider manifest-derived fields (`spec.exports`, `spec.components`, `spec.dependencies`, `spec.permissionClaims`, `spec.upgradePolicy`, `spec.restartPolicy`) are read-only and setting one is an eval error.", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-014", + "kind" : "work-item", + "owner" : "ADR046-zone-control-001, ADR046-routing-011; Nix module owner", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-routing-011", + "ADR046-zone-control-001" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 11, + "validation" : "All Phase 1 eval tests in §15.8 (`nix-eval-name-regex-enforced`, `nix-eval-verb-closed-enum`, `nix-eval-session-verb-closed-enum`, `nix-eval-relay-session-verb-known`, `nix-eval-roleref-format`, `nix-eval-subject-type-restricted`, `nix-eval-no-duplicate-subjects`, `nix-eval-bootstrap-provider-rejected`, `nix-eval-provider-missing-artifact-id`, `nix-eval-artifact-id-not-in-catalog`, `nix-eval-artifact-wrong-type`, `nix-eval-artifact-id-format`, `nix-eval-credentialref-declared`, `nix-eval-dollar-key-rejected`, all five `nix-eval-parent-zone-*` vectors, `nix-eval-zonelink-child-name-mismatch-rejected`, `nix-eval-zonelink-second-uplink-rejected`, `nix-eval-zonelink-limits-maxpendingintents-bound`); Phase 2 runs `nix-build-relay-scope-restricted`; drift test asserts the standard registry and generated option modules cover exactly all 19 canonical types; negative evals reject unknown strings, unqualified extensions, unsigned or uninstalled qualified types, and unknown `spec` fields; a positive fixture admits an installed signed qualified type and validates its strict generated schema", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-resource-compiler/src/{main,bundle,schema,validator,digest,sort,secret_lint,generation}.rs`; exposed as `pkgs.d2b-resource-compiler`; called from `nixos-modules/resource-compiler.nix`" + ], + "detailedDesign" : "Implement all Phase 2 build-time checks (§14.10 Phase 2 table): dispatch on `type` to look up ResourceTypeSchema; validate each resource's `spec` canonical JSON against the committed schema (build validation compares canonical rendered JSON against ResourceTypeSchema for each core type); compile the `d2b.artifacts.*` catalog: for each entry, build/include the derivation, verify `type` is a recognized value, compute `digest` over the derivation output, extract and hash manifest and config schema files, validate signature chain and conformance attestation; detect duplicate artifact IDs; for each Provider resource, look up `spec.artifactId` in the compiled catalog (build failure if absent or wrong type), verify `configSchemaDigest` matches schema SHA-256, validate operator `spec.config` against loaded JSON Schema using a pure-Rust validator bundled in the derivation, verify `manifestDigest` and signature chain, load manifest-derived fields (`exports`, `components`, `dependencies`, `permissionClaims`, `upgradePolicy`, `restartPolicy`) into the bundle envelope; emit private integrity-pinned artifact catalog (ID → type/digest/closure metadata) as a separate private file (never merged into the public resource bundle); check `spec.rules[*].resourceTypes` against installed Provider catalogs in the bundle (Role); verify `spec.roleRef` names an existing Role in the bundle (RoleBinding); verify `spec.subjects[*]` names resolve in bundle (RoleBinding); check ResourceType short-name collision across all Zone Providers; RFC 8785 canonical JSON serialization; per-resource `digest` computation; `bundleDigest` computation over sorted `resources` array; inline-secret heuristic lint (`--strict-secrets` flag); `generation` counter persistence in Nix module state; emit `zone-resources.json` bundle with all fields per §14.9", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-015", + "kind" : "work-item", + "owner" : "ADR046-zone-control-014; resource compiler owner", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-014" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 12, + "validation" : "All Phase 2 build tests in §15.8 (`nix-build-artifact-id-missing-from-catalog`, `nix-build-artifact-wrong-type-rejected`, `nix-build-duplicate-artifact-id`, `nix-build-artifact-store-path-absent-from-bundle`, `nix-build-artifact-store-path-absent-from-config`, `nix-build-config-schema-failure`, `nix-build-schema-digest-mismatch`, `nix-build-manifest-digest-mismatch`, `nix-build-resourcetype-collision`, `nix-build-bundle-sorted`, `nix-build-bundle-digest-stable`, `nix-build-per-resource-digest-correct`, `nix-build-credential-ref-survives-build`, `nix-build-inline-secret-lint-warning`, `nix-build-inline-secret-strict-failure`)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/configuration.rs` (Phase 3 activation, diff, delete dispatch); `packages/d2b-core-controller/src/cleanup.rs` (pending tracking, status, stuck detection, rollback verb handler)" + ], + "detailedDesign" : "Implement all Phase 3 runtime activation checks (§14.10 Phase 3 table); `bundleDigest` integrity verify; `zoneUid` consistency check; `generation` monotone check; per-resource `digest` re-verify; atomic generation advance in `store_meta`; diff computation (resources with `managedBy=configuration` absent from new bundle → async Delete; `managedBy=controller`/`managedBy=api` resources untouched); `managedBy` and `configurationGeneration` field maintenance on resources in redb store; `cleanupPendingCount` and `generationCleanupPending` maintenance; Zone.status.phase → Degraded while cleanup pending, reverts on completion; `GenerationCleanupPending`/`GenerationCleanupFailed` condition management; stuck-cleanup `GenerationCleanupFailed=True` at `cleanupStuckThreshold` (default 5 min) with exponential backoff retry; prior generation bundle retention and pruning up to configured `retainedPriorGenerationCount` (default 3, range 1..16); audit emission for all four cleanup audit kinds (§14.11); `zone.config-rollback` verb handler Primary reuse disposition: `adapt`. Preserved source-plan detail: extract exponential backoff from `begin_reconnect`.", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-016", + "kind" : "work-item", + "owner" : "ADR046-zone-control-015; ADR046-zone-control-001; configuration publication handler owner", + "parallelGroup" : "wi:core-config-hub:w5", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-core-001", + "ADR046-zone-control-001", + "ADR046-zone-control-015" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 13, + "validation" : "All Phase 3 runtime and cleanup tests in §15.8 (`nix-runtime-bundledigest-integrity`, `nix-runtime-generation-monotone`, `nix-runtime-zoneuid-mismatch-rejected`, `nix-runtime-zonename-mismatch-rejected`, `nix-runtime-activation-nonblocking`, `nix-runtime-provider-config-invalid-continues`, all `cleanup-*` and `rollback-*` tests)", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-provider/src/{registry,rpc}.rs` (new v3 Provider package); `packages/d2b-provider-toolkit/src/{adapter,conformance,fixture,registration,server,values,redaction}.rs` (new v3 toolkit)" + ], + "detailedDesign" : "**Selected**: `ProviderRegistry` lifecycle `Accepting→Draining→Retired` with drain-waiter notify-race safety (ported from `shutdown_closes_final_permit_notify_race` / `finish_drain_closes_final_permit_notify_race` tests); `InFlightPermit` + global + per-provider in-flight quota; `ProviderRegistryManager::publish` validates snapshot before swap; `RpcProviderProxy::preflight` cancellation-check, deadline-check, method/capability match, session-identity/placement exactness; `ProviderAgentAdapter` rejects attachments with missing descriptors or non-increasing indexes (lines 79–99); `GeneratedProviderServiceServer::new` single-service-per-agent requirement + shutdown via atomic accept-flag + idle notify + timeout; `check_provider_conformance` health/inspection/observability check sequence; `Fixture`/`FakeProvider`/`DeterministicClock` as conformance harness; `Redacted`/`Secret` retained unchanged. **Excluded ADR45 assumptions**: `TrustedFirstPartyInProcess` as the only accepted placement (in `session_identity_matches_placement` lines 577–598): v3 Provider resources support multiple placements per `RealmControllerPlacement` mapping (§16.2); in-process placement is retained but not exclusive. Generated ttrpc stubs in `d2b-contracts/src/generated_v2_services/` are v2-service-specific; v3 Provider processes compile their own stubs from v3 proto files. ACA workload adapter (`d2b-gateway-runtime/src/aca_workload.rs`) excluded. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-017", + "kind" : "work-item", + "owner" : "ADR046-zone-control-011, ADR046-zone-control-013; Provider registry/toolkit foundation owner", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-011", + "ADR046-zone-control-013" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 11, + "validation" : "`provider-registry-drain-waiter-race-safe` (ported from both notify-race tests), `provider-registry-publish-validates-snapshot-before-swap`, `provider-rpc-proxy-placement-exact` (ported from `provider_and_user_agent_session_identities_are_placement_exact`), `provider-agent-adapter-rejects-non-monotone-attachment-indexes`, `provider-server-shutdown-drains-in-flight-requests`, `provider-conformance-health-inspection-observability-sequence`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/zone_link.rs` (ZoneLink handler); `packages/d2b-resource-api/src/admission.rs` (request admission)" + ], + "detailedDesign" : "**Selected**: `SessionPhase` 5-phase FSM (from main `d2b-session`) drives ZoneLink session state; `Established` state → `status.connected=true` + ZoneLink `status.phase=Ready`; `Disconnected`/`Reconnecting` states → `status.connected=false` + ZoneLink `status.phase=Pending` (or `Degraded` if degraded capability); session-internal phases are not exposed as `ZoneLink.status.phase` values — only the common Resource phases (`Pending|Ready|Degraded|Failed|Unknown`) appear in `status.phase` (§3.5); `begin_reconnect` window/attempt logic → ZoneLink reconnect loop; `RealmSessionAuthority` local vs gateway pattern → ZoneLink authority types for host-local vs transport-bridge sessions; `RealmServiceServer::call` wire validation (generation, request lifetime, attachment) → Zone API request admission; `RealmServiceLimits` 15 fields → ZoneLink `spec.limits`; `connect_component_session()` Nn peer-UID path → Zone runtime bootstrap ComponentSession; `NamedStream` lifecycle → ZoneLink named-stream operations; session-lifecycle tests ported as ZoneLink phase regression tests. **Excluded ADR45 assumptions**: `RealmSessionAuthority::gateway_peer()` (lines 72–87): gateway custody and `Locality::Remote` + `CredentialCustody::GatewayGuest` are ADR45 realm-gateway patterns; v3 ZoneLink transport is bound by the resolved `spec.transportProviderRef`, `spec.transportSettings`, and `spec.transportCredentials` contract instead. Realm 7-phase `SessionLifecycle` (`Allocating→…→Running→Stopping→Stopped`) is the ADR45 realm-specific lifecycle; ZoneLink uses the 5-phase d2b-session model. `GuestBootstrapPsk`/`GuestSessionCredentialV1`: ADR45 guest bootstrap, excluded. `realm_stubs.rs` `ApiService`/`ApiFrontend` dead code excluded (§16.2).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-018", + "kind" : "work-item", + "owner" : "ADR046-zone-control-011, ADR046-zone-control-012, ADR046-zone-control-013; ZoneLink session/admission foundation owner", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-011", + "ADR046-zone-control-012", + "ADR046-zone-control-013" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 12, + "validation" : "`zonelink-reconnect-child-uid-change`, `zonelink-disconnect-unknown-phase`, `zonelink-intent-queue-limit`, `zone-session-phase-forward-sequence-refuses-repeat` (ported from `forward_sequence_reaches_running_then_refuses_to_advance`), `zone-session-failure-records-phase` (ported from `failure_mid_establishment_rolls_into_teardown_and_records_phase`), `zone-session-stop-is-idempotent` (ported), `zone-bootstrap-session-nn-peer-uid-verified`", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-contracts/src/v3/{resource_export,resource_import}.rs` (base schemas); `packages/d2b-core-controller/src/export_import.rs` (core ZoneLink export/import routing controller); shared adapter trait in `packages/d2b-provider/src/share_adapter.rs` (`ExportAdapter`/`ImportAdapter` signed-capability traits)" + ], + "detailedDesign" : "Implement the `ResourceExport` and `ResourceImport` standard ResourceTypes per §8A plus signed Provider `ProjectionFactory` metadata binding qualified Service type, qualified Binding type, allowed owner-Service backing refs, allowed Binding target refs, projection schema/fingerprint, and aggregate factory fingerprint. Admission accepts only an owner Service as `ResourceExport.resourceRef`; matches export/import/local-factory type and fingerprints; and creates exactly one same-qualified-type projection Service (`ownerRef: ResourceImport/`). It never projects Device/Endpoint/Binding and never creates Binding. Binding spec is desired consumer intent only; observations belong only in status. No cross-Zone Ref, FD, secret, path, locator, or resource grant crosses a Zone; payload bytes use bounded encrypted named streams and high-churn sessions/streams remain internal. Export removal/ZoneLink loss revokes leases and degrades the projection Service; reconnect revalidates generation and both fingerprints. D091 currency propagates Service → export → import → projection Service → authored Binding → children. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (extend ZoneLink controller).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-019", + "kind" : "work-item", + "owner" : "ADR046-zone-control-001, ADR046-provider-004; ADR046-zonelink owner; `d2b-core-controller` + `d2b-contracts` owners", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-provider-004", + "ADR046-zone-control-001" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 11, + "validation" : "§8A.7: fast hermetic factory absent/mismatch/tamper, Service-only export target, exactly-one same-type projection Service, no Device/Endpoint/Binding projection, no auto-Binding, intent-only spec/status-only observations, backing/target allowlists, finalizer/update propagation, Provider classification, canonical Nix stability including compiler-only `d2b.zones.work.parentZone = \"local-root\"`, child-local `ZoneLink/work-uplink` in `d2b.zones.work.resources`, local `zoneLinkRef` resolution, quotas/reconnect/revoke, and no FD/secret/path tests; slower real encrypted-stream integration for audio/security-key/observability/policy-gated USBIP", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/export_import_projection.rs` (local qualified Service projection lifecycle owned by `ResourceImport`)" + ], + "detailedDesign" : "Core creates exactly one same-qualified-type projection Service per `ResourceImport` and keeps it synchronized with the remote Service lease. Operators/Nix separately author same-Zone matching Binding resources with `serviceRef` plus an allowed Guest/User/Zone target; Binding specs hold desired intent only and Binding controllers write observations only to status while owning Process/Endpoint children. On revoke, mark the projection draining/revoked and let Binding controllers stop children. On delete, wait for Bindings to be deleted/retargeted (`BindingReferencesRemain`), release the lease, delete only the projection Service/provider-owned children, then clear the import finalizer. Never create/delete Binding or project Device/Endpoint.", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-020", + "kind" : "work-item", + "owner" : "ADR046-zone-control-019; `d2b-core-controller` owner", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-019" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 12, + "validation" : "Exactly one same-type Service projection owned by import; no Device/Endpoint/Binding projection; Binding never auto-created/deleted; Binding target allowlist; intent-only spec/status-only observations; owned Process/Endpoint child cleanup; pending finalizer while Binding refs remain; reconnect only after generation/factory/schema revalidation; hermetic fake-adapter + real-stream integration tiers", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/{coordinator,configuration,zonelink}.rs`" + ], + "detailedDesign" : "Per D097 core-audit findings (§8B.2): move the process-global `USBIP_BACKGROUND_RECONCILE_ACTIVE`, `FORCE_SHUTDOWN_GENERATIONS`, and `activation_locks()` state into **per-Zone** provider/resource status or a per-Zone coordinator keyed by the authority index (no process-global singletons that ignore Zone boundaries). Migrate the configuration publisher's per-VM staging symbols to **per-Zone** staging under the single configuration-publisher authority. Make ZoneLink cursor persistence and restart adoption an authority owned by the ZoneLink handler (`ownerProof`; ambiguity quarantines). All coordinated through the core authority index; no direct broker path, no process-global lock. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (move process-global state to per-Zone status/coordinator).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-021", + "kind" : "work-item", + "owner" : "ADR046-zone-control-001, ADR046-zone-control-016; `d2b-core-controller` owner", + "parallelGroup" : "wi:core-config-hub:w5", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-core-001", + "ADR046-zone-control-001", + "ADR046-zone-control-016" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 14, + "validation" : "Two Zones on one host do not share `USBIP_BACKGROUND_RECONCILE_ACTIVE`/`FORCE_SHUTDOWN_GENERATIONS`/activation-lock state; per-Zone configuration staging isolation; ZoneLink cursor adoption by `ownerProof` and quarantine on ambiguity; hermetic with fakes", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/authority.rs`; resource API admission hook" + ], + "detailedDesign" : "Admission enforces **Provider controller cardinality** via the core authority index: most Providers are `exactly-one` per Zone; the observability Provider is `at-most-one` (zero-or-one). A `Create`/activation that would install a second controller for an `exactly-one` Provider (or a second observability Provider) is rejected with `duplicateConflict` naming the incumbent owner digest before any effect; config activation goes `Degraded`.", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-022", + "kind" : "work-item", + "owner" : "ADR046-zone-control-019, ADR046-api-001; `d2b-core-controller` + resource API owners", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-api-001", + "ADR046-zone-control-019" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 12, + "validation" : "Second Provider controller for an `exactly-one` Provider rejected with `duplicateConflict`; second observability Provider rejected; single controller admitted; `Degraded` config activation names the incumbent digest; hermetic", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/{quota,emergency_policy}.rs`; `packages/d2b-contracts/src/v3/{quota,emergency_policy}.rs`" + ], + "detailedDesign" : "Implement `Quota` and `EmergencyPolicy` scope-uniqueness as `exactly-one`-per-scope authorities in the core authority index: a second `Quota`/`EmergencyPolicy` claiming the same scope is a `duplicateConflict`. Add the scope-uniqueness admission, status, and the test matrix (per D094 fast hermetic tests). Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (implementation + tests).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-023", + "kind" : "work-item", + "owner" : "ADR046-zone-control-019; `d2b-core-controller` + `d2b-contracts` owners", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-019" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 12, + "validation" : "Duplicate-scope `Quota`/`EmergencyPolicy` rejected with `duplicateConflict`; single-scope admitted; union/individual scope flags honored; fast hermetic tests", + "wave" : "W5" + }, + { + "blockers" : [], + "destinations" : [ + "`packages/d2b-core-controller/src/authority.rs` (Host-global index scope + hardware admission)" + ], + "detailedDesign" : "Extend the core authority index so `host`, `physical-device`, `seat`, and `external-service` authorities are keyed **Host-global** (`(Host, authorityClass, opaqueKeyDigest)`), admitting exactly one owner across all Zones on the host, while `zone`-scoped authorities stay Zone-local. Enforce the §8B.3 hardware rows: GPU full-device exclusive vs render-node shared; per-Guest swtpm and physical TPM exclusive (state never wiped); one Core-derived `physical-usb-backing/v1` identity digest claimed through the exact `(Host, physical-usb-backing, opaqueKeyDigest)` tuple by every USB or security-key implementation before effects, plus the separate host-global `usbip-host` module and the Host-global `Provider/device-usbip` relay `Endpoint`, exactly one per Core-derived Network UID/signed-policy-port digest with multiplexed arbitration and `usbip-network-relay-authority-conflict` on a second owner; macvtap/NIC `parentInterface` `passthru` globally exclusive across all Zones; host-shared `/dev/kvm` and `/dev/vhost-vsock` as `Provider/system-core` grants (no 28th Provider, no `kvm` busClass); globally-unique vsock CID; other fixed listener ports as `Endpoint`s; host store + per-Guest store-view writer; Network TAP/bridge. A second Zone claiming the same physical backing receives `physical-usb-backing-conflict` before any open, bind, withhold, module, relay, or attachment effect; restart adopts by `ownerProof`; Guest-stop drains dependent leases. GPU-owned `udmabuf`/video and per-session `vhost-vsock` tokens stay authority subresources/DeviceGrants (not resources/Providers). Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (Host-global index scope for host/physical-device authorities).", + "entryContracts" : [ + "ADR-046-resources-zone-control" + ], + "exitGate" : "ADR046-W5 exit criteria (ADR-046-validation-and-delivery §4): every spec/work item in this wave Merged with clean destinations, all validators green, and the ten-role panel seal recorded", + "id" : "ADR046-zone-control-024", + "kind" : "work-item", + "owner" : "ADR046-zone-control-019, ADR046-zone-control-022; `d2b-core-controller` owner", + "parallelGroup" : "wi:ADR-046-resources-zone-control", + "prerequisites" : [ + "ADR-046-resources-zone-control", + "ADR046-zone-control-019", + "ADR046-zone-control-022" + ], + "specId" : "ADR-046-resources-zone-control", + "topologicalRank" : 13, + "validation" : "Two Zones on one host cannot both claim one GPU/TPM/USB/`/dev/kvm`/passthru NIC/vsock CID/fixed port — second is `duplicateConflict`; security-key and USB implementations resolving the same physical token submit byte-identical `physical-usb-backing` tuple keys and the loser receives `physical-usb-backing-conflict` before any effect; a second USBIP relay `Endpoint` for one Core-derived Network UID/signed-policy-port digest receives `usbip-network-relay-authority-conflict`, while multiple admitted Services share the multiplexed owner and no `Network` authority owns the listener/firewall; Provider-private authority classes/digests cannot bypass the collision; render-node shared admits bounded holders; per-Guest swtpm exclusive and marker never wiped; host-global adoption by `ownerProof`; hardware D096 exportability (GPU/KVM/TPM/store/macvtap non-exportable; semantic USB policy-gated); fast hermetic with fakes", + "wave" : "W5" + } + ], + "schemaVersion" : 1, + "status" : "Proposed", + "waves" : [ + { + "parallelGroups" : [ + "W0-foundation-chain", + "W0-reference-docs", + "wi:ADR-046-decision-register", + "wi:ADR-046-resource-api-and-authorization", + "wi:ADR-046-resource-object-model", + "wi:ADR-046-resource-store-redb", + "wi:ADR-046-terminology-and-identities" + ], + "specCount" : 6, + "wave" : "W0", + "workItemCount" : 10 + }, + { + "parallelGroups" : [ + "W1-reconcile-and-bus", + "wi:ADR-046-componentsession-and-bus", + "wi:ADR-046-resource-reconciliation" + ], + "specCount" : 2, + "wave" : "W1", + "workItemCount" : 6 + }, + { + "parallelGroups" : [ + "W2-composition-and-routing", + "wi:ADR-046-primitive-resource-composition", + "wi:ADR-046-zone-routing" + ], + "specCount" : 2, + "wave" : "W2", + "workItemCount" : 19 + }, + { + "parallelGroups" : [ + "W3-provider-contract", + "wi:ADR-046-provider-model-and-packaging" + ], + "specCount" : 1, + "wave" : "W3", + "workItemCount" : 4 + }, + { + "parallelGroups" : [ + "W4-parallel-specs", + "wi:ADR-046-components-processes-and-sandbox", + "wi:ADR-046-core-controllers", + "wi:ADR-046-provider-state", + "wi:ADR-046-resources-credential", + "wi:ADR-046-resources-network", + "wi:core-config-hub:w4" + ], + "specCount" : 5, + "wave" : "W4", + "workItemCount" : 32 + }, + { + "parallelGroups" : [ + "W5-parallel-specs", + "wi:ADR-046-cli-and-operations", + "wi:ADR-046-nix-configuration", + "wi:ADR-046-resources-device", + "wi:ADR-046-resources-host-guest-process-user", + "wi:ADR-046-resources-volume", + "wi:ADR-046-resources-zone-control", + "wi:ADR-046-telemetry-audit-and-support", + "wi:core-config-hub:w5" + ], + "specCount" : 7, + "wave" : "W5", + "workItemCount" : 141 + }, + { + "parallelGroups" : [ + "W6-credentials", + "W6-interaction", + "W6-storage-network-device", + "W6-system-host-guest", + "W6-transport-observability-activation", + "wi:ADR-046-provider-activation-nixos", + "wi:ADR-046-provider-audio-pipewire", + "wi:ADR-046-provider-clipboard-wayland", + "wi:ADR-046-provider-credential-entra", + "wi:ADR-046-provider-credential-managed-identity", + "wi:ADR-046-provider-credential-secret-service", + "wi:ADR-046-provider-device-gpu", + "wi:ADR-046-provider-device-security-key", + "wi:ADR-046-provider-device-tpm", + "wi:ADR-046-provider-device-usbip", + "wi:ADR-046-provider-display-wayland", + "wi:ADR-046-provider-network-local", + "wi:ADR-046-provider-notification-desktop", + "wi:ADR-046-provider-observability-otel", + "wi:ADR-046-provider-runtime-azure-container-apps", + "wi:ADR-046-provider-runtime-azure-virtual-machine", + "wi:ADR-046-provider-runtime-cloud-hypervisor", + "wi:ADR-046-provider-runtime-qemu-media", + "wi:ADR-046-provider-shell-terminal", + "wi:ADR-046-provider-system-core", + "wi:ADR-046-provider-system-minijail", + "wi:ADR-046-provider-system-systemd", + "wi:ADR-046-provider-transport-azure-relay", + "wi:ADR-046-provider-transport-unix", + "wi:ADR-046-provider-transport-vsock", + "wi:ADR-046-provider-volume-local", + "wi:ADR-046-provider-volume-virtiofs", + "wi:core-controller-coordination:w6" + ], + "specCount" : 27, + "wave" : "W6", + "workItemCount" : 257 + }, + { + "parallelGroups" : [ + "W7-closing", + "wi:ADR-046-feasibility-and-spikes", + "wi:ADR-046-reset-and-cutover", + "wi:ADR-046-security-and-threat-model", + "wi:ADR-046-streamline", + "wi:ADR-046-validation-and-delivery" + ], + "specCount" : 5, + "wave" : "W7", + "workItemCount" : 74 + } + ] +} diff --git a/docs/specs/ADR-046-implementation-graph.md b/docs/specs/ADR-046-implementation-graph.md new file mode 100644 index 000000000..ded42138b --- /dev/null +++ b/docs/specs/ADR-046-implementation-graph.md @@ -0,0 +1,317 @@ +# ADR 0046 implementation graph (generated) + +> **Generated index — not a normative member.** This file and its companion +> [`ADR-046-implementation-graph.json`](ADR-046-implementation-graph.json) are +> deterministically generated from +> [`ADR-046-spec-set.json`](ADR-046-spec-set.json), +> [`ADR-046-work-items.json`](ADR-046-work-items.json), and the 8-wave topology +> in [`ADR-046-validation-and-delivery.md` §3](ADR-046-validation-and-delivery.md). +> They are **not** among the 55 `ADR-046-spec-set.json` members. This Proposed +> documentation set does not yet contain the future `xtask` generator; its +> disposable regeneration script was removed after producing these bytes. + +The graph maps every member spec and every work item exactly once to a +dependency-ordered launch wave (`W0`–`W7`) and a file-disjoint parallel group. +It includes every resolved security-key work-item dependency; no lexical +tie-break or omitted dependency is used. +Each JSON work-item node also embeds the manifest's exact `detailedDesign` and +`validation` text byte-for-byte. + +## Counts + +| Metric | Value | +| --- | --- | +| Waves | 8 | +| Spec nodes | 55 | +| Work-item nodes | 543 | +| Total nodes | 598 | +| Edges | 1940 | +| Max topological rank | 22 | + +## Waves (W0–W7) + +| Wave | Specs | #Specs | #Work items | Parallel groups | +| --- | --- | --- | --- | --- | +| W0 | current-code-migration-map, decision-register, resource-api-and-authorization, resource-object-model, resource-store-redb, terminology-and-identities | 6 | 10 | W0-foundation-chain, W0-reference-docs | +| W1 | componentsession-and-bus, resource-reconciliation | 2 | 6 | W1-reconcile-and-bus | +| W2 | primitive-resource-composition, zone-routing | 2 | 19 | W2-composition-and-routing | +| W3 | provider-model-and-packaging | 1 | 4 | W3-provider-contract | +| W4 | components-processes-and-sandbox, core-controllers, provider-state, resources-credential, resources-network | 5 | 32 | W4-parallel-specs | +| W5 | cli-and-operations, nix-configuration, resources-device, resources-host-guest-process-user, resources-volume, resources-zone-control, telemetry-audit-and-support | 7 | 141 | W5-parallel-specs | +| W6 | provider-activation-nixos, provider-audio-pipewire, provider-clipboard-wayland, provider-credential-entra, provider-credential-managed-identity, provider-credential-secret-service, provider-device-gpu, provider-device-security-key, provider-device-tpm, provider-device-usbip, provider-display-wayland, provider-network-local, provider-notification-desktop, provider-observability-otel, provider-runtime-azure-container-apps, provider-runtime-azure-virtual-machine, provider-runtime-cloud-hypervisor, provider-runtime-qemu-media, provider-shell-terminal, provider-system-core, provider-system-minijail, provider-system-systemd, provider-transport-azure-relay, provider-transport-unix, provider-transport-vsock, provider-volume-local, provider-volume-virtiofs | 27 | 257 | W6-credentials, W6-interaction, W6-storage-network-device, W6-system-host-guest, W6-transport-observability-activation | +| W7 | feasibility-and-spikes, reset-and-cutover, security-and-threat-model, streamline, validation-and-delivery | 5 | 74 | W7-closing | + +## Dependency DAG (waves and prep barriers) + +```mermaid +flowchart LR + subgraph W0["W0"] + current_code_migration_map["current-code-migration-map"] + decision_register["decision-register"] + resource_api_and_authorization["resource-api-and-authorization"] + resource_object_model["resource-object-model"] + resource_store_redb["resource-store-redb"] + terminology_and_identities["terminology-and-identities"] + end + subgraph W1["W1"] + componentsession_and_bus["componentsession-and-bus"] + resource_reconciliation["resource-reconciliation"] + end + subgraph W2["W2"] + primitive_resource_composition["primitive-resource-composition"] + zone_routing["zone-routing"] + end + subgraph W3["W3"] + provider_model_and_packaging["provider-model-and-packaging"] + end + subgraph W4["W4"] + components_processes_and_sandbox["components-processes-and-sandbox"] + core_controllers["core-controllers"] + provider_state["provider-state"] + resources_credential["resources-credential"] + resources_network["resources-network"] + end + subgraph W5["W5"] + cli_and_operations["cli-and-operations"] + nix_configuration["nix-configuration"] + resources_device["resources-device"] + resources_host_guest_process_user["resources-host-guest-process-user"] + resources_volume["resources-volume"] + resources_zone_control["resources-zone-control"] + telemetry_audit_and_support["telemetry-audit-and-support"] + end + subgraph W6["W6"] + provider_activation_nixos["provider-activation-nixos"] + provider_audio_pipewire["provider-audio-pipewire"] + provider_clipboard_wayland["provider-clipboard-wayland"] + provider_credential_entra["provider-credential-entra"] + provider_credential_managed_identity["provider-credential-managed-identity"] + provider_credential_secret_service["provider-credential-secret-service"] + provider_device_gpu["provider-device-gpu"] + provider_device_security_key["provider-device-security-key"] + provider_device_tpm["provider-device-tpm"] + provider_device_usbip["provider-device-usbip"] + provider_display_wayland["provider-display-wayland"] + provider_network_local["provider-network-local"] + provider_notification_desktop["provider-notification-desktop"] + provider_observability_otel["provider-observability-otel"] + provider_runtime_azure_container_apps["provider-runtime-azure-container-apps"] + provider_runtime_azure_virtual_machine["provider-runtime-azure-virtual-machine"] + provider_runtime_cloud_hypervisor["provider-runtime-cloud-hypervisor"] + provider_runtime_qemu_media["provider-runtime-qemu-media"] + provider_shell_terminal["provider-shell-terminal"] + provider_system_core["provider-system-core"] + provider_system_minijail["provider-system-minijail"] + provider_system_systemd["provider-system-systemd"] + provider_transport_azure_relay["provider-transport-azure-relay"] + provider_transport_unix["provider-transport-unix"] + provider_transport_vsock["provider-transport-vsock"] + provider_volume_local["provider-volume-local"] + provider_volume_virtiofs["provider-volume-virtiofs"] + end + subgraph W7["W7"] + feasibility_and_spikes["feasibility-and-spikes"] + reset_and_cutover["reset-and-cutover"] + security_and_threat_model["security-and-threat-model"] + streamline["streamline"] + validation_and_delivery["validation-and-delivery"] + end + W0 --> W1 --> W2 --> W3 --> W4 --> W5 --> W6 --> W7 + resource_object_model -. prep .-> resource_api_and_authorization + resource_store_redb -. prep .-> resource_api_and_authorization + terminology_and_identities -. prep .-> resource_api_and_authorization + decision_register -. prep .-> resource_object_model + terminology_and_identities -. prep .-> resource_object_model + resource_object_model -. prep .-> resource_store_redb + terminology_and_identities -. prep .-> resource_store_redb + reset_and_cutover -. prep .-> streamline + decision_register -. prep .-> terminology_and_identities + feasibility_and_spikes -. prep .-> validation_and_delivery + reset_and_cutover -. prep .-> validation_and_delivery + security_and_threat_model -. prep .-> validation_and_delivery + streamline -. prep .-> validation_and_delivery +``` + +Solid arrows show wave launch order. Dotted arrows show same-wave shared +contract prep. Work-item dependencies and actual file-overlap barriers are +fully represented in the JSON. + +## Shared prep and file-overlap barriers + +| Prerequisite | Consumer | Type | +| --- | --- | --- | +| `ADR046-nix-014` | `ADR046-cli-011` | file-overlap-order | +| `ADR046-core-001` | `ADR046-device-007` | file-overlap-order | +| `ADR046-core-001` | `ADR046-exec-013` | file-overlap-order | +| `ADR046-core-001` | `ADR046-exec-015` | file-overlap-order | +| `ADR046-core-001` | `ADR046-network-008` | file-overlap-order | +| `ADR046-device-006` | `ADR046-nix-014` | file-overlap-order | +| `ADR046-cli-011` | `ADR046-nix-019` | file-overlap-order | +| `ADR046-nix-019` | `ADR046-nix-031` | file-overlap-order | +| `ADR046-transport-unix-009` | `ADR046-qemu-media-017` | file-overlap-order | +| `ADR046-core-001` | `ADR046-telem-011` | file-overlap-order | +| `ADR046-gpu-007` | `ADR046-transport-unix-009` | file-overlap-order | +| `ADR046-qemu-media-017` | `ADR046-usbip-008` | file-overlap-order | +| `ADR046-core-001` | `ADR046-zone-control-016` | file-overlap-order | +| `ADR046-core-001` | `ADR046-zone-control-021` | file-overlap-order | +| `ADR-046-resource-object-model` | `ADR-046-resource-api-and-authorization` | shared-contract | +| `ADR-046-resource-store-redb` | `ADR-046-resource-api-and-authorization` | shared-contract | +| `ADR-046-terminology-and-identities` | `ADR-046-resource-api-and-authorization` | shared-contract | +| `ADR-046-decision-register` | `ADR-046-resource-object-model` | shared-contract | +| `ADR-046-terminology-and-identities` | `ADR-046-resource-object-model` | shared-contract | +| `ADR-046-resource-object-model` | `ADR-046-resource-store-redb` | shared-contract | +| `ADR-046-terminology-and-identities` | `ADR-046-resource-store-redb` | shared-contract | +| `ADR-046-reset-and-cutover` | `ADR-046-streamline` | shared-contract | +| `ADR-046-decision-register` | `ADR-046-terminology-and-identities` | shared-contract | +| `ADR-046-feasibility-and-spikes` | `ADR-046-validation-and-delivery` | shared-contract | +| `ADR-046-reset-and-cutover` | `ADR-046-validation-and-delivery` | shared-contract | +| `ADR-046-security-and-threat-model` | `ADR-046-validation-and-delivery` | shared-contract | +| `ADR-046-streamline` | `ADR-046-validation-and-delivery` | shared-contract | + +Only the listed `file-overlap-order` edges constrain shared files. Provider +integration ordering that touches disjoint crate trees is not represented as +file overlap. The former `wi:core-config-hub` is split into +`wi:core-config-hub:w4` and `wi:core-config-hub:w5`; each parallel group is +single-wave. The seven `assertions.nix` edges form the minimal per-wave chains +`ADR046-device-006` → `ADR046-nix-014` → `ADR046-cli-011` → +`ADR046-nix-019` → `ADR046-nix-031` in W5 and `ADR046-gpu-007` → +`ADR046-transport-unix-009` → `ADR046-qemu-media-017` → +`ADR046-usbip-008` in W6. W2 has one writer. These edges order only the shared +file; all other destinations retain their existing parallelism. + +## Parallel groups + +| Parallel group | Wave | #Nodes | +| --- | --- | --- | +| `W0-foundation-chain` | W0 | 4 | +| `W0-reference-docs` | W0 | 2 | +| `W1-reconcile-and-bus` | W1 | 2 | +| `W2-composition-and-routing` | W2 | 2 | +| `W3-provider-contract` | W3 | 1 | +| `W4-parallel-specs` | W4 | 5 | +| `W5-parallel-specs` | W5 | 7 | +| `W6-credentials` | W6 | 3 | +| `W6-interaction` | W6 | 5 | +| `W6-storage-network-device` | W6 | 7 | +| `W6-system-host-guest` | W6 | 7 | +| `W6-transport-observability-activation` | W6 | 5 | +| `W7-closing` | W7 | 5 | +| `wi:ADR-046-cli-and-operations` | W5 | 13 | +| `wi:ADR-046-components-processes-and-sandbox` | W4 | 2 | +| `wi:ADR-046-componentsession-and-bus` | W1 | 3 | +| `wi:ADR-046-core-controllers` | W4 | 1 | +| `wi:ADR-046-decision-register` | W0 | 1 | +| `wi:ADR-046-feasibility-and-spikes` | W7 | 11 | +| `wi:ADR-046-nix-configuration` | W5 | 35 | +| `wi:ADR-046-primitive-resource-composition` | W2 | 3 | +| `wi:ADR-046-provider-activation-nixos` | W6 | 7 | +| `wi:ADR-046-provider-audio-pipewire` | W6 | 13 | +| `wi:ADR-046-provider-clipboard-wayland` | W6 | 12 | +| `wi:ADR-046-provider-credential-entra` | W6 | 1 | +| `wi:ADR-046-provider-credential-managed-identity` | W6 | 5 | +| `wi:ADR-046-provider-credential-secret-service` | W6 | 6 | +| `wi:ADR-046-provider-device-gpu` | W6 | 9 | +| `wi:ADR-046-provider-device-security-key` | W6 | 35 | +| `wi:ADR-046-provider-device-tpm` | W6 | 13 | +| `wi:ADR-046-provider-device-usbip` | W6 | 9 | +| `wi:ADR-046-provider-display-wayland` | W6 | 4 | +| `wi:ADR-046-provider-model-and-packaging` | W3 | 4 | +| `wi:ADR-046-provider-network-local` | W6 | 20 | +| `wi:ADR-046-provider-notification-desktop` | W6 | 6 | +| `wi:ADR-046-provider-observability-otel` | W6 | 6 | +| `wi:ADR-046-provider-runtime-azure-container-apps` | W6 | 7 | +| `wi:ADR-046-provider-runtime-azure-virtual-machine` | W6 | 9 | +| `wi:ADR-046-provider-runtime-cloud-hypervisor` | W6 | 7 | +| `wi:ADR-046-provider-runtime-qemu-media` | W6 | 19 | +| `wi:ADR-046-provider-shell-terminal` | W6 | 13 | +| `wi:ADR-046-provider-state` | W4 | 12 | +| `wi:ADR-046-provider-system-core` | W6 | 1 | +| `wi:ADR-046-provider-system-minijail` | W6 | 6 | +| `wi:ADR-046-provider-system-systemd` | W6 | 3 | +| `wi:ADR-046-provider-transport-azure-relay` | W6 | 7 | +| `wi:ADR-046-provider-transport-unix` | W6 | 11 | +| `wi:ADR-046-provider-transport-vsock` | W6 | 7 | +| `wi:ADR-046-provider-volume-local` | W6 | 13 | +| `wi:ADR-046-provider-volume-virtiofs` | W6 | 7 | +| `wi:ADR-046-reset-and-cutover` | W7 | 11 | +| `wi:ADR-046-resource-api-and-authorization` | W0 | 2 | +| `wi:ADR-046-resource-object-model` | W0 | 2 | +| `wi:ADR-046-resource-reconciliation` | W1 | 3 | +| `wi:ADR-046-resource-store-redb` | W0 | 3 | +| `wi:ADR-046-resources-credential` | W4 | 8 | +| `wi:ADR-046-resources-device` | W5 | 7 | +| `wi:ADR-046-resources-host-guest-process-user` | W5 | 22 | +| `wi:ADR-046-resources-network` | W4 | 8 | +| `wi:ADR-046-resources-volume` | W5 | 6 | +| `wi:ADR-046-resources-zone-control` | W5 | 26 | +| `wi:ADR-046-security-and-threat-model` | W7 | 19 | +| `wi:ADR-046-streamline` | W7 | 24 | +| `wi:ADR-046-telemetry-audit-and-support` | W5 | 26 | +| `wi:ADR-046-terminology-and-identities` | W0 | 2 | +| `wi:ADR-046-validation-and-delivery` | W7 | 9 | +| `wi:ADR-046-zone-routing` | W2 | 16 | +| `wi:core-config-hub:w4` | W4 | 1 | +| `wi:core-config-hub:w5` | W5 | 6 | +| `wi:core-controller-coordination:w6` | W6 | 1 | + +## Critical path (longest dependency chain) + +1. `ADR-046-decision-register` +2. `ADR-046-terminology-and-identities` +3. `ADR-046-resource-object-model` +4. `ADR-046-resource-store-redb` +5. `ADR-046-resource-api-and-authorization` +6. `ADR-046-resource-reconciliation` +7. `ADR-046-primitive-resource-composition` +8. `ADR-046-provider-model-and-packaging` +9. `ADR-046-core-controllers` +10. `ADR-046-nix-configuration` +11. `ADR-046-provider-runtime-qemu-media` +12. `ADR046-qemu-media-001` +13. `ADR046-qemu-media-003` +14. `ADR046-qemu-media-005` +15. `ADR046-qemu-media-006` +16. `ADR046-qemu-media-009` +17. `ADR046-qemu-media-010` +18. `ADR046-qemu-media-011` +19. `ADR046-qemu-media-013` +20. `ADR046-qemu-media-014` +21. `ADR046-qemu-media-015` +22. `ADR046-qemu-media-018` +23. `ADR046-qemu-media-019` + +## Regeneration findings (D095–D098) + +- Regenerated from 55 member specs and 543 current work items; every declared heading is represented exactly once. +- `ADR046-provider-004` owns the common D098 Service/Binding base DTOs and schemas; the four implementation Providers own only strict extensions and controllers. +- `ADR046-zone-control-024` owns the shared Core-derived `physical-usb-backing` tuple; both the security-key and USB effect DAGs depend on it. +- Every `ADR046-security-key-*` dependency in `Dependency/owner` is encoded. The dependency subgraph is acyclic and uses no generator tie-break. +- Fourteen file-overlap barriers cover only the shared core + configuration/cleanup files and `nixos-modules/assertions.nix`. Each appears + both as a + `file-overlap-order` edge and in the dependent node's `prerequisites`, so the + ready-wave query enforces it. Soft cross-Provider integration order remains + file-disjoint and concurrent. +- No repository generator exists at this Proposed stage. `ADR046-streamline-001`/`024` and `ADR046-delivery-004`/`009` own the future canonical implementation and policy gate. + +## Ready-wave algorithm + +A node is ready when every id in `prerequisites` is done: + +```bash +jq --argjson done "$DONE" ' + .nodes[] | select((.prerequisites - $done) | length == 0) + | {id, kind, wave, parallelGroup, topologicalRank} +' docs/specs/ADR-046-implementation-graph.json +``` + +A ready, file-disjoint group left unlaunched without a recorded blocker violates +the anti-serialization invariant. + +## References + +- [ADR 0046](../adr/0046-d2b-3-provider-control-plane.md) +- [Decision register](ADR-046-decision-register.md) +- [Validation and delivery](ADR-046-validation-and-delivery.md) +- [Spec-set manifest](ADR-046-spec-set.json) +- [Work-item manifest](ADR-046-work-items.json) diff --git a/docs/specs/ADR-046-nix-configuration.md b/docs/specs/ADR-046-nix-configuration.md new file mode 100644 index 000000000..69e311a9a --- /dev/null +++ b/docs/specs/ADR-046-nix-configuration.md @@ -0,0 +1,2964 @@ +# ADR 0046 Nix configuration and resource compilation + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-nix-configuration` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 2 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `nixos-modules/`, Nix resource compiler, generated bundle artifacts | +| Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-primitive-resource-composition`, `ADR-046-provider-model-and-packaging`, `ADR-046-core-controllers` | +| Supersedes | Current `nixos-modules/options-realms*.nix`, `options-envs.nix`, `options-vms.nix`, `index.nix`, `bundle*.nix`, `*-json.nix`, and generated `/etc/d2b/*.json` | + +## Source, reuse, and evidence policy + +This spec uses `b5ddbed6` as its authoritative factual baseline. + +**Old current names versus new target names.** The baseline uses `Realm`/ +`RealmId`/`RealmPath`/`WorkloadId`/`NodeId` everywhere. These are the current +live symbols. The target names `Zone`, `Host`, `Guest`, `ResourceRef` do not +exist in the baseline. A current source search must use the old names. +Specifically: + +- `RealmId` / `RealmPath` / `d2b.realms.*` → target `Zone` / `d2b.zones.*` +- `WorkloadId` / `d2b.realms..workloads.*` → target `Guest` (for VM/sandbox/ + cloud/remote execution), or target user-only `Host` (for unsafe-local workloads). + The target physical/local-execution parent `Host` is a new ResourceType. +- `NodeId` / `NodeKind` / `WorkloadPlacement` → target `Host` or `Guest` + depending on NodeKind value (see classification table below). +- `ProcessRole` / `VmProcessDag` / runner / systemd helper → target `Process`, + `EphemeralProcess`, or controller/probe/bootstrap per the disposition table below. +- `StoragePathSpec` / `storage.json` / `store-view` / filesystem-view → + target `Volume` where independent lifecycle; locks/internal handles remain + implementation mechanisms. +- `d2b-realm-provider` traits/adapters → target Zone-local `Provider` resource + plus controller/service/worker Process components. Reachability varies by + adapter (see reachability section). +- `d2b-realm-router` / `d2b-realm-transport` → target `d2b-bus` and + `ComponentSession` (new; may copy/adapt main commit `a1cc0b2d`). These crates + are compile-checked in `d2bd` via `realm_stubs.rs` (explicitly + `dead_code`-allowed, not called from the running daemon at baseline). +- `RealmControllersJson` / `realm-controllers.json` → runtime-created Zone + self-resource, compiler-only `parentZone` topology sealed into allocator + bootstrap state, and child-local ZoneLink transport/route state. +- `RealmWorkloadsLauncherV2Json` / `realm-workloads-launcher-v2.json` → + target Process resource annotations in `zones//processes.json`. +- `Capability` enum → see capability disposition table below (mapping resolved; + verb set owned by resource-api/authz foundation spec). + +No main-branch behavior is assumed live. Every reuse work item names an +exact baseline source file and a precise v3 destination. + +## Purpose + +This spec defines how operators declare d2b 3.0 resource objects in Nix, how +those declarations compile into Zone resource stores, and how generated bundle +artifacts are produced, staged, and rolled back. It covers: + +- Zone self-resource declaration and naming; +- Provider package catalog and `Provider/` install resources; +- `Host`, `Guest`, and shared `ExecutionPolicy` specs; +- `Process`, `EphemeralProcess`, `Volume`, `Network`, `Device`, `User`, + `Credential`, `ResourceExport`, and `ResourceImport` specs; +- `Role` and `RoleBinding` resources and RBAC compilation; +- controller placement templates; +- ref validation at eval time; +- exact schemas and defaults for every folded field; +- the universal Nix authoring and validation contract: canonical ResourceSpec + JSON shape, ResourceTypeSchema and Provider-schema validation, Credential ref + enforcement, and bundle integrity; +- the required `src/`/`tests/`/`integration/`/`README.md` layout for every + `packages/d2b-provider--/` crate and the workspace + policy gate that enforces it; +- active-configuration generation, staging, and rollback across Zones; +- the resource cleanup contract: configuration-owned vs controller-created + classification, async absent-resource deletion, finalizer-safe owner-child + cascade, prior-generation retention, status/audit/tests; +- normalized index and bundle artifacts; +- package closures into Hosts and Guests; +- prohibited fields (secrets, freeform paths); +- conflict detection and rejection; +- the artifact catalog (`d2b.artifacts`) separating derivation-valued inputs + from ResourceSpecs, with `artifactId`/`systemArtifactId` as plain bounded + identifiers validated at build time; +- exact mapping from current option paths and Rust symbols to future files; and +- complete work items. + +## Architectural invariants + +These invariants must be enforced at eval time (Nix assertion or type system), +not deferred to runtime: + +1. Every `*Ref` field follows `/`. No scheme, + Zone prefix, query string, relative segment, or implicit type is accepted. +2. `resource_name` matches `^[a-z][a-z0-9-]*$` — the same LABEL_PATTERN as + `RealmId`/`WorkloadId` in `d2b-realm-core/src/ids.rs`. +3. Every resource ref resolves to a declared resource of the stated type in + the same Zone. Cross-Zone refs are always rejected; cross-Zone operations + route through a same-Zone `ZoneLink` plus non-Ref routing metadata. +4. Owner cycles fail at eval time. +5. No secret value, credential bytes, raw numeric UID/GID, freeform host path, + ambient capability list, raw seccomp program, or arbitrary socket address + appears in any Nix-emitted resource spec or generated artifact. +6. The Provider package catalog is offline and sorted by exact content digest. + No version ranges, `latest`, PATH scan, or runtime marketplace. +7. Configuration generation is hermetic and deterministic. Running the same + Nix derivation twice with the same inputs produces byte-identical output. +8. Staged configuration becomes active only after eval-time validation passes. + No partial activation is possible. +9. Every generated artifact in `/etc/d2b/` is owned `root:d2bd` mode `0640` + unless the type table below specifies otherwise. +10. No current behavior is removed until the equivalent resource/Provider + successor is integrated and tested. + +## Current-symbol classification tables + +### NodeKind → Host or Guest + +Current source: `packages/d2b-realm-core/src/node.rs`, `NodeKind` enum. + +| Current `NodeKind` variant | Current meaning | v3 target ResourceType | +| --- | --- | --- | +| `FullHost` | A full d2b host (KVM, broker, vsock, device control) | `Host` resource | +| `Gateway` | A realm gateway guest VM | `Guest` with explicitly selected existing runtime Provider (e.g., `Provider/runtime-cloud-hypervisor`) | +| `ProviderManaged` | Limited-capability provider-managed node | `Guest` under the managing Provider | + +`Host` is a new ResourceType with no current equivalent. Its Nix declaration is +the v3 successor to `d2b.realms.` with `placement = "host-local"` plus the +per-realm path-partition entry in `allocator.json`. + +`NodeKind::Gateway` maps to a `Guest` resource whose `providerRef` is explicitly +selected by the operator from the existing catalog (e.g., `Provider/runtime-cloud-hypervisor` +or `Provider/runtime-qemu-media`). There is no dedicated gateway runtime Provider +entry in the initial catalog. Gateway functionality is a declaration pattern (a +Guest whose role is to mediate realm/zone bridging), not a distinct ResourceType +or Provider kind. + +### WorkloadProviderKind → Guest or Host ExecutionPolicy + +Current source: `packages/d2b-realm-core/src/workload.rs`, `WorkloadProviderKind` +and `IsolationPosture` enums. These drive current `WorkloadExecutionPosture`. + +| Current value | Current meaning | v3 target | +| --- | --- | --- | +| `LocalVm` | Locally supervised NixOS VM (Cloud Hypervisor) | `Guest` + `providerRef: Provider/runtime-cloud-hypervisor` | +| `QemuMedia` | Locally supervised QEMU external-media runner | `Guest` + `providerRef: Provider/runtime-qemu-media` | +| `ProviderManaged` | Runtime owned by a provider adapter | `Guest` + exact frozen `providerRef` selected in config (e.g., `Provider/runtime-azure-container-apps`, `Provider/runtime-azure-virtual-machine`) | +| `UnsafeLocal` | Host-user process, no isolation boundary | User-only `Host` under `Provider/system-core` with `spec.isolationPosture: "none"` | + +For current `WorkloadProviderKind::ProviderManaged` workloads, the `providerRef` +in the compiled `Guest` resource is the exact frozen catalog entry selected by +the operator in their Nix config. Current live ACA paths use +`WorkloadProviderKind::ProviderManaged` backed by the ACA adapter +(`d2b-realm-provider/src/provider.rs`); these map to +`Provider/runtime-azure-container-apps`. Current Azure VM paths map to +`Provider/runtime-azure-virtual-machine`. The operator must select the exact +catalog entry name; the compiler never infers `providerRef` from a current +`ProviderId` value. + +### ProcessRole disposition + +Current source: `packages/d2b-core/src/processes.rs`, `ProcessRole` enum. +Every `VmProcessDag` node carries one `ProcessRole`. The target +`Process`/`EphemeralProcess` classification is determined per variant. + +Evaluation note: a ProcessRole variant that has no independent lifecycle, no +independent owner, and is purely an implementation mechanism (e.g., a transient +health probe) may become a non-resource controller action. The threshold from +`ADR-046-primitive-resource-composition` applies. + +| Current `ProcessRole` | Current description | v3 classification | Target Provider | +| --- | --- | --- | --- | +| `HostReconcile` | Host reconciliation before VM-specific startup | Controller action (not a Process resource; owned by `Provider/system-core`) | `Provider/system-core` | +| `StoreVirtiofsPreflight` | Store and virtiofs preflight validation | `EphemeralProcess` | `Provider/volume-virtiofs` | +| `SwtpmPreStartFlush` | swtpm pre-start flush step | `EphemeralProcess` | `Provider/device-tpm` | +| `Swtpm` | swtpm sidecar (long-lived) | `Process` | `Provider/device-tpm` | +| `Virtiofsd` | virtiofsd sidecar (long-lived) | `Process` | `Provider/volume-virtiofs` | +| `Video` | Optional video sidecar | `Process` | `Provider/device-gpu` | +| `Gpu` | GPU/graphics sidecar | `Process` | `Provider/device-gpu` | +| `GpuRenderNode` | GPU render-node-only mode | `Process` | `Provider/device-gpu` | +| `Audio` | Audio sidecar | `Process` | `Provider/audio-pipewire` | +| `CloudHypervisorRunner` | Cloud Hypervisor VMM | `Process` (owned by Guest) | `Provider/runtime-cloud-hypervisor` | +| `QemuMediaRunner` | QEMU media runner | `Process` (owned by Guest) | `Provider/runtime-qemu-media` | +| `VsockRelay` | vsock relay sidecar | `Process` | `Provider/transport-vsock` | +| `OtelHostBridge` | Host-to-observability-VM OTLP bridge | `Process` | `Provider/observability-otel` | +| `GuestSshReadiness` | Legacy SSH readiness probe (compat window) | Retired at v3 cutover; no v3 Process equivalent; no compatibility period | — | +| `GuestControlHealth` | Authenticated guest-control Health probe | `EphemeralProcess` | `Provider/system-core` | +| `Usbip` (long-lived backend/proxy) | USBIP long-lived proxy/backend | `Process` | `Provider/device-usbip` | +| `Usbip` (per-busid attach/detach) | USBIP per-busid attach/detach helper | `EphemeralProcess` | `Provider/device-usbip` | +| `SecurityKeyFrontend` | Guest CTAPHID relay frontend | `Process` | `Provider/device-security-key` | +| `WaylandProxy` | Host-jailed Wayland proxy | `Process` | `Provider/display-wayland` | + +`ProcessRole::VsockRelay` becomes a `Process` resource owned by +`Provider/transport-vsock`. It is not an implementation mechanism internal to +`Provider/runtime-cloud-hypervisor`; it has an independent lifecycle (shared +across Guests using vsock transport) and therefore satisfies the +primitive-resource-composition threshold for an independent Process resource. + +`ProcessRole::GuestSshReadiness` is retired at the v3 clean cutover. The +baseline comment explicitly marks it as "Replaced by GuestControlHealth"; +there is no v3 `EphemeralProcess` equivalent and no compatibility period. +Operators with SSH-dependent tooling must migrate to the authenticated +guest-control path (`GuestControlHealth` / `ProcessRole::GuestControlHealth`) +before adopting v3. + +`ProcessRole::Usbip` covers two distinct resource kinds. The long-lived +USBIP backend/proxy (runs continuously) becomes a `Process` resource. Each +per-busid attach or detach operation (one-shot) becomes an `EphemeralProcess` +resource. Both are owned by `Provider/device-usbip`. The `Process` controller +starts `EphemeralProcess` instances for individual attach/detach operations; +they are not polymorphic on a single resource instance. + +### Capability enum disposition + +Current source: `packages/d2b-realm-core/src/capability.rs`, `Capability` +enum. Current capabilities are positive-assertion advertisement values. + +The `Capability` enum must not be preserved as a Nix option or ResourceType +field merely because of the name. Each value must be individually classified. + +| Current `Capability` value | Current purpose | v3 target | +| --- | --- | --- | +| `Lifecycle` | Workload create/start/stop/inspect | Implicit grant from `Role` binding with `Host`/`Guest` verbs | +| `Exec` | Command execution (admin-only) | Exact service/method selector under session verb `invoke`; there is no `exec` resource verb | +| `Pty` | Interactive pseudo-terminal | Exact terminal service/method or stream selector under session verbs `invoke`, `open-stream`, and, only for local FD transfer, `attach` | +| `Logs` | Durable execution logs with cursors | Exact log service/stream selector under session verbs `invoke`/`open-stream`; no resource-verb alias | +| `FileCopy` | Bounded file copy | Not in initial verb set; reimplemented as Volume view copy op if needed | +| `PortForward` | One stream per connection | Not in initial verb set | +| `PersistentShell` | Named shell operations | Service capability of `Provider/shell-terminal` | +| `Vsock`, `Virtiofs` | Transport availability | Provider descriptor capability field (not a Role verb) | +| `WindowForwarding`, `DisplayStreaming`, `Clipboard` | Display/clipboard transport | Provider descriptor capability field for `Provider/display-wayland`/`Provider/clipboard-wayland` | +| `AudioPlayback`, `AudioCapture` | Audio | Provider descriptor capability field for `Provider/audio-pipewire` | +| `Hid` | Named HID device operations | Provider descriptor capability field for security-key Provider | +| `Usb` | Named USB device operations | Provider descriptor capability field for `Provider/device-usbip` | +| `GpuAccel` | Local GPU acceleration | Provider descriptor capability field for `Provider/device-gpu` | +| `Snapshots` | Snapshots | Typed Provider descriptor capability field; absent means fail closed; no Nix option | +| `Hotplug` | Device hotplug | Typed Provider descriptor capability field; absent means fail closed; no Nix option | +| `EphemeralSessions` | Provider-managed ephemeral sessions | Provider descriptor capability field; not a Role verb | +| `ProviderManagedIsolation` | Non-host-owned isolation boundary | Typed Provider descriptor capability field; absent means fail closed; no Nix option | +| `ConfiguredLaunch` | Execute a configured launcher item | Exact configured-launch service/method selector under session verb `invoke`; no resource-verb alias | + +The verb set for `Role.rules[*].verbs` is owned by the resource-api and authz +foundation spec. The compiler validates that every declared verb is in the +closed set published by that spec; any verb not in that set is rejected at eval +time. Core-reserved verbs (verbs bound to internal controller identity and not +grantable to operator principals) cannot be declared in Nix RoleBindings; the +compiler rejects them with a structured eval error. Cross-reference: +`ADR-046-resource-api-and-authorization`. + +`Capability::Snapshots`, `Hotplug`, and `ProviderManagedIsolation` are retained +as typed Provider descriptor capability fields, declared in the Provider's +dossier/manifest. A Provider that does not declare one of these fields is +treated as not supporting that capability; dependent resources fail closed +(e.g., a Guest requesting a snapshot on a Provider without `Snapshots` in its +capability descriptor is a config-publication error). No Nix option forces or +defaults these fields; they are set exclusively by the Provider author in the +Provider dossier. + +### Current reachability summary + +This table records what is wired versus compile-only at baseline `b5ddbed6`. +Evidence class per `ADR-046-terminology-and-identities.md`. + +| Component | Reachability at baseline | Evidence source | +| --- | --- | --- | +| `d2b-realm-core` identifiers, routing metadata, capability model | Compiled into `d2bd`; `RealmId`/`WorkloadId`/`RealmPath`/`RealmTarget`/`CapabilitySet` used in live access-resolver and identity-config paths | `packages/d2bd/src/realm_access_resolver.rs`, `lib.rs` | +| `d2b-realm-core` pure engines (`allocator_engine.rs`, `route_engine.rs`, `identity_store.rs`) | Compile-tested; pure in-memory — no live mutation | Inline module docs: "performs no netlink… live host mutation" / "never generates keys, signs data, writes files" | +| `d2b-realm-provider` `RuntimeProvider`/`WorkloadProvider`/`HostSubstrateProvider` traits | Implemented by live local-vm/ACA providers wired into `d2bd` | `packages/d2bd/src/lib.rs`: `WorkloadProvider` used; `realm_stubs.rs` documents that future gateway wiring is NOT called | +| `d2b-realm-router` `OperationRouter`, `DurableExecTable` | Imported by `d2bd` via `Cargo.toml` but used ONLY in `realm_stubs.rs` which is `dead_code`-allowed and explicitly "NOT called from the running daemon" | `packages/d2bd/src/realm_stubs.rs` header comment | +| `d2b-realm-transport` `LocalTcpTransport`, loopback | Compile-check and conformance tests only; no live socket opened | `packages/d2b-realm-transport/src/lib.rs` docs: "loopback connects… no real socket is opened" | +| `d2b-realm-codec-protobuf` | Compile-check only at baseline (no caller found in daemon) | `packages/d2b-realm-codec-protobuf/src/lib.rs` | +| `realm_access_resolver.rs` in `d2bd` | Live and wired: reads `/etc/d2b/realm-controllers.json`, resolves `RealmPath`/`WorkloadId` targets for auth | `packages/d2bd/src/realm_access_resolver.rs` | +| `d2b-realm-core` `RealmIdentityConfigJson`/`RealmIdentityConfigSummary` | Live: loaded by `d2bd` from `/etc/d2b/realm-identity.json` | `packages/d2bd/src/lib.rs`: `DEFAULT_REALM_IDENTITY_CONFIG_PATH` | +| ACA/Relay/gateway Providers | Live paths exist (implemented `WorkloadProvider`); not covered further in this spec | `d2b-realm-provider` implementors wired into `d2bd` | +| Per-VM process DAG (`VmProcessDag`, `ProcessNode`, `ProcessRole`) | Live: generated by Nix, read by broker/daemon at runtime | `packages/d2b-core/src/processes.rs`; `nixos-modules/processes-json.nix` | +| `StorageJson` / `SyncJson` / `AllocatorJson` | Live: generated by Nix, read by broker | `nixos-modules/storage-json.nix`, `sync-json.nix`, `allocator-json.nix` | +| `RealmWorkloadsLauncherV2Json` | Live: generated by Nix, read by launcher | `packages/d2b-core/src/realm_workloads_launcher.rs`; `nixos-modules/realm-workloads-launcher-v2-json.nix` | + +## Zone declaration + +Each Zone is declared under `d2b.zones.`. The option path replaces +`d2b.realms.` (current: `nixos-modules/options-realms.nix`). The Zone +`` matches `^[a-z][a-z0-9-]*$` — the same LABEL_PATTERN as `RealmId`. + +```nix +d2b.zones.local-root = { + label = "Local root"; # parentZone is forbidden on this Zone +}; + +d2b.zones.dev = { + label = "Development"; # optional; defaults to attribute name + parentZone = "local-root"; # required for every non-root Zone +}; +``` + +Zone-level Nix options (`label`, `parentZone`, `retainedGenerations`, +`trustedPublishers`) are compiler/configuration-service settings. They control +build behavior and are not emitted into the Zone ResourceSpec. `parentZone` is +a plain Zone name, not a `ResourceRef`. It is required for every non-root Zone, +must resolve to another declared `d2b.zones.`, and is forbidden on the +distinguished local-root Zone. The compiler turns the resulting child→parent +map into the sealed private allocator-bootstrap topology; it never emits +`parentZone` into a Zone resource bundle or `Zone.spec`, which remains exactly +`{}`. + +The scalar `parentZone` setting selects exactly one allocator owner for the +child. One optional child-local uplink `ZoneLink` supplies that edge's transport +binding and local route/session state. A non-root Zone with no enabled ZoneLink +still has an authored parent but remains disconnected. There is no `parentRef` +in the Zone spec and no reciprocal parent-store ZoneLink resource. + +Every declared Zone causes its runtime to maintain exactly one +`Zone/` self-resource with an empty spec. The self-resource is +controller-created, not emitted in the Nix-authored resource bundle: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Zone +metadata: + name: dev + zone: dev + uid: + generation: 1 + revision: + ownerRef: null + finalizers: [] +spec: {} +status: + phase: Ready +``` + +`uid` is never specified in Nix. It is assigned by the Zone runtime at first +activation. A Nix redeclaration of the same name must not invent a new UID. + +Zone UIDs are assigned by the Zone runtime store at first activation. Nix +binds a Zone exclusively by its declared name and config identity. A UID is +never pinned, predicted, or validated in Nix; bundle manifests bind Zone +_name_ only and UIDs are resolved at runtime. A Nix redeclaration of an +existing Zone name must not invent or specify a UID. The +`d2b-activation-helper` reads the runtime-assigned UID from the Zone store +after first activation; subsequent generations are linked to that store-held +UID automatically. + +### ZoneLink + +A `ZoneLink` resource is the child Zone's local uplink to the allocator- +established parent. It lives in the CHILD Zone's resource bundle and is +declared in that child Zone's `resources` set. `spec.childZoneName` is an +identity anchor and MUST equal the enclosing child Zone name. The parent +allocator owns privileged listener creation, namespace/cgroup placement, and +route allocation, but it does not create a reciprocal resource in the parent +store. The compiler resolves the enclosing Zone's `parentZone`, and the sealed +bootstrap allocation binds this local ZoneLink UID and child identity to that +one selected parent edge. There is no `parentRef` in either Zone's spec and no +cross-Zone ResourceRef. Current source for zone hierarchy: +`d2b-realm-core/src/realm.rs` (`RealmPath` dotted hierarchy — e.g., +`work.personal`); `d2b.realms..parent` option in +`nixos-modules/options-realms.nix`. + +```nix +# Declared in the CHILD Zone's resources: +d2b.zones.dev.resources.dev-uplink = { + type = "ZoneLink"; + spec = { + childZoneName = "dev"; # must equal the enclosing Zone name + transportProviderRef = "Provider/transport-unix"; # always explicit; no default + }; +}; +``` + +An eval assertion verifies that every `ZoneLink` in +`d2b.zones..resources` has `spec.childZoneName = ""`, that each non-root +Zone has at most one uplink resource (enabled or disabled), and that the +local-root Zone has none. +The compiler validates the `parentZone` map before publication: `local-root` +must omit the setting, every other Zone must have one resolved scalar parent, +no Zone may name itself, the graph must be acyclic, and every ancestry path +may contain at most 16 Zone names (the retained `MAX_REALM_LABELS` bound). +Conflicting scalar definitions fail through normal Nix module merging, so one +child cannot acquire two parents. There is no bidirectional `parentRef` or +undefined reciprocal link; `parentZone` is the topology input and the +child-local ZoneLink is the sole resource declaration for its transport and +local route state. + +`ZoneLink.spec.transportProviderRef` is always explicit. There is no default +transport and no inference. Omitting `transportProviderRef` from a `ZoneLink` +spec is a structured eval error. The operator must select a declared Provider +(e.g., `Provider/transport-unix` for local-host ZoneLinks or +`Provider/transport-vsock` for cross-host links). + +Compiled ZoneLink bundle entry: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: ZoneLink +metadata: + name: dev-uplink + zone: dev + ownerRef: null +spec: + childZoneName: dev + transportProviderRef: Provider/transport-unix +``` + +## Artifact catalog + +Derivation-valued inputs — Provider package closures, NixOS system closures, +and any other Nix build output a resource spec must reference — are declared +in a separate named catalog. ResourceSpecs remain pure schema mirrors containing +only plain bounded string IDs; no embedded Nix derivation handles or +convenience wrappers appear inside any spec field. + +### Declaration + +```nix +d2b.artifacts. = { + package = ; # required Nix derivation + type = "provider" # closed type tag + | "nixos-system" + | "nixos-module-set" + | "config-bundle"; +}; +``` + +`` follows `^[a-z][a-z0-9-]*$`. Two entries sharing the same `` is a +NixOS eval error (Nix attrset uniqueness). A spec field referencing an `` +absent from `d2b.artifacts` fails the NixOS build. A spec field referencing +an `` whose `type` does not match what the field expects (e.g., a +`"provider"` artifact in a `systemArtifactId` field that expects +`"nixos-system"`) fails the build with a structured type-mismatch error. + +`Artifact` is not a ResourceType; artifact IDs are NOT ResourceRefs. Fields +that hold artifact IDs use plain unambiguous names (`artifactId`, +`systemArtifactId`) with no `*Ref` suffix. + +Example declarations: + +```nix +d2b.artifacts = { + system-core = { package = pkgs.d2b-provider-system-core; type = "provider"; }; + system-systemd = { package = pkgs.d2b-provider-system-systemd; type = "provider"; }; + runtime-ch = { package = pkgs.d2b-provider-runtime-cloud-hypervisor; type = "provider"; }; + dev-vm-system = { package = pkgs.nixosSystem { modules = [ ... ]; }; type = "nixos-system"; }; +}; +``` + +### Emitted artifact catalog + +The NixOS build emits a private, integrity-pinned `artifact-catalog.json` +(installed `root:d2bd` 0640) alongside the Zone bundles. Store paths are +restricted from PUBLIC ResourceSpecs, status fields, audit records, and OTEL +telemetry. The private `artifact-catalog.json` must contain sufficient Nix +store location data — directly as `storePath` fields, or via a private +integrity-bound locator — for `d2b-activation-helper` to resolve and stage +each built artifact: + +```json +{ + "schemaVersion": "v1", + "entries": [ + { + "artifactId": "system-core", + "type": "provider", + "storePath": "/nix/store/aabbcc...-d2b-provider-system-core", + "packageDigest": "sha256:aabbcc...", + "closureDigest": "sha256:ddeeff...", + "closureSize": 12345678 + }, + { + "artifactId": "dev-vm-system", + "type": "nixos-system", + "storePath": "/nix/store/112233...-nixos-system", + "packageDigest": "sha256:112233...", + "closureDigest": "sha256:445566...", + "closureSize": 876543210 + } + ] +} +``` + +`storePath` is a private field read only by `d2b-activation-helper` and the +Zone runtime for staging. It never appears in any public ResourceSpec, status +field, audit record, or OTEL telemetry export. `d2b-activation-helper` verifies +the artifact catalog digest against a `bundle.json`-sibling manifest entry +before staging. The catalog is content-addressed: same derivation inputs → +byte-identical output. + +### Validation + +| Rule | Layer | +| --- | --- | +| `` matches `^[a-z][a-z0-9-]*$` | Eval | +| No duplicate `d2b.artifacts.` keys | Eval (Nix attrset uniqueness) | +| Every `artifactId` / `systemArtifactId` in any spec exists in `d2b.artifacts` | Build | +| `type` of the artifact matches the expected type for the spec field | Build | +| Provider `artifactId` has `type = "provider"`; trust root validated | Build | +| `systemArtifactId` / `source.systemArtifactId` has `type = "nixos-system"` | Build | +| Store paths absent from all public ResourceSpecs, status fields, audit records, and OTEL telemetry | Build/Runtime | + +## Provider package catalog + +The catalog replaces the ad hoc per-crate package outputs in `flake.nix` and +`nixos-modules/host-daemon.nix`. Current provider construction is implicit +(direct Rust construction in `d2bd`). The v3 catalog is an offline sorted +exact-digest artifact. + +### Catalog declaration + +```nix +d2b.providerCatalog = { + system-core = { + artifactId = "system-core"; # must exist in d2b.artifacts with type = "provider" + trust = { publisherRef = "d2b-official"; }; + }; + system-systemd = { + artifactId = "system-systemd"; + trust = { publisherRef = "d2b-official"; }; + }; + runtime-cloud-hypervisor = { + artifactId = "runtime-ch"; # artifact ID may differ from catalog entry name + trust = { publisherRef = "d2b-official"; }; + }; + # ... one entry per Provider in the frozen initial catalog +}; +``` + +### Catalog artifact + +Emits `/etc/d2b/provider-catalog.json` (sorted by `providerName`). The +`artifactId` field links each Provider catalog entry to the corresponding +artifact catalog entry. `packageDigest` and `settingsSchemaDigest` are +populated by the build from the artifact catalog and Provider package closure. +Store paths do not appear in this public catalog; staging data lives in the +private `artifact-catalog.json`: + +```json +{ + "schemaVersion": "v1", + "entries": [ + { + "providerName": "system-core", + "artifactId": "system-core", + "packageDigest": "sha256:aabbcc...", + "executableDigest": "sha256:...", + "manifestDigest": "sha256:...", + "settingsSchemaDigest": "sha256:...", + "publisherRef": "d2b-official", + "systems": ["x86_64-linux"], + "apiMajor": 3, + "apiMinor": 0 + } + ] +} +``` + +The trust root model uses a built-in `d2b-official` signing root embedded in +the Nix module (covers all initial catalog entries). Additional trusted +publisher roots are per-Zone Nix compiler settings, not Zone ResourceSpec +fields. They are declared at the Zone option level and consumed only during +the NixOS build: + +```nix +d2b.zones.dev.trustedPublishers = { + acme-corp = { signingKey = ""; }; +}; +``` + +A Provider entry whose `publisherRef` is not the built-in `d2b-official` root +and is not registered in `d2b.zones..trustedPublishers` fails install +closed. An absent or unrecognized `publisherRef` is never a warning; it is a +hard failure at catalog resolution time. + +### Provider install resource + +Catalog presence does not install a Provider. Each Zone declares installed +Providers separately: + +```nix +d2b.zones.dev.resources.system-core = { + type = "Provider"; + spec = { + artifactId = "system-core"; # plain bounded ID; must exist in d2b.artifacts with type="provider" + config = {}; # validated against Provider's signed settings schema; no raw host paths + }; +}; +``` + +Compiles to a `Provider` resource in that Zone's generated bundle: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Provider +metadata: + name: system-core + zone: dev + ownerRef: null +spec: + artifactId: system-core + config: {} +``` + +`artifactId` is a plain bounded string (not a ResourceRef; `Artifact` is not a +ResourceType). The build resolves `artifactId` against `d2b.artifacts` (type +must be `"provider"`) and `d2b.providerCatalog` (trust validation); the catalog +is frozen with no `ProviderCatalogEntry` ResourceType. `packageDigest` and other +manifest-derived fields are private compiled catalog metadata and are not part of +the Provider ResourceSpec. `status` is read-only runtime state and is never +emitted in Nix-authored bundles. + +`config` values are validated at build time against the Provider's signed settings +schema. `config` must not contain raw host paths, Nix store path strings, or any +opaque filesystem references — all derivation outputs and volume source policies +are expressed as artifact IDs or opaque source policy IDs, not literal paths. + +Core `ProviderDeployment` reads the signed manifest/catalog selected by +`artifactId` and creates the Provider's static controller/service `Process` +graph. Provider controllers never bootstrap their own controller/service +Processes; they may only create authorized dynamic child resources at runtime. + +### Provider crate layout + +Every `packages/d2b-provider--/` Rust crate in the +workspace must contain exactly the following four paths. Missing any path — or +having an empty `tests/` or `integration/` directory with no Rust source file — +fails the workspace policy check (`make test-policy`): + +| Path | Required contents | +| --- | --- | +| `src/` | Crate implementation source and compiled binaries (one per process role declared in the Provider dossier). Colocated `#[cfg(test)]` unit tests are permitted here. No integration or provider-system tests. | +| `tests/` | Hermetic Cargo integration tests: ResourceType schema round-trips; controller/service/worker lifecycle; conformance via `d2b-provider-toolkit::conformance` (all declared provider-type axes); fault injection. No container, Host, Guest, or cross-process fixtures. | +| `integration/` | Heavier fixtures and scenarios requiring container launch, Host/Guest interaction, cross-process rendezvous, or provider-system state. Invoked by existing test orchestration (`make test-integration`, `make test-host-integration`). Must contain at least one `.rs` source file or fixture. | +| `README.md` | Provider identity and config schema; declared ResourceTypes and their spec/status fields; controllers, services, workers, and binaries with their process roles; placement constraints; dependencies and RBAC requirements; security posture, state lifecycle, and telemetry labels/cardinality; build, test, and integration commands; future standalone-repo usage notes. Minimum 200 bytes. | + +The workspace policy test +(`packages/d2b-contract-tests/tests/provider-crate-layout.rs`) scans every +workspace member matching `packages/d2b-provider-*-*` and fails for any +missing or empty path. The gate runs as part of `make test-policy`. + +Every work item in any spec that introduces a new +`packages/d2b-provider--/` crate must: +- list all four required paths in its `Destination` field; +- include the layout policy gate (`make test-policy`) in its `Tests` field; and +- include a `README.md` stub commit in its first commit before any other + implementation lands. + +### Provider state Volumes (optional) + +A `ProviderStateSet` is an optional, query-time logical grouping of the +*declared* `Volume` resources owned by a `Provider`. It is not a `ResourceType`, +is never stored as its own artifact or row, and is empty for a `Provider` that +declares no state Volume. + +Bounded non-secret operational state belongs in the owning resource's `status` +subresource and the core Operation ledger by default (D087). A component +declares a state `Volume` only when a payload passes the storage-need test +(secret/sensitive private recovery data; large/binary/file content; private +data unsafe for status readers; or bounded-but-revision-unsuitable data with a +demonstrated recovery need). Core `ProviderDeployment` creates the static +controller/service `Process` resources and, before launching each component +`Process`, creates one private, framework-created state `Volume` for every +*declared* `stateNamespaces` entry in that component's signed state +declarations. A stateless component declares no namespace and receives no +`Volume`; there is no empty identity-only state `Volume`. Provider controllers +never bootstrap their own controller/service Processes or their own declared +per-component state Volumes. + +Each declared state Volume uses the canonical full `Volume` schema, extended for +provider state with `stateSchema`, `persistenceClass`, `sensitivityClass`, +`quotaBytes`, `quota.maxBytes`, `quota.maxInodes`, `identityMarker`, and sealing +fields. Every declared state Volume has `kind = "state"` and +`persistenceClass = "persistent"`. `source.settings.sourcePolicyId` is always +set to the component's opaque private storage policy ID. `quotaBytes` must be +strictly greater than zero; `quota.maxBytes` and `quota.maxInodes` must also be +strictly greater than zero. Declaring `quotaBytes = 0` for a state namespace is +rejected at build and admission time. Layout principals use `User/` refs +drawn from bounded, Nix-preprovisioned pools, and Nix must provision the +referenced `User` resources in advance. + +Nix never authors runtime state Volumes. Nix does not declare them, does not +allocate their internal paths, and does not manage their runtime-only +identities; Core creates only the signed *declared* optional state Volumes at +runtime. Each component mounts only its own declared state view through its +`mounts`; there is no separate non-Volume compartment concept. + +There is no bootstrap state-Volume exception. The fixed bootstrap components +(the first `volume-local` controller instance on each execution target, and +where present `system-core` and `system-minijail`) declare no state Volume and +reach Ready from resource `status`, the core Operation ledger, and independent +external observation (D086, superseded by D087). Because no component needs a +state Volume before a `volume-local` instance is Ready, there is no closed +bootstrap cycle, no per-execution-target local bootstrap storage mechanism, and +no hidden bootstrap store. A Guest still bootstraps its own Guest-local +`volume-local` instance from Guest-local primitives only, never a leaked +parent-Host dirfd or resource handle. + + + +`Host` is a new ResourceType; there is no current equivalent. The closest +current concepts are `NodeKind::FullHost` (`d2b-realm-core/src/node.rs`), +`RealmControllerPlacement::HostLocal` (`d2b-realm-core/src/realm.rs`), and +the per-realm path-partition in `allocator-json.nix`. The option +`d2b.realms..placement = "host-local"` is the current declaration of a +host-resident realm. + +```nix +d2b.zones.dev.resources.host-system = { + type = "Host"; + spec = { + providerRef = "Provider/system-core"; + defaultDomain = "system"; + allowedDomains = ["system" "user"]; + budget = { cpu = {}; memory = {}; pids = {}; fds = {}; io = {}; storage = {}; network = {}; }; + }; +}; +``` + +Compiles to: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Host +metadata: + name: host-system + zone: dev + ownerRef: null +spec: + providerRef: Provider/system-core + defaultDomain: system + allowedDomains: [system, user] + defaultUserRef: null + budget: { ... } + networkAttachments: [] + deviceAttachments: [] + volumeDefaults: {} +``` + +### Unsafe-local Host + +The current `d2b.realms..workloads..kind = "unsafe-local"` workload +(current type: `WorkloadProviderKind::UnsafeLocal`, current isolation: +`IsolationPosture::UnsafeLocal`, current files: +`nixos-modules/unsafe-local-workloads-json.nix`, +`nixos-modules/unsafe-local-helper.nix`, +`packages/d2b-core/src/unsafe_local_workloads.rs`) maps to a user-only `Host` +resource in v3. It is never a `Guest` and is not a v3 Provider. + +The target shape is a `Host` resource reconciled by `Provider/system-core` with +`defaultDomain: user`, `allowedDomains: [user]`, and `defaultUserRef: User/`. +Child processes are normal `Process` resources selecting any installed Process +Provider (`Provider/system-systemd`, `Provider/system-minijail`). No special +Provider or process type exists for unsafe-local execution. + +The no-isolation posture is preserved explicitly across all surfaces: + +- **Host status** — `Provider/system-core` sets a stable `NoIsolation` condition + on `Host` status. The condition is present whenever + `spec.isolationPosture` is `"none"`; it is never absent or + cleared by a later upgrade. +- **CLI/UI** — `d2b host inspect` and any status display always render the + no-isolation warning when the Host carries the `NoIsolation` condition. + The warning text is not suppressible by operator flag. +- **Audit** — every `ProcessEffect` audit event under this Host carries a + closed boolean field `no_isolation=true`. No isolation label appears in OTEL + telemetry attributes. + +```nix +d2b.zones.dev.resources.host-unsafe-local = { + type = "Host"; + spec = { + providerRef = "Provider/system-core"; + defaultDomain = "user"; + allowedDomains = ["user"]; + defaultUserRef = "User/alice"; + isolationPosture = "none"; # promoted Host base field; required for user-only + # system-core Hosts; cannot be set to any other value + }; +}; +``` + +Eval assertions: + +- `spec.isolationPosture` set to any value other than `"none"` for a + user-only (`defaultDomain: user`, `allowedDomains: [user]`) + `Provider/system-core` Host is rejected at eval time. +- A Process with `executionRef: Host/host-unsafe-local` and `domain: system` + is rejected at eval time. +- No `Guest` ref is emitted for an unsafe-local declaration. + +`isolationPosture: "none"` is a promoted Host base field declared at top-level +`spec.isolationPosture` in the `Host` ResourceTypeSchema (visible to all +controllers and inspectable via the resource API). It is not a freeform field; +the schema restricts it to the bounded enum. `status.isolationPosture` reflects +`none` when `spec.isolationPosture` is `none`. The `NoIsolation` status +condition and the `no_isolation=true` `ProcessEffect` audit field are required +whenever this posture is in effect; OTEL telemetry never carries an isolation +label. + +## Guest resource + +`Guest` is the v3 successor to `d2b.vms.` (current: `nixos-modules/options-vms.nix`) +for NixOS VM and QEMU media workloads, and to `d2b.realms..workloads.` +with `kind = "local-vm"` or `"qemu-media"` (current: +`nixos-modules/options-realms-workloads.nix`). The current `WorkloadId` maps +to `Guest/` and the current `RealmTarget` address +`..d2b` maps to `Zone//Guest/`. + +```nix +d2b.zones.dev.resources.dev-vm = { + type = "Guest"; + spec = { + providerRef = "Provider/runtime-cloud-hypervisor"; + systemArtifactId = "dev-vm-system"; # plain bounded ID; type="nixos-system" in d2b.artifacts + defaultDomain = "system"; + allowedDomains = ["system"]; + budget = { cpu = { cores = 4; }; memory = { bytes = 4294967296; }; }; + networkAttachments = [{ networkRef = "Network/dev-lan"; }]; + deviceAttachments = [{ deviceRef = "Device/dev-tpm"; }]; + # Canonical selected-Provider extension envelope (D089): validated against the + # Provider named by providerRef. No raw host paths; named closure-entry IDs only. + provider = { + schemaId = "runtime-cloud-hypervisor.d2bus.org/Guest/spec"; + schemaVersion = "1.0"; + settings = { + vsockCid = 42; + memoryBacking = "shared"; + }; + }; + }; +}; +``` + +The canonical `spec.provider = { schemaId, schemaVersion, settings }` envelope +(D089) replaces the former Host/Guest/Credential `providerSettings` and Device +`settings`; `spec.providerRef` and every other `spec.*` field is ResourceType +base. The `spec.provider.settings` object is declared by the selected Provider's +signed, versioned, deny-unknown JSON Schema and validated against +`spec.providerRef` at eval time and at API admission; it may not shadow a base +field, and any field shared across +implementations is promoted to the ResourceType base rather than carried here. +Values that reference Nix derivation outputs (e.g., a closure, a kernel module +path) are serialized as named closure-entry identifiers or content digests +validated against the package manifest. Raw Nix store path strings (e.g., +`/nix/store/-foo`) are rejected at eval time and must never appear in +emitted resource spec JSON. + +**Update policy and currency triggers (D091).** The base spec carries a +provider-neutral `spec.updatePolicy` (disruptive upgrades default to `manual`; +automatic application of non-disruptive changes MAY be permitted); a +`spec.provider` extension may add implementation knobs but cannot bypass it. +Selecting a new artifact/image ID, a new `systemArtifactId` NixOS generation, or +a new Provider package generation in Nix is a currency trigger: activation +records the observed/target generation/digest IDs, and the owning controller's +`assess_update` reports `status.update.state = UpgradeRequired` (with the +matching closed-enum reason) rather than applying a disruptive change in place. +The operator applies it with `d2b upgrade --apply` (optionally +`--recursive`). + +**Endpoint resources (D092).** A stable endpoint is the `Endpoint` ResourceType, +never an inline field. Core creates a component's static `Endpoint` resources +from the signed manifest's Endpoint templates, and dynamic controllers create +their owned Endpoints; a static `Endpoint` may be Nix/API-authored only where +the ResourceType schema permits. An authored `Endpoint` spec carries only closed +`endpointClass`/`transport`/`locality`/`purpose` values and bounded fingerprints +— never a raw path/address/CID/port/fd/credential (those resolve privately +through the effect adapter at runtime). `ProcessSpec` carries no inline +`endpoints`; a Process's stable endpoints are owned `Endpoint` resources with +`producerRef`, and consumers reference `Endpoint/`. +Endpoint `visibility` accepts only `owner`, `provider`, or `zone`; finer access +belongs in the bounded `consumerPolicy`. Nix examples and generated option help +must not invent additional visibility tokens. + +**ResourceExport and ResourceImport (D096).** Cross-Zone singleton sharing uses +ordinary `d2b.zones..resources.` declarations. The Nix compiler does +not redefine the types: `type = "ResourceExport"` and `type = "ResourceImport"` +emit the exact `ADR-046-resources-zone-control` §8A base fields plus the strict +`spec.provider` extension when declared by the Provider schema. They bring the +standard ResourceType catalog to 19 entries; `Endpoint` remains standard. +Qualified semantic/provider-neutral `*Service` and `*Binding` types are not +standard types or Provider implementation namespaces. + +The Provider's signed projection factory binds the exact Service/Binding pair, +allowed owner-Service backing refs, allowed Binding target refs, projection +schema/fingerprint, and factory fingerprint. Build admission fails closed when +that metadata is missing or mismatched. `ResourceExport.resourceRef` MUST name +the local owner Service; Device, Endpoint, and Binding targets are rejected. +`ResourceImport` may reference only local `providerRef` and `zoneLinkRef`; its +`exportKey` is bounded and never a remote ResourceRef. It deterministically +creates one same-qualified-type local projection Service with the configured +stable name. Nix/operator authors the matching local Binding separately. +There is no `*State` or metadata-key compatibility alias; those spellings fail +schema validation rather than lowering to Binding. + +```nix +# Local-root owner Zone: the semantic type is implementation-independent; the +# selected Provider validates backingRefs against its signed factory. +d2b.zones.local-root.resources.host-audio = { + type = "audio.d2bus.org.AudioService"; + spec = { + providerRef = "Provider/audio-pipewire"; + backingRefs = [ "Device/host-mic" "Endpoint/audio-local" ]; + }; +}; + +# Export only the owner Service, never its Device/Endpoint backing. +d2b.zones.local-root.resources.mic-export = { + type = "ResourceExport"; + spec = { + providerRef = "Provider/audio-pipewire"; + resourceRef = "audio.d2bus.org.AudioService/host-audio"; + serviceType = "audio.d2bus.org.AudioService"; + projectionSchemaFingerprint = "sha256:..."; + factoryFingerprint = "sha256:..."; + operations = [ "capture" ]; + arbitration = "exclusive"; + quota = { fairness = "fifo"; leaseDeadlineMs = 30000; }; + consumerZonePolicy = { + zones = [ "Zone/work" ]; + capabilityCeiling = [ "capture" ]; + }; + visibility = "named-zones"; + updatePolicy = { mode = "manual-disruptive"; }; + revocationPolicy = { graceMs = 5000; }; + }; +}; + +# Compiler-only topology selects the allocator owner; this is not emitted into +# Zone.spec or any ResourceSpec. +d2b.zones.work.parentZone = "local-root"; + +# The consumer's uplink is a local resource. It supplies transport/local route +# state while the selected parent allocator owns all privileged effects. +d2b.zones.work.resources.work-uplink = { + type = "ZoneLink"; + spec = { + childZoneName = "work"; + transportProviderRef = "Provider/transport-unix"; + }; +}; + +# Work Zone: import exactly one local AudioService projection. +d2b.zones.work.resources.mic-import = { + type = "ResourceImport"; + spec = { + providerRef = "Provider/audio-pipewire"; + zoneLinkRef = "ZoneLink/work-uplink"; + exportKey = "host/mic-export"; + expectedServiceType = "audio.d2bus.org.AudioService"; + expectedProjectionSchemaFingerprint = "sha256:..."; + expectedFactoryFingerprint = "sha256:..."; + projectionName = "host-audio"; + requestedCapabilities = [ "capture" ]; + requestedQuota = { leaseDeadlineMs = 30000; }; + updatePolicy = { mode = "manual-disruptive"; }; + disconnectPolicy = { mode = "degrade"; }; + }; +}; + +# Authored local consumption Binding. The import controller never creates it. +d2b.zones.work.resources.work-mic = { + type = "audio.d2bus.org.AudioBinding"; + spec = { + providerRef = "Provider/audio-pipewire"; + serviceRef = "audio.d2bus.org.AudioService/host-audio"; + targetRef = "Guest/workstation"; + mode = "capture"; + }; +}; +``` + +The Binding spec above is desired consumer intent only; observed realization +belongs only in its `status`. The canonical compiled resources, including the +core-created projection Service +identity, have stable inspectable names. Provider-specific transparent sugar MAY +lower to this shape, but its generated ResourceExport, ResourceImport, projection +Service name, and Binding MUST be byte-stable across equivalent evaluations and +visible in `d2b config render`, `d2b resource list`, and the canonical bundle. +Sugar may not hide, merge, or auto-create a Binding at runtime. + +Ordinary Binding resources reference the local projection Service +(`metadata.ownerRef: ResourceImport/`), never the ResourceImport itself. +The Binding controller owns any Process/Endpoint children. High-churn +sessions/streams are internal and are not emitted as Nix resources. + +This is the canonical minimal base: it remains valid without +`spec.provider`. That strict extension may carry PipeWire-specific settings, +but PipeWire and all other implementation/protocol details stay out of the +base behavior, status, fingerprints, and generated semantic ResourceType names; +`providerRef` is the sole opaque implementation selector. The other frozen +pairs follow the same rule: +`security-key.d2bus.org.SecurityKeyService` + +`security-key.d2bus.org.SecurityKeyBinding`, +`telemetry.d2bus.org.TelemetryService` + +`telemetry.d2bus.org.TelemetryBinding`, and `usb.d2bus.org.UsbService` + +`usb.d2bus.org.UsbBinding`. + +**Authority descriptors (D097).** A resource that owns a scarce or singleton +backing carries its signed `AuthorityDescriptor` (schema in +`ADR-046-resource-object-model` §Authority and cardinality) — either as base +fields the ResourceTypeSchema declares or through the owning Provider's signed +schema; the Nix compiler renders it verbatim (no bespoke vocabulary). The +`authorityKey` is authored only as an opaque key **class**/selector, never a raw +path/serial/address. Eval-time admission enforces the authority index at build +time within a Zone bundle: two configuration-managed authority resources that +resolve to the same `(Zone/scope, authorityClass, opaqueKeyDigest)` for an +`exactly-one`/`zero-or-one` authority (or exceed a `bounded-many` bound) are a +hard eval error naming the incumbent, mirroring the runtime `duplicateConflict`; +an `exportability: forbidden` authority declared as a `ResourceExport` target is +rejected. The requested share mode in a resource's base/provider spec may not +exceed or contradict its `AuthorityDescriptor`. + +**Entra identity Guest composition (D093).** `credential-entra` login/token +acquisition is grounded in an Entrablau-enabled identity `Guest`, never a Host +login. The consumer declares the identity Guest (with a `systemArtifactId` whose +NixOS system composes `inputs.entrablau.nixosModules.default`), installs/binds +the login-service `Endpoint`, and declares the `Credential` with +`identityGuestRef`/`loginEndpointRef`/`consumerRef`: + +```nix +# Identity Guest whose NixOS system composes the sibling Entrablau module. +# d2b core never imports entrablau; the consumer flake does. +d2b.zones.work.resources.entra-identity = { + type = "Guest"; + spec = { + providerRef = "Provider/runtime-cloud-hypervisor"; + systemArtifactId = "entra-identity-system"; # its NixOS system imports inputs.entrablau.nixosModules.default + defaultDomain = "system"; + allowedDomains = ["system"]; + }; +}; + +# The login/token service Endpoint the Entrablau service produces inside the Guest. +d2b.zones.work.resources.entra-login = { + type = "Endpoint"; + spec = { + providerRef = "Provider/credential-entra"; + producerRef = "Guest/entra-identity"; # produced inside the identity Guest + endpointClass = "service"; + transport = "vsock"; # closed class only; no CID/path + purpose = "credential-entra.d2bus.org/entra-login-token"; + locality = "guest-local"; + visibility = "provider"; + consumerPolicy = { + allowedSubjects = [ + "Provider/credential-entra" + "Provider/runtime-azure-container-apps" + ]; + allowedOperations = [ "resolve" ]; + }; + }; +}; + +d2b.zones.work.resources.work-entra = { + type = "Credential"; + spec = { + providerRef = "Provider/credential-entra"; + identityGuestRef = "Guest/entra-identity"; + loginEndpointRef = "Endpoint/entra-login"; + audience = "api://work-graph"; # non-secret audience only + consumerRef = "Provider/runtime-azure-container-apps"; + allowedOperations = ["acquire-token"]; + rotation = { policy = "on-demand"; }; + }; +}; +``` + +Build/eval validation asserts: `identityGuestRef`, `loginEndpointRef`, +`consumerRef`, and `scope.executionRef` are all the same Zone; the `Endpoint` +`purpose` is exactly `credential-entra.d2bus.org/entra-login-token`, its schema +and `providerRef` match the credential-entra login-service contract, and the +Endpoint is Guest-placed (Host placement is rejected), uses +`visibility = "provider"` (`"zone"` is rejected), and has an exact +`consumerPolicy` whose `allowedSubjects` contains both +`Provider/credential-entra` and the Credential's exact `consumerRef` +(`Provider/runtime-azure-container-apps`) and whose `allowedOperations` is +exactly `resolve`; the Credential scope and consumer placement are consistent; +and no store path or token appears in any emitted spec. + +### Package closures into Guests + +Current source: `nixos-modules/closures-json.nix` — `pkgs.closureInfo` per VM, +outputs `/etc/d2b/closures/.json`. Current `VmProcessDag.nodes` contains +a `ProcessRole::Virtiofsd` node whose `share.source` sentinel +`"/nix/store"` (string literal) is the eval-time marker for the per-VM +hardlink-farm share path; see `nixos-modules/processes-json.nix` and +`nixos-modules/store.nix`. + +In v3, every Guest that runs a closure-based OS pins its system artifact in +the artifact catalog, then references it by plain ID in the Guest spec: + +```nix +# Step 1: declare the NixOS system derivation in the artifact catalog +d2b.artifacts.dev-vm-system = { + package = pkgs.nixosSystem { modules = [ ... ]; }; + type = "nixos-system"; +}; + +# Step 2: reference it by ID in the Guest spec (mirrors the canonical schema) +d2b.zones.dev.resources.dev-vm = { + type = "Guest"; + spec = { + providerRef = "Provider/runtime-cloud-hypervisor"; + systemArtifactId = "dev-vm-system"; # validated type="nixos-system" at build + # ... other spec fields + }; +}; +``` + +The compiler: + +1. Resolves `systemArtifactId = "dev-vm-system"` from `d2b.artifacts`; validates + `type = "nixos-system"`. +2. Computes `pkgs.closureInfo { rootPaths = [artifacts.dev-vm-system.package]; }`. +3. Emits a `Volume/-nix-store` resource with + `source.kind = "nix-closure"` and `source.systemArtifactId = "dev-vm-system"`. +4. Emits a virtiofs attachment from that Volume to the Guest with + `mountPath: /nix/store`. +5. Records closure digest, closure size, and private store path in the artifact + catalog entry for `"dev-vm-system"`. The store path is a private field in + `artifact-catalog.json`; it is never emitted in public ResourceSpecs, status + fields, audit records, or OTEL telemetry. + +The per-VM hardlink farm path is derived by `Provider/volume-virtiofs` at +runtime from the artifact catalog entry and the Zone's `stateDir`; it never +appears in any resource spec or status field. The current `share.source == +"/nix/store"` sentinel in `nixos-modules/processes-json.nix` is the eval-time +equivalent; its exact migration mapping is covered in ADR046-nix-017. + +## Process and EphemeralProcess + +Current source: `packages/d2b-core/src/processes.rs` — `ProcessesJson`, +`VmProcessDag`, `ProcessNode`, `ProcessRole`. The per-VM DAG drives +`nixos-modules/processes-json.nix` which emits `/etc/d2b/processes.json`. + +### Common spec + +```nix +d2b.zones.dev.resources.wayland-proxy = { + type = "Process"; + metadata.ownerRef = "Provider/display-wayland"; + spec = { + providerRef = "Provider/system-systemd"; + executionRef = "Host/host-system"; + domain = "user"; + userRef = "User/alice"; + processClass = "service"; + template = "wayland-proxy-host"; + configRef = "Volume/wayland-proxy-config"; + credentialRefs = []; + mounts = [ + { volumeRef = "Volume/wayland-proxy-state"; + view = "proxy"; + mountPath = "/state"; + access = "read-write"; + required = true; } + ]; + sandbox = {}; + budget = {}; + networkUsage = { + networkRef = null; + ports = []; + allowEgress = false; + }; + deviceUsage = []; + telemetry = {}; + desiredLifecycle = "running"; + restartPolicy = { + class = "on-failure"; + backoffBase = "1s"; + backoffMax = "60s"; + backoffMultiplier = 2.0; + maxRestarts = 5; + resetAfter = "300s"; + }; + readiness = { + initialDelay = "0s"; + timeout = "30s"; + failureThreshold = 3; + successThreshold = 1; + class = "ready-condition"; + }; + healthCheck = { + enabled = false; + interval = "30s"; + timeout = "5s"; + failureThreshold = 3; + class = "provider-defined"; + }; + adoptionPolicy = "adopt-on-restart"; + drainTimeout = "30s"; + }; +}; + +# Stable endpoints are separate owned resources, never inline Process fields. +d2b.zones.dev.resources.wayland-host-socket = { + type = "Endpoint"; + metadata.ownerRef = "Process/wayland-proxy"; + spec = { + providerRef = "Provider/display-wayland"; + producerRef = "Process/wayland-proxy"; + endpointClass = "service"; + transport = "unix"; + purpose = "wayland-host-socket"; + locality = "host-local"; + visibility = "zone"; + consumerPolicy = { + allowedOperations = [ "resolve" ]; + }; + }; +}; +``` + +`providerRef` selects the Process implementation. `metadata.ownerRef` selects +the semantic owner whose signed component descriptor resolves `template` to an +exact executable and digest. No package reference or executable is accepted in +`ExecutionSpec`. + +### Prohibited fields + +The following are never accepted in any Process or EphemeralProcess Nix +declaration. They replace the free-form fields that current `ProcessNode` +carries (current: `unit`, `binary_path`, `argv`, numeric UID/GID in sandbox): + +- Raw executable path (current `ProcessNode.binary_path` — now resolved by + Provider from signed package manifest); +- Raw environment variable map (current `SpawnRunnerPlanOp` env fields); +- Numeric UID or GID (now `userRef: User/`); +- Raw seccomp BPF program (now named profile ref); +- Ambient capability bitmask; +- Arbitrary socket path; +- Credential or secret bytes. + +### EphemeralProcess + +Current source: `ProcessRole::StoreVirtiofsPreflight`, `SwtpmPreStartFlush`, +`GuestControlHealth`, `GuestSshReadiness` (disposition table above). + +```nix +d2b.zones.dev.resources.store-sync-dev-vm = { + type = "EphemeralProcess"; + metadata.ownerRef = "Provider/volume-virtiofs"; + spec = { + providerRef = "Provider/system-minijail"; + executionRef = "Host/host-system"; + domain = "system"; + userRef = null; + processClass = "worker"; + template = "store-sync"; + configRef = "Volume/store-sync-config"; + credentialRefs = []; + mounts = [ + { volumeRef = "Volume/dev-vm-nix-store"; + view = "sync-source"; mountPath = "/source"; access = "read-only"; + required = true; } + { volumeRef = "Volume/dev-vm-store-farm"; + view = "sync-target"; mountPath = "/target"; access = "read-write"; + required = true; } + ]; + sandbox = {}; + budget = {}; + networkUsage = null; + deviceUsage = []; + telemetry = {}; + startDeadline = "30s"; + runtimeDeadline = "300s"; + successfulTtl = "1h"; + failedTtl = "24h"; + incidentHold = false; + }; +}; +``` + +`startDeadline` and `runtimeDeadline` use Go-style bounded duration strings. +Unbounded deadlines are rejected at eval time. + +## Volume resource + +Current source: `packages/d2b-core/src/storage.rs` — `StorageJson`, +`StorageRoot`, `StoragePathSpec` with `owner`/`group`/`mode`/`accessAcl`/ +`defaultAcl`/`noFollow`/`createPolicy`/`repairPolicy`/`cleanupPolicy`. +The current `nixos-modules/storage-json.nix` emitter generates these rows +per VM using the `mkPath` helper. The v3 Volume layout/views replaces this +with a single ResourceType that carries the same fine-grained policy. + +```nix +d2b.zones.dev.resources.wayland-proxy-state = { + type = "Volume"; + spec = { + providerRef = "Provider/volume-local"; + source = { + executionRef = "Host/host-system"; + settings = { + kind = "local-path"; + sourcePolicyId = "wayland-proxy-state"; # opaque ID resolved through + # volume-local's private + # allowedHostPaths catalog + }; + }; + layout = [ + { path = "socket-dir"; + type = "directory"; + ownerRef = "User/alice"; # replaces PrincipalRef "uid"/"gid" in StoragePathSpec + groupRef = "User/alice"; + mode = "0700"; + noFollow = true; + createPolicy = "create-if-never-provisioned"; + repairPolicy = "exact-owner"; + cleanupPolicy = "owner-controlled"; } + ]; + views = { + proxy = { path = "."; rights = ["read" "write" "create" "delete" "traverse"]; }; + }; + attachments = []; + }; +}; +``` + +For `local-path` and `block-image` sources, `source.settings.sourcePolicyId` +is an opaque bounded ID that references an entry in volume-local's private +`config.allowedHostPaths` catalog. The raw host path is resolved only inside +the private effect-adapter/broker path and never appears in the public `Volume` +spec, status, or audit record. + +`layout[*].ownerRef` and `layout[*].groupRef` accept only `User/` +typed Zone refs. Numeric UID/GID strings (e.g., `"1000"`) are rejected at +eval time. There is no legacy numeric ref migration period: current +`PrincipalRef { kind: "uid", value: "..." }` entries are a clean-reset +migration; operators must declare corresponding `User` resources before +migrating storage layout declarations (tracked in ADR046-nix-009). + +### Virtiofs Volume + +Current source: `ProcessRole::Virtiofsd` in `VmProcessDag`; the current +`share.source` sentinel; `store.nix` for the per-VM hardlink farm. + +```nix +d2b.zones.dev.resources.dev-vm-nix-store = { + type = "Volume"; + spec = { + providerRef = "Provider/volume-virtiofs"; + source = { + kind = "nix-closure"; # desired-state marker + executionRef = "Host/host-system"; + systemArtifactId = "dev-vm-system"; # references artifact catalog; type="nixos-system" + }; + views = { + guest-ro = { path = "."; rights = ["read" "traverse"]; }; + }; + attachments = [ + { executionRef = "Guest/dev-vm"; + transport = "virtiofs"; + mountPath = "/nix/store"; + view = "guest-ro"; + access = "read-only"; } + ]; + }; +}; +``` + +## Network resource + +Current source: `nixos-modules/options-envs.nix` — `d2b.envs..*` with +`lanSubnet`, `uplinkSubnet`, `mtu`, `mssClamp`, `externalNetwork.*`, +`portForwards`. Current `index.nix` (`netMeta`) computes derived bridge names, +IP addresses, and DHCP ranges from these options. The `lib.mkForce` +neutralizer in `net.nix` is the live DHCP/NAT-prevention mechanism for the +net VM's uplink interface. + +```nix +d2b.zones.dev.resources.dev-lan = { + type = "Network"; + spec = { + providerRef = "Provider/network-local"; + lanSubnet = "10.100.0.0/24"; + uplinkSubnet = "10.100.1.0/30"; + mtu = 1500; + mssClamp = true; + dhcp = { enable = true; rangeStart = "10.100.0.200"; rangeEnd = "10.100.0.250"; }; + dns = { enable = true; }; + nat = { enable = true; }; + eastWest = { allow = false; }; + externalNetwork = { enable = false; }; + }; +}; +``` + +CIDR ranges must not overlap with any other Network in the same Zone. The eval +assertion is the v3 successor to the current CIDR-overlap assertion in +`nixos-modules/assertions.nix`. + +CIDR/ref/conflict validation is dual-layer: (1) a pure Nix eval assertion runs +at `nix flake check` for all statically known inputs — this is the primary +shift-left gate; (2) the configuration-publication controller repeats the same +validation before staging a new generation. Dynamic allocations that are not +known at eval time (e.g., DHCP ranges assigned by an external IPAM system) +are validated only at the runtime layer (2). Both layers are required; neither +may be omitted. + +## Device resource + +Current sources: `nixos-modules/components/tpm.nix` (swtpm; `ProcessRole::Swtpm` +and `SwtpmPreStartFlush`), `components/usbip.nix` (`ProcessRole::Usbip`), +`components/graphics.nix` (`ProcessRole::Gpu`, `GpuRenderNode`, `Video`). +Current per-VM toggle: `d2b.vms..tpm.enable`, `d2b.vms..usbip.*`, +`d2b.vms..graphics.*`. + +```nix +d2b.zones.dev.resources.dev-tpm = { + type = "Device"; + spec = { + providerRef = "Provider/device-tpm"; + stateVolumeRef = "Volume/dev-vm-tpm-state"; + # No raw device path. The Provider enumerates TPM hardware and swtpm state. + }; +}; + +d2b.zones.dev.resources.dev-usbip = { + type = "Device"; + spec = { + providerRef = "Provider/device-usbip"; + deviceRef = null; # opaque ref resolved by Provider at runtime + }; +}; +``` + +Raw device node paths (`/dev/tpm0`, hidraw nodes) never appear in emitted +resource specs. The Provider is the sole authority for device enumeration. + +## User resource + +Current source: `nixos-modules/host-users.nix` — d2b system user/group +declarations; `d2b-realm-core/src/identity_store.rs` — `IdentityStore` owns +UID/session lifecycle metadata (pure in-memory). Current per-realm +`d2b.realms..allowedUsers`/`allowedGroups` options. + +```nix +d2b.zones.dev.resources.alice = { + type = "User"; + spec = { + source = "nss"; + groups = []; + }; +}; +``` + +Compiles to: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: User +metadata: + name: alice + zone: dev + ownerRef: null +spec: + source: nss + groups: [] +status: + phase: Unknown + observedUid: null + observedGid: null + observedHome: null + sessionManagerAvailable: false +``` + +`observedUid` and `observedGid` are status observations, never spec fields. +The current `PrincipalRef { kind: "uid", value: "..." }` form in storage +contracts must not appear in v3 User spec; it is a pre-migration form only. + +## Credential resource + +Current source: `packages/d2b-realm-core/src/identity_config.rs` +(`RealmIdentityConfigJson`, `IdentityConfigEntry`) — live in `d2bd` at +`/etc/d2b/realm-identity.json`. Current `d2b-realm-provider/src/credential.rs` +(`CredentialProvider` trait, `CredentialPlane`). + +```nix +d2b.zones.dev.resources.work-entra = { + type = "Credential"; + spec = { + providerRef = "Provider/credential-entra"; + identityGuestRef = "Guest/entra-identity"; + loginEndpointRef = "Endpoint/entra-login"; + scope = { + executionRef = "Guest/aca-gateway"; # optional Host or Guest placement restriction + domainFilter = "system"; # optional: system | user + }; + audience = "azure-resource-manager"; # Provider-validated opaque audience token; no secrets + consumerRef = "Provider/runtime-azure-container-apps"; + allowedOperations = [ "acquire-token" "refresh-token" "revoke-token" "inspect-metadata" ]; + rotation = { + policy = "proactive"; + proactiveWindowMs = 300000; + maxLeaseLifetimeMs = 3600000; + }; + expiry = { hardDeadlineMs = 28800000; }; + revocation = { + onOwnerDelete = "immediate"; + onProviderGeneration = "immediate"; + }; + # spec.provider.settings validated against Provider's signed schema; no secret bytes. + provider = { + schemaId = "credential-entra.d2bus.org/Credential/spec"; + schemaVersion = "1.0"; + settings = { tenantId = "..."; }; # optional; only when Provider schema declares it + }; + }; +}; +``` + +Credential spec fields mirror the `Credential` ResourceTypeSchema exactly: +`providerRef`, `scope.{executionRef,domainFilter,userRef}`, `audience`, +`consumerRef`, `allowedOperations`, `rotation`, `expiry`, `revocation`, +conditional `identityGuestRef`/`loginEndpointRef`, and optional canonical +`spec.provider` extension (`spec.provider.settings` is permitted when the +Provider schema declares it; validated against signed Provider schema; no +secret bytes). No invented fields (`credentialType`, `ownerRef`, `domain`). +The emitted spec never contains key material, token bytes, or PEM. Current +`identity_config.rs` values that carry credential bytes are forbidden from the +emitted spec; they remain inside the Provider's external secret service. + +## Role and RoleBinding + +Current source: `packages/d2b-realm-core/src/access.rs` — +`RealmAccessBinding`, `RealmAccessClientBinding`, `RealmAccessClientContract`, +and `CapabilityPreflightStatus` model current access rules. Current live access +resolution is in `d2bd/src/realm_access_resolver.rs`. + +```nix +d2b.zones.dev.resources.process-operator = { + type = "Role"; + spec = { + rules = [ + { resourceTypes = ["Process"]; + verbs = ["get" "list" "watch" "update-status"]; } + { resourceTypes = ["Host" "Guest"]; + verbs = ["get" "list"]; } + ]; + }; +}; + +d2b.zones.dev.resources.process-operator-binding = { + type = "RoleBinding"; + spec = { + roleRef = "Role/process-operator"; + subjects = [ + "Provider/system-systemd" + "Provider/system-minijail" + ]; + scopeNarrowing = null; + }; +}; +``` + +Eval assertions: + +- `roleRef` must resolve a declared `Role` in the same Zone. +- Every `subjects` entry must be a `/` canonical ref string. + The initial closed subject ResourceType set is: `Zone`, `Provider`, `Host`, + `Guest`, `Process`, `User`. `Group` is not a subject ResourceType; user group + membership may narrow User admission at runtime but is not declared as a + RoleBinding subject. +- A subject referencing a `Provider`, `Host`, `Guest`, `Process`, or `User` + must resolve a declared resource of that type in the same Zone. A `Zone` + ResourceRef subject can name only the runtime-created local self Zone; + adjacent enrolled Zone transport subjects use an exact trusted + `externalPrincipalSelector` generated outside untrusted Provider/request + input. +- Verbs must be from the closed set published by the resource-api and authz + foundation spec; the compiler rejects any verb not in that set at eval time. +- Resource `verbs` uses the exact closed set `get`, `list`, `watch`, `create`, + `update-spec`, `update-status`, `update-metadata`, `update-finalizers`, + `delete`, `use-credential`, and `admin-credential`. +- `sessionVerbs` uses the separate closed set `connect`, `invoke`, + `open-stream`, `relay`, `attach`, `cancel`, `observe`, `audit-export`, and + `support-bundle`. `relay`, `audit-export`, and `support-bundle` are valid only + in `sessionVerbs`; generated option help describes relay as one-hop + ZoneLink-scoped forwarding and binds the two diagnostic verbs only to + `d2b.audit.v3.AuditService/Export` and + `d2b.support.v3.SupportService/GenerateBundle`, with no resource authority. +- A relay-bearing Role/RoleBinding is rejected before bundle activation unless + it has exact Zone/resource/name bounds and core-generated ZoneLink provenance, + or durable explicit local-admin policy permits that exact bounded grant. + Wildcard, empty/all-name, Provider-self-asserted, and ordinary + operator-self-asserted relay grants fail closed. + +The initial closed subject ResourceType set (`Zone`, `Provider`, `Host`, +`Guest`, `Process`, `User`) is extended only by a future foundation spec +update. No `Group` ResourceType exists in the initial set; user group facts +(e.g., supplementary groups) may narrow User admission within the runtime +authz layer but are never RoleBinding subjects. + +## Controller placement + +Current source: `nixos-modules/options-realms.nix` — `d2b.realms..providers` +attrset with `kind`, `placement`, `capabilityRefs`, `configRef`, +`providerSpecificPlacement`. The `placementKinds` list +(`host-local`, `gateway-vm`, `cloud-full-host`, `provider-controller`, +`provider-agent`, `provider-specific`) in `options-realms.nix` is the current +list; these collapse into `executionRef: Host/` or `Guest/` per the +NodeKind table above. + +In v3, placement bindings are not a generic sibling field of the Provider +ResourceSpec. The Provider ResourceSpec carries only `artifactId` and `config`. +Placement bindings are expressed in one of two ways: + +1. **As validated `config` fields.** Where the Provider's signed deployment + template schema defines placement-related config keys (e.g., which Host or + Guest to use for each controller or worker process), the operator supplies + those values inside the `config` object. Their structure, names, and + validation rules are defined entirely by the Provider's schema — not the + framework. The Nix build validates `config` against the Provider's signed + settings schema. + +2. **As controller-created Process resources.** At runtime, the Provider + controller creates the required `Process` resources using the execution + targets declared in the Zone. Nix does not declare these Process resources + directly for Provider-internal processes; they are dynamic children managed + by the Provider controller. + +If an operator-supplied `config` key names a placement target (e.g., a Host or +Guest) that no longer matches a declared resource, config validation rejects it +at build time against the Provider's signed schema (which encodes which +`executionRef`-typed fields exist). Removed or renamed config keys produce a +structured build error; there is no warning-only path. + +```nix +# Provider with placement expressed as config fields per its signed schema: +d2b.zones.dev.resources.display-wayland = { + type = "Provider"; + spec = { + artifactId = "display-wayland"; + config = { + # Placement-typed config keys defined by display-wayland's signed schema. + # Names and types are determined by the Provider, not the framework. + hostExecutionRef = "Host/host-system"; + guestExecutionRef = "Guest/dev-vm"; + }; + }; +}; +``` + +## Ref validation + +All ref validation runs at Nix eval time: + +| Rule | Assertion | +| --- | --- | +| ResourceRef format | `/` where name matches `^[a-z][a-z0-9-]*$` | +| Type exists in catalog | Type is in the closed standard catalog or an approved vendor type | +| Intra-Zone resolution | The named resource is declared in `d2b.zones..resources` | +| Cross-Zone ref rejection | Any ref containing a Zone component is rejected; no cross-Zone ResourceRef allowed | +| `providerRef` resolution | The named Provider is declared in `d2b.zones..resources` with `type = "Provider"` | +| `executionRef` resolution | The named Host or Guest is declared in `d2b.zones..resources` with `type = "Host"` or `"Guest"` | +| `userRef` resolution | The named User is declared in `d2b.zones..resources` with `type = "User"` | +| `ownerRef` resolution | The owning resource is declared in `d2b.zones..resources` and is not the resource itself | +| `ownerRef` acyclicity | No chain of `ownerRef`s forms a cycle | +| `roleRef` resolution | The named Role is declared in `d2b.zones..resources` with `type = "Role"` | +| `subjects` entries | Each entry is a canonical `/` ref string resolving to a declared resource of the stated type in the same Zone; type must be in the closed subject set | +| `transportProviderRef` resolution | The named Provider is declared in `d2b.zones..resources` with `type = "Provider"`; required on every ZoneLink; no default | +| `childZoneName` check | Plain Zone name, not a ResourceRef; it equals the enclosing `d2b.zones.` key, each non-root Zone has at most one uplink resource, and local root has none | +| `parentZone` topology | Compiler-only plain Zone name, not a ResourceRef; omitted on `local-root`, required everywhere else, resolves to one declared Zone, is not self, forms no cycle, and keeps every ancestry path within 16 Zone names | +| `ResourceExport` Service-only ref | `providerRef` and `resourceRef` resolve locally; `resourceRef` has the factory's qualified `*Service` type; Device/Endpoint/Binding/backing and cross-Zone targets reject | +| `ResourceImport` local route/factory | `providerRef` and `zoneLinkRef` resolve locally; `exportKey` is bounded/not a Ref; expected Service type and projection/factory fingerprints match the signed export and local factory | +| Qualified Binding refs | `serviceRef` resolves to the same-Zone matching Service; target ref resolves to a factory-allowed same-Zone Guest/User/Zone; Binding spec is intent-only, observations are status-only, and Binding is non-exportable | +| `artifactId` / `systemArtifactId` format | Plain bounded string `^[a-z][a-z0-9-]*$`; not a `/` ResourceRef; no `*Ref` suffix | +| `catalogEntryId` check | The named entry exists in `d2b.providerCatalog`; resolved to its `artifactId` | + +Failed validation emits a structured eval error identifying the exact option +path and rejected value. + +Vendor ResourceType names (e.g., `acme.io.Widget`) appearing in any Nix ref +field before the exporting Provider is installed reject at eval time and block +config publication. There is no deferred-warning or allow-with-warning path. +The compiler's closed ResourceType set is extended only when a Provider +declares the type in its installed dossier. + +## Nix authoring and validation contract + +### Universal resource spec shape + +Every Zone resource is declared under the unified `d2b.zones..resources` +attribute set using a `type`/optional-authored-`metadata`/`spec` envelope that +mirrors the canonical ResourceSpec JSON schema directly: + +```nix +d2b.zones..resources. = { + type = ""; # string discriminator matching a known ResourceType + metadata.ownerRef = "Provider/owner"; # optional; canonical metadata, never spec + spec = { + # Exact ResourceType spec fields — identical names to the canonical JSON schema. + # No field renaming; no parallel bespoke vocabulary. + providerRef = "Provider/..."; + # ... + }; +}; +``` + +`metadata.name` is derived from the attribute key (``). +`metadata.zone` is derived from the enclosing zone attribute key (``). +`apiVersion` is defaulted to `"resources.d2bus.org/v3"` — never specified in Nix. +`status` is omitted from all emitted artifacts and is read-only; the Zone +runtime fills `uid`, `generation`, `revision`, `timestamps`, and management +metadata at first activation and on subsequent reconciles. All three status +layers (D088) — the universal `ResourceStatus` base, the ResourceType-common +`status.resource`, and any Provider-specific `status.provider` — are runtime +observation and are never authored in Nix. The `status.provider.details` +extension schema is signed into and registered with the **Provider package** +(resolved via `spec.artifactId`), not authored or emitted by the Zone Nix +configuration. Nix authors `type`, optional permitted metadata such as +`ownerRef`, and `spec`; metadata never appears inside `spec`. + +The `managedBy` field (`configuration | controller | api`) is a core-set +management metadata field set exclusively by the core runtime. It is not +specified in Nix, not emitted by the Nix build, and not accepted in any +resource spec input. The Nix compiler rejects any resource whose `spec` +contains a `managedBy` key. + +The `spec` field uses the **exact same field names and nesting** as the +ResourceTypeSchema JSON — there is no second bespoke Nix vocabulary. A spec +field called `providerRef` in the JSON schema is `providerRef` in Nix; a +nested struct called `budget.cpu.cores` maps to `budget.cpu.cores` in Nix. + +Generated Nix option types, defaults, allowed values, and inline documentation +are derived from the same committed ResourceTypeSchema +(`docs/reference/schemas/v3/.json`) and Provider schema +(`settingsSchemaDigest` in `provider-catalog.json`) — the module system and the +build validator use the same source of truth. + +There are no Nix-only fields inside a resource declaration. `type` is the +ResourceType discriminator, permitted authored `metadata` fields map to the +canonical metadata envelope, and `spec` is emitted verbatim. To disable a +resource, omit it from `d2b.zones..resources` or use the ResourceType's +own desired-state fields if that type defines them. + +All spec fields are emitted verbatim into the canonical JSON envelope. +Derivation references and NixOS system closures belong in `d2b.artifacts`, not +inside any resource spec field. + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "", + "metadata": { + "name": "", + "zone": "", + "ownerRef": null, + "finalizers": [] + }, + "spec": { /* exact spec fields, identical to Nix input */ } +} +``` + +`uid`, `generation`, `revision`, and `timestamp` fields are absent from +Nix-emitted artifacts. `metadata.ownerRef` defaults to `null`; it may be +authored only for ResourceTypes that permit configuration-assigned ownership +and never appears in `spec`. `finalizers` defaults to `[]` in emitted bundles +and is managed exclusively at runtime. + + +### ResourceTypeSchema validation + +Every ResourceType has a committed JSON Schema under +`docs/reference/schemas/v3/.json` generated from canonical Rust +DTOs in `d2b-contracts` by `cargo xtask gen-schemas`. The Nix build derivation +validates every emitted `spec` against this schema before the derivation +succeeds. The drift gate `make test-drift` enforces `xtask gen-schemas` + +`git diff --exit-code` to prevent silent schema/code drift. + +| Validation rule | Layer | +| --- | --- | +| Every `spec` field type-checks against committed JSON Schema | Build | +| All required fields present; no unknown top-level `spec` fields | Build | +| `spec` must not contain `managedBy` (core-set runtime field) | Eval | +| `resource_name` matches `^[a-z][a-z0-9-]*$` | Eval | +| All `*Ref` fields follow `/` | Eval | +| All refs resolve within the same Zone | Eval | +| `ownerRef` acyclicity | Eval | +| `domain` ∈ `allowedDomains` of the target Host/Guest | Eval | +| CIDR ranges non-overlapping within Zone | Eval | +| Vendor ResourceType installed before use in any ref | Eval | +| `artifactId` / `systemArtifactId` exists in `d2b.artifacts` | Build | +| `artifactId` artifact has `type = "provider"` + trust validated | Build | +| `systemArtifactId` artifact has `type = "nixos-system"` | Build | +| `source.systemArtifactId` artifact has `type = "nixos-system"` | Build | +| Numeric/string bounds (e.g., vsockCid range) | Eval | +| `config` matches Provider's signed settings schema (Provider ResourceSpec) | Build | +| `config` must not contain raw host paths or store path strings | Build | +| `spec.provider.settings` matches Provider's signed settings schema (where schema declares it) | Build | +| `spec.provider.settings` must not contain raw host paths, store paths, or secret bytes | Build | +| Store paths absent from all public ResourceSpecs, status, audit, and OTEL telemetry | Build/Runtime | +| Export/import/local Provider `serviceType`, `projectionSchemaFingerprint`, and `factoryFingerprint` match exactly; signed factory is present | Build | +| `ResourceImport` consumer Zone authorized by `ResourceExport.consumerZonePolicy` | Eval/Build | +| `ResourceImport.requestedCapabilities` is a subset of the export `operations` and capability ceiling | Eval/Build | +| `ResourceExport.resourceRef` names only the factory-bound owner `*Service`; Device/Endpoint/Binding/Credential/backing refs reject | Eval/Build | +| Owner Service backing refs and authored Binding target ref belong to the factory allowlists | Eval/Build | +| Audio/security-key/observability factories allowed; USBIP additionally passes policy; every other Provider export rejects | Build | +| Transparent sugar lowers to byte-stable inspectable canonical export/import/Binding resources and a stable projection-Service name | Build/Drift | + +A structured eval error identifies the exact NixOS option path and rejected +value for every rule violation. + +### Provider-specific settings validation + +The `Provider` ResourceSpec uses a `config` field validated against the +Provider's signed settings schema. Other semantic ResourceTypes (`Host`, `Guest`, +`Credential`, and others) may use the canonical `spec.provider` extension only +when their ResourceTypeSchema declares it; if the schema does not declare +`spec.provider`, the field is absent and must not appear in Nix input. All other +spec fields are those declared in the ResourceTypeSchema exactly. Nix mirrors +each ResourceSpec 1:1; no blanket rule applies across types. + +Validation is offline; no network access occurs during the build. The schema +fingerprint is recorded in `provider-catalog.json` under `settingsSchemaDigest` +(private compiled catalog metadata, not a ResourceSpec field); a schema mismatch +is a build error. + +Rules: +- Additional fields not declared in the Provider schema are rejected + (`additionalProperties: false`). +- `config` and any `spec.provider.settings` value must not contain raw host paths or + Nix store path strings; derivation outputs are expressed as artifact IDs or + opaque source policy IDs. No provider-specific field in + `spec.provider.settings` may accept secret bytes. +- Numeric, string, and boolean bounds declared in the Provider schema are + enforced at build time; out-of-bounds values fail the derivation. + +### Credentials and secrets + +No secret value (credential bytes, token, PSK material, key PEM, password, +certificate DER/PEM, bearer token, HMAC key) may appear in any Nix spec field, +any `spec.provider.settings` value, or generated artifact. See "Prohibited +fields summary" below for the complete list. Provider-required secrets are +always declared as `Credential/` refs. The `Credential` resource spec +carries configuration-only fields (`providerRef`, `scope`, `audience`, +`consumerRef`, `allowedOperations`, `rotation`, `expiry`, `revocation`, and +optional `spec.provider` extension where the Provider schema declares it) — +never credential bytes, tokens, or key material. + +Actual secret bytes are injected at runtime via the broker's `StoreCredential` +op, which is never invoked from Nix. An eval assertion rejects any string field +matching known secret patterns (PEM `-----BEGIN` headers, JWT `eyJ...` prefix, +hex strings ≥ 32 bytes in secret-typed spec fields). + +### Bundle integrity + +The Zone resource bundle and the artifact catalog emitted by each Nix derivation +build are: + +1. **Sorted**: every `*.json` file contains resources sorted by + `metadata.name`. `bundle.json` file entries are sorted by filename. + `artifact-catalog.json` entries are sorted by `artifactId`. +2. **Content-addressed**: `candidateId` is the sha256 of the concatenation of + all per-file digests (Zone bundle files plus artifact catalog) in canonical + sort order. `contentId` is the sha256 of canonical sorted resource content, + stable across runtime-only metadata mutations (`uid`, `generation`, + `revision`) applied after activation. +3. **Integrity-pinned**: `d2b-activation-helper` verifies every per-file digest + against `bundle.json` before staging, including `artifact-catalog.json`; + any mismatch fails activation closed. +4. **Hermetic**: same Nix inputs → byte-identical derivation output. No + timestamps, randomness, or network access inside the derivation. +5. **Offline**: all required Provider JSON Schemas are in Provider package + closures already in the Nix store at build time; no Provider dossier network + fetch occurs during derivation evaluation or build. +6. **D070-compliant**: store paths are excluded from all public ResourceSpecs, + status fields, audit records, and OTEL telemetry. The private + `artifact-catalog.json` (root:d2bd 0640) contains `storePath` fields that + are readable only by the Zone runtime and activation helper for staging. + + + +### Generation compilation + +Current source: `nixos-modules/bundle.nix` — monolithic bundle derivation, +`SHA256SUMS`, integrity chain. Current `d2b._bundle` attrset in +`bundle-artifacts.nix`. The v3 design replaces the monolithic bundle with +one content-addressed derivation per Zone. + +Each Nix evaluation produces one immutable configuration generation: a closed +set of Zone resource bundles plus the provider catalog. Each bundle is a +content-addressed derivation: + +``` +/nix/store/-d2b-config-zone-dev/ + zone.json — Zone self-resource + providers.json — Provider resources for this Zone + hosts.json — Host resources + guests.json — Guest resources + processes.json — Process and EphemeralProcess resources + volumes.json — Volume resources + networks.json — Network resources + devices.json — Device/User/Credential resources + roles.json — Role and RoleBinding resources + index.json — cross-resource index for this Zone + bundle.json — manifest of all files + content digests + +/nix/store/-d2b-artifact-catalog/ + artifact-catalog.json — private integrity-pinned catalog; storePath per entry for staging +``` + +### Activation path + +``` +Nix eval + → derivation build (hermetic, offline) + → /nix/store/-d2b-config-zone-/ (immutable) + → system activation (d2b-activation-helper) + 1. verify bundle.json digest chain; + 2. validate resource refs, owners, RBAC cross-checks; + 3. stage new generation into Zone runtime (not yet active); + 4. atomically swap active pointer; + 5. trigger configuration-publication controller handler; + 6. record prior generation pointer for rollback window. +``` + +Steps 1–3 are fail-closed. Step 4 is atomic. + +### Rollback + +```bash +d2b zone rollback dev --generation +``` + +Re-stages the prior generation's bundle and swaps the active pointer. The +configuration-publication controller reconciles affected resources. + +Generation retention is a per-Zone Nix compiler setting outside the Zone +ResourceSpec. It is declared at the Zone option level: + +```nix +d2b.zones.dev.retainedGenerations = 3; # default 3; range 1..16 +``` + +An eval assertion enforces `1 ≤ retainedGenerations ≤ 16`. The default is 3. +The minimum of 1 ensures at least one prior generation is always available for +rollback. The maximum of 16 is eval-enforced; values above 16 are rejected with +a structured error. `retainedGenerations` is consumed only during build and +runtime generation bookkeeping; it is never emitted into any Zone ResourceSpec +or bundle JSON. + +### Cross-Zone generation ordering + +When child Zone A selects parent Zone B with `A.parentZone` and has a local +uplink `ZoneLink`, neither bundle carries the other Zone's generation or a +cross-Zone `cursorRef`. +Advertisement cursors are runtime status owned by A's local ZoneLink handler +and the parent allocator/route engine. + +Zone activations are independent. Zone A activates without waiting for Zone B. +Until its parent allocation is established and advertisements are current, A's +local `ZoneLink` (plus dependent imports) is `Degraded`. It reconciles +asynchronously when the parent route becomes reachable and current. Neither +Zone claims a commit on behalf of the other, and there is no cross-Zone atomic +activation. + +### Resource cleanup contract + +#### Configuration-owned vs controller-created resources + +The configuration-publication controller classifies every resource in the Zone +runtime store using the core-set `managedBy` and `configurationGeneration` +fields set by the runtime at activation time: + +- **Configuration-owned** (`managedBy=configuration`): resources whose + `managedBy` field is `configuration`. The `configurationGeneration` field + records the generation index at which the resource was last reconciled by + config publication. The controller diffs `configurationGeneration` + name + against the new generation's bundle to identify absent resources. +- **Controller-managed** (`managedBy=controller`) and **API-managed** + (`managedBy=api`): resources set by runtime controllers or the resource API. + These are **never** touched by the configuration-publication controller. No + `ownerRef` inference, no label matching, and no "absent from emitted files" + logic is used to determine cleanup eligibility for these resources. + +The configuration-publication controller **only** enqueues for Delete the +resources that carry `managedBy=configuration` and whose `name`+`type` pair is +absent from the new generation's bundle. All other resources are untouched. + +#### Absent-resource deletion + +When a new configuration generation activates: + +1. The configuration-publication controller reads the new generation's bundle + `*.json` files to form the new configuration-owned name set. +2. Resources carrying `managedBy=configuration` whose name+type is absent from + the new set are enqueued for asynchronous `Delete`. +3. **Activation does not block on cleanup.** Step 4 of the activation path + (atomic pointer swap) completes before cleanup begins. +4. Each resource enqueued for Delete transitions to `status.phase: Pending` + with a `PendingDeletion` condition (`reason: AbsentFromConfiguration`). The + Zone's aggregate `status.phase` becomes `Degraded` until all pending deletes + complete. +5. Deletion is finalizer-safe: + - A resource with active finalizers receives a `DeletionBlocked` condition. + The finalizer-holding controller must remove its finalizer before deletion + proceeds; the cleanup controller waits and does not forcibly strip + finalizers. + - When a configuration-owned parent is enqueued for Delete, the parent's + controller is responsible for observing the parent's `PendingDeletion` + condition and reconciling owned children before clearing its finalizer. + The cleanup controller cascades only to resources that also carry + `managedBy=configuration`; controller-managed children of a deleted + configuration-owned parent are handled by the parent's controller. +6. When all finalizers are clear and reconciliation is complete, a single store + transaction appends the `Deleted` revision and removes the resource row and + its indexes. Subsequent `Get` returns not-found. The `ResourceDelete` audit + event is appended afterward with dedup/exactly-once recovery. + +#### Prior generation retention and pruning + +Prior generation bundles are retained according to `retainedGenerations` +(default 3, range 1..16). Pruning rules: + +- A generation is eligible for pruning when it has been superseded by at least + `retainedGenerations` newer activated generations. +- Pruning removes the generation's bundle pointer from the Zone runtime store. + It does not forcibly interrupt in-flight deletions from that generation. +- On `d2b zone rollback dev --generation N`: configuration-owned resources + absent from generation N that are undergoing cleanup are re-adopted into + generation N's configuration-owned set. Resources that have already reached + `Deleted` and been removed are re-created by the configuration-publication + controller reconciler. + +#### Status, errors, and audit + +| Field | Values | +| --- | --- | +| `Zone.status.phase` | `Ready` — all configuration-owned resources reconciled; `Degraded` — deletion pending or ZoneLink lagging; `Pending` — new generation staged and pointer swapped, reconciliation in progress | +| `Resource.status.phase` | `Pending` — awaiting deletion completion; `Deleted` — a single store transaction appends the `Deleted` revision and removes the resource row and its indexes; subsequent `Get` returns not-found; `ResourceDelete` audit event is appended afterward with dedup/exactly-once recovery | +| `Resource.status.conditions[PendingDeletion]` | Present when resource is enqueued for deletion; `reason: AbsentFromConfiguration` | +| `Resource.status.conditions[DeletionBlocked]` | Present when a finalizer prevents deletion completion | +| `Resource.status.conditions[ReconcileError]` | Present on reconciliation failure | + +Every absent-resource deletion initiated by a generation change emits a +structured audit event: + +```json +{ + "kind": "ResourceDelete", + "source": "ConfigurationPublicationController", + "zone": "", + "resourceType": "", + "resourceName": "", + "generationIndex": , + "configurationGeneration": , + "reason": "AbsentFromConfiguration" +} +``` + +#### Tests for removed-resource cleanup + +| Test | Tier | Description | +| --- | --- | --- | +| Two-generation bundle diff | nix-unit | Generation 1 declares resource R; generation 2 omits R. Verify R absent from generation 2 bundle `*.json`; generation 1 bundle retains R. | +| Async cleanup activation | Integration | Activate generation 1 (R present, `managedBy=configuration`, phase Ready). Activate generation 2 (R absent). Verify R enters Pending/PendingDeletion; Zone phase Degraded. Complete cleanup. Verify a single store transaction appends the `Deleted` revision and removes the resource row and its indexes; subsequent `Get` returns not-found; `ResourceDelete` audit event is appended afterward with dedup/exactly-once recovery; Zone phase Ready. | +| Audit record | Integration | After async cleanup: verify structured `ResourceDelete` event with correct zone/type/name/generationIndex/configurationGeneration/reason fields. | +| Finalizer-blocked deletion | Integration | R holds active finalizer. Activate generation 2 (R absent). Verify R enters DeletionBlocked condition. Remove finalizer. Verify deletion completes: a single store transaction appends the `Deleted` revision and removes the resource row and its indexes; subsequent `Get` returns not-found; `ResourceDelete` audit event is appended afterward with dedup/exactly-once recovery; Zone phase returns Ready. | +| Controller-managed preservation | Integration | A resource carrying `managedBy=controller` exists. Activate generation 2. Verify the config-publication controller never touches that resource, regardless of ownerRef or bundle absence. | +| API-managed preservation | Integration | A resource carrying `managedBy=api` exists. Activate generation 2. Verify the config-publication controller never enqueues it for deletion. | +| Rollback after partial cleanup | Integration | Activate generation 2 (R absent, cleanup in progress). Before cleanup completes, roll back to generation 1. Verify R is re-adopted or re-created and returns to Ready. | +| Retention window enforcement | Integration | Activate generations 1–5 with `retainedGenerations=3`. Verify generation 1 is pruned and no longer available for rollback after generation 4 activates. | + + + +Current source: `nixos-modules/index.nix` — `cfg._index.*` attribute tree +(`enabledEnvs`, `enabledVms`, `netMeta`, `declaredRealms`, `enabledRealms`, +`runtimeRows`). The `_index` is used by all other Nix emitters to avoid +repeated `filterAttrs` passes. + +The v3 normalized index is a single cross-Zone artifact emitted to +`/etc/d2b/index.json`: + +```json +{ + "schemaVersion": "v1", + "zones": { + "dev": { + "hosts": ["host-system"], + "guests": ["dev-vm"], + "networks": ["dev-lan"], + "providers": ["system-core", "system-systemd", "runtime-cloud-hypervisor"] + } + }, + "executionIndex": { + "Host/host-system": { + "zone": "dev", + "providerRef": "Provider/system-core", + "processes": ["wayland-proxy"] + }, + "Guest/dev-vm": { + "zone": "dev", + "providerRef": "Provider/runtime-cloud-hypervisor", + "processes": [] + } + }, + "networkIndex": { + "Network/dev-lan": { + "zone": "dev", + "lanSubnet": "10.100.0.0/24", + "attachedGuests": ["Guest/dev-vm"] + } + }, + "closureIndex": { + "Guest/dev-vm": { + "closureArtifact": "/etc/d2b/closures/dev-vm.json" + } + } +} +``` + +The index is derived; it is never edited directly. If it disagrees with the +resource bundle files the activation tool rejects the generation. The drift +gate enforces `xtask gen-index` + `git diff --exit-code`. + +## Bundle artifacts + +### `/etc/d2b/` layout + +| File | Owner | Description | +| --- | --- | --- | +| `provider-catalog.json` | `root:d2bd 0640` | Offline Provider catalog | +| `index.json` | `root:d2bd 0640` | Cross-Zone normalized index | +| `zones//bundle.json` | `root:d2bd 0640` | Zone bundle manifest + digest chain | +| `zones//zone.json` | `root:d2bd 0640` | Zone self-resource | +| `zones//providers.json` | `root:d2bd 0640` | Provider resources | +| `zones//hosts.json` | `root:d2bd 0640` | Host resources | +| `zones//guests.json` | `root:d2bd 0640` | Guest resources | +| `zones//processes.json` | `root:d2bd 0640` | Process/EphemeralProcess resources | +| `zones//volumes.json` | `root:d2bd 0640` | Volume resources | +| `zones//networks.json` | `root:d2bd 0640` | Network resources | +| `zones//devices.json` | `root:d2bd 0640` | Device/User/Credential resources | +| `zones//roles.json` | `root:d2bd 0640` | Role/RoleBinding resources | +| `closures/.json` | `root:d2bd 0640` | Per-Guest closure map | +| `minijail-profiles/.json` | `root:d2b-priv-broker 0640` | Minijail sandbox profiles | +| `privileges.json` | `root:d2bd 0640` | Broker op catalog (retained site-wide) | +| `realm-controllers.json` | `root:d2bd 0640` | Retained during migration; see ADR046-nix-008 | +| `realm-identity.json` | `root:d2bd 0640` | Retained during migration; see ADR046-nix-009 | + +### Bundle manifest format + +`zones//bundle.json`: + +```json +{ + "schemaVersion": "v1", + "candidateId": "", + "contentId": "", + "generationIndex": 1, + "files": [ + { "name": "zone.json", "digest": "sha256:..." }, + { "name": "providers.json", "digest": "sha256:..." }, + { "name": "hosts.json", "digest": "sha256:..." }, + { "name": "guests.json", "digest": "sha256:..." }, + { "name": "processes.json", "digest": "sha256:..." }, + { "name": "volumes.json", "digest": "sha256:..." }, + { "name": "networks.json", "digest": "sha256:..." }, + { "name": "devices.json", "digest": "sha256:..." }, + { "name": "roles.json", "digest": "sha256:..." } + ] +} +``` + +`candidateId`/`contentId` serve the same binding role as the current +`d2b._bundle` integrity chain (current source: `nixos-modules/bundle.nix`), +scoped per Zone. + +## Conflict detection + +The Nix compiler detects and rejects at eval time: + +| Conflict | Rule | +| --- | --- | +| Duplicate Zone name | Impossible by Nix construction: a Zone name is the unique `d2b.zones.` attr key; there is no Zone `id` field | +| Missing/extra Zone parent | Any non-root Zone omits `parentZone`, or `local-root` defines it | +| Conflicting Zone parent | Separate module definitions resolve one Zone's scalar `parentZone` to different values; normal Nix option merging rejects the conflict | +| Unknown/self Zone parent | `parentZone` does not resolve to a declared Zone or equals the child Zone | +| Zone topology cycle/depth | The compiler-only `parentZone` graph cycles or an ancestry path exceeds 16 Zone names | +| CIDR overlap | Two Networks in the same Zone with overlapping subnets | +| Guest name reserved prefix | A `Guest/` starting with `sys-` | +| Owner cycle | Any `ownerRef` chain that loops | +| Type collision | Two `d2b.zones..resources` entries with the same attribute key (Nix prevents this by construction) or two entries with different keys but emitting the same `/` pair (eval-checked for cross-type uniqueness) | +| Provider already installed | Two `d2b.zones..resources` entries of `type = "Provider"` with the same `catalogEntryId` | +| Catalog entry absent | A `catalogEntryId` not in `d2b.providerCatalog`, or an `artifactId`/`systemArtifactId` not in `d2b.artifacts` | +| Artifact type mismatch | An artifact ID used in a field that expects a different `type` (e.g., `"nixos-system"` where `"provider"` is required) | +| Duplicate artifact ID | Two `d2b.artifacts.` entries with the same key | +| Missing ref target | Any `*Ref` field whose target is not declared | +| Role verb unknown | A resource or session verb not in its canonical closed set; `relay` is session-only | +| Relay grant invalid | Relay is wildcard/unbounded, not ZoneLink-scoped, self-asserted, or lacks core/admin-policy provenance | + +## Current-code mapping + +Every current v3 baseline Nix source path and Rust symbol is mapped below to +its v3 destination. Sources are at `b5ddbed6`. No source is removed until the +destination is integrated and tested. + +### Nix modules + +| Current source | Current purpose | Current live? | v3 destination | Work item | +| --- | --- | --- | --- | --- | +| `nixos-modules/options-realms.nix` | `d2b.realms..*` — `RealmId`/`RealmPath`/`RealmControllerPlacement`/`EntrypointMode` options | Yes | `nixos-modules/options-zones.nix` (Zone/Host/Guest/Provider options) | ADR046-nix-001 | +| `nixos-modules/options-realms-workloads.nix` | `d2b.realms..workloads.*` — `WorkloadId`/`WorkloadProviderKind`/`IsolationPosture` options | Yes | `nixos-modules/options-zones-resources.nix` (Guest/Host/Process) | ADR046-nix-001 | +| `nixos-modules/options-realms-network.nix` | `d2b.realms..network.*` | Yes | Network resource spec in `options-zones-resources.nix` | ADR046-nix-001 | +| `nixos-modules/options-envs.nix` | `d2b.envs..*` — env isolation substrate | Yes | Network resource spec; Env concept retired | ADR046-nix-002 | +| `nixos-modules/options-vms.nix` | `d2b.vms..*` — per-VM options | Yes | Guest resource spec; per-VM toggles become Device resources | ADR046-nix-002 | +| `nixos-modules/options-daemon.nix` | `d2b.site.*` daemon options | Yes | `options-site.nix` (retained + extended) | ADR046-nix-003 | +| `nixos-modules/index.nix` | `cfg._index.*` — `netMeta`/`enabledVms`/`realmRows` | Yes | `index.nix` (rewritten); emits `/etc/d2b/index.json` | ADR046-nix-004 | +| `nixos-modules/bundle-artifacts.nix` | `d2b._bundle.*` internal artifact table | Yes | `bundle-zones.nix` (per-Zone) + `bundle-artifacts.nix` (helpers retained) | ADR046-nix-005 | +| `nixos-modules/bundle.nix` | Bundle derivation + SHA256SUMS + integrity chain | Yes | Rewritten per-Zone; `bundle-zones.nix` | ADR046-nix-005 | +| `nixos-modules/processes-json.nix` | `VmProcessDag`/`ProcessRole`/`binaryPath`/argv | Yes | `resources-zones-processes.nix`; emits `zones//processes.json` | ADR046-nix-006 | +| `nixos-modules/storage-json.nix` | `StorageJson`/`StoragePathSpec`/ownership/ACL contract rows | Yes | `resources-zones-volumes.nix`; rows migrate to Volume resources | ADR046-nix-007 | +| `nixos-modules/sync-json.nix` | `SyncJson` OFD lock contract rows | Yes | Internal to `d2b-contracts`; removed from Nix artifacts | ADR046-nix-007 | +| `nixos-modules/allocator-json.nix` | `AllocatorJson` — realm/env bridge assignments, socket paths | Yes | Privileged effects remain allocator-owned; parent edges come from compiler-only `parentZone`; Network/Host resources carry declarative intent | ADR046-nix-008 | +| `nixos-modules/realm-controller-config-json.nix` | `RealmControllersJson`/`realm-controllers.json` | Yes (read by live d2bd `realm_access_resolver`) | Runtime-created Zone self-resource + sealed `parentZone` allocator topology + child-local ZoneLink | ADR046-nix-008 | +| `nixos-modules/realm-workloads-launcher-v2-json.nix` | `RealmWorkloadsLauncherV2Json`/`realm-workloads-launcher-v2.json` | Yes | Provider/display-wayland + Provider/shell-terminal Process configs in `zones//processes.json` | ADR046-nix-009 | +| `nixos-modules/realm-identity-config-json.nix` | `RealmIdentityConfigJson`/`realm-identity.json` | Yes (read by live d2bd) | Credential resource specs; identity config inside Provider resources | ADR046-nix-009 | +| `nixos-modules/unsafe-local-workloads-json.nix` | `unsafe-local-workloads.json` | Yes | User-only Host + Process resources | ADR046-nix-010 | +| `nixos-modules/privileges-json.nix` | `privileges.json` broker op catalog | Yes | Retained at `/etc/d2b/privileges.json` | ADR046-nix-011 | +| `nixos-modules/closures-json.nix` | `closures/.json` per-VM closure maps | Yes | Retained at `/etc/d2b/closures/.json`; emitter rewritten | ADR046-nix-012 | +| `nixos-modules/minijail-profiles.nix` | `minijail-profiles/.json` | Yes | Retained at same path; emitter adapted to Zone Guest refs | ADR046-nix-012 | +| `nixos-modules/manifest.nix` | `manifest.json` `manifestVersion` contract | Yes | `zones//bundle.json`; `manifestVersion` → `schemaVersion` | ADR046-nix-013 | +| `nixos-modules/host-json.nix` | `host.json` host-side config | Yes | Folded into Host resource in `zones//hosts.json` | ADR046-nix-013 | +| `nixos-modules/assertions.nix` | Eval-time invariants (CIDR, platform, VM names) | Yes | Retained and extended | ADR046-nix-014 | +| `nixos-modules/host.nix`, `host-daemon.nix`, `host-activation.nix`, `host-users.nix` | Host NixOS modules, `d2bd`/`d2b-priv-broker` units, activation helper, users/groups | Yes | Retained and adapted to Zone bundle activation | ADR046-nix-015 | +| `nixos-modules/network.nix`, `net.nix` | Bridge/NAT/DHCP systemd-networkd units | Yes | Reconciled by `Provider/network-local`; retained until Provider successor | ADR046-nix-016 | +| `nixos-modules/store.nix` | Per-VM `/nix/store` hardlink farm; `share.source == "/nix/store"` sentinel | Yes | Reconciled by `Provider/volume-virtiofs`; retained until Provider successor | ADR046-nix-017 | +| `nixos-modules/components/` (graphics, tpm, usbip, audio) | Per-VM toggleable features | Yes | Each becomes a Provider install resource + Device/Guest spec | ADR046-nix-018 | + +### Rust symbols + +The following current Rust symbols need explicit v3 destination assignments +because their names carry old terminology. Each is a current-live path unless +marked compile-only. + +| Current symbol | Current crate/file | Current live? | v3 destination | Work item | +| --- | --- | --- | --- | --- | +| `RealmId`, `RealmPath` | `d2b-realm-core/src/ids.rs`, `realm.rs` | Live in d2bd | `ZoneId` (`^[a-z][a-z0-9-]*$`), `ZonePath` in `d2b-contracts/src/v3/identity.rs` | ADR046-identities-001 | +| `WorkloadId` | `d2b-realm-core/src/ids.rs` | Live | `ResourceName` + `ResourceRef` for `Guest/` or `Process/` | ADR046-identities-001 | +| `NodeId`, `NodeSummary`, `NodeKind` | `d2b-realm-core/src/node.rs` | Live in metadata | `Host` / `Guest` ResourceType (see NodeKind table) | ADR046-identities-001 | +| `ProviderId` | `d2b-realm-core/src/ids.rs` | Live | `ResourceName` for `Provider/` | ADR046-identities-001 | +| `RealmTarget` / `WorkloadTarget` (`..d2b`) | `d2b-realm-core/src/target.rs`, `d2b-core/src/workload_identity.rs` | Live in resolver | `Zone/` + `Guest/` ResourceRef | ADR046-identities-001 | +| `RealmControllerPlacement` (`HostLocal`, `GatewayVm`, `CloudFullHost`, `ProviderController`, `ProviderAgent`) | `d2b-realm-core/src/realm.rs` | Metadata only | `Host.providerRef` + `Guest.providerRef` per NodeKind table | ADR046-nix-001 | +| `EntrypointMode` (`HostResident`, `GatewayBacked`) | `d2b-realm-core/src/realm.rs` | Metadata only | `Host` vs `Guest` ExecutionPolicy distinction | ADR046-nix-001 | +| `VmProcessDag`, `ProcessNode`, `ProcessRole` | `d2b-core/src/processes.rs` | Live (processes.json consumed by broker) | `Process`/`EphemeralProcess` per disposition table | ADR046-nix-006 | +| `ProcessesJson`, `ProcessRole.CloudHypervisorRunner` | `d2b-core/src/processes.rs`, `nixos-modules/processes-json.nix` | Live | `Process` under `Provider/runtime-cloud-hypervisor` owned by `Guest` | ADR046-nix-006 | +| `ProcessRole.Virtiofsd` + `share.source == "/nix/store"` sentinel | `d2b-core/src/processes.rs`, `nixos-modules/processes-json.nix` | Live | `Process` under `Provider/volume-virtiofs` + Volume nix-closure source | ADR046-nix-006 | +| `StorageJson`, `StoragePathSpec`, `PrincipalRef { kind: "uid"|"user" }` | `d2b-core/src/storage.rs`, `nixos-modules/storage-json.nix` | Live (storage.json consumed by broker) | `Volume` layout/views; `PrincipalRef` uid-kind → `User/` ref | ADR046-nix-007 | +| `SyncJson`, OFD lock rows | `d2b-core/src/sync.rs`, `nixos-modules/sync-json.nix` | Live | Internal `d2b-contracts` implementation mechanism; removed from Nix artifacts | ADR046-nix-007 | +| `RealmControllersJson`, `RealmControllerMetadataSummary` | `d2b-core/src/realm_controller_config.rs`; read live by d2bd `realm_access_resolver` | Live | Runtime Zone self-resource + compiler-only `parentZone` allocator topology + child-local ZoneLink transport/route state; `realm-controllers.json` retained during migration | ADR046-nix-008 | +| `RealmWorkloadsLauncherV2Json`, `LauncherWorkloadSummary` | `d2b-core/src/realm_workloads_launcher.rs` | Live | Process resource annotations in `zones//processes.json` | ADR046-nix-009 | +| `RealmIdentityConfigJson` | `d2b-realm-core/src/identity_config.rs`; loaded live by d2bd | Live | Credential resource `scope`/`audience`/`allowedOperations`/`spec.provider.settings` fields; `realm-identity.json` retained during migration | ADR046-nix-009 | +| `WorkloadProviderKind`, `IsolationPosture`, `WorkloadExecutionPosture` | `d2b-realm-core/src/workload.rs` | Live in launcher metadata | `LocalVm`/`QemuMedia`/`ProviderManaged` → `Guest.providerRef` per table; `UnsafeLocal` → user-only `Host` with `spec.isolationPosture: "none"` (never `Guest`; not a v3 Provider) | ADR046-nix-001 | +| `Capability` enum, `CapabilitySet` | `d2b-realm-core/src/capability.rs` | Live in provider advertisement | Role verbs / Provider descriptor fields per Capability disposition table | ADR046-nix-001 | +| `RuntimeProvider`, `WorkloadProvider`, `HostSubstrateProvider` traits | `d2b-realm-provider/src/provider.rs` | Live (ACA/local-vm implement these) | Provider component descriptors + `ADR-046-provider-.md` dossiers | ADR046-provider-001 | +| `OperationRouter`, `DurableExecTable`, `TargetResolver` | `d2b-realm-router/src/`; `d2bd/src/realm_stubs.rs` | COMPILE-ONLY at baseline (`dead_code`-allowed, not called) | `d2b-bus` routing; adapt `RealmSessionAuthority`/`CredentialCustody`/`RealmServiceLimits` from `main:packages/d2b-realm-router/src/service_v2.rs` | ADR046-nix-033 | +| `TransportProvider`, `loopback`, `LocalTcpTransport` | `d2b-realm-transport/src/lib.rs`, `local_tcp.rs` | COMPILE-ONLY / conformance tests only | `Provider/transport-unix` (Unix seqpacket); `Provider/transport-vsock`; transport primitives adapt `main:packages/d2b-session-unix/src/` | ADR046-nix-026 | + +### Systemd unit mapping + +No current unit is removed until the resource/Provider successor is integrated. + +| Current unit | Current role | v3 treatment | +| --- | --- | --- | +| `d2bd.service` | PID1 local-root controller | Retained; becomes fixed Zone core controller launcher | +| `d2bd.socket` | Local-root public socket | Retained unchanged | +| `d2b-priv-broker.service` | Privileged broker | Retained unchanged | +| `d2b-priv-broker.socket` | Broker socket activation | Retained unchanged | +| Per-env bridge units (`br--lan`, `br--up`) | Network bridge, systemd-networkd | Owned by `Provider/network-local` Process resources after migration | +| `sys--net` VM unit | Auto-declared net VM | Replaced by `Network` + `Guest/sys--net` under `Provider/network-local` | +| Per-VM store-sync | Store hardlink farm sync | Replaced by `EphemeralProcess` under `Provider/volume-virtiofs` | +| Per-VM swtpm | TPM state (`ProcessRole::Swtpm`) | Replaced by `Process` under `Provider/device-tpm` | +| Per-VM GPU sidecar | Cloud Hypervisor VMM (`ProcessRole::CloudHypervisorRunner`) | Replaced by `Process` under `Provider/runtime-cloud-hypervisor` | +| `d2b@` | Legacy per-workload template | Not created in v3 | + +## Prohibited fields summary + +Never accepted in any Nix-authored resource spec or generated artifact: + +- Credential/secret bytes, token values, PSK material, key PEM, passwords; +- Freeform host paths outside closure-backed derivation outputs; +- Raw numeric UID or GID in spec fields; +- Ambient capability bitmasks or named capability lists in Process spec; +- Raw seccomp BPF programs (only named Provider-owned profile refs); +- Arbitrary socket addresses or raw file descriptor numbers; +- Provider-internal `argv` or environment variable maps; +- `packageRef`, renamed `network`/`devices`/`restart` aliases, or inline + Process `endpoints` (use owner-resolved `template`, canonical + `networkUsage`/`deviceUsage`/`restartPolicy`, and separate Endpoint resources); +- `eval` or `builtins.exec` in Nix resource compiler expressions; +- `RealmTarget` format strings (`..d2b`) in any spec field. + +## Feasibility proof required + +| Proof | Description | +| --- | --- | +| Zone self-resource round-trip | Nix Zone declaration → runtime-created Zone self-resource → resource API GET returns `spec = {}` with no compiler-only settings | +| Provider install resource | Nix catalog + Provider resource → core controller lifecycle → Ready status | +| Host system/user Process | Process under Host/host-system with system-systemd; locally held pidfd | +| Guest with closure Volume | Guest + virtiofs Volume → per-VM store farm → guest `/nix/store` without direct host store export | +| Cross-Zone ZoneLink | Child Zone's `parentZone` selects one allocator owner; its local uplink has self-matching `childZoneName` and local `transportProviderRef`; the selected allocator binds the private edge and owns privileged route creation; no reciprocal resource, `parentRef`, or emitted `parentZone` | +| Configuration rollback | Two-generation rollback restores prior Zone resource state | +| Ref validation rejection | Malformed or missing ref fails eval with structured error | +| Conflict detection | `parentZone` missing/unknown/self/cycle/depth, CIDR overlap, owner cycle, and duplicate type/name rejection at eval time | +| ProcessRole parity | Every `ProcessRole` variant has a corresponding test case in the Process/EphemeralProcess resource schema | +| Unsafe-local Host | User-only `Host` with `spec.isolationPosture: "none"` reconciled by `Provider/system-core`; child Processes use normal Process Providers; `NoIsolation` condition present in Host status; `no_isolation=true` in `ProcessEffect` audit events; OTEL never labels isolation; CLI/UI warning non-suppressible; no `Guest` emitted | +| ResourceTypeSchema validation | Every emitted `spec` validates against committed JSON Schema at build time; schema drift gate passes; unknown field in `spec.provider.settings` (where schema declares it) or Provider `config` fails build | +| Credential ref enforcement | PEM header in spec field fails eval; `Credential/` ref accepted; no secret bytes in any emitted artifact | +| Bundle integrity | Byte-identical rebuild from identical inputs; `candidateId`/`contentId` match computed values; file digest mismatch fails activation | +| Absent-resource async Delete | Generation 1 has resource R (`managedBy=configuration`); generation 2 omits R; R enters Pending/PendingDeletion; Zone Degraded; a single store transaction appends the `Deleted` revision and removes the resource row and its indexes; subsequent `Get` returns not-found; `ResourceDelete` audit event is appended afterward with dedup/exactly-once recovery; Zone Ready | +| Controller-managed preserved | Resource with `managedBy=controller` untouched by config-publication controller; never enqueued for deletion regardless of bundle absence | +| Finalizer-safe deletion | Resource with active finalizer enters DeletionBlocked condition; stays Pending; deletion completes after finalizer removed: a single store transaction appends the `Deleted` revision and removes the resource row and its indexes; subsequent `Get` returns not-found; `ResourceDelete` audit event is appended afterward with dedup/exactly-once recovery | +| Provider crate layout gate | Stub `d2b-provider-test-missing-integration/` missing `integration/` fails `make test-policy`; complete stub with all four paths passes | +| Artifact catalog resolution | Declare `d2b.artifacts.dev-vm-system = { package = …; type = "nixos-system"; }`; build succeeds; `artifact-catalog.json` contains matching entry with correct type/digests/storePath; `storePath` absent from all public ResourceSpecs, status, audit, OTEL; reference with wrong type fails build; absent artifact ID fails build | + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `nixos-modules/options-realms*.nix`; `options-vms.nix`; `options-envs.nix`; `index.nix`; `bundle*.nix`; `*-json.nix`; `d2b-realm-core` ids/realm/workload/capability/allocation; `d2b-core` processes/storage/workload-identity; live `realm_access_resolver`; live `realm-controllers.json`/`realm-identity.json` | +| Evidence class | Nix schemas and generated artifacts are live/reachable; `d2b-realm-router`/`d2b-realm-transport` are compile-only stubs at baseline; Zone/Host/Guest/ResourceRef/Provider resources are ADR-only | +| Behavior retained | Hermetic deterministic eval, offline derivations, integrity-pinned artifacts, no secrets in generated JSON, CIDR/platform/name assertions, fine-grained storage ownership/ACL/no-follow, minijail profiles, pidfd/adoption | +| Required delta | Zone/Resource option schema, per-Zone bundle layout, normalized index rewrite, configuration-publication controller hook, ZoneLink cursor, rollback activation path, Host ResourceType (new), ProcessRole disposition implementation | +| Reuse path | Extract pure schema validators, CIDR/name assertion logic, closure-info pattern, and index helpers per work item; `d2b-realm-core` ID validators adapted to `ZoneId`/`ResourceName`; main `a1cc0b2d` ComponentSession/Provider implementation adapted per ADR046-bus-* items | +| Replacement/deletion | No current options or artifact emitters are removed until the named destination is integrated and all tests pass; `realm-controllers.json` and `realm-identity.json` retained during migration | +| Feasibility proof | Items in the proof table above | +| Future owner | Work items below; Provider dossiers; `ADR-046-core-controllers.md` configuration-publication handler | + +## Main-commit ComponentSession and Provider reuse inventory + +Exact reuse sources are at `main:a1cc0b2da4a08ca3240a770a972fe4da6f912bef` (W9 +commit "coordinate toolkit and sibling cutover"). None of these paths are +implemented on the v3 baseline `b5ddbed6`; they are reuse sources, not current +evidence. Every item specifies the exact main-commit file/symbol/tests, the +selected behavior, the v3 destination, and the ADR45 assumptions that must be +excluded or adapted before integration. + +### Reuse summary + +| Main package | Key files/symbols | v3 destination | Work item | +| --- | --- | --- | --- | +| `d2b-session` | `engine.rs`, `handshake.rs`, `lifecycle.rs`, `scheduler.rs`, `record.rs`, `fragmentation.rs`, `transport.rs`, `driver.rs`, `server.rs`, `metrics.rs` | `packages/d2b-bus/src/session/` | ADR046-nix-023 | +| `d2b-session` | `attachment.rs`, `streams.rs`, `cancellation.rs`, `deadline.rs`, `bootstrap.rs` | `packages/d2b-bus/src/session/` | ADR046-nix-024, ADR046-nix-025 | +| `d2b-contracts` | `v2_component_session.rs` — all wire constants + types | `packages/d2b-contracts/src/v3/component_session.rs` | ADR046-nix-027 | +| `d2b-contracts` | `generated_v2_services/` (24 service + 24 ttrpc files) | `packages/d2b-contracts/src/v3/services/` | ADR046-nix-028 | +| `d2b-session-unix` | `adapter.rs`, `socket.rs`, `descriptor.rs`, `credit.rs`, `pidfd.rs`, `systemd.rs`, `vsock.rs` | `packages/d2b-bus/src/transport/unix/` | ADR046-nix-026 | +| `d2b-provider` | `registry.rs`, `rpc.rs`, `instance.rs`, `context.rs` | `packages/d2b-provider/src/` (adapt in place) | ADR046-nix-029 | +| `d2b-provider-toolkit` | `adapter.rs`, `server.rs`, `conformance.rs`, `registration.rs` | `packages/d2b-provider-toolkit/src/` (adapt in place) | ADR046-nix-030 | +| `d2b-client` | `client.rs`, `session.rs`, `target.rs`, `service.rs`, `daemon_service.rs`, `guest_service.rs`, `host_socket.rs` | `packages/d2b-client/src/` (adapt in place) | ADR046-nix-032 | +| `d2b-realm-router` | `service_v2.rs` | `packages/d2b-bus/src/routing/zone_service.rs` | ADR046-nix-033 | +| `d2bd` | `provider_registry.rs` | `packages/d2bd/src/provider_registry.rs` (adapt in place) | ADR046-nix-034 | +| `d2bd` | `provider_effects.rs` | `packages/d2bd/src/provider_effects.rs` (adapt in place) | ADR046-nix-035 | + +**ComponentSession wire contract — cross-reference only:** The choices for +`EndpointRole`, `ServicePackage`, `EndpointPurpose`, `PurposeClass`, and +`Locality` variant naming (including any rename of `RealmController`, +`RealmBroker`, `RealmV2`, `RealmPeer`, `RealmBootstrap` to Zone-prefixed +names), the exact Zone service name replacing `"d2b.realm.v2.RealmService"`, +the v3 `TargetInput` variant shape for Zone/Resource addressing, the v3 +`PROVIDER_BUNDLE_VERSION` and `PROVIDER_BUNDLE_SCHEMA_VERSION`, and the v3 +wire operation replacing `VmLifecycleRequest` — are all owned by the +ComponentSession/d2b-bus foundation spec and the provider-registry contract +work item. These are not defined here. + +Invariants that apply regardless of naming decisions: wire tag values (numeric) +are stable and must not change regardless of string/identifier renames. Zone +service target is addressed as `ResourceRef/Zone`; Guest lifecycle operations +address resources as `ResourceRef` (`Guest/`) with a Zone context +(`Zone/`). Bundle/schema version constants are generated by the owning +contract work item (ADR046-nix-034/ADR046-nix-035). Cross-reference: +`ADR-046-componentsession-and-bus` and the d2b-contracts v3 work item. + +## Implementation work items + +### ADR046-nix-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0; `d2b-contracts` identities (ADR046-identities-001, ADR046-identities-002) | +| Current source | `nixos-modules/options-realms.nix` (`RealmId`/`RealmPath`/`RealmControllerPlacement`/`EntrypointMode` labels); `options-realms-workloads.nix` (`WorkloadId`/`WorkloadProviderKind`/`IsolationPosture`/`WorkloadExecutionPosture`); `options-realms-network.nix`; `d2b-realm-core/src/realm.rs`, `workload.rs`, `capability.rs`, `ids.rs` (symbols to adapt) | +| Reuse action | adapt | +| Destination | `nixos-modules/options-zones.nix` (Zone-level options: `label`, `parentZone`, `retainedGenerations`, `trustedPublishers` — compiler settings, not Zone spec fields); `nixos-modules/options-zones-resources.nix` (unified `resources` attrset) | +| Detailed design | `d2b.zones..resources. = { type = ""; spec = { ... }; }` — single attrset covering all ResourceTypes; `type` discriminates dispatch; `spec` fields mirror exact ResourceTypeSchema field names and nesting; Nix option types/defaults/docs generated from `docs/reference/schemas/v3/.json`; no Nix-only fields inside resource declarations; `metadata.name` derives from attr key; `metadata.zone` derives from enclosing zone attr key; `apiVersion` defaulted; `uid`/`generation`/`revision`/`status`/`managedBy` never in Nix; `resource_name` regex `^[a-z][a-z0-9-]*$`; ref validation assertions; `WorkloadProviderKind` → Guest/Host mapping per disposition table above; `Capability` → Role verb mapping per resource-api/authz foundation spec; Zone self-resource spec is `{}`; `parentZone` is a required non-root/forbidden-root compiler-only plain Zone name compiled into sealed allocator topology; `retainedGenerations`/`trustedPublishers` are likewise Zone-level compiler settings not emitted in Zone spec | +| Integration | `nixos-modules/default.nix` imports new options files; old realms options coexist until ADR046-nix-002 | +| Data migration | Operator configs migrate `d2b.realms.*` → `d2b.zones.*`; `d2b.vms.*` → `d2b.zones..resources.*` with `type = "Guest"` | +| Validation | nix-unit vectors for each ResourceType; ref-validation rejection vectors; malformed ref error shape; resource/session verb closed-enum vectors accept `relay` only in `sessionVerbs`; relay wildcard/unbounded/Provider-self-asserted fixtures fail before activation while an exact core-generated ZoneLink fixture passes; Endpoint visibility accepts exactly `owner|provider|zone` and uses `consumerPolicy` for finer bounds; `parentZone` is missing on non-root/forbidden on local-root/unknown/self/cyclic/over-depth rejection; scalar module conflicts prove one parent; child-local ZoneLink `childZoneName` must equal its enclosing Zone key; a second uplink resource (even disabled) and a local-root uplink fail eval; missing/local-unresolved `transportProviderRef` fails eval; `managedBy` in spec rejected at eval; Zone spec is `{}` (no `parentZone`, `parentRef`, `retainedGenerations`, etc.) | +| Tests | `tests/unit/nix/cases/zones-options.nix`, `tests/unit/nix/cases/zones-ref-validation.nix`, `tests/unit/nix/cases/zones-zonelink.nix` | +| Drift pin | `make nix-unit-pin` after adding cases | +| Removal proof | `options-realms*.nix` removed after `options-zones*.nix` achieves parity and parity drift test passes | + +### ADR046-nix-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-001; env/VM migration | +| Current source | `nixos-modules/options-envs.nix` (`lanSubnet`/`uplinkSubnet`/`mtu`/`mssClamp`/`externalNetwork.*`); `nixos-modules/options-vms.nix` (`d2b.vms..*`; `ProcessRole` toggle options for components) | +| Reuse action | adapt | +| Destination | `Network` resource fields in `nixos-modules/options-zones-resources.nix`; `Guest` resource fields | +| Detailed design | `d2b.envs.work.lanSubnet` → `d2b.zones.work.resources.work-lan = { type = "Network"; spec = { lanSubnet = "..."; ... }; }`; CIDR overlap assertion migrated; `sys-` reserved prefix and VM-name regex retained; `d2b.vms..tpm.enable` → `d2b.zones..resources.vm-tpm = { type = "Device"; spec = { providerRef = "Provider/device-tpm"; ... }; }` | +| Integration | `options-zones-resources.nix` emits Network/Guest resources consumed by the rewritten index/bundle and Provider/network-local/device Providers; assertions run during eval before bundle emission. | +| Data migration | Operator configs migrate env/VM component options into Zone Network/Guest/Device resources; full d2b 3.0 reset means no v2 runtime state/config import beyond rewritten Nix declarations. | +| Validation | nix-unit CIDR rejection; eval assertion for `sys-` prefix; VM-name regex | +| Tests | `tests/unit/nix/cases/zones-network.nix`, `tests/assertions-eval.sh` extended | +| Drift pin | `make nix-unit-pin` | +| Removal proof | `options-envs.nix`, `options-vms.nix` removed after migration parity test passes | + +### ADR046-nix-003 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-001; site options | +| Current source | `nixos-modules/options-daemon.nix`, `options-site.nix`, `options-host.nix` | +| Reuse action | adapt | +| Destination | `nixos-modules/options-site.nix` (retained); per-Zone options in `options-zones.nix` | +| Detailed design | `d2b.zones..retainedGenerations` (default 3, range 1..16, compiler setting — not emitted in Zone spec); `d2b.site.stateDir` maps to Zone storage roots; `d2b.site.usePrebuiltHostTools` retained Primary reuse disposition: `adapt`. Preserved source-plan detail: retain and extend. | +| Integration | `options-site.nix` retains site defaults consumed by `options-zones.nix` and `bundle-zones.nix`; activation-helper and Zone storage roots read `stateDir`/`usePrebuiltHostTools` settings. | +| Data migration | Operator site options stay in place; new per-Zone `retainedGenerations` defaults apply on first v3 activation; no v2 runtime state import. | +| Validation | nix-unit case verifies `retainedGenerations` default/range, `site.stateDir` mapping, and `usePrebuiltHostTools` retention. | +| Tests | `tests/unit/nix/cases/site-options.nix` | +| Drift pin | `make nix-unit-pin` | +| Removal proof | No removal; file extended only | + +### ADR046-nix-004 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-001, ADR046-nix-002 | +| Current source | `nixos-modules/index.nix` — `cfg._index.*`: `enabledEnvs`, `enabledVms`, `netMeta` (derives bridge names/IPs from `lanSubnet`/`uplinkSubnet`), `declaredRealms`, `enabledRealms`, `workloadsInEnv`, `runtimeRows` | +| Reuse action | adapt | +| Destination | `nixos-modules/index.nix` (rewritten); emits `/etc/d2b/index.json` | +| Detailed design | Cross-Zone normalized index: zone/host/guest/network/closure entries; executionIndex; networkIndex; closureIndex; sorted output; `cfg._index` attribute tree retained as internal helper during migration Primary reuse disposition: `adapt`. Preserved source-plan detail: rewrite. | +| Integration | `options-zones-resources.nix` feeds rewritten `index.nix`; `bundle-zones.nix`, resource emitters, and activation-helper consume `/etc/d2b/index.json`. | +| Data migration | Index is rebuilt from v3 Zone resources at activation; full d2b 3.0 reset; no v2 index state import. | +| Validation | nix-unit golden vectors for index shape; drift gate: `xtask gen-index` round-trip | +| Tests | `tests/unit/nix/cases/index-zones.nix`; `tests/unit/gates/drift-check.sh` extended | +| Drift pin | `make nix-unit-pin`; `make flake-matrix-pin` if flake checks change | +| Removal proof | `cfg._index.envMeta`, `cfg._index.realms.*` sub-trees removed after all callers migrate to Zone resource lookups | + +### ADR046-nix-005 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-004 | +| Current source | `nixos-modules/bundle-artifacts.nix` (`artifactModule` submodule, `installFileName`, `mode 0640` ownership); `nixos-modules/bundle.nix` (bundle derivation, SHA256SUMS, `d2b._bundle` integrity chain) | +| Reuse action | adapt | +| Destination | `nixos-modules/bundle-zones.nix` (per-Zone bundle derivation); common helpers retained in `bundle-artifacts.nix` | +| Detailed design | Per-Zone `bundle.json` with `candidateId`/`contentId` binding; SHA256 digest chain; `generationIndex`; atomic activation pointer; `manifestVersion` → `schemaVersion` rename Primary reuse disposition: `adapt`. Preserved source-plan detail: extend and rewrite. | +| Integration | `d2b-activation-helper` reads `bundle.json` per Zone; validates digest chain before staging | +| Data migration | Per-Zone bundles are generated from v3 resource declarations; full d2b 3.0 reset; no v2 monolithic bundle state import. | +| Validation | Artifact-shape contract tests in `packages/d2b-contract-tests/tests/`; determinism test (build twice, diff outputs) | +| Tests | `tests/unit/nix/cases/bundle-zones.nix`; `tests/unit/gates/drift-check.sh` for schema drift | +| Drift pin | `make test-drift` | +| Removal proof | Monolithic `bundle.json` and `d2b._bundle` artifact table retired after all Zone bundle tests pass | + +### ADR046-nix-006 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-005; Process Provider work items (ADR046-primitives-002) | +| Current source | `nixos-modules/processes-json.nix` (`VmProcessDag`, `ProcessRole`, `binaryPath`, `argv`, `share.source == "/nix/store"` sentinel); `packages/d2b-core/src/processes.rs` (`ProcessRole` enum — all variants in disposition table above) | +| Reuse action | adapt | +| Destination | `nixos-modules/resources-zones-processes.nix`; emits `zones//processes.json` | +| Detailed design | Process/EphemeralProcess resource serialization per disposition table and the frozen `ExecutionSpec`: exact `providerRef`/`executionRef`/domain/user/processClass/template/config/credential/mount/sandbox/budget/`networkUsage`/`deviceUsage`/telemetry names; semantic owner from `metadata.ownerRef` with template fallback through the Process Provider; no `packageRef`, free-form `binaryPath`, `argv`, renamed network/device fields, or inline endpoints; stable produced endpoints are separate owned `Endpoint` resources with `producerRef`; Process uses canonical `restartPolicy` fields; EphemeralProcess uses `runtimeDeadline` and has no restart policy; VsockRelay → `Process` under `Provider/transport-vsock`; GuestSshReadiness retired at v3 cutover; Usbip long-lived backend/proxy → `Process`, Usbip per-busid attach/detach → `EphemeralProcess`, all owned by `Provider/device-usbip` Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | `processes.json` replaces `cfg._bundle.processesJson`; Process Providers read the new format | +| Data migration | Processes are emitted from v3 Process/EphemeralProcess resources; full d2b 3.0 reset; no v2 process artifact import. | +| Validation | Process and EphemeralProcess exact-schema vectors; reject `packageRef`, `network`, `devices`, `endpoints`, `restart`, and `runDeadline`; accept canonical `networkUsage`, `deviceUsage`, `restartPolicy`, `runtimeDeadline`, and separately owned Endpoint with `producerRef`; no-raw-path assertion; ProcessRole parity test (every variant has a test case) | +| Tests | `tests/unit/nix/cases/zones-processes.nix`; `tests/unit/nix/cases/zones-process-exact-mirror.nix`; `packages/d2b-contract-tests/tests/processes-schema.rs` | +| Drift pin | `make test-drift` after schema changes | +| Removal proof | `processes-json.nix` and current `processes.json` schema removed after all Process Providers consume `zones//processes.json` | + +### ADR046-nix-007 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-005; Volume Provider work items (ADR046-primitives-003) | +| Current source | `nixos-modules/storage-json.nix` (all `mkPath` calls, `PrincipalRef` `uid`/`gid`/`user` kinds, `StorageRoot`/`StoragePathSpec`/`repairPolicy`/`cleanupPolicy`); `packages/d2b-core/src/storage.rs` (`StorageJson`, `StoragePathSpec`); `nixos-modules/sync-json.nix` (`SyncJson` OFD lock rows); `packages/d2b-core/src/sync.rs` | +| Reuse action | adapt | +| Destination | `nixos-modules/resources-zones-volumes.nix`; emits `zones//volumes.json`; OFD lock rows move to `d2b-contracts` internals | +| Detailed design | Volume layout/views/ACL/no-follow/repair preserving current policy; `PrincipalRef { kind: "uid" }` → `User/` typed ref only; OFD rows removed from Nix artifacts Primary reuse disposition: `adapt`. Preserved source-plan detail: extract storage policy → adapt; retire sync rows. | +| Integration | Volume resources feed `zones//volumes.json`; the Volume Provider consumes it for path/view/ACL reconciliation; `d2b-contracts` internals own OFD lock rows. | +| Data migration | Volume resources are regenerated from v3 config; full d2b 3.0 reset; no v2 storage/sync state import. | +| Validation | Volume schema vectors; ACL/no-follow/view policy tests | +| Tests | `tests/unit/nix/cases/zones-volumes.nix`; `packages/d2b-contract-tests/tests/volumes-schema.rs` | +| Drift pin | `make test-drift` | +| Removal proof | `storage-json.nix`, `sync-json.nix`, and `/etc/d2b/storage.json`/`sync.json` removed after Volume controller parity | + +### ADR046-nix-008 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-004; Zone/Network migration | +| Current source | `nixos-modules/allocator-json.nix` (realm/env bridge assignments, `allocatorStateDir`, socket paths, `providerPlacement`); `nixos-modules/realm-controller-config-json.nix` (emits `realm-controllers.json`); `packages/d2b-core/src/realm_controller_config.rs` (`RealmControllersJson`, `RealmControllerMetadataSummary`; read live by `d2bd/src/realm_access_resolver.rs` from `/etc/d2b/realm-controllers.json`) | +| Reuse action | adapt | +| Destination | Compiler-only `parentZone` map in `nixos-modules/options-zones.nix`; local-root allocator bootstrap compiler/sealer; runtime-created Zone self-resource with `spec = {}`; child-local ZoneLink resource; `realm-controllers.json` RETAINED during migration (live d2bd reads it) | +| Detailed design | Resolve each non-root `parentZone` to one declared Zone; reject a root value, missing/unknown/self/conflicting/cyclic parent, and ancestry deeper than 16 names. Canonicalize sorted child→parent rows and feed them to the local-root allocator's private bootstrap sealer. Parent allocators derive/pre-bind privileged socket/listener state from that topology; no socket path or `parentZone` enters `Zone.spec` or a ResourceRef. Child-local ZoneLinks provide transport/local route state. `realm-controllers.json` remains published until `realm_access_resolver` is replaced. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt and retire. | +| Integration | Nix Zone options → validated topology compiler → local-root allocator bootstrap sealer; ZoneLink controller consumes only its sealed selected-parent binding. During migration, `realm-controllers.json` remains available to the legacy resolver until the Zone topology/ZoneLink path takes over. | +| Data migration | Topology is regenerated from `d2b.zones..parentZone`; full d2b 3.0 reset; no allocator/realm-controller state import. | +| Validation | Zone self-resource round-trip with exact empty spec; `parentZone` required/root-forbidden/existence/one-parent/self/cycle/16-name-depth vectors; topology seal excludes `parentZone` from Zone/resource bundles; bootstrap socket/listener regression tests | +| Tests | `tests/unit/nix/cases/zones-bootstrap.nix`; `realm_access_resolver` contract test | +| Drift pin | `make nix-unit-pin` | +| Removal proof | Legacy `allocator-json.nix`, `realm-controller-config-json.nix`, and `/etc/d2b/allocator.json`/`realm-controllers.json` removed ONLY after the sealed `parentZone` topology and ZoneLink resolver are live; the allocator bootstrap mechanism itself remains | + +### ADR046-nix-009 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-006; display/credential Provider work items | +| Current source | `nixos-modules/realm-workloads-launcher-v2-json.nix` (`RealmWorkloadsLauncherV2Json`, `LauncherWorkloadSummary`; live); `nixos-modules/realm-identity-config-json.nix` (`RealmIdentityConfigJson`; live, read by d2bd from `/etc/d2b/realm-identity.json`); `packages/d2b-core/src/realm_workloads_launcher.rs`; `packages/d2b-realm-core/src/identity_config.rs` | +| Reuse action | adapt | +| Destination | Provider/display-wayland and Provider/shell-terminal Process configs in `zones//processes.json`; `Provider/credential-entra` Credential resource; `realm-identity.json` RETAINED during migration | +| Detailed design | Launcher metadata folded into Process resource annotations; identity config → Credential resource fields (`providerRef`, `identityGuestRef`, `loginEndpointRef`, `scope`, `audience`, `consumerRef`, `allowedOperations`, canonical `spec.provider` extension where Provider schema declares it; no secret bytes); `realm-identity.json` must remain until d2bd `RealmIdentityConfigJson` loading is replaced by Credential resource reader | +| Integration | Provider/display-wayland, Provider/shell-terminal, and Provider/credential-entra consume Process/Credential resources; `d2bd` continues reading `realm-identity.json` until the Credential reader lands. | +| Data migration | Launcher and identity config are re-emitted as v3 resources; full d2b 3.0 reset; no v2 launcher/identity state import. | +| Validation | Launcher metadata shape regression; Entra identity-Guest fixture requires login Endpoint purpose `credential-entra.d2bus.org/entra-login-token`, `visibility = "provider"` (and rejects `"zone"`), exact `consumerPolicy.allowedSubjects` containing both `Provider/credential-entra` and the Credential's configured `consumerRef`, and exact canonical `resolve` operation; no-secret assertion vectors | +| Tests | `tests/unit/nix/cases/zones-launcher-metadata.nix`; `tests/unit/nix/cases/zones-credential-entra.nix` covers both required subjects, provider visibility, zone-visibility rejection, and exact Endpoint operations; no-secret vectors | +| Drift pin | `make nix-unit-pin` | +| Removal proof | `realm-workloads-launcher-v2-json.nix`/`realm-identity-config-json.nix` and `/etc/d2b/realm-workloads-launcher-v2.json`/`realm-identity.json` removed ONLY after display/credential Providers read resource configs | + +### ADR046-nix-010 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-001; unsafe-local migration | +| Current source | `nixos-modules/unsafe-local-workloads-json.nix` (`WorkloadProviderKind::UnsafeLocal`/`IsolationPosture::UnsafeLocal`; current `unsafe-local-workloads.json` artifact); `nixos-modules/unsafe-local-helper.nix` (user-domain process/helper definitions); `packages/d2b-core/src/unsafe_local_workloads.rs` | +| Reuse action | adapt | +| Destination | User-only `Host` resource in `zones//hosts.json` (`spec.isolationPosture: "none"`, `defaultDomain: user`, `allowedDomains: [user]`, `defaultUserRef: User/`); child `Process` resources in `zones//processes.json` using normal Process Providers; shell session supervisor → `Process` under `Provider/shell-terminal`; never a `Guest`; not a v3 Provider | +| Detailed design | `isolationPosture: "none"` is a promoted Host base field declared at top-level `spec.isolationPosture` in the Host schema; enforced at eval time; user-only Host rejects system-domain Process refs; `NoIsolation` condition in Host status; `status.isolationPosture: none`; every `ProcessEffect` audit event under this Host carries `no_isolation=true`; OTEL telemetry never carries an isolation label; CLI/UI warning non-suppressible | +| Integration | The unsafe-local Nix emitter produces a user-only Host plus Process resources; Provider/system-core and shell-terminal consume them; CLI/UI and audit/telemetry surfaces read resulting status/events. | +| Data migration | unsafe-local declarations migrate to Host/Process resources in Nix; full d2b 3.0 reset; no helper runtime state import. | +| Validation | User-only Host rejection of system-domain Process refs; `spec.isolationPosture != "none"` assertion rejection for user-only system-core Hosts; `NoIsolation` condition present in status; `no_isolation=true` in `ProcessEffect` audit event; OTEL attribute absent; no Guest emitted for unsafe-local declaration | +| Tests | `tests/unit/nix/cases/zones-unsafe-local.nix`; `tests/host-integration/unsafe-local-helper.nix` extended | +| Drift pin | `make nix-unit-pin` | +| Removal proof | `unsafe-local-workloads-json.nix` and unsafe-local-specific Nix code removed after user-only Host/Process resources pass all `tests/host-integration/unsafe-local-helper.nix` tests | + +### ADR046-nix-011 + +| Field | Value | +| --- | --- | +| Dependency/owner | Broker privileges owner | +| Current source | `nixos-modules/privileges-json.nix` | +| Reuse action | copy-unchanged | +| Destination | `nixos-modules/privileges-json.nix` (retained); `/etc/d2b/privileges.json` (retained, site-wide) | +| Detailed design | Broker op catalog is not Zone-scoped; no structural change required Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: retain. | +| Integration | `privileges-json.nix` remains a site-wide bundle artifact consumed by broker authorization and drift gates; Zone resource emission references it without Zone scoping. | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Existing `tests/unit/gates/drift-check.sh` | +| Removal proof | Not removed in this spec | + +### ADR046-nix-012 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-005; ADR046-nix-022 (artifact catalog emitter); Provider/volume-virtiofs | +| Current source | `nixos-modules/closures-json.nix` (keyed by `d2b.vms.`); `nixos-modules/minijail-profiles.nix` | +| Reuse action | adapt | +| Destination | `nixos-modules/closures-json.nix` (rewritten, keyed by artifact ID from `d2b.artifacts` with `type = "nixos-system"`); `nixos-modules/minijail-profiles.nix` (retained, adapted to reference Zone Guests) | +| Detailed design | Closure emitter iterates `d2b.artifacts` entries with `type = "nixos-system"`, computes `pkgs.closureInfo`, records `storePath`/digest/size in artifact catalog (private root:d2bd 0640 field; absent from all public ResourceSpecs/status/audit/OTEL); `Guest.spec.systemArtifactId` links Guest to artifact; `Volume.source.systemArtifactId` links Volume to artifact; minijail profile emitter structurally unchanged; old `d2b.vms.` keying retired | +| Integration | Artifact catalog and Guest/Volume resources reference closure entries; activation-helper and Provider/volume-virtiofs consume private `storePath` metadata while public surfaces remain path-free. | +| Data migration | Closure entries are regenerated by artifact ID from `d2b.artifacts`; full d2b 3.0 reset; no v2 VM-keyed closure state import. | +| Validation | Closure map round-trip; per-VM store hardlink integrity; `storePath` present in private catalog; `storePath` absent from all emitted public ResourceSpecs and status/audit/OTEL surfaces | +| Tests | `tests/unit/nix/cases/closures-zones.nix`; `tests/unit/nix/cases/artifact-catalog-store-path-public-absent.nix` | +| Drift pin | `make nix-unit-pin` | +| Removal proof | Old `d2b.vms.*`-keyed closure entries removed after all Guests use `zones//guests.json` and artifact catalog | + +### ADR046-nix-013 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-005; manifest contract | +| Current source | `nixos-modules/manifest.nix` (`manifestVersion` pinned contract); `nixos-modules/host-json.nix` | +| Reuse action | replace | +| Destination | Per-Zone `zones//bundle.json` (`schemaVersion`); Host resource in `zones//hosts.json` | +| Detailed design | `manifestVersion` → `schemaVersion`; `host.json` host config folded into Host resource spec; CHANGELOG entry for rename required | +| Integration | `bundle-zones.nix` writes `schemaVersion` into per-Zone `bundle.json`; activation-helper validates it and Host resource consumers replace `host.json` readers. | +| Data migration | Manifest/host artifacts are regenerated as Zone bundle/Host resources; full d2b 3.0 reset; no v2 manifest state import. | +| Validation | Schema drift gate; CHANGELOG enforcement | +| Tests | `tests/unit/gates/drift-check.sh` extended for `schemaVersion` | +| Drift pin | `make test-drift` | +| Removal proof | `manifest.nix` and `host.json` emitters removed after Zone bundle activation path passes | + +### ADR046-nix-014 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-001, ADR046-nix-002 | +| Current source | `nixos-modules/assertions.nix` (CIDR overlap, `sys-` prefix, VM-name regex, platform gate) | +| Reuse action | adapt | +| Destination | `nixos-modules/assertions.nix` | +| Detailed design | Migrate existing assertions to Zone/Resource terminology; add ref validation, owner cycles, CIDR overlap (Zones), provider resolution, RoleBinding verb set assertions Primary reuse disposition: `adapt`. Preserved source-plan detail: extend in place. | +| Integration | `assertions.nix` validates Zone/Resource declarations before index, bundle, and schema validation derivations run; failing configs block activation artifacts. | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | Each new assertion has a failing-config test vector | +| Tests | `tests/assertions-eval.sh` extended; `tests/unit/nix/cases/assertions-zones.nix` | +| Drift pin | `make nix-unit-pin` | +| Removal proof | No removal; extended only | + +### ADR046-nix-015 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-001; host activation | +| Current source | `nixos-modules/host.nix`, `host-daemon.nix` (fixed local-root endpoint set), `host-activation.nix`, `host-users.nix` | +| Reuse action | adapt | +| Destination | Same files; updated to use Zone bundle activation path and Zone resource state dirs | +| Detailed design | `d2b-activation-helper` updated to validate/stage per-Zone bundles; `d2bd.service` updated to read Zone bundle; `d2b` group retained for `SO_PEERCRED` Primary reuse disposition: `adapt`. Preserved source-plan detail: retain and adapt. | +| Integration | Host activation units call `d2b-activation-helper` against per-Zone bundles, then `d2bd.service` reads the staged Zone bundle; `SO_PEERCRED` authorization remains via the `d2b` group. | +| Data migration | Host activation reads newly staged Zone bundles on first v3 switch; full d2b 3.0 reset; no v2 daemon runtime state import. | +| Validation | Host-integration test with Zone bundle activation and daemon readiness | +| Tests | `tests/host-integration/` extended for Zone activation | +| Removal proof | No removal; adapted in place | + +### ADR046-nix-016 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-002; Provider/network-local dossier | +| Current source | `nixos-modules/network.nix`, `nixos-modules/net.nix` (including `lib.mkForce` DHCP neutralizer and per-env MTU/MSS/east-west wiring) | +| Reuse action | copy-unchanged | +| Destination | Network reconciliation by `Provider/network-local` Process resources | +| Detailed design | Current bridge/NAT/DHCP/firewall Nix units retained; `Provider/network-local` controller emits equivalent configuration from Network resources; `lib.mkForce` neutralization preserved Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: retain until Provider successor. | +| Integration | Network resources feed Provider/network-local parity implementation while `net.nix`/`network.nix` remain the fallback; network eval tests compare both paths. | +| Data migration | Network resource specs are generated from v3 Nix declarations; full d2b 3.0 reset; no v2 bridge/NAT runtime state import. | +| Validation | `tests/net-vm-network-eval.sh` passes against Network resource spec | +| Tests | `tests/unit/nix/cases/zones-network-parity.nix` | +| Drift pin | `make nix-unit-pin` | +| Removal proof | `network.nix`/`net.nix` removed after `Provider/network-local` parity and `tests/net-vm-network-eval.sh` passes | + +### ADR046-nix-017 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-012; Provider/volume-virtiofs | +| Current source | `nixos-modules/store.nix` (`ProcessRole::Virtiofsd` and `share.source == "/nix/store"` sentinel; per-VM hardlink farm) | +| Reuse action | copy-unchanged | +| Destination | Per-VM store reconciliation by `Provider/volume-virtiofs` EphemeralProcess/Process resources | +| Detailed design | `store.nix` retained; `Provider/volume-virtiofs` controller creates equivalent EphemeralProcess; per-VM store path derived from Zone stateDir + Guest name via Provider, not raw path in spec Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: retain until Provider successor. | +| Integration | Guest/Volume resources drive Provider/volume-virtiofs, which stages hardlink farm processes consumed by Guest process resources; `store.nix` remains until parity. | +| Data migration | Full d2b 3.0 reset; no v2 store metadata import | +| Validation | Store hardlink integrity; no direct `/nix/store` export | +| Tests | Existing store integrity tests extended with Zone/Guest resource fixture | +| Removal proof | `store.nix` removed after `Provider/volume-virtiofs` manages farm lifecycle and existing store tests pass | + +### ADR046-nix-018 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-002; Provider dossiers for graphics, tpm, usbip, audio | +| Current source | `nixos-modules/components/graphics.nix` (`ProcessRole::Gpu`, `GpuRenderNode`, `Video`); `components/tpm.nix` (`ProcessRole::Swtpm`, `SwtpmPreStartFlush`); `components/usbip.nix` (`ProcessRole::Usbip`); `components/audio/` (`ProcessRole::Audio`) | +| Reuse action | replace | +| Destination | `Provider/device-tpm`, `Provider/device-usbip`, `Provider/device-gpu`, `Provider/audio-pipewire` resource install declarations in `options-zones-resources.nix` | +| Detailed design | `d2b.vms..tpm.enable = true` → `d2b.zones..resources.vm-tpm = { type = "Device"; spec = { providerRef = "Provider/device-tpm"; ... }; }`; all component eval assertions migrated to `assertions.nix`; GuestSshReadiness retired at v3 cutover; Usbip long-lived backend/proxy → `Process`, per-busid attach/detach → `EphemeralProcess`, both owned by `Provider/device-usbip` Primary reuse disposition: `replace`. Preserved source-plan detail: each component becomes a Provider install resource + Device/Guest spec field. | +| Integration | Device/Guest specs feed Provider/device-* and Provider/audio-pipewire install declarations; migrated assertions/tests consume Zone resource configs instead of component toggles. | +| Data migration | Component toggles migrate to v3 Device/Guest resource declarations; full d2b 3.0 reset; no v2 component runtime state import. | +| Validation | Existing component eval tests; `tests/usbip-gating-eval.sh`; `tests/video-contract-eval.sh` | +| Tests | `tests/unit/nix/cases/zones-devices.nix` | +| Drift pin | `make nix-unit-pin` | +| Removal proof | `components/` Nix units removed after Provider resource install achieves parity and all component eval tests pass against Zone resource configs | + +### ADR046-nix-019 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-005; ADR046-nix-001; ADR046-cli-011 assertions/resource-option shape; `d2b-contracts` schema generation (ADR046-nix-027) | +| Current source | `nixos-modules/bundle-artifacts.nix` (`artifactModule` submodule, mode/ownership); `nixos-modules/bundle.nix` (digest chain, SHA256SUMS); `packages/xtask/src/main.rs` (`gen-schemas`); no current per-ResourceType JSON Schema under `docs/reference/schemas/v3/` | +| Reuse action | adapt | +| Destination | `docs/reference/schemas/v3/.json` for each ResourceType; `nixos-modules/resource-schema-validation.nix` (validates emitted spec against committed JSON Schema at build time); `nixos-modules/provider-settings-validation.nix` (validates `spec.provider.settings` where declared in schema, and Provider `config`, against Provider-embedded schema at build time); `nixos-modules/assertions.nix` (Credential ref enforcement, secret-pattern rejection) | +| Detailed design | `cargo xtask gen-schemas` emits one JSON Schema per ResourceType under `docs/reference/schemas/v3/`; Nix derivation reads these schemas from `pkgs.d2b-resource-schemas` and validates every emitted `spec` JSON before producing the Zone bundle; Provider-settings validation reads `settingsSchemaDigest` from `provider-catalog.json` and resolves the schema from the Provider package closure; Credential ref enforcement: eval assertion rejects any `spec` string field matching `-----BEGIN`, `eyJ`, or a hex string ≥ 32 bytes in a secret-typed field; `managedBy` in any input spec rejected at eval (core-set runtime field, never in Nix input); bundle integrity: `candidateId`/`contentId` computed over canonical sorted output Primary reuse disposition: `adapt`. Preserved source-plan detail: extend xtask schema generation; new Nix eval/build validation hooks. | +| Integration | Validation hooks wired into `bundle-zones.nix` derivation; `d2b-activation-helper` re-verifies digest chain at staging | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | Schema round-trip: emit spec, validate against schema, verify byte-identical re-emit; `spec.provider.settings` rejection test (unknown field, out-of-bounds value, raw store path, secret bytes) where schema declares the field; Provider `config` rejection test (unknown field); Credential ref enforcement: PEM-in-spec rejected; secret-pattern-in-spec rejected; valid `Credential/` ref accepted; `managedBy` in spec input rejected at eval | +| Tests | `tests/unit/nix/cases/resource-schema-validation.nix`; `tests/unit/nix/cases/provider-settings-validation.nix`; `tests/unit/nix/cases/credential-ref-enforcement.nix`; `tests/unit/nix/cases/managed-by-rejection.nix`; `packages/d2b-contract-tests/tests/resource-schema-round-trip.rs` | +| Drift pin | `make test-drift` after any `gen-schemas` run; `make nix-unit-pin` after adding cases | +| Removal proof | Not removed; extended as new ResourceTypes are added | + +### ADR046-nix-020 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-005; ADR046-nix-001; configuration-publication controller (ADR-046-core-controllers) | +| Current source | No current equivalent; current `bundle.nix` replaces all artifacts atomically with no per-resource cleanup tracking | +| Reuse action | create | +| Destination | Configuration-publication controller handler in `packages/d2bd/src/config_publication.rs`; `ConfigurationOwnedClassifier`; `AbsentResourceReaper`; `Zone` status conditions in `d2b-contracts/src/v3/zone_status.rs`; cleanup audit emitter in `d2b-state/src/audit_segments.rs` | +| Detailed design | `ConfigurationOwnedClassifier`: classify resources by core-set `managedBy` field only — `managedBy=configuration` resources are owned by config publication; `managedBy=controller` and `managedBy=api` resources are never touched. At activation, diff new-generation bundle name+type set against all resources with `managedBy=configuration` in the Zone store; resources absent from the new bundle are enqueued for Delete. Never infer ownership from `ownerRef`, labels, or absence from emitted files. `AbsentResourceReaper`: processes the Absent queue asynchronously; does not block pointer swap (step 4); sets `status.phase=Pending` + `PendingDeletion` condition (`reason: AbsentFromConfiguration`); waits for all finalizers to clear; commits a single store transaction that appends the `Deleted` revision and removes the resource row and its indexes; subsequent `Get` returns not-found; the `ResourceDelete` audit event is appended afterward with dedup/exactly-once recovery. Zone phase: `Pending` during pointer-swap-to-first-reconcile window; `Degraded` while any `managedBy=configuration` resource carries `PendingDeletion` or a ZoneLink lags; `Ready` when all reconciled. Generation pruning: prune when `generationIndex ≤ activeIndex - retainedGenerations` AND all enqueued resources from that generation have reached `Deleted`. Rollback: re-adopt `managedBy=configuration` resources in `Pending/PendingDeletion` back to the rollback target generation's owned set | +| Integration | `d2b-activation-helper` sets `managedBy=configuration` + `configurationGeneration` on every resource it activates; controller reads these fields to determine owned set — never `ownerRef` or bundle membership alone | +| Data migration | Full d2b 3.0 reset; existing resources gain `managedBy`/`configurationGeneration` only when first v3 activation creates them; no v2 state/config import. | +| Validation | Classification: `managedBy=controller` resource never enqueued (even if absent from bundle). `managedBy=api` resource never enqueued. `managedBy=configuration` resource absent from new bundle always enqueued. Finalizer safety: resource with active finalizer enters DeletionBlocked; not force-deleted; stays in `Pending`. Final deletion: a single store transaction appends the `Deleted` revision and removes the resource row and its indexes; subsequent `Get` returns not-found; `ResourceDelete` audit event is appended afterward with dedup/exactly-once recovery. Zone status: `Pending` during activation; `Degraded` while PendingDeletion outstanding; `Ready` when clean. Audit: `ResourceDelete` event includes `configurationGeneration` field. | +| Tests | `tests/unit/nix/cases/cleanup-two-generation.nix` (bundle diff); `tests/host-integration/cleanup-activation.nix` (async cleanup, Pending→Deleted, single store transaction appends `Deleted` revision + removes row/indexes, subsequent `Get` not-found, `ResourceDelete` audit event appended afterward with dedup/exactly-once recovery, Zone Pending→Degraded→Ready); `tests/host-integration/cleanup-finalizer.nix` (DeletionBlocked, stays Pending, cleared on finalizer removal, single store transaction appends `Deleted` revision + removes row/indexes, subsequent `Get` not-found); `tests/host-integration/cleanup-controller-managed.nix` (managedBy=controller preserved); `tests/host-integration/cleanup-api-managed.nix` (managedBy=api preserved); `tests/host-integration/cleanup-rollback.nix` (rollback re-adoption); `tests/host-integration/cleanup-retention-window.nix` (generation pruning) | +| Drift pin | `make nix-unit-pin`; `make test-drift` if status/audit schema changes | +| Removal proof | Not removed; extended as new ResourceTypes are added | + +### ADR046-nix-021 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-001; `d2b-contracts` workspace policy tests | +| Current source | `packages/d2b-contract-tests/tests/` (existing workspace policy lints: `tests/workspace-member-sort.rs`, `tests/crate-naming.rs`); no current Provider crate layout gate exists | +| Reuse action | create | +| Destination | `packages/d2b-contract-tests/tests/provider-crate-layout.rs`; workspace scan in `packages/xtask/src/main.rs` (extend `check-workspace` or add `check-provider-layout` subcommand) | +| Detailed design | Parse the root `packages/Cargo.toml` workspace member list; for every member path matching `packages/d2b-provider-*-*`: assert (1) `src/` directory exists and contains at least one `.rs` file; (2) `tests/` directory exists and contains at least one `.rs` file; (3) `integration/` directory exists and contains at least one `.rs` or fixture file; (4) `README.md` exists and is ≥ 200 bytes. All four conditions required; any single failure fails the test with a structured message naming the crate and missing path. Test runs as `cargo test -p d2b-contract-tests provider_crate_layout`; wired into `make test-policy`. | +| Integration | Wired into `make test-policy` (same gate family as existing workspace policy tests); no new `Makefile` target needed unless `test-policy` does not yet exist | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | Fixture: add a stub `packages/d2b-provider-test-missing-integration/` with `src/lib.rs` and `tests/smoke.rs` but no `integration/` and no `README.md`; assert test fails naming both missing paths. Add complete stub with all four paths; assert test passes. | +| Tests | `packages/d2b-contract-tests/tests/provider-crate-layout.rs` with fixture sub-directories under `packages/d2b-contract-tests/fixtures/`; included in `make test-policy` | +| Drift pin | `make test-policy`; re-run after any new `d2b-provider-*-*` crate is added to the workspace | +| Removal proof | Not removed; extended as new Provider crates are added to the workspace | + +### ADR046-nix-022 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-005 (bundle derivation); `d2b-contracts` schema generation (ADR046-nix-027) | +| Current source | No current equivalent for a separate artifact catalog. Current `nixos-modules/bundle.nix` embeds package derivation references inline. Current `nixos-modules/closures-json.nix` uses `pkgs.closureInfo` keyed by `d2b.vms.`. | +| Reuse action | create | +| Destination | `nixos-modules/artifact-catalog.nix` (new emitter); `nixos-modules/options-artifacts.nix` (new option: `d2b.artifacts. = { package; type; }`); `/etc/d2b/artifact-catalog.json` (output artifact, `root:d2bd` 0640); `nixos-modules/bundle-zones.nix` (extend to include artifact catalog digest in `bundle.json`); `nixos-modules/options-zones-resources.nix` (replace `closureRef` / `nixosSystem` helpers with `systemArtifactId` validation) | +| Detailed design | `d2b.artifacts.` attrset option: `id` matches `^[a-z][a-z0-9-]*$`; `type ∈ { "provider", "nixos-system", "nixos-module-set", "config-bundle" }`; no other fields. Emitter computes `pkgs.closureInfo` for each entry and writes `artifact-catalog.json` with sorted entries (by `artifactId`) containing `artifactId`, `type`, `storePath` (private, for activation-helper staging), `packageDigest`, `closureDigest`, `closureSize`. `storePath` is a private field of the root:d2bd 0640 file; it is never emitted in public ResourceSpecs, status fields, audit records, or OTEL telemetry. The `bundle.json` manifest includes the artifact catalog file entry and its SHA256 digest. `d2b-activation-helper` reads `storePath` from the catalog to resolve and stage each artifact; verifies catalog digest before staging. Build-time validation: `artifactId` / `systemArtifactId` / `source.systemArtifactId` fields in resource specs resolve against `d2b.artifacts`; type-mismatch fails with a structured error. `d2b.providerCatalog..package` option is removed; replaced by `d2b.providerCatalog..artifactId`. `Guest.spec.systemArtifactId` replaces the former `nixosSystem` Nix-only helper. `Volume.source.systemArtifactId` replaces `source.closureRef`. | +| Integration | `nixos-modules/closures-json.nix` rewritten (ADR046-nix-012) to key by artifact ID; `nixos-modules/bundle-zones.nix` includes artifact catalog in integrity chain | +| Data migration | Artifact catalog entries are generated from new `d2b.artifacts` declarations; full d2b 3.0 reset; no v2 `closureRef`/`nixosSystem` metadata import. | +| Validation | Artifact catalog round-trip: declare artifact, build, verify JSON entry present with correct type/digests/storePath; missing artifact ID fails build with structured error; wrong-type artifact fails build; `storePath` absent from all public ResourceSpecs and status/audit/OTEL surfaces | +| Tests | `tests/unit/nix/cases/artifact-catalog.nix` (declaration, resolution, storePath present in private catalog); `tests/unit/nix/cases/artifact-catalog-type-mismatch.nix` (build failure for wrong type); `tests/unit/nix/cases/artifact-catalog-missing-id.nix` (build failure for absent ID); `tests/unit/nix/cases/artifact-catalog-public-surfaces.nix` (storePath absent from all emitted ResourceSpecs); `packages/d2b-contract-tests/tests/artifact-catalog-schema.rs` | +| Drift pin | `make test-drift` (artifact catalog schema); `make nix-unit-pin` | +| Removal proof | Not removed; extended as new artifact types are added | + + +### ADR046-nix-023 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR-046-componentsession-and-bus spec; ADR046-nix-027 (wire contract) must land first | +| Current source | Main commit source row below: `packages/d2b-session/` at `a1cc0b2d`, including session engine, handshake, lifecycle, scheduler, record protection, fragmentation, transport, driver, server, and metrics modules. | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/session/` (new crate `d2b-bus`); `ComponentSessionDriver` becomes the central abstraction for all Zone bus sessions. | +| Detailed design | Complete session protocol: preface negotiation, Noise handshake profiles, record protection, replay cache, fair scheduler, fragmentation/reassembly, keepalive, ttrpc bridge, generation discovery, and `ComponentSessionDriver` application surface. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; adapt ADR45 EndpointPurpose/ServicePackage names listed in `ADR45 exclusions` while preserving wire tag values.. | +| Integration | `d2b-bus` session runtime is used by Zone bus sessions for local-root controller to broker, controller to guest agent, and controller to provider agent; `serve_ttrpc_services` exposes service handlers. | +| Data migration | Full d2b 3.0 reset; no v2 session runtime state/config import | +| Validation | Port `component_session.rs` and `noise_vectors.rs`; run `make test-rust`; Noise KAT vectors must pass after copy. | +| Removal proof | ADR45 EndpointPurpose/ServicePackage realm names listed in `ADR45 exclusions` are adapted during copy; no prior v3 session owner to remove. | +| Main commit source | `packages/d2b-session/src/engine.rs` (`SessionEngine`, `SessionEvent`); `packages/d2b-session/src/handshake.rs` (`NoiseHandshake`, `HandshakeCredentials` Nn/Kk/IkPsk2 variants, `HandshakeRole`, `NegotiatedOffer`, `EstablishedHandshake`, `encode_offer`, `negotiate_offer`, `x25519_public_key`, `encode_generation_discovery_request`, `accept_generation_discovery_request`, `encode_generation_discovery_response`, `decode_generation_discovery_response`); `packages/d2b-session/src/lifecycle.rs` (`SessionLifecycle`, `SessionPhase`, `KeepaliveAction`); `packages/d2b-session/src/scheduler.rs` (`FairScheduler`, `QueueClass`, `OutboundFrame`); `packages/d2b-session/src/record.rs` (`RecordProtector`, `ProtectedRecord`; replay cache 1024 entries); `packages/d2b-session/src/fragmentation.rs` (`Fragmenter`, `Reassembler`, `Fragment`); `packages/d2b-session/src/transport.rs` (`OwnedTransport`, `TransportDescriptor`, `TransportPacket`, `TransportError`); `packages/d2b-session/src/driver.rs` (`ComponentSessionDriver` trait with 20 async methods, `SessionDriverHandle`); `packages/d2b-session/src/server.rs` (`serve_ttrpc_services`, `SessionServerError`); `packages/d2b-session/src/metrics.rs` (`MetricEvent`, `MetricsSink`, `NoopMetrics`) | +| Tests at main | `packages/d2b-session/tests/component_session.rs` — full Nn/Kk/IkPsk2 session lifecycle, fragmentation, attachments, named streams, cancellation, keepalive; `packages/d2b-session/tests/noise_vectors.rs` — KAT against `docs/reference/component-session-v2-vectors.json` | +| Selected behavior | Complete session protocol: preface negotiation, Noise handshake (all three profiles), record protection with 1024-entry replay cache, fair two-class scheduler, fragmentation/reassembly, keepalive (ping/timeout/close), ttrpc bridge (`serve_ttrpc_services`), generation discovery (pre-handshake version probe), `ComponentSessionDriver` as the sole application-layer control surface | +| v3 destination | `packages/d2b-bus/src/session/` (new crate `d2b-bus`); `ComponentSessionDriver` becomes the central abstraction for all Zone bus sessions (local-root controller ↔ broker, controller ↔ guest agent, controller ↔ provider agent) | +| ADR45 exclusions | `HandshakeOffer` fields `purpose: EndpointPurpose` and `service: ServicePackage` reference ADR45 enum values (`RealmPeer`, `RealmBootstrap`, `RealmV2`); adapt variant names per ADR-046-componentsession-and-bus owning spec; wire tag values are stable and must not change; `Locality::GuestLocal` remains valid for vsock guest sessions | +| Drift pin | `make test-rust` (session crate); Noise KAT vectors must pass after copy | + +### ADR046-nix-024 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-023; ADR046-nix-027 | +| Current source | Main commit source row below: `packages/d2b-session/src/attachment.rs` and `packages/d2b-session/src/streams.rs` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/session/` (same crate as ADR046-nix-023). | +| Detailed design | Attachment ownership validates descriptors only after authenticated decrypt; unconsumed payloads close on drop. NamedStreamMux preserves half-close, reset, per-stream limits, aggregate limits, and credit-based flow control. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; keep attachment/stream semantics and descriptor validation, with no ADR45 realm adaptation required for this item.. | +| Integration | `d2b-bus` session transport validates attachments after authenticated decrypt and exposes named streams to ttrpc/services; Unix/vsock transports provide payloads. | +| Data migration | Full d2b 3.0 reset; no v2 attachment or stream state/config import | +| Validation | Port `component_session.rs` attachment ownership, stream mux, and credit-accounting tests. | +| Removal proof | None — net-new; no prior owner to remove | +| Main commit source | `packages/d2b-session/src/attachment.rs` (`AttachmentPayload` trait with `validate_descriptor()`, `close()`, `as_any()`, `into_any()`; `OwnedAttachment` with `unbound()`/`bind_received()`/`bind_outbound()`/`validate_payload_descriptor()`/`into_payload()`; `AttachmentValidationError` enum: Kind/ObjectType/Access/CloseOnExec/Other); `packages/d2b-session/src/streams.rs` (`NamedStreamMux`, `StreamId`, `StreamEvent` enum: Data/RemoteClosed/Reset, `StreamPhase` enum: Open/HalfClosedLocal/HalfClosedRemote/Closed/Reset) | +| Tests at main | `packages/d2b-session/tests/component_session.rs` (attachment ownership, stream mux, credit accounting) | +| Selected behavior | Attachment lifecycle: `OwnedAttachment::unbound()` for transport-received before descriptor auth; `validate_descriptor()` called only after authenticated decryption; `into_payload()` transfers ownership without close; `Drop` closes remaining payload. `NamedStreamMux` with per-stream and aggregate queue byte limits; half-close semantics; credit-based flow control | +| v3 destination | `packages/d2b-bus/src/session/` (same crate as ADR046-nix-023); `AttachmentPayload` and `OwnedAttachment` are transport-neutral and require no ADR45 adaptation | +| ADR45 exclusions | `AttachmentDescriptor` from `v2_component_session` has `kind: AttachmentKind` and `object_type: KernelObjectType`; these values are wire-stable and carry no ADR45 realm naming; no exclusions for this item | + +### ADR046-nix-025 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-023; ADR046-nix-027 | +| Current source | Main commit source row below: `packages/d2b-session/src/cancellation.rs`, `deadline.rs`, and `bootstrap.rs` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/session/`. | +| Detailed design | Per-generation `RequestRegistry`, request-wide cancellation, `DeadlineBudget::admit_metadata()` as the inbound metadata gate, and single-use `BootstrapAdmission::consume()` replay protection. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; adapt `CancelRequest`/`CancelResponse` module paths if v3 services move the common proto types.. | +| Integration | `RequestRegistry`, `DeadlineBudget`, and `BootstrapAdmission` feed all `d2b-bus` inbound ttrpc calls and resource API cancellation paths. | +| Data migration | Full d2b 3.0 reset; no v2 cancellation/deadline/bootstrap state import | +| Validation | Port cancel-before-dispatch, cancel-after-dispatch, generation-mismatch cancel, and deadline admit tests; add v3 common-proto path adapter test. | +| Removal proof | ADR45 common proto type paths are adapted to the v3 service module; no prior v3 owner to remove. | +| Main commit source | `packages/d2b-session/src/cancellation.rs` (`Cancellation` with `cancel()`/`is_cancelled()`/`cancelled()` async notify; `RequestRegistry` with generation scoping, `register()`/`mark_dispatched()`/`cancel()`/`cancel_all()`/`cancel_generated()`/`complete()`/`remove()`/`signal()`/`active()`); `packages/d2b-session/src/deadline.rs` (`DeadlineBudget` with `admit_metadata()` validating clock skew ≤30 s, request lifetime ≤15 min, idempotency key, peer ttrpc timeout); `packages/d2b-session/src/bootstrap.rs` (`Secret32` zeroizing 32-byte key; `BootstrapPsk`; `AdmittedBootstrapPsk`; `BootstrapAdmission` single-use PSK with operation-ID + replay-nonce check) | +| Tests at main | `packages/d2b-session/tests/component_session.rs` (cancel-before-dispatch, cancel-after-dispatch, generation-mismatch cancel, deadline admit) | +| Selected behavior | `RequestRegistry` is per-generation; calling `cancel_all()` cancels every outstanding request; `DeadlineBudget::admit_metadata()` is the single gate for all inbound request metadata; `BootstrapAdmission::consume()` single-use PSK prevents replay | +| v3 destination | `packages/d2b-bus/src/session/` | +| ADR45 exclusions | `cancel_generated()` calls `common::CancelRequest`/`CancelResponse` from `v2_services`; the `common.rs` proto type paths may change when services are versioned to v3 (ADR046-nix-028); the underlying `CancelRequest`/`CancelAck`/`CancelResult` contract from `v2_component_session` is wire-stable and requires no change | + +### ADR046-nix-026 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-023 | +| Current source | Main commit source row below: `packages/d2b-session-unix/src/{adapter,credit,descriptor,pidfd,socket,systemd,vsock}.rs` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/transport/unix/`. | +| Detailed design | Unix seqpacket/stream transports, pidfd identity verification, six-scope credit pool with host/process bounds, systemd-activated seqpacket listener support, and length-prefixed vsock framing. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; retain host-socket/native-vsock feature gates and replace only ADR45 socket-name strings with Zone bootstrap config.. | +| Integration | `d2b-bus` listeners and Provider/guest sessions instantiate UnixSeqpacket/UnixStream/vsock transports; credit pool enforces attachment FD budgets before handing payloads to session runtime. | +| Data migration | Full d2b 3.0 reset; no v2 Unix transport state/config import | +| Validation | Port `unix_session.rs` seqpacket, stream, pidfd, FD attachment, and credit pool tests; add Zone bootstrap socket-name fixture. | +| Removal proof | ADR45 socket-name strings listed in exclusions are replaced by Zone bootstrap config; no prior v3 transport owner to remove. | +| Main commit source | `packages/d2b-session-unix/src/adapter.rs` (feature `host-socket`) — `UnixSeqpacketTransport`, `UnixStreamTransport`, `UnixAttachmentPayload`, `OwnedUnixAttachment`, `PeerIdentityPolicy` (Pathname/InheritedSocketpair), `DescriptorPolicyResolver`, `PathnamePeerVerifier`; `packages/d2b-session-unix/src/credit.rs` — `CreditBundle`, `CreditError`, `CreditPool`, `CreditScope`, `CreditScopeSet`, `ProcessCreditLimit`; `packages/d2b-session-unix/src/descriptor.rs` — `ReceivedPacket`, `AcceptedAttachment`, `DescriptorPolicy`, `FirstPacketCredentials`, `ObjectIdentity`, `PeerCredentials`, `PidfdIdentityPolicy`, `VerifiedPacket`; `packages/d2b-session-unix/src/pidfd.rs` — `DigestEvidenceCallback`, `PidfdEvidence`, `PidfdIdentityVerifier`, `PidfdInfoSource`, `ProcPidfdIdentityVerifier`, `ProcSelfFdInfoSource`, `parse_pidfd_fdinfo`; `packages/d2b-session-unix/src/socket.rs` — `AncillaryCapacity`, `OutboundPacket`, `PacketBurst`, `SendBurst`, `SentPacket`, `SeqpacketSocket`, `StreamRead`, `StreamSocket`, `prearmed_seqpacket_pair`; `packages/d2b-session-unix/src/systemd.rs` — `ActivatedSeqpacketListener`, `ActivatedSeqpacketListeners`, `SystemdActivationError`; `packages/d2b-session-unix/src/vsock.rs` (feature `native-vsock`) — `FramedVsockTransport`, `NativeVsockListener`, `NativeVsockTransport` | +| Tests at main | `packages/d2b-session-unix/tests/unix_session.rs` — seqpacket pair, Unix stream, pidfd identity, FD attachment validation, credit pool exhaustion and recovery | +| Selected behavior | Audited Unix seqpacket and stream transports implementing `OwnedTransport`; pidfd identity verification reads `/proc//fdinfo/` for `st_dev`/`st_ino`; 6-scope credit pool with per-process and per-host limits (`MAX_PROCESS_ATTACHMENT_CREDITS=2048`, `MAX_HOST_ATTACHMENT_CREDITS=8192`); systemd `SD_LISTEN_FDS` seqpacket activation; vsock framing with length-prefixed records | +| v3 destination | `packages/d2b-bus/src/transport/unix/` — adapt as the Unix transport backend for `d2b-bus` sessions; keep the `host-socket`/`native-vsock` feature gates intact; the transport code itself has no ADR45 realm bindings | +| ADR45 exclusions | `ActivatedSeqpacketListeners` reads socket names from `SD_LISTEN_FDS`; socket names are bound to current `d2bd.socket` / `d2b-priv-broker.socket` unit names; v3 socket paths come from Zone bootstrap config — activation code is reusable, socket name strings are not. `PeerIdentityPolicy::accepted()` is transport-layer code and has no ADR45 binding | + +### ADR046-nix-027 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR-046-componentsession-and-bus; naming and wire enumeration decisions per ADR-046-componentsession-and-bus owning spec before final adoption | +| Current source | Main commit source row below: `packages/d2b-contracts/src/v2_component_session.rs` at `a1cc0b2d` plus committed session/noise tests. | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/component_session.rs`. | +| Detailed design | Canonical wire values, bounded vectors, binary size constants, closed-enum tag codecs, and fail-closed validation for the ComponentSession protocol. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse wire constants/types from main commit `a1cc0b2d`; rename ADR45 realm-oriented variants only as directed by the bus owning spec and preserve numeric tags.. | +| Integration | `d2b-contracts` v3 component_session module is consumed by `d2b-bus`, `d2b-session`, `d2b-session-unix`, `d2b-client`, and provider toolkit for shared wire constants/enums. | +| Data migration | Full d2b 3.0 reset; no v2 ComponentSession contract state import | +| Validation | Port component session and Noise vector tests; add wire-tag stability gates for renamed role/service/purpose variants. | +| Removal proof | ADR45 realm-named enum variants are renamed only if the owning spec requires while preserving wire tags; no prior v3 contract owner to remove. | +| Main commit source | `packages/d2b-contracts/src/v2_component_session.rs` (entire file, 2500+ lines at `a1cc0b2d`): wire constants — `PREFACE_MAGIC=*b"D2BCS2\r\n"`, `COMPONENT_SESSION_MAJOR=2`, `COMPONENT_SESSION_MINOR=0`, all `MAX_*` limits, `FRAGMENT_HEADER_LEN=24`, `RECORD_HEADER_LEN=24`; structs — `BoundedVec`, `ComponentSessionPreface`, `HandshakeOffer`, `EndpointPolicy`, `EndpointPolicyIdentity`, `LimitProfile`, `TransportBinding`, `AttachmentPolicy`, `RequestEnvelope`, `AdmittedDeadline`, `RecordHeader`, `FragmentHeader`, `SendSequence`, `ReceiveSequence`, `KeepaliveRecord`, `CloseRecord`, `GuestSessionCredentialV1`, `GuestBootstrapCredentialV1`, `GuestBootstrapPsk`, `BootstrapPskBinding`, `BootstrapPskState`; enums — `EndpointPurpose`(19 variants), `PurposeClass`(3), `EndpointRole`(19), `ServicePackage`(15), `NoiseProfile`(3), `Locality`(4), `TransportClass`(5+), `AttachmentKind`, `KernelObjectType`, `CloseReason`, `ContractError`, `BinaryError`; closed-enum wire tag codec | +| Tests at main | `packages/d2b-session/tests/component_session.rs` (wire protocol round-trip); `packages/d2b-session/tests/noise_vectors.rs` (KAT from `docs/reference/component-session-v2-vectors.json`) | +| Selected behavior | Canonical wire values and fail-closed validation for the session protocol; `BoundedVec` serde+JsonSchema; all binary size constants are stable wire commitments and must not change without a `COMPONENT_SESSION_MAJOR` bump | +| v3 destination | `packages/d2b-contracts/src/v3/component_session.rs`; `COMPONENT_SESSION_MAJOR` stays 2 unless the wire handshake format changes; KAT vectors in `docs/reference/component-session-v2-vectors.json` must still pass after copy | +| ADR45 exclusions | `EndpointRole` variants `RealmController`(3), `RealmBroker`(5): may rename per ADR-046-componentsession-and-bus owning spec; wire tag values 3 and 5 are stable and must not change. `ServicePackage` variants `RealmV2`(2, `"d2b.realm.v2"`) and `DaemonV2`(1): `RealmV2` may rename per owning spec; wire tag 2 is stable. `EndpointPurpose` variants `RealmPeer`(3), `RealmBootstrap`(4): may rename per owning spec; wire tags 3 and 4 are stable. `PurposeClass` and `Locality` variant names: confirm per owning spec; no "realm" prefix in either enum so rename is unlikely but must be verified before adoption | + +### ADR046-nix-028 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-027; Zone service naming per ADR-046-componentsession-and-bus owning spec | +| Current source | Main commit source row below: `packages/d2b-contracts/src/generated_v2_services/`, `v2_guest_services.rs`, and `v2_component_session.rs` service inventory at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/services/`. | +| Detailed design | Versioned ttrpc service modules preserve server dispatch tables, shared request/cancel/outcome/error types, and service fingerprint inventory. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse generated ttrpc service tables from main commit `a1cc0b2d`; adapt ADR45 Realm/Workload fields and service names to v3 Zone/Resource naming per owning bus spec.. | +| Integration | Generated v3 services feed `d2b-bus` server/client dispatch, provider-toolkit conformance, and `SERVICE_INVENTORY` fingerprint validation. | +| Data migration | Full d2b 3.0 reset; no v2 generated service state import | +| Validation | Port provider-toolkit conformance and service fingerprint assertions; add v3 service-name gate for Zone service and ResourceName field adaptation. | +| Removal proof | ADR45 realm/user WorkloadId fields listed in exclusions are adapted to Zone/ResourceName; `generated_v2_services` remains only as reuse source. | +| Main commit source | `packages/d2b-contracts/src/generated_v2_services/` (48 files at `a1cc0b2d`): `activation.rs`+`_ttrpc.rs`, `broker.rs`+`_ttrpc.rs`, `clipboard.rs`+`_ttrpc.rs`, `clipboard_picker.rs`+`_ttrpc.rs`, `daemon.rs`+`_ttrpc.rs`, `guest.rs`+`_ttrpc.rs`, `notify.rs`+`_ttrpc.rs`, `provider_audio.rs`+`_ttrpc.rs`, `provider_credential.rs`+`_ttrpc.rs`, `provider_device.rs`+`_ttrpc.rs`, `provider_display.rs`+`_ttrpc.rs`, `provider_infrastructure.rs`+`_ttrpc.rs`, `provider_network.rs`+`_ttrpc.rs`, `provider_observability.rs`+`_ttrpc.rs`, `provider_runtime.rs`+`_ttrpc.rs`, `provider_storage.rs`+`_ttrpc.rs`, `provider_substrate.rs`+`_ttrpc.rs`, `provider_transport.rs`+`_ttrpc.rs`, `realm.rs`+`_ttrpc.rs`, `runtime_systemd_user.rs`+`_ttrpc.rs`, `security_key.rs`+`_ttrpc.rs`, `shell.rs`+`_ttrpc.rs`, `terminal.rs`, `tty.rs`+`_ttrpc.rs`, `user.rs`+`_ttrpc.rs`, `wayland.rs`+`_ttrpc.rs`, `common.rs`, `mod.rs`; also `packages/d2b-contracts/src/v2_guest_services.rs`, `v2_component_session.rs`'s `SERVICE_INVENTORY` + fingerprint functions | +| Tests at main | `packages/d2b-provider-toolkit/tests/conformance.rs` (all 11 provider-type axes); `packages/d2b-session/tests/component_session.rs` (service fingerprint assertions) | +| Selected behavior | Each `*_ttrpc.rs` defines the ttrpc `Arc` server type and method dispatch table; `common.rs` defines shared `RequestMetadata`, `CancelRequest`/`CancelResponse`, `Outcome`, `ErrorKind`; `SERVICE_INVENTORY` indexes all services for schema-fingerprint verification | +| v3 destination | `packages/d2b-contracts/src/v3/services/` (versioned sub-path); service interfaces adopted as-is initially; breaking method changes require a new proto major version | +| ADR45 exclusions | `realm.rs` service `"d2b.realm.v2.RealmService"` and its `RealmId`-typed fields → rename per ADR-046-componentsession-and-bus owning spec; `user.rs` and `runtime_systemd_user.rs` reference `WorkloadId` in some method contexts → adapt to `ResourceName` on copy; the 11 provider service files (`provider_*.rs`) carry no realm naming and require no ADR45 adaptation | + +### ADR046-nix-029 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-027; ADR046-nix-028; EndpointRole naming per ADR-046-componentsession-and-bus owning spec | +| Current source | Main commit source row below: `packages/d2b-provider/src/{registry,rpc,instance,context}.rs` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-provider/src/` (adapt in place). | +| Detailed design | ProviderRegistry snapshot admission/drain, bounded in-flight caps, typed RpcProviderProxy dispatch, and build-time RegistryLimits validation. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse ProviderRegistry/RPC runtime from main commit `a1cc0b2d`; adapt `SessionIdentity` to Zone naming and update EndpointRole/ServicePackage names per owning bus spec.. | +| Integration | Zone controller publishes active ProviderRegistry snapshots; provider-toolkit RPC proxy admits sessions through `d2b-bus` and dispatches typed Provider operations. | +| Data migration | Full d2b 3.0 reset; no v2 ProviderRegistry runtime state import | +| Validation | Port `d2b-provider` `runtime.rs` tests; add v3 AdmissionOptions peer-role and SessionIdentity zone-name gates. | +| Removal proof | ADR45 role/service names are adapted per owning bus spec; no prior v3 ProviderRegistry owner to remove. | +| Main commit source | `packages/d2b-provider/src/registry.rs` (`ProviderRegistry`, `ProviderRegistryBuilder`, `RegistryLimits` defaults: total_in_flight=256, per_provider_in_flight=32, `AdmissionOptions`); `packages/d2b-provider/src/rpc.rs` (`AuthenticatedProviderRpc` trait, `RpcProviderProxy`, `RpcCall`, `RpcOperation` enum: Health/Capabilities/Method, `RpcPayload` enum: None/Operation/Plan/Adoption/LeaseRequest/Lease, `RpcResponse` enum: Health/Capabilities/Plan/Handle/Observation/ObservabilityQuery/Mutation/Lease, `SessionIdentity`, `ProviderClock`, `SystemProviderClock`); `packages/d2b-provider/src/instance.rs` (`ProviderInstance` enum, 11 variants: Runtime/Infrastructure/Transport/Substrate/Credential/Display/Network/Storage/Device/Audio/Observability); `packages/d2b-provider/src/context.rs` (`OwnedOperationContext`, `CancellationToken`) | +| Tests at main | `packages/d2b-provider/tests/runtime.rs` | +| Selected behavior | `ProviderRegistry` admits sessions against a versioned generational snapshot and drains before rotation; per-provider in-flight cap prevents one slow provider from consuming all capacity; `RpcProviderProxy` converts typed `RpcCall` into the correct per-ProviderType ttrpc service invocation through `AuthenticatedProviderRpc`; `RegistryLimits` validated at build time | +| v3 destination | `packages/d2b-provider/src/` (adapt in place); `ProviderRegistry` becomes the Zone controller's active Provider registry; `ProviderInstance` enum extended with new Provider types as dossiers are ratified | +| ADR45 exclusions | `AdmissionOptions.peer_role: EndpointRole` contains ADR45 role values — update variant names per ADR-046-componentsession-and-bus owning spec; `SessionIdentity` contains `provider_generation` and `service: ServicePackage` — the generation type is stable; `ServicePackage::ProviderV2` may rename per owning spec; `v2_identity::ProviderId`/`ProviderType` carry no realm naming and require no adaptation | + +### ADR046-nix-030 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-029; ADR046-nix-028; EndpointRole and ServicePackage naming per ADR-046-componentsession-and-bus owning spec | +| Current source | Main commit source row below: `packages/d2b-provider-toolkit/src/{adapter,server,conformance,registration}.rs` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-toolkit/src/` (adapt in place). | +| Detailed design | ProviderAgentAdapter validates ComponentSession identity, GeneratedProviderServiceServer dispatches all ProviderType method families with bounded stores/in-flight state, and conformance kit gates every Provider implementation. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse provider toolkit from main commit `a1cc0b2d`; adapt endpoint role/service variant names and v2_identity imports while preserving wire tags.. | +| Integration | Provider processes embed `GeneratedProviderServiceServer`; `ProviderAgentAdapter` validates ComponentSession identity before invoking registry instances; conformance kit gates every Provider crate. | +| Data migration | Full d2b 3.0 reset; no v2 provider-toolkit runtime state import | +| Validation | Port `conformance.rs`; assert endpoint role/service wire tags remain 7/4 after any variant rename. | +| Removal proof | ADR45 `v2_identity` imports are adapted to ZoneId/ResourceName; no prior v3 toolkit owner to remove. | +| Main commit source | `packages/d2b-provider-toolkit/src/adapter.rs` (`ProviderAgentAdapter::new()` validates session identity — checks `peer_role == EndpointRole::ProviderAgent`, `service == ServicePackage::ProviderV2`, `binding.agent_generation == identity.provider_generation`; `invoke_session()` checks attachment index ordering); `packages/d2b-provider-toolkit/src/server.rs` (`GeneratedProviderServiceServer` with per-session object stores — `MAX_SESSION_PLANS=256`, `MAX_SESSION_HANDLES=1024`, `MAX_SESSION_LEASES=1024`, `MAX_AGENT_IN_FLIGHT=64`; atomic `accepting`/`in_flight`; `idle: Notify`; routes all 11 ProviderType ttrpc method families); `packages/d2b-provider-toolkit/src/conformance.rs` (`check_provider_conformance`, `check_descriptor_conformance`, `ConformanceError` enum); `packages/d2b-provider-toolkit/src/registration.rs` (`register_exact_instances`, `ToolkitError` enum) | +| Tests at main | `packages/d2b-provider-toolkit/tests/conformance.rs` — `every_axis_passes_identical_in_process_and_rpc_conformance` tests all 11 `ProviderType` variants both in-process and through the full RPC path | +| Selected behavior | `ProviderAgentAdapter` is the descriptor-bound validation gate between a ComponentSession and a provider instance; `GeneratedProviderServiceServer` is the agent-side ttrpc dispatch engine; conformance kit provides a reference test harness for every Provider implementation; `register_exact_instances` is the canonical pattern for building a test registry from static descriptors | +| v3 destination | `packages/d2b-provider-toolkit/src/` (adapt in place); conformance tests in `packages/d2b-provider-toolkit/tests/conformance.rs` must pass unchanged after the ADR45 exclusions are adapted | +| ADR45 exclusions | `ProviderAgentAdapter::new()` hard-checks `peer_role == EndpointRole::ProviderAgent` (tag 7) and `service == ServicePackage::ProviderV2` (tag 4) — update the Rust enum variant names if the owning spec renames them per ADR-046-componentsession-and-bus; wire tag values 7 and 4 must not change. `v2_identity::{RealmId, WorkloadId}` appear in test context imports — adapt `RealmId` → `ZoneId`, `WorkloadId` → `ResourceName` on copy | + +### ADR046-nix-031 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-provider-004, ADR046-zone-control-019, ADR046-zone-control-020, ADR046-zone-control-024; Nix resource compiler owner | +| Current source | None — D096/D098 semantic Service/Binding authoring and projection factories are net-new in v3 | +| Reuse action | create | +| Destination | `nixos-modules/resources-sharing.nix`; `nixos-modules/assertions.nix`; `tests/unit/nix/cases/resource-sharing.nix` | +| Detailed design | Compile the eight exact D098 semantic Service/Binding types plus `ResourceExport`/`ResourceImport` without a second vocabulary. Nix authors authority Services and consumer Bindings, never projection Services; Binding requires same-Zone `serviceRef` and allowed target, while all observed realization remains absent from spec. Core receives a stable projection name/factory fingerprint and creates exactly one same-type Service with ResourceImport ownership, `providerRef`, semantic base/import fields, and no `spec.provider`. Reject Device/Endpoint/Binding export/projection, implementation-qualified/former `*State` aliases, implementation detail in base schemas, cross-Zone refs, USB export without every policy opt-in, and any USB/security-key physical resource that does not use the exact Core-derived `physical-usb-backing` tuple class. | +| Integration | The per-Zone generation uses ADR046-provider-004 common schema fingerprints and ADR046-zone-control-019/020 factory metadata; the four initial Provider modules contribute only strict Provider extensions and implementation config. | +| Data migration | Full d2b 3.0 reset; old audio/USB/security-key/observability options lower to new authority Service plus authored Binding only where the owning dossier explicitly preserves migration, and no old public type alias survives | +| Validation | Fast Nix eval/build tests cover all four exact pairs, common base schema discovery, canonical minimal base without `spec.provider`, stable lowering across repeated evaluations, same-Zone refs/targets, strict Provider extension placement, Service-only export, exactly-one Core projection metadata with no `spec.provider`, no auto-Binding, status-field rejection in spec, USB/security-key resolution to one byte-identical Host-global `(Host, physical-usb-backing, opaqueKeyDigest)` collision before effects, Provider-private-class bypass rejection, and rejection of all forbidden names/aliases. | +| Removal proof | Provider-local duplicate Nix base schemas and old direct Device/Endpoint/`*State` projection emitters are removed after the shared emitter and all four dossier migration tests pass. | + +### ADR046-nix-032 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-029; ADR046-nix-027; TargetInput v3 shape per ADR-046-componentsession-and-bus owning spec | +| Current source | Main commit source row below: `packages/d2b-client/src/{client,session,target,service,daemon_service,guest_service,host_socket}.rs` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-client/src/` (adapt in place). | +| Detailed design | Transport-neutral typed async client, ComponentSessionConnector abstraction, precise SessionFailure classification, signed metadata envelopes, named-stream client abstraction, and host-socket connector. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse async client from main commit `a1cc0b2d`; adapt TargetInput, ServiceOwner, service packages, and local socket identity to Zone/ResourceRef semantics.. | +| Integration | CLI/controllers construct TargetInput/MetadataInput; `d2b-client` resolves Zone/Resource routes via `d2b-bus` and reuses ComponentSessionConnector for calls/streams. | +| Data migration | Full d2b 3.0 reset; no v2 client state/config import | +| Validation | Port `d2b-client` tests; add ResourceRef TargetInput and Zone bootstrap socket identity gates. | +| Removal proof | ADR45 TargetInput Realm/Workload forms are replaced by Zone/ResourceRef forms; no prior v3 client owner to remove. | +| Main commit source | `packages/d2b-client/src/client.rs` (`Client`, `ConnectedClient`, `MetadataInput`, `CallOptions`, `CancellationToken`, `Response`, `RetryPolicy`, `SystemClock`, `WallClock`); `packages/d2b-client/src/session.rs` (`ComponentSessionConnector` trait, `ConnectedSession` with `driver: SharedDriver` + `ttrpc_socket: Socket` + `limits: LimitProfile`, `SessionCall`, `SessionReply`, `SessionFailure` enum: BeforeDispatch/Retryable/Ambiguous/Disconnected/Deadline/Cancelled/Protocol, `NamedStream`, `SharedDriver`); `packages/d2b-client/src/target.rs` (`TargetInput` enum, `ServiceOwner`, `ResolvedTarget`, `RouteRecord`, `RouteTable`, `TransportKind`, `TransportSelection`); `packages/d2b-client/src/service.rs` (`GeneratedClient`, `MethodHandle`, `ServiceHandle`, `ServiceKind`); `packages/d2b-client/src/daemon_service.rs` (`DaemonClient`, `DaemonMethod`, `DaemonLifecycleRequest`, `DaemonTerminal`, `daemon_call_options`); `packages/d2b-client/src/guest_service.rs` (`GuestClient`, `GuestOperation`, `GuestCancelCall`, `GuestInspectCall`, `GuestRetainedLogCall`); `packages/d2b-client/src/host_socket.rs` (feature `host-socket`) — `HostSocketConnector`, `local_daemon_endpoint_identity` | +| Tests at main | `packages/d2b-client/tests/client.rs` | +| Selected behavior | Transport-neutral typed async client; `ComponentSessionConnector` abstracts connection setup; `SessionFailure` provides precise failure classification for retry policy; `MetadataInput` constructs signed request envelopes with clock-bounded lifetimes; `NamedStream` exposes named-stream channel as a client-side abstraction; `HostSocketConnector` is the reference Unix socket connection implementation | +| v3 destination | `packages/d2b-client/src/` (adapt in place); client becomes the primary CLI and controller access path for Zone-local and cross-Zone ComponentSession services | +| ADR45 exclusions | `TargetInput::Workload { realm: RealmId, workload: WorkloadId }` → v3 shape addresses resources as `ResourceRef` (e.g., `Zone/`, `Guest/`) per ADR-046-componentsession-and-bus owning spec; `TargetInput::Realm(RealmId)` → `TargetInput::Zone(ZoneId)`; `ServiceOwner::Workload { realm, workload }` → `ServiceOwner::Resource { zone: ZoneId, resource: ResourceName }`. `HostSocketConnector::local_daemon_endpoint_identity()` returns identity pinned to current `d2bd.socket` path — v3 socket path comes from Zone bootstrap config and must not be hard-coded. `DaemonClient`/`DaemonMethod` verb set per resource-api/authz foundation spec | + +### ADR046-nix-033 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-023; ADR046-nix-027; naming and service name per ADR-046-componentsession-and-bus owning spec | +| Current source | Main commit source row below: `packages/d2b-realm-router/src/service_v2.rs` and tests at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/routing/zone_service.rs`. | +| Detailed design | ZoneSessionAuthority preserves no-host-credential custody, gateway/ZoneLink custody behavior, bounded concurrent dispatch, and graceful shutdown semantics. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse routing service behavior from main commit `a1cc0b2d`; rename RealmSessionAuthority/service identifiers to Zone terms per owning bus spec while preserving custody behavior.. | +| Integration | `d2b-bus` Zone service handles route admission and shortcut lifecycle; ZoneSessionAuthority feeds session/audit/metric labels with stable custody state. | +| Data migration | Full d2b 3.0 reset; no v2 routing state import | +| Validation | Port `realm_service_v2.rs` and topology harness tests with Zone naming; add service-name wire gate. | +| Removal proof | ADR45 RealmService name and RealmId fields are adapted to Zone service; no prior v3 router owner to remove. | +| Main commit source | `packages/d2b-realm-router/src/service_v2.rs` (`RealmSessionAuthority` struct with `realm: RealmId`, `peer_role: EndpointRole`, `locality: Locality`, `purpose: PurposeClass`, `custody: CredentialCustody`; `CredentialCustody` enum: None/GatewayGuest; constructors `local_controller()`/`gateway_peer()`/`new()`; `REALM_SERVICE_NAME = "d2b.realm.v2.RealmService"`; constants `DEFAULT_MAX_REALM_BINDINGS=256`, `DEFAULT_MAX_SHORTCUTS=256`, `DEFAULT_MAX_MUTATION_RECORDS=1024`, `DEFAULT_AUDIT_CAPACITY=1024`; internal `MAX_DISPATCH_IN_FLIGHT=64`, `SHUTDOWN_TIMEOUT=5s`; `RealmServiceLimits`; dispatch using `Arc(MAX_DISPATCH_IN_FLIGHT)` and `JoinSet<()>`); `packages/d2b-realm-router/tests/realm_service_v2.rs`; `packages/d2b-realm-router/tests/transport_topology_harness.rs` | +| Tests at main | `packages/d2b-realm-router/tests/realm_service_v2.rs` — routing service tests; `packages/d2b-realm-router/tests/transport_topology_harness.rs` — topology harness | +| Selected behavior | `RealmSessionAuthority` enforces that host-local sessions hold no realm credentials (`CredentialCustody::None`) while gateway sessions hold `GatewayGuest` custody — this is the runtime enforcement of ADR 0032 "relay identity is not local auth"; concurrent dispatch with `Semaphore(64)` bound; 5-second graceful shutdown via `JoinSet` | +| v3 destination | `packages/d2b-bus/src/routing/zone_service.rs`; `RealmSessionAuthority` renames to `ZoneSessionAuthority`; `CredentialCustody` is behavior-stable and requires no rename; `REALM_SERVICE_NAME` updates per ADR-046-componentsession-and-bus owning spec | +| ADR45 exclusions | `realm: RealmId` field in `RealmSessionAuthority` → `zone: ZoneId`; `REALM_SERVICE_NAME = "d2b.realm.v2.RealmService"` → v3 service name per ADR-046-componentsession-and-bus owning spec; `EndpointRole::LocalRootController`, `RealmController`, `RemotePeer` used in `new()` validation — may rename per owning spec; wire tags remain stable; `PurposeClass::Local`/`Enrolled`/`Bootstrap` — confirm per owning spec | + +### ADR046-nix-034 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-029; ADR046-nix-028; ADR046-nix-027; PROVIDER_BUNDLE_VERSION bump required on any bundle artifact format change | +| Current source | Main commit source row below: `packages/d2bd/src/provider_registry.rs` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2bd/src/provider_registry.rs` (adapt in place). | +| Detailed design | Named ProviderCompositionError variants, first-party factory instantiation, bundle resolver validation, version/schema constants, and monotone lifecycle operation IDs. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse fail-closed provider composition from main commit `a1cc0b2d`; adapt v2 identity/bundle artifact types to v3 provider registry contracts and bump bundle versions.. | +| Integration | `provider_registry.rs` composes the v3 provider catalog into active ProviderRegistry snapshots consumed by `d2b-bus`/provider runtime and bundle validation. | +| Data migration | Full d2b 3.0 reset; provider bundle format bumps to v3 and no v2 provider registry state import. | +| Validation | Port provider_registry composition tests; add PROVIDER_BUNDLE_VERSION/SCHEMA_VERSION v3 bump gate and ZoneId/ResourceName binding test. | +| Removal proof | ADR45 `provider_registry_v2` bundle artifact types are replaced by `d2b_contracts::v3::provider_registry` after v3 bundle schema lands. | +| Main commit source | `packages/d2bd/src/provider_registry.rs` (`ProviderCompositionError` enum with 26 named variants including `AzureVmForbidden`, `LegacyRunnerForbidden`, `NondispatchableCapability`, `ProcessIdentityMismatch`, `LifecycleBudgetExceeded`; all first-party factory instantiations: `PipewireVhostUserAudioFactory`, `HostMediatedDeviceFactory`, `WaylandDisplayFactory`, `LocalRealmNetworkFactory`, `LocalObservabilityFactory`, `AzureContainerAppsRuntimeProviderFactory`, `LocalRuntimeProviderFactory` CH/QEMU/systemd-user, `LocalStorageFactory`, `HostSubstrateProviderFactory` Linux/NixOS, `AzureRelayProviderFactory`, `LocalTransportFactory`; constants `PROVIDER_BUNDLE_VERSION: u32 = 13`, `PROVIDER_BUNDLE_SCHEMA_VERSION: &str = "v2"`, `AZURE_VM_IMPLEMENTATION_ID: &str = "azure-vm"`, static `NEXT_LIFECYCLE_OPERATION_ID: AtomicU64`) | +| Tests at main | `packages/d2bd/src/` integration tests exercising composition (search `#[cfg(test)]` blocks in `provider_registry.rs`) | +| Selected behavior | Fail-closed composition: every error is named; `AzureVmForbidden` explicitly rejects non-production implementations; bundle loaded through `load_bundle_resolver()` and validated against `PROVIDER_BUNDLE_VERSION`; `NEXT_LIFECYCLE_OPERATION_ID` provides monotone IDs across restarts | +| v3 destination | `packages/d2bd/src/provider_registry.rs` (adapt in place); `PROVIDER_BUNDLE_VERSION` bumps when bundle artifact format changes; `PROVIDER_BUNDLE_SCHEMA_VERSION` updates from `"v2"` to `"v3"`; `ProviderCompositionError` variants retained with v3-specific variants added | +| ADR45 exclusions | Uses `d2b_contracts::v2_identity::{RealmId, WorkloadId, RealmPath as ProviderRealmPath}` in binding contexts → adapt to `ZoneId`/`ResourceName`; `d2b_contracts::provider_registry_v2` module types (`ProviderBindingV2ConsumerView`, `ProviderRegistryEntryV2`, `ProviderRegistryV2`) are ADR45 bundle artifact types → v3 replaces with `d2b_contracts::v3::provider_registry`; `PROVIDER_BUNDLE_VERSION = 13` is the ADR45 pinned version — a bump is required before v3 adoption; numeric value is determined when the v3 bundle format is finalized in this work item | + +### ADR046-nix-035 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-nix-034; ADR046-nix-029; GuestLifecycleRequest ResourceRef addressing per ADR-046-componentsession-and-bus owning spec | +| Current source | Main commit source row below: `packages/d2bd/src/provider_effects.rs` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2bd/src/provider_effects.rs` (adapt in place). | +| Detailed design | DaemonEffectAdapters bind semantic effect ports; ProviderLifecycleDispatch bounds lifecycle mutations and routes broker start/stop through blocking adapters with idempotency-key deduplication. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse descriptor-bound effect dispatch from main commit `a1cc0b2d`; adapt lifecycle request wire type from VM name to Zone-scoped Guest ResourceRef.. | +| Integration | `ProviderLifecycleDispatch` receives v3 Guest lifecycle operations from `d2b-bus`/core controller and routes broker start/stop through descriptor-bound effect ports. | +| Data migration | Full d2b 3.0 reset; no v2 lifecycle mutation state import | +| Validation | Port `provider_effects.rs` lifecycle dispatch tests; add ResourceRef Guest lifecycle request and BrokerCallerRole compatibility gates. | +| Removal proof | `VmLifecycleRequest`/`vm_name` path is replaced by a v3 ResourceRef Guest lifecycle op after broker/public wire contract versioning. | +| Main commit source | `packages/d2bd/src/provider_effects.rs` (`DaemonEffectAdapters` struct binding all semantic port traits; `ProviderLifecycleDispatch` with `MAX_TRACKED_LIFECYCLE_MUTATIONS=256` and `BTreeMap`; all effect port imports: `AudioEffectPort`/`AudioQueryPort`, `DeviceEffectPort`/`DeviceQueryPort`, `DisplayEffectPort`, `NetworkEffectPort`, `BoundedExportSink`/`ObservabilityExportPort`/`ObservabilityQueryPort`, `RuntimeAdoptionControl`/`RuntimeConfiguredItemControl`/`RuntimeControlPort`/`RuntimeEnsureControl`/`RuntimeOperationControl`/`RuntimePlanDecision`, `StorageEffectPort`, `HostSubstratePort`, `LocalEndpointPort`; lifecycle dispatch functions `dispatch_broker_vm_start_on_blocking_adapter`, `dispatch_broker_vm_stop_on_blocking_adapter`; test helpers `reset_test_runtime_lifecycle_calls()`, `test_runtime_lifecycle_calls()`, `TEST_RUNTIME_START_CALLS`/`TEST_RUNTIME_STOP_CALLS` thread-locals) | +| Tests at main | `packages/d2bd/src/provider_effects.rs` `#[cfg(test)]` blocks; `packages/d2bd/src/provider_registry.rs` composition tests | +| Selected behavior | Each effect adapter is descriptor-bound at composition time in `provider_registry.rs`; `ProviderLifecycleDispatch` tracks in-flight lifecycle mutations with a bounded BTreeMap and idempotency-keyed deduplication; `dispatch_broker_vm_start/stop_on_blocking_adapter` routes to the broker via a blocking task adapter; test helpers provide per-test reset of lifecycle call counters | +| v3 destination | `packages/d2bd/src/provider_effects.rs` (adapt in place); effect port bindings retained; lifecycle dispatch updated from `VmLifecycleRequest` to a v3 Guest lifecycle op addressed by `ResourceRef` (`Guest/`) with Zone context (`Zone/`); exact wire type per ADR-046-componentsession-and-bus owning spec | +| ADR45 exclusions | `VmLifecycleRequest` from `d2b-contracts/src/public_wire.rs` uses `vm_name: String` (ADR45 daemon wire); v3 requires a `GuestLifecycleRequest` or equivalent with `ResourceRef` addressing; `BrokerCallerRole` from `broker_wire.rs` is an ADR45 broker identity type — keep in place and flag for broker wire update when broker contract is versioned; `DaemonAuditSinkStatus` references current audit shape — keep until v3 audit contract is defined | diff --git a/docs/specs/ADR-046-primitive-resource-composition.md b/docs/specs/ADR-046-primitive-resource-composition.md new file mode 100644 index 000000000..c31b59d20 --- /dev/null +++ b/docs/specs/ADR-046-primitive-resource-composition.md @@ -0,0 +1,449 @@ +# ADR 0046 primitive ResourceSpec composition + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-primitive-resource-composition` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | System/Host/Guest/Process/Volume Provider contracts | +| Depends on | `ADR-046-resource-object-model`, `ADR-046-resource-reconciliation` | +| Supersedes | Current implementation-shaped ProcessRole composition | + +## ResourceType threshold + +A behavior is a separate ResourceType only when it needs at least one: + +- independent identity; +- independent lifecycle; +- independent controller/status; +- independent owner/finalizer; +- sharing by several parents/consumers; +- Provider substitution. + +Otherwise it is a field or usage edge inside its parent ResourceSpec. + +## Frozen standard catalog + +Core control: + +| ResourceType | Responsibility | +| --- | --- | +| Zone | Zone self identity, policy, API/store status | +| ZoneLink | Child-local uplink delegation, transport, cursor/health; compiler-only `parentZone` selects the parent allocator | +| Provider | Installed package/config/controllers/schemas/services/status | +| Role | Bounded native RBAC rules | +| RoleBinding | Subjects to Role with narrowing; no time-based expiry | +| Quota | Zone-wide/shared aggregate ceilings and observed usage | +| EmergencyPolicy | Disable scopes/actions and emergency status | + +Standard execution/shared: + +| ResourceType | Responsibility | +| --- | --- | +| Host | Physical/local host execution/policy/budget parent | +| Guest | VM, sandbox, cloud, or remote execution/policy/budget parent | +| Process | Long-lived supervised process | +| EphemeralProcess | One-shot asynchronous process with result retention | +| Volume | Shareable storage, layout/ACL/views, Host/Guest attachments | +| Network | Independently shared network fabric | +| Device | Inventoried/exclusive-or-shared device arbitration | +| User | Named identity, UID/session observations, ACL/process subject | +| Credential | Opaque rotating credential/lease lifecycle | +| Endpoint | Stable managed service/device/transport/control/data attachment point (D092); no raw locator | +| ResourceExport | Cross-Zone share of a scarce singleton resource via a single Provider authority; no cross-Zone Ref (D096) | +| ResourceImport | Cross-Zone share of a scarce singleton resource via a single Provider authority; no cross-Zone Ref (D096) | + +Provider-specific semantic ResourceTypes may extend this set, always qualified +`.d2bus.org.` (for example `display-wayland.d2bus.org.WaylandSession`). +Standard types above are always unqualified; a Provider-specific type name is +never bare/unqualified. + +Whether a given entity is a standard/qualified ResourceType or a permitted +opaque handle is decided by the entity promotion test in +`ADR-046-resource-object-model` § Entity promotion test (D092): a stable, +cross-boundary, independently-managed identity is a ResourceType; a +controller-internal or high-churn handle (pidfd, fd index, per-session named +stream, `OwnedTransport`, `operationId`) stays an opaque ID. + +## ResourceSpec shape + +### Three-layer spec shape (D089) + +D089 freezes every primitive ResourceSpec as three layers. Layer 1 is the +universal Resource envelope and metadata. Layer 2 is the ResourceType base spec +at top-level `spec.*`, including `spec.providerRef`; provider-neutral required +and optional fields plus shared semantics live there. Layer 3 is the optional +canonical selected-Provider extension +`spec.provider = { schemaId, schemaVersion, settings }`; it is the only +Provider-specific desired extension for primitive ResourceTypes and for any +qualified Provider-defined ResourceType. It omits `providerRef` and +`observedProviderGeneration`: `spec.providerRef` is base, and spec is desired +rather than observed. + +Every Provider `ResourceApiBinding` MUST implement the exact base spec schema +version and fingerprint for each ResourceType it serves, accept the canonical +minimal valid base Spec, and pass base lifecycle/status/error/finalizer +conformance. A Provider MAY reject an optional base capability only through its +signed standard capability matrix and a typed provider-neutral +`unsupported-capability` error; it MUST NOT ignore, reinterpret, rename, +duplicate, weaken, or require extension data for base-required behavior. +`spec.provider.settings` is strict deny-unknown, bounded, schema-versioned and +digested, validated against `spec.providerRef` at Nix build and API admission, +and fails with `spec-provider-schema-invalid` or `spec-provider-shadow` when +invalid or shadowing/restating/overriding/renaming/duplicating a base field. +Shared fields and semantics are promoted to the ResourceType base and never live +in `spec.provider`; generic CLI/controllers operate on base spec plus base +status. For the same Provider, the `spec.provider` and `status.provider` schemas +align. + +The Provider ResourceType is the single D075 exception: it keeps +`spec.artifactId` and `spec.config` as its self-description because a Provider +has no non-circular `spec.providerRef` and therefore does not use the +`spec.provider` extension envelope for its install spec. + +## Folded fields + +### Host and Guest shared ExecutionPolicy + +Inline: + +- Host/Guest Provider-specific type/settings; +- defaultDomain/allowedDomains/defaultUserRef; +- optional `quotaRef` plus inline requested CPU/memory/pids/fds/I/O/storage/ + network amounts; +- network attachments; +- device attachments; +- Volume attachment defaults; +- provider-specific settings; +- boot/identity/capability requirements. + +### Process and EphemeralProcess + +Inline: + +- sandbox namespaces/seccomp/capabilities/LSM; +- CPU/memory/pids/fds/I/O/thread/concurrency budgets (canonical nested `budget`); +- plain `template` ID and sealed `configRef`/`credentialRefs` (never a + command/binary/argv field); +- Volume mounts by volumeRef/view/mountPath/access; +- `networkUsage`/`deviceUsage` (never `network`/`devices`); +- `endpoints`; +- bus/telemetry bindings; +- readiness/health/deadlines; +- user-domain portals. + +### Volume + +Inline: + +- files/directories; +- state/tmp/cache; +- owner/group/mode; +- access/default ACLs; +- no-follow/inheritance/recursive; +- create/repair/cleanup/restart/adoption; +- named views; +- same-Zone Host/Guest attachments and transport settings. + +### Internal-only + +Not ResourceTypes: + +- controller instances; +- pidfds; +- redb/OFD/controller locks; +- transient leases that have no independent external lifecycle; +- broker operations/syscalls; +- sandbox compiler fragments; +- process-group/cgroup implementation handles; +- route/session implementation handles. + +## Host + +Physical/local execution is an explicit Host: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Host +metadata: + name: host-system + zone: dev +spec: + providerRef: Provider/system-core + defaultDomain: system + allowedDomains: [system, user] + budget: { ... } +status: + observedGeneration: 1 + phase: Ready + ... +``` + +A Zone may have several Hosts: + +- system-default Host accepting system and selected user processes; +- user-default Host for one configured defaultUserRef that rejects + system processes; +- additional policy/budget-separated Hosts. + +The v3 successor to unsafe-local is a user-only Host: + +```yaml +providerRef: Provider/system-core +defaultDomain: user +allowedDomains: [user] +defaultUserRef: User/alice +``` + +Its status/UI preserves an explicit no-isolation posture. It cannot satisfy an +isolated/managed/credential-separated policy and never falls back from another +Host/Guest Provider. It is not a separate Provider. + +## Guest + +VM/sandbox/remote Guests select their own installed Provider. +They use the same ExecutionPolicy fields as Host and may add Provider-specific +boot/identity/runtime settings. + +## Process common execution spec + +Process and EphemeralProcess share: + +```yaml +providerRef: Provider/system-systemd +executionRef: Host/host-system +domain: system # optional; defaults from Host/Guest ExecutionPolicy +userRef: null # required/inherited for user +processClass: controller # controller | service | worker +template: controller-main +configRef: Volume/example-config +credentialRefs: [] +mounts: + - volumeRef: Volume/example-state + view: controller + mountPath: /state + access: read-write +sandbox: { ... } +budget: { ... } +networkUsage: null +deviceUsage: [] +telemetry: { ... } +# no inline endpoints (D092): stable endpoints are owned Endpoint resources with producerRef +``` + +The owning semantic Provider is metadata.ownerRef; its signed component/ +process template supplies the executable package/digest. `template` is a plain +bounded ID, not a ResourceRef. No free-form executable, raw host path, numeric +UID/GID, raw seccomp program, +ambient capability list, caller-selected broker op, credential bytes, or +arbitrary socket address is accepted. Package/template/provider schemas resolve +those implementation details. These are the exact frozen common field names +(see `ADR-046-resources-host-guest-process-user`); they are never renamed to +`network`, `devices`, a command/binary/argv field, or an endpoint kind/path/ +service field. + +## Process + +Adds: + +- desired lifecycle; +- readiness/health; +- restart/backoff; +- adoption/drain; +- long-lived retention; +- provider-specific process identity. + +Status includes mandatory locally held pidfd evidence (never the fd/PID in +public status), wait/reap owner, phase/conditions, ready/exit observations, and +resource revisions. + +## EphemeralProcess + +Adds: + +- one-shot input; +- start/runtime deadlines; +- terminal output handles/digests; +- successfulTtl default `1h`; +- failedTtl default `24h`. + +It is the one-shot process itself, not a Job that references a Process. +Pending/running does not expire. Terminal cleanup begins at completedAt, respects +finalizers/incident hold, and writes cleanupEligibleAt. + +## Process Providers + +### system-systemd + +- non-forking transient system unit/scope or user scope; +- unit InvocationID+cgroup+MainPID/start-time binding; +- opens/verifies mandatory pidfd; +- systemd owns wait/reap; +- exact userRef for user scope; +- no per-Provider static PID1 template unit. + +### system-minijail + +- Process controller calls the `ProcessLaunchEffectPort` (ProviderSupervisor) + with the resource UID and compiled sandbox/resource digests; the effect + adapter resolves the plan and the broker performs the spawn; +- clone3(CLONE_PIDFD); +- d2b owns wait/reap; +- pidfd/cgroup/process start identity; +- no direct Provider broker access — the Provider process never imports or + calls the broker itself. + +Both implement identical ResourceTypes and status/error conformance. + +## system-core bootstrap + +The fixed core-controller process is also Provider/system-core. It and the +fixed Provider/system-minijail controller are the two Provider bootstrap +exceptions. system-core: + +- reconciles Host resources; +- discovers/reconciles local User resources/status; +- runs before the first Host exists. + +It does not implement Process, EphemeralProcess, Volume, Network, Device, or +Credential. + +Every Provider/controller other than those two bootstrap controllers is +represented by a Process under a Host or Guest. + +## Volume + +Volume spec preserves fine-grained current storage policy: + +```yaml +providerRef: Provider/volume-local +source: + executionRef: Host/host-system + settings: + kind: local-path + sourcePolicyId: +layout: + - path: state + type: directory + ownerRef: User/example-system + groupRef: User/example-system + mode: "0700" + accessAcl: [] + defaultAcl: [] + noFollow: true + recursive: false + sensitivity: private + createPolicy: create-if-never-provisioned + repairPolicy: exact-owner + cleanupPolicy: owner-controlled +views: + controller: + path: state + rights: [read, write, create, delete, traverse] +attachments: + - executionRef: Guest/dev-vm + transport: virtiofs + mountPath: /mnt/state + view: controller + access: read-write +``` + +`source.settings` never carries a raw host path in the authored spec. +`sourcePolicyId` is an opaque bounded string ID; the Provider and its +controller see the ID and semantic settings only. Raw path resolution is +private Nix/bundle/effect authority (see `ADR-046-resources-volume`). The +`transport: virtiofs` attachment is served by the separate `volume-virtiofs` +Provider, which owns only the attachment lifecycle/status and its owned +virtiofsd Process — never the Volume's own `providerRef`/layout/ownership +fields. + +All layout paths are relative to the anchored Volume root. A raw host path is +never an authored spec field; the `source.settings.sourcePolicyId` on the +`local-path`/`block-image` source kinds resolves, only inside the Volume +Provider's private Nix/bundle/effect authority, to the actual host path +against that Provider's allowlisted root policy. It never appears in public +status/audit, and it never reaches the Provider process as a literal path. + +Virtiofs attachment controller may create: + +- owned Host EphemeralProcess/Process for setup/virtiofsd; +- target Guest Provider attachment update; +- status per export and guest mount. + +Process mounts independently select Volume view/access for sandbox exposure. + +## Composite ownership example + +Cloud Hypervisor Guest owns: + +- VMM Process under Host; +- virtiofsd Process per Volume attachment; +- audio/video/GPU worker Processes where required; +- Provider-specific endpoint/device resources only when they satisfy the + separate ResourceType threshold. + +If any child mutates, Guest owner reconcile runs and reasserts the complete +child graph. + +Leaf Process Providers create no child merely to represent their internal +systemd unit/minijail state; they write Process status. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `d2b-core/src/processes.rs`, `storage.rs`, `sync.rs`; Nix processes/storage/sync emitters; d2bd DAG; broker SpawnRunner; unsafe-local scopes; guest exec | +| Evidence class | Current process/storage contracts are reachable/generated; unified minimal resources are ADR-only | +| Behavior retained | Fine-grained ownership/ACL/no-follow, minijail profiles, pidfd/adoption, systemd user scopes, typed role/readiness, state fail-closed | +| Required delta | Host/Guest parent model, common Process/EphemeralProcess, generic Volume, Provider implementations, owner child graphs | +| Reuse path | Exact ProcessRole/storage-path work items copy/adapt current algorithms/tests | +| Replacement/deletion | No current role/path is removed without a resource/Provider successor | +| Feasibility proof | Host system/user Process; non-Host Process; virtiofs host-path→guest-mount Volume | +| Future owner | Resource/Provider dossier work items | + +## Implementation work items + +### ADR046-primitives-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0; resource contracts | +| Current source | `packages/d2b-core/src/processes.rs`, `minijail_profile.rs`, `storage.rs`; `d2b-contracts/src/broker_wire.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/host.rs`, `guest.rs`, `execution_policy.rs`, `process.rs`, `volume.rs`, `user.rs`, `network.rs`, `device.rs`, `credential.rs` | +| Detailed design | Complete minimal ResourceType schemas and shared execution/Volume sub-schemas Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Provider dossiers/controller descriptors bind exact types | +| Data migration | Full reset | +| Validation | Schema vectors and folded-field/no-duplicate-type policy tests | +| Removal proof | Old DTOs removed only by owning future slices | + +### ADR046-primitives-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | Process contracts; system Provider slices | +| Current source | broker SpawnRunner/pidfd; d2bd supervisor; unsafe-local helper; guest exec runner | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-systemd/`, `packages/d2b-provider-system-minijail/`, shared neutral process conformance library | +| Detailed design | Common Process/EphemeralProcess, provider-specific launch/pidfd/wait/adoption/status Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Process controller registration under Host/Guest; d2b-bus ResourceClient/status | +| Data migration | Current ProcessRoles converted by exact disposition table | +| Validation | Shared conformance plus Host/Guest/user integration | +| Removal proof | Role branches removed only after successor Provider tests | + +### ADR046-primitives-003 + +| Field | Value | +| --- | --- | +| Dependency/owner | Volume contract; Volume Provider slices | +| Current source | `storage-json.nix`, `d2b-core/src/storage.rs`, store/TPM/runtime path owners, virtiofsd argv/ProcessRole | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-volume-*/`; `nixos-modules/resources-volume.nix` | +| Detailed design | Fine-grained Volume layout/views, host-path policy, virtiofs attachments/status/owned Process Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Host/Guest/Process refs and Volume controller | +| Data migration | Full reset; Provider-specific state export only where separately specified | +| Validation | ACL/no-follow/marker, sharing/views, virtiofs host/guest mount tests | +| Removal proof | storage.json rows removed only after Volume successor parity | diff --git a/docs/specs/ADR-046-provider-model-and-packaging.md b/docs/specs/ADR-046-provider-model-and-packaging.md new file mode 100644 index 000000000..3fca6854f --- /dev/null +++ b/docs/specs/ADR-046-provider-model-and-packaging.md @@ -0,0 +1,688 @@ +# ADR 0046 Provider model and packaging + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-model-and-packaging` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | Provider contracts/toolkit, package catalog, Nix integration | +| Depends on | `ADR-046-resource-object-model`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-primitive-resource-composition` | +| Supersedes | Current direct Provider construction/composition | + +## Provider resource + +A Provider is installed in a Zone as: + +```text +Provider/ +``` + +Package presence alone is not installation. providerRef resolves only a Ready +Provider resource in the same Zone. + +Provider ResourceSpec is exactly: + +```yaml +spec: + artifactId: # selects a signed Nix artifact-catalog + manifest entry + config: {} # root configuration validated against the manifest's signed JSON Schema +``` + +No other field is authored in Provider spec. Every other Provider property is +resolved from the signed manifest/catalog entry the `artifactId` selects, not +authored or independently duplicated in the resource row: + +- exact package/executable/manifest/config/schema/service digests; +- publisher/signature/trust/conformance/provenance/SBOM identity; +- support channel and compatibility range; +- exported ResourceTypes/schemas; +- controller component descriptors; +- service component descriptors; +- worker Process templates; +- dependency aliases; +- permission claims; +- CLI projection; +- events/telemetry/state contracts; +- per bound ResourceType, the exact base spec and base status schema version/fingerprint the `ResourceApiBinding` implements, and the signed **standard capability matrix** of supported/unsupported optional base capabilities (D089); +- signed export/import adapter capabilities and one projection factory for each + capability the Provider marks cross-Zone exportable (D096); +- registered `spec.provider` extension schemas (D089): per owned or bound ResourceType, the qualified `schemaId`, `schemaVersion`, and signed strict JSON Schema for the `spec.provider.settings` object the Provider accepts; +- registered `status.provider` extension schemas (D088): per owned or written ResourceType, the qualified `schemaId`, `schemaVersion`, and signed strict JSON Schema for the `status.provider.details` object the Provider may write; +- component placement templates; +- upgrade/drain/restart policy. + +Manifest/package/component/resource/status/generated properties are read-only +derived data, never authored Provider spec fields. Core ProviderDeployment +reads this signed manifest/catalog entry and creates the Provider's static +component graph from it (see `ADR-046-components-processes-and-sandbox`). + +**Base-schema conformance and extension registration (D088/D089).** Each +`ResourceApiBinding` declares and MUST implement the exact ResourceType base +spec/status schema version/fingerprint, accept the canonical minimal valid base +Spec, and pass the base lifecycle/status/error/finalizer conformance suite. A +Provider that accepts a Provider-specific `spec.provider` extension or writes a +`status.provider` extension registers both extension schemas in its signed +manifest: the qualified immutable `schemaId`, the `schemaVersion`, and the strict +JSON Schema for `settings`/`details` (unknown-field-denied, size/cardinality +bounded; `details` additionally redacted/non-secret). The resource store +validates every `spec.provider`/`status.provider` write against the installed +Provider's registered schema at Nix build and API admission, rejecting an +unregistered/version-mismatched `schemaId`/`schemaVersion`, an unknown field, or +a `settings`/`details` that shadows a base field. A Provider MAY refuse an +optional base capability only via its signed capability matrix and the +provider-neutral `unsupported-capability` result; it never ignores, reinterprets, +renames, duplicates, or weakens a base field. Fields shared across +implementations of a ResourceType are promoted to the ResourceType base +(`spec.*`/`status.resource`) and are never registered or written under +`spec.provider`/`status.provider` (see `ADR-046-resource-object-model` § Spec / +§ Status). The `spec.provider` and `status.provider` schemas align for the same +Provider. + +This rule applies unchanged to D096 Service/Binding types: their qualified +namespaces and base spec/status are semantic and provider-neutral. `providerRef` +selects the implementation, strict `spec.provider` carries only that +implementation's settings, and every implementation MUST accept the canonical +minimal base without `spec.provider`. PipeWire, OTEL, USBIP, CTAPHID, package, +binary, and adapter details never enter the semantic base; `providerRef` remains +the sole opaque implementation selector. + +**Common semantic contract catalog (D098).** The canonical base DTOs and schemas +are owned by the shared contract catalog, not by any initial implementation +crate. They are discoverable before a Provider package is selected: + +| Family | Service ResourceType | Binding ResourceType | Shared contract module | +| --- | --- | --- | --- | +| audio | `audio.d2bus.org.AudioService` | `audio.d2bus.org.AudioBinding` | `d2b-contracts::v3::semantic_services::audio` | +| security key | `security-key.d2bus.org.SecurityKeyService` | `security-key.d2bus.org.SecurityKeyBinding` | `d2b-contracts::v3::semantic_services::security_key` | +| telemetry | `telemetry.d2bus.org.TelemetryService` | `telemetry.d2bus.org.TelemetryBinding` | `d2b-contracts::v3::semantic_services::telemetry` | +| USB | `usb.d2bus.org.UsbService` | `usb.d2bus.org.UsbBinding` | `d2b-contracts::v3::semantic_services::usb` | + +For each catalog ResourceType `.`, its common schema IDs are +`//spec` and `//status`. This slash form is a +schema identity only; the API ResourceType remains the exact dot-qualified name +in the table, and a ResourceRef appends `/` to that ResourceType. + +Each module exports the strict base spec/status DTOs, schema IDs, versions, +fingerprints, minimal valid fixtures, and projection-factory type binding. Every +Provider implementation binds those exact fingerprints and adds only its own +strict extension schemas. It cannot copy, fork, weaken, or privately redefine +the base. There are no implementation-qualified or former `*State` aliases. + +**Export/import adapters and projection factories (D096).** A Provider that +marks a capability cross-Zone exportable MUST advertise signed `ExportAdapter`, +`ImportAdapter`, and `ProjectionFactory` metadata. Each factory is immutable +within a Provider artifact and binds: + +| Field | Contract | +| --- | --- | +| `serviceType` | One qualified semantic/provider-neutral `*Service` ResourceType; this is both the owner authority type and consumer projection type | +| `bindingType` | Its qualified semantic/provider-neutral `*Binding` ResourceType expressing local consumer intent | +| `allowedBackingRefTypes` | Closed set of same-Zone `Device`, `Endpoint`, or qualified semantic backend types the owner Service may reference | +| `allowedBindingTargetRefTypes` | Closed subset of `Guest`, `User`, and `Zone` that a Binding may target | +| `projectionSchema` | Signed, strict, deny-unknown semantic base schema for the projection Service; contains only standard `providerRef`, semantic base/import fields, and no `spec.provider`, implementation-specific field, raw locator, path, credential, secret, FD, or bytes | +| `projectionSchemaFingerprint` | SHA-256 of that canonical schema | +| `factoryFingerprint` | SHA-256 binding all fields above plus the semantic projection-protocol version; never Provider/adapter identity or implementation detail | + +The owner `*Service` is the one real authority and references its local backing. +`ResourceExport.resourceRef` MUST target that Service, never a `Device`, +`Endpoint`, or `*Binding`. An import creates exactly one same-qualified-type local +projection Service (`ownerRef: ResourceImport/`). It never creates a +Device, Endpoint, or Binding. Operators/Nix author matching local Binding +resources that reference `serviceRef` plus an allowed consuming target. Binding +spec is desired intent only; all observations are written only to `status`. The +Provider's Binding controller creates owned Process/Endpoint children. +High-churn leases, sessions, ceremonies, transfers, and streams remain internal +records. + +The adapters perform only semantic admission, arbitration, projection +materialization, and bounded observation. Core owns `ResourceExport`/ +`ResourceImport` routing, base lifecycle, projection-Service ownership, and +layered status writes. Provider install, Nix build, and API admission all fail +closed if a required factory is absent, its signature is invalid, the Service/ +Binding pair or allowed refs do not match, or either fingerprint differs from +the advertisement/import expectation. `Service` and `Binding` remain qualified +semantic types and do not enlarge the 19-type standard catalog. Export/import +preserves `serviceType` exactly even when owner and consumer select different +conformant implementations through local `providerRef`. It never copies or +requires the remote `spec.provider` extension, and Core does not synthesize a +local one. Routing derives from the signed local Provider descriptor, +`providerRef`, and ResourceImport record; implementation observation is +permitted only in `status.provider`. The strict +descriptor accepts only `bindingType` and `allowedBindingTargetRefTypes`; the +former `stateType`/`allowedStateTargetRefTypes` spellings and qualified `*State` +types are not aliases and are rejected. + +**Authority descriptors (D097).** For every scarce or singleton backing it owns +(a physical device, singleton external service, per-Zone/Host/user/seat service, +fixed listener/store, or globally-unique policy), a Provider descriptor MUST +declare a signed `AuthorityDescriptor` on the owning typed Resource: +`authorityScope`, an opaque non-authorizing `authorityKey` class, +`cardinality`, `arbitration`, the single `authorityRef` owner service, a typed +`duplicateConflict`, an adoption/restart `ownerProof`, an update/recycle +strategy, `exportability` (`forbidden`/`explicit-export`, cross-Zone only via +D096), and a quota/fairness policy (see +[`ADR-046-resource-object-model` §Authority and cardinality](ADR-046-resource-object-model.md)). +Backing Device/Endpoint/backend descriptors remain `forbidden`; only an +approved qualified owner `*Service` descriptor may carry `explicit-export`. +The descriptor is not a new opaque public ID and adds no new ResourceType unless +an audit proves no existing type can own the lifecycle. Core rejects a +conflicting authority Resource/Process against its authority index before any +external effect; a shared/multiplexed backing still has exactly one authority +owner. + +**Currency, upgrade, and expedited reconcile (D090/D091).** Every controller +component implements, alongside ordinary reconcile, the toolkit methods +`assess_update`, `plan_upgrade`, and `execute_upgrade`, and populates the +universal `status.update` currency object. A controller MUST report +`UpgradeRequired` for a disruptive change rather than apply it in place; +non-disruptive changes reconcile normally. Upgrades preserve the Resource +UID/spec identity and durable/state/secret Volumes and TPM identity where +possible, recycling only realization and owned ephemeral Processes/endpoints; +`Replace` of the resource-row identity is used only when explicitly required and +planned with ownership/state transfer. The signed capability matrix and base +conformance suite (D089) additionally cover currency states, disruption classes, +and the expedited (`waitForReconcile`) path — a controller performs no external +effect, finalizer release, or status mutation on an expedited mutation until it +receives Core's `CommittedRevisionProof`, and it returns a bounded +disposition/projection for the one expedited pass. Upgrade disruption policy is +a provider-neutral base field; a `spec.provider` extension may add +implementation knobs but cannot bypass it. + +Provider status contains: + +- common resource status; +- package/trust/API/conformance result; +- required/optional component status; +- exported ResourceType/service readiness; +- dependency health; +- controller leases/watch status; +- state schema/migration health; +- disabled/quarantined condition; +- aggregate Provider generation. + +Provider status is derived, not self-declared: core computes the aggregate +Provider status from component/dependency/process health. A Provider +controller writes status only for the ResourceTypes it owns and for the +authorized children/status fields its descriptor grants; it never authors its +own aggregate Provider-resource status. When a Provider controller writes an +owned resource's status it writes the three layers (universal base, +`status.resource`, and its own `status.provider`) atomically in one mutation +(D088). + +## Crate/package boundary + +Every Provider maps to one independently buildable crate and signed package. + +One Provider crate: + +- declares one Provider identity; +- may build several controller/service/worker binaries; +- may share an internal library among those binaries; +- depends only on public neutral contracts/toolkit/SDK crates and approved + ecosystem dependencies; +- does not import d2bd, broker, Zone-store, Nix-emitter, or another Provider's + implementation internals; +- has one Nix package/conformance output; +- has one `ADR-046-provider-.md` dossier. + +Every Provider crate contains: + +```text +d2b-provider--/ + src/ + tests/ + integration/ + README.md +``` + +- `src/`: implementation, component binaries, internal modules, and colocated + unit tests; +- `tests/`: hermetic Cargo integration, ResourceType/controller conformance, + fault, redaction, schema, and fake-port tests; +- `integration/`: heavier container/Host/Guest/cross-process/provider-system + fixtures and scenarios invoked by existing repository test orchestration; +- `README.md`: Provider identity/config, ResourceTypes, controllers/services/ + workers/binaries, placement, dependencies/RBAC, security/state/telemetry, + build/test/integration commands, and standalone-repository consumption. + +Workspace policy rejects a Provider crate missing any of these paths. + +This boundary must allow moving the crate to its own GitHub repository without +splitting semantics or copying daemon internals. + +Common libraries are Provider-neutral. A common library cannot register a +second Provider identity or become a hidden multi-Provider composition binary. + +## Provider components + +Component types: + +| Type | Responsibility | +| --- | --- | +| controller | Owns one or more ResourceTypes and async reconcile loop | +| service | Serves typed runtime/internal ComponentSession methods; no ResourceType ownership | +| worker | Narrow Process/EphemeralProcess with no ResourceClient, d2b-bus/dependency-portal, Credential, CLI, broker, or child-spawn authority; all resources/FDs/config are inherited via LaunchTicket | + +Every component is a separate Process except the fixed system-core and +system-minijail bootstrap controllers. Core ProviderDeployment creates every +component's static Process (per the signed manifest's component descriptors) +and, before launching that Process, only the state Volumes the component has +**declared** under the storage-need test, as part of the Provider's optional +**ProviderStateSet** (`ADR-046-provider-state`: the logical, query-time +grouping of the *declared* Volume resources owned by `Provider/` — not a +ResourceType or a stored artifact of its own, and empty for a Provider that +declares no state Volume). Bounded non-secret operational state belongs in the +owning resource's `status` subresource and the core Operation ledger by default +(D087); a component declares a state Volume only when a specific payload is a +secret or sensitive private datum, is large/binary/file content, is private +data unsafe for status readers, or is bounded but revision-unsuitable with a +demonstrated recovery need. A stateless component declares no state Volume, +receives none, and contributes none to the ProviderStateSet; there is no empty +identity-only Volume and no separate "compartment" concept. A Provider +controller never bootstraps its own Process; it may only create authorized +dynamic children (further Process/EphemeralProcess or other primitive/vendor +resources) once it is itself running. Creating a declared state Volume normally +requires a `Provider/volume-local` controller instance to be running on that +same execution target (Host, Guest, or user-domain local-storage owner); +because the fixed bootstrap components (`system-core`, `system-minijail`, and +the first `volume-local` instance on each target) keep their bounded non-secret +operational state in `status`/the core Operation ledger and declare no state +Volume, no component needs a Volume before a `volume-local` instance is Ready, +so there is no bootstrap state-Volume cycle and no bootstrap-storage exception +(D086, superseded by D087). See +`ADR-046-components-processes-and-sandbox` for the full static-deployment and +optional-component-state-Volume contract, and `ADR-046-resources-volume` for +the canonical Volume schema every declared state Volume uses. + +Descriptor fields include: + +- component ID/type/binary/template; +- exported ResourceTypes/methods; +- supported Host/Guest Provider capabilities; +- allowed `system|user` domains; +- cardinality; +- config projection; +- required/optional dependencies; +- ResourceRefs/templates it may create/use; +- state/Volume views; +- Process Provider selection constraints; +- permission claims; +- readiness/health/drain; +- process/sandbox/budget maximums. + +The same ResourceType is declared once. Several controller instances may run +under different Hosts/Guests/domains without duplicate Process schemas. + +## system-core bootstrap + +The one fixed core-controller process per Zone is also +`Provider/system-core`. It and the fixed Provider/system-minijail controller are +the only Providers not represented by Process resources. + +It owns: + +- Host reconciliation; +- local User discovery/status. + +It does not own: + +- Process/EphemeralProcess (`system-systemd`, `system-minijail`); +- Volume; +- Network; +- Device; +- Credential; +- semantic runtime/desktop/cloud resources. + +After system-core creates the first Host, system-minijail launches every other +Provider/controller/service/worker as a Process under a Host or Guest. + +## Process Provider family + +### Provider/system-systemd + +Implements Process and EphemeralProcess for systemd-capable Hosts/Guests: + +- non-forking transient system service/scope; +- transient user scope through fixed user supervisor; +- InvocationID+cgroup+MainPID/start-time verification; +- mandatory local pidfd; +- systemd wait/reap ownership; +- no per-Provider static PID1 template units. + +Neither system-systemd's controller nor the process it launches calls +systemd's D-Bus/socket API or `pidfd_open` directly; it validates the +ExecutionSpec/SandboxSpec and calls the `ProcessLaunchEffectPort` +(ProviderSupervisor), which is the sole caller of the systemd effect owner. + +### Provider/system-minijail + +Implements the same ResourceTypes: + +- compiled inline Process sandbox; +- clone3(CLONE_PIDFD); +- d2b wait/reap ownership; +- cgroup/namespace/FD/adoption validation. + +system-minijail's controller never imports or calls the broker directly. It +validates the ExecutionSpec/SandboxSpec and calls the `ProcessLaunchEffectPort` +(ProviderSupervisor) with the resource UID and compiled sandbox digest; +ProviderSupervisor is the sole caller of the broker's `clone3`/spawn effect. + +Future Process Providers pass the same conformance without schema changes. + +## Configuration projection + +One Provider-owned root JSON Schema is evaluated before launch. The signed +component graph defines deterministic projections: + +- fields visible to each component; +- defaults/validation; +- sensitivity; +- ResourceRef/dependency bindings; +- component schema digest. + +Components cannot read sibling config. Secrets are Credential refs, not config values. A signed Provider component may +be selected as a raw-token consumer only through the Credential spec/RBAC and +the KK end-to-end sensitive ComponentSession contract. Root/component digests +bind Provider resource, Process resources, ComponentSessions, state, status, +and audit. + +## Provider dependencies + +Manifest declares aliases: + +```text +runtime +volume +network +credential +transport +``` + +Zone config binds each alias to an exact Provider ResourceRef/service +fingerprint. A component asks d2b-bus for an alias; it never receives a global +registry/route table or arbitrary Provider endpoint. + +Synchronous dependency cycles fail configuration. Optional dependencies produce +declared degraded behavior only. + +## Package catalog + +Nix authoring first declares derivations separately: + +```nix +d2b.artifacts.provider-wayland = { + package = inputs.wayland-provider.packages.${system}.default; + type = "provider"; +}; +``` + +The Provider ResourceSpec then uses `artifactId = "provider-wayland"`. Nix +compiles an offline sorted exact-digest catalog: + +- Provider/package/publisher/version; +- package/executable/manifest/component/descriptor/config digests; +- systems/platform; +- API/service compatibility; +- signature/root epoch/revocation/deny status; +- provenance/SBOM/license/vulnerability evidence; +- conformance attestation; +- support channel; +- support contact. + +Selection is exact digest. No runtime marketplace, download, PATH scan, +directory discovery, latest, or version-range solving. + +Artifact is not a ResourceType; `artifactId` is a plain bounded ID, not a +ResourceRef. The private catalog may retain a Nix store path for activation, +but resource spec/status/audit never expose it. + +## Trust + +Production admission requires: + +- exact digest; +- trusted publisher/root epoch; +- valid signature/rotation/revocation; +- no emergency deny; +- accepted provenance/SBOM/license/vulnerability policy; +- exact package/API conformance attestation. + +First- and third-party Providers use the same admission and sandbox. Trust does +not bypass runtime restrictions. + +## Compatibility + +- Provider API major exact; +- minor additive only; +- protobuf numbers never reused; +- exact descriptor fingerprint selected before launch; +- no handshake downgrade/fallback; +- removal after deprecation window or new major; +- state schema compatibility/migration checked independently. + +## Distribution bundles + +A bundle is a signed package catalog only. It does not: + +- merge Providers into one process/sandbox; +- union config/permissions; +- apply last-wins overrides; +- provide runtime discovery. + +Duplicate Provider names, command namespaces, ResourceTypes, incompatible +fingerprints, or policy conflicts reject the generation. + +## Toolkit + +Official Rust toolkit provides: + +- async ResourceClient/Reconciler loop; +- ComponentSession/d2b-bus lifecycle; +- generated typed Provider/service clients/servers; +- config/schema projection; +- Volume/pidfd-free Provider state helpers; +- operation/checkpoint/event/telemetry helpers; +- fake core/store/bus/supervisor/effect clients; +- fault injection; +- black-box conformance; +- Provider flake/project templates. + +Wire/state-machine golden vectors remain language-neutral. + +## Provider dossier requirement + +Every Provider dossier specifies: + +- exact crate/package/providerRef; +- root config schema/defaults/bounds/secrets; +- ResourceTypes implemented/consumed; +- controller watch/reconcile/finalize; +- services/CLI/events; +- every binary Process template/placement; +- Volume/state/credential use; +- dependencies/permission claims; +- pidfd/wait/reap where Process Provider; +- telemetry/audit/doctor/support; +- for each cross-Zone exportable capability, the exact qualified `*Service` and + `*Binding` names, signed projection-factory metadata, allowed Service backing + refs, allowed Binding target refs, schema/fingerprints, adapter behavior, + arbitration, finalizers, and update/status propagation; or an explicit + `exportability: forbidden`; +- failure/upgrade/migration; +- exact v3 source→future destination work items and tests, each naming the + old test selector/file with a keep/adapt/move/delete disposition and removal + gate (D094); +- test placement and execution budgets (D094): `src/` units and `tests/` are + fast, in-process, deterministic, parallel-safe hermetic tests within the + `ADR-046-validation-and-delivery` §10.16 budgets (individual normal test p95 + ≤50 ms, no wall-clock sleep; per-crate `--lib --tests` ≤2 s), and every real + process/socket/container/Nix/guest/hardware/live scenario lives only in + `integration/`. + +## Frozen initial Provider catalog + +Every row requires one Provider crate/package and one +`ADR-046-provider-.md` dossier. + +### System, Host, and Guest + +| Provider | Implements | Description/processes | +| --- | --- | --- | +| `system-core` | Host, User | Fixed core-controller bootstrap; reconciles one or more Hosts and local User discovery/status only | +| `system-systemd` | Process, EphemeralProcess | Transient non-forking system/user units/scopes, pidfd verification, systemd wait/reap | +| `system-minijail` | Process, EphemeralProcess | Broker/minijail/clone3 sandboxed process, local pidfd and d2b wait/reap | +| `runtime-cloud-hypervisor` | Guest | Local NixOS VM lifecycle; owns VMM and guest-bootstrap child resources/Processes | +| `runtime-qemu-media` | Guest | QEMU media/physical-media lifecycle and QMP-mediated child Processes | +| `runtime-azure-container-apps` | Guest | Azure Container Apps sandbox lifecycle and remote agent integration | +| `runtime-azure-virtual-machine` | Guest | Full-host Azure VM lifecycle, bootstrap, and optional child Zone hosting | + +Unsafe-local is not a Provider. It is a user-only Host under +system-core. + +### Storage/network/device + +| Provider | Implements | Description/processes | +| --- | --- | --- | +| `volume-local` | Volume | Anchored local durable/ephemeral storage, fine-grained layout/ACL/views, bind/tmpfs/local source behavior and store-view mode | +| `volume-virtiofs` | Volume attachment controller | Host source Volume to target Guest virtiofs export/mount; owns virtiofsd Processes and attachment status | +| `network-local` | Network | Local bridge/namespace/address/DHCP/DNS/NAT/firewall/egress and Host/Guest attachment | +| `device-tpm` | Device | TPM allocation, swtpm Process, persistent TPM Volume/state and identity | +| `device-usbip` | Device; `usb.d2bus.org.UsbService`; `usb.d2bus.org.UsbBinding` | USB inventory/arbitration/export/attach/firewall and USBIP Process/EphemeralProcess; implementation detail stays out of semantic USB base | +| `device-security-key` | Device; `security-key.d2bus.org.SecurityKeyService`; `security-key.d2bus.org.SecurityKeyBinding` | Security-key inventory/ceremony/CID/lease/session; unprivileged Host relay and Guest frontend Processes; CTAPHID/hidraw details stay out of semantic security-key base | +| `device-gpu` | Device | Combined GPU/render/VFIO/video/media arbitration and GPU/video worker Processes | + +Azure/ACA-specific network remains inside Guest Providers until an +independently shared Azure Network is required. + +### Interaction + +| Provider | Implements | Description/processes | +| --- | --- | --- | +| `display-wayland` | Provider-specific display/session types | Wayland/display policy, Host/Guest proxies, window identity/rails and endpoint Processes | +| `audio-pipewire` | `audio.d2bus.org.AudioService`; `audio.d2bus.org.AudioBinding` | Audio policy/session, Host/user components, vhost-user-sound Processes; PipeWire stays an implementation detail | +| `clipboard-wayland` | Provider-specific clipboard types | Selection/bridge/transfer/presentation and Host/user/Guest Processes | +| `notification-desktop` | Provider-specific notification types | Observe/project/action/ack/presentation Processes | +| `shell-terminal` | `shell-terminal.d2bus.org.ShellSession` | Persistent terminal session/supervisor, open/attach/detach/kill and named terminal streams | + +One-shot exec is EphemeralProcess, not an exec Provider. + +### Credentials + +| Provider | Implements | Description/processes | +| --- | --- | --- | +| `credential-secret-service` | Credential | Exact-user Secret Service/keyring leases and typed operations | +| `credential-entra` | Credential | Entra-bound credential leases/operations without token export | +| `credential-managed-identity` | Credential | Host/Guest cloud managed-identity leases/operations | + +### Transport/observability/activation + +Transport Providers are carriage services only; they never own ZoneLink. The +core ZoneLink handler alone reads/writes/finalizes ZoneLink and owns +Noise/session/reconnect/route/idempotency/intent state, calling typed +`OpenTransport`/`CloseTransport`/`ObserveTransport` on the installed Transport +Provider. A Transport Provider returns only an opaque `OwnedTransport`/ +byte-stream handle and observations; it holds no ZoneLink state itself. + +| Provider | Implements | Description/processes | +| --- | --- | --- | +| `transport-unix` | Transport carriage (`OpenTransport`/`CloseTransport`/`ObserveTransport`) | Local Unix/socketpair endpoints, peer evidence, FD-capable local channels | +| `transport-vsock` | Transport carriage (`OpenTransport`/`CloseTransport`/`ObserveTransport`) | Host/Guest vsock channels, expected CID and no FD transfer | +| `transport-azure-relay` | Transport carriage (`OpenTransport`/`CloseTransport`/`ObserveTransport`) | Remote Azure Relay reachability; relay identity is carriage only | +| `observability-otel` | `telemetry.d2bus.org.TelemetryService`; `telemetry.d2bus.org.TelemetryBinding` | Telemetry endpoint/export/collector integration and health; OTEL stays an implementation detail | +| `activation-nixos` | Provider-specific activation types | NixOS generation plan/apply/inspect/adopt/rollback | + +Cross-resource composition is ordinary controller behavior. There is no +special orchestrator Provider. + +### D096 exportability classification + +Cross-Zone export is deny-by-default. The initial classification is: + +| Semantic Service/Binding pair | Initial Provider | D096 classification | +| --- | --- | --- | +| `audio.d2bus.org.AudioService` / `audio.d2bus.org.AudioBinding` | `audio-pipewire` | exportable | +| `security-key.d2bus.org.SecurityKeyService` / `security-key.d2bus.org.SecurityKeyBinding` | `device-security-key` | exportable | +| `telemetry.d2bus.org.TelemetryService` / `telemetry.d2bus.org.TelemetryBinding` | `observability-otel` | exportable | +| `usb.d2bus.org.UsbService` / `usb.d2bus.org.UsbBinding` | `device-usbip` | policy-gated exportable; Provider, Zone, export, and device policy all opt in | +| every other semantic type / frozen initial Provider | — | forbidden unless a later reviewed semantic contract adds a pair and a conformant Provider binds it | + +Approval applies to the qualified Service only. A matching Binding, its backing +Device/Endpoint, Credentials, and internal session/stream records are never +export targets. These semantic names are frozen independently of the initial +implementations. Provider dossiers bind implementations and extensions but +cannot alias, rename, or vendor-qualify the base types. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `d2b-realm-provider`; live ACA/Relay/gateway; unwired d2b-host-providers; direct d2bd construction; current Nix package outputs | +| Evidence class | Mixed: ACA/Relay/gateway reachable; host adapters/transport/codec/client mostly unwired; generic registry/toolkit absent | +| Behavior retained | Typed Provider traits/errors/capabilities, fail-closed absence, circuit breaker, credential planes, redaction, injected test seams | +| Required delta | Provider resource/catalog/trust, one crate per Provider, process components, toolkit/conformance, exact dependencies | +| Reuse path | Extract current semantic logic with evidence-specific work items; do not copy dead scaffolds as live | +| Replacement/deletion | Direct d2bd constructors/factories removed only after Provider resource/Process/service integration | +| Feasibility proof | Out-of-tree template, multi-binary Provider, signed package, exact process bootstrap and resource controller | +| Future owner | Work items below and Provider dossiers | + +## Implementation work items + +### ADR046-provider-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0; Provider contract/catalog owner | +| Current source | `packages/d2b-realm-provider/src/{provider,capabilities,error,credential,rate_limit,conformance}.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/provider.rs`, `packages/d2b-provider/src/lib.rs`, `packages/d2b-provider-toolkit/` | +| Detailed design | Provider resource/manifest/components/dependencies/services/trust/compatibility/toolkit Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Zone config/catalog → Provider resource → Process components → bus/resource routes | +| Data migration | Full reset | +| Validation | Contract vectors, fake/malicious Provider, one-crate/one-identity policy | +| Removal proof | Old trait crate retired only after all Provider dossiers migrate | + +### ADR046-provider-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | Provider contract; package/Nix integrator | +| Current source | `packages/Cargo.toml`; `flake.nix`; `nixos-modules/host-daemon.nix`; current source package derivations | +| Reuse action | adapt | +| Destination | one `packages/d2b-provider--/` per Provider with mandatory src/, tests/, integration/, README.md; generic Nix Provider package/catalog emitter | +| Detailed design | Split current combined/composition crates; exact outputs/manifests/conformance/layout/documentation | +| Integration | Provider package installed/registered per Zone | +| Data migration | No package compatibility path | +| Validation | Workspace naming/dependency/output/dossier/catalog parity policy | +| Removal proof | Combined crate removed only after every live implementation has a Provider successor | + +### ADR046-provider-003 + +| Field | Value | +| --- | --- | +| Dependency/owner | Process contracts; system Provider owners | +| Current source | `d2bd` DAG/broker spawn; unsafe-local helper; guestd/exec runner; `d2b-host` runtime provider | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-core/`, `d2b-provider-system-systemd/`, `d2b-provider-system-minijail/` | +| Detailed design | Bootstrap system-core; common Process/EphemeralProcess providers and pidfd conformance Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Host/Guest providerRef/domain/userRef, local supervisors, resource status | +| Data migration | Current roles converted under reset | +| Validation | Shared conformance and host/user/non-Host tests | +| Removal proof | Current role launch paths removed after parity | + +### ADR046-provider-004 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-provider-001; shared semantic Service/Binding contract owner | +| Current source | None — D098 common semantic Service/Binding bases are net-new ADR 0046 contracts | +| Reuse action | create | +| Destination | `packages/d2b-contracts/src/v3/semantic_services/{mod,audio,security_key,telemetry,usb}.rs`; generated schema artifacts for the eight exact qualified ResourceTypes | +| Detailed design | Define one shared strict base spec/status DTO and schema contract for each frozen D098 Service/Binding pair, including exact semantic type/schema IDs, versions, fingerprints, minimal valid base fixtures without `spec.provider`, authority/projection Service union, same-Zone Binding `serviceRef`/target rules, D088 `status.resource` layering, status-only observations, and projection-factory type binding. A Core-generated projection permits only `providerRef`, semantic base/import fields, and ResourceImport ownership; it rejects `spec.provider`. Register no implementation-qualified or former `*State` alias. | +| Integration | Provider manifests and ResourceApiBindings consume the common catalog fingerprint; ADR046-zone-control-019/020 use the same factory metadata to admit an owner Service and core-create one same-type projection Service; the four initial Provider dossiers supply only strict implementation extensions/controllers. | +| Data migration | Full d2b 3.0 reset; no prior public Service/Binding names or aliases are imported | +| Validation | Shared contract tests cover exact names, strict serde/schema round trips, common base discoverability without any Provider package, canonical minimal base acceptance without `spec.provider`, same-Zone refs/targets, owner/projection discrimination, Core projection rejection of `spec.provider`, common fields only under `status.resource`, implementation observation only under `status.provider`, status-only observations, no Device/Endpoint/Binding projection, implementation-detail rejection, semantic factory-fingerprint stability under Provider/adapter identity changes, and rejection of every implementation-qualified/former `*State` alias. Each initial and fake alternate Provider must pass the identical base conformance fixture. | +| Removal proof | Any Provider-local duplicate base DTO/schema is removed before that Provider is registered; only strict Provider extension DTOs remain implementation-owned. | diff --git a/docs/specs/ADR-046-provider-state.md b/docs/specs/ADR-046-provider-state.md new file mode 100644 index 000000000..1451b1284 --- /dev/null +++ b/docs/specs/ADR-046-provider-state.md @@ -0,0 +1,1212 @@ +# ADR 0046 Provider payload state + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-state` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-provider-volume-local`, `d2b-contracts` v3 Volume/schema extensions, Provider toolkit, `nixos-modules` Zone bundle emitter | +| Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-primitive-resource-composition`, `ADR-046-provider-model-and-packaging`, `ADR-046-resource-store-redb`, `ADR-046-componentsession-and-bus` | +| Supersedes | `d2b-core/src/storage.rs` generated contract, ADR 0034 storage/sync/adoption model for v3 | + +## Purpose + +This spec governs how Provider components record, own, initialize, and recover their durable operational state in d2b 3.0. The default surface for bounded non-secret operational state is the owning resource's `status` subresource. A Provider component declares durable payload state as a Volume resource **only** when a specific payload passes the [storage-need test](#storage-need-test); such state Volumes are always Volume resources, and there is no `ProviderState` ResourceType. + +## Status-first state model (D087) + +Bounded non-secret controller/Provider operational state belongs in the owning resource's `status` subresource whenever possible (D087). Resource `status` is the default durable observation and recovery surface. It is: + +- **Revisioned** — every status write advances the resource revision and is retained in `revision_log` until compaction (D028); +- **Optimistic-status-writer controlled** — a status write carries an expected revision and a stale writer receives a conflict (D005); +- **RBAC-readable and redacted** — status is a separately authorized subresource (D027) readable by authorized API subjects only, and every field is bounded and redacted; +- **Reverified after restart** — status records observation, never authority; after a Zone or controller restart the owning controller reverifies status against external reality (running processes, cgroup leaves, external cloud/handle state) before relying on it. + +The spec remains the desired-state authority. Status is observation only and is **never** a host-mutation or repair authority: a controller may not treat a status field as a substitute for reverifying external reality, and status can never grant, carry, or stand in for a privileged effect. + +Bounded non-secret operational state that belongs in `status` includes: + +- reconcile stage and phase detail (closed enums / conditions); +- opaque, non-authorizing external handles, IDs, and digests (cloud operation handles, external resource IDs, content digests) that are safe for authorized API readers and independently revalidated; +- adoption observations after restart (e.g. observed running-process identity re-derived from cgroup leaves); +- bounded counters and last-successful checkpoints; +- dependency-readiness observations. + +Reconcile writes status only on a **material change**; there are no high-frequency byte streams, logs, metrics, command output, or ring buffers in status. Watches, revision compaction, and backpressure remain bounded per `ADR-046-resource-store-redb` and `ADR-046-resource-reconciliation`. + +For `ResourceExport`/`ResourceImport` (D096), per-session lease handles, stream +session state, credits, and payload bytes are high-churn runtime state, not +resource status or Provider payload state. Their statuses carry only bounded +lease summaries, counts, generation digests, readiness, and D091 currency; they +never carry raw bytes, paths, device handles, FDs, tokens, or authority-conferring +session handles. + +### Status prohibitions + +Resource `status` MUST NOT contain: + +- secrets, raw tokens, keys, or PSKs, or any credential source handle that confers authority; +- private endpoint, path, argv, environment, PID, or systemd-unit data; +- terminal, clipboard, or CTAP byte content; +- raw cloud error bodies or other unbounded provider diagnostics; +- large binary blobs; +- unbounded collections; +- any content whose churn would bloat revision history. + +An opaque handle may appear in status only if it is bounded, non-secret, non-authorizing, safe for authorized API readers, and independently revalidated by the owning controller against external reality. + +### State-authority split + +Each kind of state has exactly one owner; no owner duplicates another's data: + +| State | Sole owner | +| --- | --- | +| In-flight idempotency, retry, and transaction progress | Core Operation ledger | +| Latest bounded result / checkpoint / observation | Owning resource `status` | +| Security history | Authoritative audit stream | +| Metrics and traces | OTEL | +| High-frequency content streams / rings | Owning process memory | +| Filesystem paths and artifact integrity | Nix / private artifact catalog | +| Secret, large, binary, or revision-unsuitable durable payload with a recovery need | An explicitly declared Provider state Volume | + +Provider state Volume payloads never duplicate resource refs, generation, backoff, idempotency, or session state held by resource rows, resource status, or the core Operation ledger. + +## Storage-need test + +A component declares a Provider state Volume only when a concrete payload passes an explicit storage-need test. A payload qualifies when at least one of the following holds: + +1. **Secret / sensitive private recovery data** — the payload is or seals a secret (key material, PSK, sealed enrollment/admission record) and cannot enter status; +2. **Large / binary / file content** — the payload is a large or binary file that is unsuitable for the bounded status API; +3. **Private data unsafe for status readers** — the payload is private data that must not be visible to authorized status readers under RBAC; +4. **Bounded but revision-unsuitable data with a demonstrated recovery need** — the payload is bounded but its churn would bloat revision history, and there is a demonstrated recovery need that status cannot satisfy. + +A component whose durable operational state is fully derivable from spec, status, the core Operation ledger, or independent external observation declares **no** state Volume. There is no empty, identity-only state Volume, and no state Volume is created for a stateless component. + +## No ProviderState ResourceType + +Decision D032 folds file, directory, ACL, and filesystem-view concerns into one `Volume` ResourceType. A declared Provider payload state is a Volume with a per-component view and a `stateSchema` extension in its spec. A separate `ProviderState` ResourceType would duplicate the owner/finalizer/lifecycle model already present in Volume and add a second governance layer for the same physical bytes. The Volume ResourceType is extended instead. + +This decision is final within this spec and does not require a new decision register entry. + +## ProviderStateSet (optional logical concept) + +A **ProviderStateSet** is the OPTIONAL, query-time grouping of the declared Volume resources in a Zone whose `metadata.ownerRef` resolves to `Provider/`. It is a query-time grouping, not a ResourceType or stored artifact, and it is **empty** for a Provider that declares no state Volume. + +```text +ProviderStateSet(zone, provider-name) = + { v : Volume | v.metadata.zone == zone + && v.metadata.ownerRef == "Provider/" } +``` + +**Core ProviderDeployment ownership.** Core ProviderDeployment creates only the declared state Volumes in the ProviderStateSet from the signed state declarations in the Provider manifest, before launching the owning component's Process. The Provider controller does not invoke Volume creation APIs; operators and Nix never author component state Volumes. + +**Export children excluded.** `virtiofs.d2bus.org.Export` children have `ownerRef: Volume/` (not `ownerRef: Provider/`) and are excluded from the ProviderStateSet. Only source Volumes are included. + +**Only declared components included.** ProviderStateSet includes only the Volumes of components that declared one under the [storage-need test](#storage-need-test). A component that keeps its bounded non-secret operational state in `status`/the core Operation ledger contributes no Volume. There is never an empty identity-only Volume. + +The controller deletes all ProviderStateSet Volumes when the Provider is removed, after finalizers on any owning Process or EphemeralProcess complete. + +## Provider component state declaration + +A semantic Provider component declares a state namespace in its component descriptor **only** when a payload passes the [storage-need test](#storage-need-test). Components whose operational state fits in `status`/the core Operation ledger declare no `stateNamespaces` entry, receive no state Volume, and contribute no Volume to the ProviderStateSet. A component never declares an empty-payload (`schemaId: null`) identity-only namespace. + +```yaml +stateNamespaces: + - id: main-state + kind: state # always "state" for component state Volumes + schemaId: example-provider.d2bus.org/controller/main-state + schemaVersion: "1.0" + schemaDigest: sha256: + persistenceClass: persistent # required; ephemeral/cache/config rejected + sensitivityClass: private # private | internal | shared-read + migrationPolicy: pre-launch-required # pre-launch-required | online-optional | none + quotaBytes: 104857600 # required nonzero + storageNeed: secret # required: secret | large-binary | private-unsafe-for-status | revision-unsuitable + sealingRequired: false + placementMode: null # omitted for Host-targeted; guest-local or host-backed-guest for Guest + hostCustodyPermitted: false # required true only for host-backed-guest + views: + main: + rights: [read, write, create, delete, traverse] + worker-read: + path: public + rights: [read, traverse] +``` + +Fields: + +| Field | Rules | +| --- | --- | +| `id` | Stable component-local alphanumeric namespace identifier | +| `kind` | Always `state` for component state Volumes; `staging` for migration staging Volumes | +| `schemaId` | Qualified immutable schema name: `//`; a declared namespace always carries a non-null payload schema | +| `schemaVersion` | Semver `MAJOR.MINOR`; major increment requires migration | +| `schemaDigest` | Exact SHA-256 hex of the canonical schema definition | +| `persistenceClass` | Must be `persistent`; `ephemeral`, `cache`, and `config` are rejected with `component-persistence-class-forbidden` | +| `sensitivityClass` | `private`: single-process; `internal`: same-Provider multi-component; `shared-read`: cross-Provider read-only | +| `migrationPolicy` | `pre-launch-required`: component Process is not started until migration completes; `online-optional`: Provider may start while migration runs; `none`: no migration logic | +| `quotaBytes` | Required nonzero; minimum 4096 bytes enforced (zero rejected with `component-quota-zero`) | +| `storageNeed` | Required justification the payload satisfies: `secret`, `large-binary`, `private-unsafe-for-status`, or `revision-unsuitable`; a namespace whose payload is fully derivable from spec/status/core ledger/external observation is rejected with `component-state-not-justified` | +| `sealingRequired` | If true the Provider controller must bind a `sealingCredentialRef` before the Volume is marked Ready | +| `placementMode` | For Guest-targeted components: `guest-local` (source inside Guest, Host never holds bytes/paths/dirfds) or `host-backed-guest` (source on Host with virtiofs Export, requires `hostCustodyPermitted: true`). Omitted for Host-targeted components. Frozen in signed manifest; no fallback. | +| `hostCustodyPermitted` | Required `true` for `host-backed-guest`; must be absent or `false` for `guest-local`. Core ProviderDeployment rejects `host-backed-guest` without `hostCustodyPermitted: true` with `placement-host-custody-violation`. | +| `views` | Named views declared in the component descriptor; subset is also declared in the Volume spec | + +`schemaId`, `schemaVersion`, `schemaDigest`, `kind`, `persistenceClass`, `storageNeed`, `placementMode`, `hostCustodyPermitted`, and `views` are signed into the component descriptor and the Provider package digest. Any change increments the component descriptor version and the Provider resource generation. + +## Volume creation and ownership + +Core ProviderDeployment creates one Volume per **declared** component state namespace per execution target from the signed state declarations in the Provider manifest, before launching the owning component's Process. Components that declare no state namespace get no Volume. The Provider controller does not invoke Volume creation APIs. Operators and Nix never author component state Volumes. Each declared Volume: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Volume +metadata: + name: example-provider--controller--main-state--host-system + zone: dev + ownerRef: Provider/example-provider +spec: + providerRef: Provider/volume-local # all source Volumes use volume-local; volume-virtiofs owns only Export children + kind: state # required for component state Volumes + persistenceClass: persistent # required; ephemeral/cache/config rejected + sensitivityClass: private + stateSchema: + schemaId: example-provider.d2bus.org/controller/main-state + schemaVersion: "1.0" + schemaDigest: sha256: + migrationPolicy: pre-launch-required + quotaBytes: 104857600 + quota: + maxBytes: 104857600 + maxInodes: 4096 + enforcement: none + sealingCredentialRef: null + source: + executionRef: Host/host-system + settings: + kind: local-path + sourcePolicyId: provider-state-persistent # opaque source policy; no host path (D082) + layout: + - path: state + type: directory + ownerRef: User/example-provider-system + groupRef: User/example-provider-system + mode: "0700" + sensitivity: private + createPolicy: create-if-never-provisioned + repairPolicy: exact-owner + cleanupPolicy: owner-controlled + noFollow: true + views: + main: + path: state + rights: [read, write, create, delete, traverse] + worker-read: + path: state/public + rights: [read, traverse] + identityMarker: + class: broker-maintained + markerRoot: provider-state-markers + snapshotPolicy: null + retentionPolicy: null +``` + +Volume naming convention: `------`. The convention is enforced at runtime by checking that every Volume in the ProviderStateSet matches a declared namespace and execution target. + +Process resources mount the Volume using the declared view: + +```yaml +mounts: + - volumeRef: Volume/example-provider--controller--main-state--host-system + view: main + mountPath: /state + access: read-write + required: true +``` + +**Admission invariants.** Core ProviderDeployment enforces these constraints when creating a Volume from a declared component descriptor state namespace: + +| Constraint | Violation error | +| --- | --- | +| `kind: state` required for all component state Volumes | `component-kind-invalid` | +| declared namespace payload must pass the storage-need test (a `storageNeed` justification of `secret`, `large-binary`, `private-unsafe-for-status`, or `revision-unsuitable`); state fully derivable from spec/status/core ledger/external observation is rejected | `component-state-not-justified` | +| `persistenceClass: persistent` required; `ephemeral`, `cache`, and `config` rejected | `component-persistence-class-forbidden` | +| `quotaBytes ≥ 4096`; zero or smaller values are rejected; base `quota.maxBytes` equals `quotaBytes` and `quota.maxInodes` is nonzero | `component-quota-too-small` | +| `host-backed-guest` requires `hostCustodyPermitted: true` in signed descriptor | `placement-host-custody-violation` | +| Credential, audit, remote-node, or cloud-control schemas require `guest-local` | `guest-local-required` | +| layout `ownerRef`/`groupRef` must reference a Nix-preprovisioned User principal or bounded system pool; runtime-created principals rejected | `volume-principal-not-preprovisioned` | + +## No bootstrap state Volume + +Because every fixed bootstrap component — `system-core`, `system-minijail`, and the first `volume-local` controller instance on each execution target — keeps its bounded non-secret operational state in resource `status` and the core Operation ledger and declares **no** Provider state Volume, no component requires a state Volume before a `volume-local` instance is Ready. The previously specified mandatory-state bootstrap cycle and its per-execution-target local bootstrap storage mechanism are therefore removed entirely (D086, superseded by D087). There is no hidden bootstrap store, no closed bootstrap cycle, and no implicit framework-created bootstrap Volume. + +A fixed bootstrap component that reaches Ready, adopts running processes, and re-derives its observed state from the core Operation ledger, resource `status`, and independent external observation (cgroup-leaf scanning, fresh pidfds, marker reverification against external reality) needs no durable payload Volume. If a future bootstrap component needs secret or large private recovery state that cannot enter status, it requires a new reviewed design that declares an ordinary optional state Volume under the [storage-need test](#storage-need-test); it does not reintroduce a bootstrap-storage exception. + +## State placement under Host/Guest/user execution + +### System-domain Host process + +A stateful system-domain Process under `Host/` uses a Volume backed by `Provider/volume-local` with `source.executionRef` pointing to the same Host. The volume-local Provider manages all layout, ACL, and lifecycle operations on the host filesystem under the anchored Volume root. + +### User-domain Host process + +A stateful user-domain Process under `Host/` with `domain: user` and a resolved `userRef` uses a Volume whose source is the per-user subtree. The Volume layout owner/group entries bind `User/` references. The volume-local Provider resolves these at provision time through the Host's User resource. A user-domain Volume must not be shared with a system-domain Volume or any other user's Volume. + +### Guest (VM/sandbox/remote) process + +A stateful Provider component whose Process runs under `Guest/` uses one of two explicit placement modes, frozen in the signed component descriptor. There is no fallback or runtime selection. + +#### guest-local + +The source Volume lives inside the Guest; the Host never holds bytes, dirfds, or identity markers for it: + +```yaml +spec: + providerRef: Provider/volume-local + kind: state + persistenceClass: persistent + source: + executionRef: Guest/ + settings: + sourcePolicyId: # opaque ID into volume-local's allowedHostPaths catalog for this Guest domain + placementMode: guest-local # Host never holds bytes, dirfds, or paths for this Volume +``` + +- Reconciled by the volume-local controller running inside the Guest. +- The Guest-local `volume-local` instance reaches Ready without a state Volume (it keeps its bounded operational state in `status`/the core Operation ledger); all declared Volumes for that Guest domain are created by Core ProviderDeployment after volume-local is Ready in that domain. +- The Host volume-local controller holds no dirfd, path, byte content, or identity marker for this Volume. +- **Mandatory for** components carrying gateway or realm credentials, remote-node registration, audit state, or cloud control plane state. Core ProviderDeployment rejects `placementMode: host-backed-guest` for these schema categories with `guest-local-required`. + +#### host-backed-guest + +The source Volume lives on the Host; volume-local creates a `virtiofs.d2bus.org.Export` child resource per attachment to provide Guest access: + +```yaml +spec: + providerRef: Provider/volume-local + kind: state + persistenceClass: persistent + source: + executionRef: Host/ + settings: + sourcePolicyId: # opaque ID into volume-local's allowedHostPaths catalog + placementMode: host-backed-guest + hostCustodyPermitted: true # required; absent or false → placement-host-custody-violation +``` + +- volume-local creates one `virtiofs.d2bus.org.Export` child (ownerRef: Volume/, providerRef: volume-virtiofs) per `attachments[]` entry. volume-virtiofs owns only the virtiofsd worker Process and Export lifecycle; it does not own or replicate source bytes. +- Permitted only when the signed descriptor explicitly carries `hostCustodyPermitted: true`. +- All lifecycle operations (migration, sealing, snapshots, relocation, incident hold, destruction) apply to the source Volume. + +**ProviderStateSet.** In both modes the source Volume has `ownerRef: Provider/` and is included in the ProviderStateSet. `virtiofs.d2bus.org.Export` children have `ownerRef: Volume/` and are excluded. For `guest-local` there are no Export children. + +All lifecycle decisions in this spec — migration, sealing, snapshots, relocation, incident hold, and destruction — apply to the source Volume in both modes. For `host-backed-guest` relocation, Export children follow the source Volume per `ADR-046-primitive-resource-composition`. + +## No cross-domain shared dirfd + +The volume-local Provider never hands out a dirfd, file descriptor, or raw path that would be accessible to a process in a different domain or a different User. This invariant is enforced through: + +1. Volume layout entries bind exact `ownerRef`/`groupRef`/`mode` values; the provider validates inode owner against those references before exposing a view. +2. Process mount specs select a named view; the volume-local Provider validates that the mounting Process's domain and userRef are compatible with the declared Volume `sensitivityClass`. +3. A `private` Volume is mounted by exactly one Process at a time; the provider rejects concurrent mounts outside the same component instance. +4. An `internal` Volume is mountable only by Processes controlled by the same Provider, determined through the registered controller Provider/owner chain. +5. `shared-read` allows cross-Provider read-only view mounts but prohibits any write access to the shared path. +6. The volume-local Provider never passes a host filesystem fd to a process in a different domain. For `host-backed-guest` state, volume-local creates a `virtiofs.d2bus.org.Export` child (providerRef: volume-virtiofs) that exposes the declared view over virtiofs; the underlying dirfd is never passed across domains directly. For `guest-local` state, the Host volume-local controller holds no dirfd, path, byte content, or identity marker for the Volume; all filesystem operations are performed by the volume-local controller inside the Guest. + +Any violation fails the Process launch with a typed `volume-domain-mismatch` error. + +## Volume stateSchema extension + +The `stateSchema` block in the Volume spec: + +| Field | Rules | +| --- | --- | +| `schemaId` | Matches the exact `schemaId` in the component descriptor; immutable after first provision | +| `schemaVersion` | Current desired schema version; a change triggers migration | +| `schemaDigest` | Hash of the canonical schema definition; validated before migration | +| `migrationPolicy` | Inherited from component descriptor at creation; immutable per Volume | + +The volume-local Provider stores the installed schema version in the Volume's identity marker file. On every open it compares the marker's `installedSchemaVersion` to the spec's `schemaVersion`. A mismatch is reflected in Volume status before any Process is allowed to mount the view. + +### Extended Volume status for state + +ResourceType-specific status additions: + +| Field | Values and rules | +| --- | --- | +| `stateSchemaPhase` | `current` \| `migration-required` \| `migrating` \| `migration-committed` \| `migration-failed` | +| `installedSchemaVersion` | Semver string of the version currently on disk; null before first provision | +| `markerStatus` | `verified` \| `missing` \| `replaced` \| `unknown` | +| `sealingStatus` | `none` \| `sealed` \| `rotation-pending` \| `rotation-failed` | +| `quotaUsage` | Optional bounded current `{ usedBytes, inodeCount }` reported by the provider | +| `lastMigrationAt` | Optional RFC 3339 UTC completion timestamp of the last migration | + +`stateSchemaPhase != current` and `markerStatus != verified` both block a `pre-launch-required` component Process from moving to Ready. + +## Identity markers and fail-closed detection + +Every persistent or cache-class Volume provisioned by the volume-local Provider has an identity marker. The marker: + +1. is a regular file under a broker-maintained root outside the Volume's own tree (equivalent to the TPM marker root at `swtpm-markers`); +2. records the Volume's `(st_dev, st_ino)` at first provision, the schemaId, schemaVersion, and a tamper-evident digest; +3. is written by the broker at provision time (`class: broker-maintained`) or by the Provider controller at post-provision init (`class: provider-maintained`); +4. is checked on every daemon restart and on every Process launch that mounts the Volume. + +Failure modes and responses: + +| Condition | Response | +| --- | --- | +| Marker missing after prior provision | `markerStatus: missing`; Volume → `Failed`; blocked Processes → `Degraded` | +| `st_ino` mismatch (directory replaced) | `markerStatus: replaced`; Volume → `Failed`; never silently re-provision | +| Marker present but Volume root absent | `markerStatus: missing`; Volume → `Failed` | +| `installedSchemaVersion` > spec version | Volume → `Failed`; `stateSchemaPhase: migration-failed`; manual rollback required | + +None of these cases auto-recover. Operator intervention clears the condition after confirming integrity. + +## Quota enforcement + +When `quotaBytes > 0`, the volume-local Provider: + +1. Checks the available space on the backing filesystem before provisioning and reports `quota-insufficient` if the requested quota cannot be reserved. +2. Enforces a per-Volume maximum using the Volume root's filesystem quota where the backing filesystem supports it, or a soft-check on write by the Provider. +3. Reports `quotaUsage` in Volume status at a bounded polling interval (max every 60 s). +4. Rejects mounts whose views have `rights: [write, create]` when the Volume is at or above quota, returning a typed `volume-quota-exceeded` error. + +Quota metadata lives in the Volume spec and is validated against the component descriptor at creation. A mismatch between descriptor and Volume spec fails Volume admission with `quota-mismatch`. + +## Within-Volume transactions + +Provider components may write structured state atomically using the toolkit's Volume transaction helpers (backed by `d2b-state`'s `AtomicFilesystem`). The write protocol: + +1. Writer opens a temp file in the Volume view's anchored root with `O_CLOEXEC | O_TMPFILE`. +2. Writer serializes payload as canonical JSON and writes a `StateEnvelope` wrapping the digest and generation. +3. Writer calls `fsync` on the temp fd. +4. Writer calls `linkat` to rename the temp fd into the target relative path. +5. Writer calls `fsync` on the parent directory fd (anchored, never caller-controlled). + +Step 4 is atomic on Linux. A crash between steps 3 and 5 leaves the old file intact. The toolkit validates the StateEnvelope digest and generation bound before exposing the payload to the caller. + +The `generation` field in `StateEnvelope` is the component's own optimistic version counter, not the Zone resource generation. It is used for expected-previous generation checks in the component's own application logic. + +No cross-Volume, cross-process, or cross-schema transaction is defined. Multi-object consistency uses the cross-component migration protocol. + +## Schema migration + +### Pre-launch migration + +A Volume whose `migrationPolicy: pre-launch-required` and `stateSchemaPhase: migration-required` (i.e., `installedSchemaVersion != spec.stateSchema.schemaVersion`) blocks the owning component's Process from starting. + +The Provider controller's reconcile handler: + +1. Detects `stateSchemaPhase: migration-required` on the Volume via watch. +2. Sets a `Migrating` condition on the Volume status. +3. Creates an EphemeralProcess with `ownerRef: Volume/` and a signed migration template from the Provider package. +4. The EphemeralProcess runs the migration operator binary, which: + a. opens the Volume view via its declared mount; + b. reads the installed schema version from the marker; + c. runs the schema-specific migration operator up to the target version; + d. writes the new marker with `installedSchemaVersion = target`; + e. exits 0. +5. On `EphemeralProcess.status.phase = Succeeded`, the Provider controller updates Volume status to `stateSchemaPhase: current` and clears the `Migrating` condition. +6. The component Process may now start. + +### Online migration (online-optional) + +The component Process starts while the EphemeralProcess migration runs concurrently. The component must be capable of handling the old schema version until migration completes. The controller coordinates the cutover by setting a `MigrationPending` condition; the component observes it through its ComponentSession service interface and switches to the new schema layout after the condition clears. + +### Migration operator requirements + +Migration operators must be: + +- deterministic given the same source schema version and source data; +- idempotent (safe to re-run after a crash at any point); +- roll-forward only: an operator never downgrades data already at the target version. + +## Cross-component migration coordination + +When a Provider has N stateful components sharing a related schema (e.g., a controller and a service that jointly own a coordinated state layout), the migration involves all N Volumes together. + +### Prepare phase + +1. Provider controller sets a `PrepareMigration` condition on all N Volumes simultaneously via a ResourceMutationBatch. +2. All N component Processes acknowledge the condition through their ComponentSession and stop mutating their state views. +3. Each Process signals readiness by setting a `MigrationReady` condition on its own Process status. + +### Staging Volume + +The controller creates a staging Volume for each migrating Volume: + +```yaml +type: Volume +metadata: + name: example-provider--controller--main-state--host-system--staging + ownerRef: Volume/example-provider--controller--main-state--host-system +spec: + providerRef: Provider/volume-local + persistenceClass: ephemeral + source: + executionRef: Host/host-system +``` + +The staging Volume has `ownerRef` pointing to its source Volume. It is used as the migration workspace and is destroyed after successful commit or rollback. + +### Commit phase + +1. All N migration EphemeralProcess jobs complete and report `Succeeded`. +2. Controller atomically swaps staging content into the primary Volumes using the toolkit's `AtomicFilesystem.rename_into` helper. +3. Controller updates all N Volume status entries to `stateSchemaPhase: current` and removes `PrepareMigration` conditions. +4. Staging Volumes are deleted (no finalizer required; ephemeral lifecycle). +5. Component Processes are unblocked to mount the migrated views. + +### Precommit rollback + +If any migration EphemeralProcess reports `Failed` before the commit swap: + +1. Controller sets `MigrationAborted` condition on all N Volumes. +2. Staging Volumes are deleted. +3. All N Volumes remain at their pre-migration `installedSchemaVersion`. +4. Component Processes that were running under `online-optional` continue on the old schema. +5. Volume `stateSchemaPhase` is set to `migration-failed` with a bounded typed reason. + +Rollback is only valid before the atomic commit swap. After commit, only roll-forward (a further migration to the target version) is valid. The spec is deterministic: the migration EphemeralProcess reports one of `Succeeded` or `Failed`; the controller never enters an ambiguous mid-swap state. + +### Roll-forward after interrupted commit + +If the Zone daemon crashes after the atomic file-level rename but before Volume status is updated: + +1. On restart, the volume-local Provider detects `installedSchemaVersion == target` in the marker. +2. The Volume status is corrected to `stateSchemaPhase: current` by the Provider controller's startup reconcile. +3. The staging Volume is detected as orphaned and GC'd under the unclaimed cleanup policy. + +## Secret sealing + +A Volume whose `stateSchema.sensitivityClass: private` and `sealingRequired: true` requires a `sealingCredentialRef`: + +```yaml +sealingCredentialRef: Credential/example-provider-state-key +``` + +The referenced Credential must be Ready before the Volume can be provisioned. The volume-local Provider: + +1. reads the Credential lease to obtain the envelope encryption key material; +2. wraps each `StateEnvelope` under the envelope key before writing; +3. never stores the raw key material on disk; +4. updates `sealingStatus` to `sealed` in Volume status. + +Key rotation is triggered by a Credential generation change: + +1. Controller detects `Credential.status.observedGeneration` has advanced. +2. Controller sets `sealingStatus: rotation-pending` on the Volume. +3. A rotation EphemeralProcess re-encrypts the Volume content under the new key. +4. On success, `sealingStatus: sealed` is restored. +5. A rotation failure sets `sealingStatus: rotation-failed`; the Volume is still readable under the old key until resolved. + +The sealed content format and KDF parameters are owned by the volume-local Provider. No raw credential bytes or key material enter Volume status, audit records, OTEL spans, or logs. + +## Snapshots + +A Volume snapshot is an immutable point-in-time copy of the Volume's active view, created by an EphemeralProcess snapshot job owned by the Provider controller. + +Volume spec: + +```yaml +snapshotPolicy: + retainCount: 3 + retainDurationHours: 168 # 7 days; 0 = retain only by count + triggerOnMigration: true # automatically snapshot before every migration + triggerOnRelocation: true +``` + +Volume status: + +```yaml +snapshots: + - id: snap- + createdAt: 2026-07-22T00:00:00Z + schemaVersion: "1.0" + sizeBytes: 12345678 + trigger: pre-migration + phase: Ready # Ready | Failed | Expired +``` + +Snapshots are stored in a Provider-private path under the Volume root (e.g., `.snapshots/`) and are never exposed through the component's own views. Snapshot retention uses the normal EphemeralProcess cleanup policy; expired snapshots are removed by the Provider controller. + +## Staging Volumes + +Staging Volumes have `persistenceClass: ephemeral` and `ownerRef` pointing to the parent Volume. Their lifecycle: + +1. Created by the Provider controller for migration, relocation, or large atomic-swap operations. +2. Mounted by migration EphemeralProcess resources only; no long-lived component Process mounts a staging Volume. +3. Removed on successful commit or failed rollback before the component Process is unblocked. +4. Detected as unclaimed and GC'd under the unclaimed Volume cleanup policy if the owning Provider is removed before cleanup completes. + +## Retention policy + +Volume retention policy applies to `ephemeral` and `cache`-class Volumes after the owning Provider or component Process is deleted: + +```yaml +retentionPolicy: + successfulTtlHours: 1 + failedTtlHours: 24 + incidentHoldEnabled: true +``` + +`successfulTtlHours` / `failedTtlHours` start from `deletionRequestedAt`. A Volume under incident hold ignores both TTLs until the hold is cleared. + +A `persistent` Volume is never auto-expired; it is only removed after the Provider controller's reconcile explicitly deletes it with expected revision. + +## Destruction + +The Provider controller deletes a Volume by: + +1. Setting `deletionRequestedAt` through the resource API. +2. Waiting for all mounting Processes to stop (finalizer). +3. The volume-local Provider's controller handler: removes layout paths using fd-relative `unlinkat` operations anchored within the Volume root, followed by `fsync` on the parent directory. +4. Where `sensitivityClass: private` and `sealingRequired: true`, the provider shreds key material before layout removal. +5. Removes the identity marker file. +6. Removes the Volume root directory. +7. Commits the finalizer removal. +8. Core emits a `Deleted` event (event-only; no further resource row mutation occurs in this step). +9. Core removes the resource row and all index entries atomically. +10. Core appends a post-commit audit record using a dedup/exactly-once recovery key. + +The layout removal is ordered leaf-first and parent-last. Partial removal is detected on restart by the marker check; a partially removed Volume that still has a valid marker is quarantined rather than silently re-provisioned. + +## Relocation + +State relocation moves a Volume's backing store from one Host or execution target to another. A relocation: + +1. Provider controller sets a `Relocating` finalizer on the source Volume and stops component Processes that mount it. +2. Creates a destination Volume (may be in the same or a different Zone, if a future cross-Zone extension permits; otherwise same Zone). +3. Creates a relocation EphemeralProcess that copies the source Volume tree to the destination using anchored read and write operations. +4. On successful copy: controller mounts the destination Volume in place of the source; removes the source finalizer; deletes the source Volume. +5. On failed copy: source Volume and its finalizer remain; operator resolves. + +Cross-Host relocation is a prerequisite for Guest migration (moving the source Volume that backs a Guest's virtiofs attachment from one host to another). The `virtiofs.d2bus.org.Export` child resources (owned by volume-virtiofs) are reconciled by volume-virtiofs to point to the new source after the copy completes; the exact protocol is governed by `ADR-046-primitive-resource-composition` Volume attachment spec. + +## Incident hold + +An incident hold blocks Volume destruction and any migration commit: + +```yaml +# Held by an authorized operator or incident controller +conditions: + - type: IncidentHold + status: "True" + reason: active-incident + message: bounded operator description + observedGeneration: 3 + lastTransitionAt: 2026-07-22T00:01:00Z +``` + +The `IncidentHold` condition: + +- is set by an authorized administrative Role via the status subresource; +- blocks `deletionRequestedAt` processing, migration commit, and staging Volume removal; +- does not block read-only mounts or status observation; +- is cleared only by the same administrative Role; +- is preserved through daemon restart and Zone reconcile. + +An EphemeralProcess that completes while under incident hold retains its terminal status and output beyond its normal TTL until the hold is cleared. + +## Worker subviews + +Worker Processes with narrow access receive a named Volume view with limited rights: + +```yaml +mounts: + - volumeRef: Volume/example-provider--controller--main-state--host-system + view: worker-read + mountPath: /read-state + access: read-only +``` + +The `worker-read` view is declared in the Volume spec and the component descriptor. The volume-local Provider enforces the view rights at mount time: a worker mount that requests any right absent from the view declaration fails with `volume-view-rights-exceeded`. + +Worker Process resources are not owners of the Volume and carry no finalizer. Stopping the worker does not affect the Volume lifecycle. + +## Unclaimed Volume GC + +A Volume is unclaimed when: + +- its `metadata.ownerRef` resolves to a Provider resource that has been deleted; or +- the Provider resource's ProviderStateSet declaration no longer includes this Volume's `stateSchema.schemaId`; or +- the Volume was created by a migration workflow that was abandoned (staging Volume with a missing or deleted owning Volume). + +The core cleanup controller identifies unclaimed Volumes via the owner_index on each garbage collection pass. Unclaimed persistent Volumes: + +1. receive a `Unclaimed` condition from the core cleanup controller; +2. are reported in Zone status and operator-visible diagnostics; +3. are not automatically deleted: operator confirms via an explicit delete request. + +Unclaimed ephemeral and staging Volumes are automatically deleted after a configurable unclaimed TTL (default 1 h). + +## Status, audit, and OTEL + +### Volume status (state extensions) + +See the extended status fields table in the [Volume stateSchema extension](#volume-stateschema-extension) section. + +### Audit events + +Each state-lifecycle transition emits one audit record through the Zone audit stream. Event kinds for state: + +| Event kind | Trigger | Payload | +| --- | --- | --- | +| `volume-provisioned` | First-provision complete | zone, volume-ref, schemaId, schemaVersion, persistenceClass | +| `volume-migration-start` | Migration EphemeralProcess created | zone, volume-ref, from-version, to-version, migration-policy | +| `volume-migration-committed` | Migration committed | zone, volume-ref, from-version, to-version | +| `volume-migration-failed` | Migration EphemeralProcess failed | zone, volume-ref, from-version, to-version, bounded-reason | +| `volume-migration-rolled-back` | Migration precommit rollback completed | zone, volume-ref | +| `volume-snapshot-created` | Snapshot EphemeralProcess succeeded | zone, volume-ref, snapshot-id, trigger | +| `volume-relocation-start` | Relocation EphemeralProcess created | zone, volume-ref, from-execution-ref | +| `volume-relocation-committed` | Relocation complete | zone, volume-ref, to-execution-ref | +| `volume-incident-hold-set` | IncidentHold condition added | zone, volume-ref, actor | +| `volume-incident-hold-cleared` | IncidentHold condition removed | zone, volume-ref, actor | +| `volume-sealing-rotation-start` | Credential rotation triggered | zone, volume-ref | +| `volume-sealing-rotation-committed` | Credential rotation complete | zone, volume-ref | +| `volume-destroyed` | Volume fully destroyed | zone, volume-ref, schemaId | + +No bytes of Volume content, credential material, migration data, raw paths, or process argv enter audit records. + +### OTEL metrics + +Cardinality-bounded metric labels are: `provider`, `schema_id`, +`schema_version`, `persistence_class`, `operation`, `trigger`, and `outcome`. +Zone identity is carried only by the bounded `d2b.zone` OTEL resource +attribute, never by a metric label. + +| Metric | Unit | Labels | +| --- | --- | --- | +| `d2b_volume_state_size_bytes` | Gauge, bytes | provider, schema_id | +| `d2b_volume_state_migration_total` | Counter | provider, schema_id, outcome | +| `d2b_volume_state_migration_duration_ms` | Histogram | provider, schema_id | +| `d2b_volume_state_snapshot_total` | Counter | provider, schema_id, trigger | +| `d2b_volume_state_marker_check_total` | Counter | provider, outcome | +| `d2b_volume_state_quota_exceeded_total` | Counter | provider | + +No schema content, raw path, instance ID, process arguments, or credential identifier enters any metric label. + +## Async controller integration + +A Provider controller that declares one or more state Volumes follows this integration pattern in its async reconcile loop. A controller with no declared state Volume writes its bounded operational observations to resource `status` on material change and skips the Volume-specific steps below. + +1. **Watch** the declared Volumes in the ProviderStateSet (possibly none) and all EphemeralProcess resources it owns. +2. On `spec-generation-changed` for the Provider resource: diff the new declared component descriptor state namespaces against existing Volumes; create, update, or delete Volumes as needed. +3. On `Volume.status.stateSchemaPhase = migration-required` for any declared Volume: dispatch the migration workflow (staging Volume, EphemeralProcess, commit/rollback). +4. On `Volume.status.markerStatus = missing | replaced`: set `Degraded` on affected component Processes and surface the typed condition. +5. Gate each component Process's desired phase on its mounted Volume's `phase == Ready` and `stateSchemaPhase == current`. Use a declarative dependency selector in the controller descriptor that includes every Volume the component's Process mounts. +6. On EphemeralProcess completion, apply the commit or rollback protocol, then delete the EphemeralProcess normally. + +The reconcile context contains the authorized async ResourceClient provided by the d2b-bus / ComponentSession stack (see `ADR-046-componentsession-and-bus`). No handler holds a blocking filesystem call across an `await`; Volume layout operations run in a bounded blocking adapter. Async sub-operations (migration, sealing, snapshot, relocation EphemeralProcess coordination) carry a cancellation token derived from the same session so that controller shutdown propagates atomically. + +A controller that creates a Volume it does not subsequently manage (e.g., a controller that delegates migration to a sibling) must not hold the Volume ownerRef; the actual Volume owner performs all status writes. + +## Nix configuration surface + +### Resource authoring shape + +All Zone resources are authored in Nix using a single generic attrset that mirrors the canonical ResourceSpec schema: + +```nix +d2b.zones..resources. = { + type = "..."; # exact ResourceType name + spec = { + # Exact spec fields — same names and nesting as the canonical ResourceSpec JSON + # No renaming, no alternative vocabulary + }; + # status is omitted: it is read-only, filled by the Zone runtime at runtime +}; +``` + +The NixOS module derives: +- `metadata.name` from the attr key (``) +- `metadata.zone` from the enclosing Zone attr key (``) +- `metadata.apiVersion` as `"resources.d2bus.org/v3"` (defaulted; not written by the user) + +Core fills `metadata.uid`, `metadata.generation`, `metadata.resourceVersion`, `metadata.creationTimestamp`, `metadata.managementFields`, and all `status.*` fields at runtime. None of these appear in the Nix source or the emitted bundle. + +### Provider resource example + +```nix +d2b.zones.dev.resources.example-provider = { + type = "Provider"; + spec = { + # spec.artifactId: plain ID into d2b.artifacts (type=provider); no store path in the ResourceSpec + artifactId = "example-provider"; + # spec.config: exact field names from the Provider artifact's config schema + config = { + controller = { + mainState = { + quotaBytes = 104857600; + snapshotPolicy = { + retainCount = 3; + retainDurationHours = 168; + triggerOnMigration = true; + }; + # Provider schema marks sealingCredentialRef as credentialRef: true + # Only "Credential/" accepted — raw key values fail eval + sealingCredentialRef = "Credential/example-provider-state-key"; + }; + }; + }; + }; +}; +``` + +### Nix option types and schema origin + +Nix option types, defaults, and documentation for every `spec.*` field are generated from two schema sources: + +| Field scope | Schema source | Validation level | +| --- | --- | --- | +| `spec.*` core fields (`artifactId` and ResourceType-common fields) | Committed `ResourceTypeSchema` for `Provider` in `d2b-contracts` | Eval-time (Nix type system) + build-time (JSON Schema) | +| `spec.config.*` | Provider artifact's embedded config schema, exported as a Nix module by the Provider package; resolved at eval time via `spec.artifactId` looked up in `d2b.artifacts` | Eval-time (`lib.evalModules` against Provider config schema module) + build-time (JSON Schema from `provider.json`) | + +The NixOS module resolves `spec.artifactId` against the `d2b.artifacts` entries at eval time to obtain the Provider package and load its config schema module. Every `spec.config.*` field type, default, and documentation is sourced directly from the Provider artifact's own config schema — no bespoke module code in d2b core is needed for Provider-specific config. + +### Eval-time validation + +During `nixos-rebuild eval`: + +1. **ResourceTypeSchema conformance**: `spec.*` core fields are type-checked against Nix option types generated from the committed `ResourceTypeSchema` for `type`. Unknown `spec` keys fail the eval. +2. **Provider config conformance**: `spec.config.*` fields are validated inside `lib.evalModules` against the Provider artifact's config schema module (resolved via `spec.artifactId` from `d2b.artifacts`). Unknown keys, type mismatches, and out-of-bounds values fail the eval. +3. **Credential-ref guard**: any `spec.*` or `spec.config.*` field the schema marks `credentialRef: true` accepts only strings matching `Credential/[a-z][a-z0-9-]*`. A raw value (anything that does not match that pattern) fails with a typed `credential-value-must-be-ref` error. Secret material never enters the Nix store or the emitted bundle. +4. **Reference bounds**: `*Ref` fields that reference Zone resources by `/` are validated for syntactic correctness at eval time; referential existence is validated by the Zone runtime at bundle activation time. +5. **Zone-level conflict detection**: the module aggregates all resources of a given `type` in the Zone and enforces constraints published in the ResourceTypeSchema (e.g., uniqueness invariants among Providers in the same component scope). + +### Build-phase validation and bundle emission + +After eval, the build derivation: + +1. Resolves `spec.artifactId` against the `d2b.artifacts` catalog to locate the Provider package; reads the Provider's signed `provider.json` manifest. The artifact catalog (installed root:d2bd 0640) carries the store path and content digests; no store path appears in the emitted ResourceSpec. +2. Validates the fully rendered `spec` object (including all `spec.config` fields) against the full JSON Schema in the manifest, rejecting any field not present in the Provider's `ResourceTypeSchema` or `configSchema`. +3. Emits the canonical sorted Zone resource bundle at `/etc/d2b/zones//resources.json`. + +The rendered resource JSON for the example above: + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Provider", + "metadata": { + "name": "example-provider", + "zone": "dev" + }, + "spec": { + "artifactId": "example-provider", + "config": { + "controller": { + "mainState": { + "quotaBytes": 104857600, + "snapshotPolicy": { + "retainCount": 3, + "retainDurationHours": 168, + "triggerOnMigration": true + }, + "sealingCredentialRef": "Credential/example-provider-state-key" + } + } + } + } +} +``` + +The input bundle contains only `metadata.name`, `metadata.zone`, and `spec`. `metadata.managedBy` and `configurationGeneration` are **not** serialized in the bundle; the configuration service sets them in the resource store when it persists activated resources. + +The Nix `spec` fields map to the JSON `spec` object with **no field renaming and no alternative nesting**: what the user writes under `spec` in Nix appears unchanged in the canonical JSON `spec`. Build validation compares this rendered JSON against the committed `ResourceTypeSchema` for `Provider` before writing the bundle. + +Bundle envelope: + +```json +{ + "schemaVersion": "v3", + "zone": "dev", + "generation": 7, + "contentId": "sha256:", + "emittedAt": "2026-07-22T04:00:00Z", + "resources": [ /* sorted rendered resource objects */ ] +} +``` + +Bundle properties: + +| Property | Rules | +| --- | --- | +| `resources` order | Lexicographically sorted by `(type, metadata.zone, metadata.name)` | +| `contentId` | SHA-256 of the canonical-sorted UTF-8 `resources` array alone (no envelope fields); deterministic across independent builds of the same Nix inputs | +| Resource metadata | Input bundle contains only `metadata.name` and `metadata.zone` per resource; `managedBy`, `configurationGeneration`, `uid`, `generation`, `resourceVersion`, and timestamps are set by the configuration service / core when persisting activated resources, never by the bundle emitter | +| Credential refs | Remain as `"Credential/"` strings in the bundle; raw key material never appears | +| Schema validation | Build fails if any rendered resource spec violates the committed `ResourceTypeSchema` for its type | + +The build emitter recomputes `contentId` over the final sorted `resources` array and writes it into the envelope. The Zone daemon recomputes `contentId` on load and aborts activation with `bundle-integrity-failure` on mismatch. + +## Generation-based cleanup contract + +### Configuration-managed vs controller-created resources + +The Zone runtime distinguishes two resource ownership classes: + +| Class | Source | `metadata.managedBy` (set on activation) | Responsible for deletion | +| --- | --- | --- | --- | +| **Configuration-managed** | Emitted by Nix Zone resource bundle; configuration service sets `managedBy: configuration` when persisting activated resources | `"configuration"` | Zone runtime: resources absent from the new bundle receive an async Delete | +| **Controller-created** | Created by Provider controllers at runtime (migration EphemeralProcesses, staging Volumes, dynamic Guests, worker subview allocations, etc.); controller sets `managedBy: controller` | `"controller"` | Owning Provider controller via reconcile; Zone runtime never deletes them solely because they are absent from the bundle | + +The `managedBy` enum also includes `"api"` for resources created directly through the resource API. Generation-diff cleanup targets only `managedBy: configuration` resources; `managedBy: controller` and `managedBy: api` resources are never deleted by the generation-diff path. + +The Zone runtime never deletes a `managedBy: controller` resource because it is absent from the incoming bundle. A Provider controller is fully responsible for reconciling and deleting its controller-created children as parent resources change. + +### Applying a new generation + +When the Zone daemon reads a new bundle with a higher `generation` and a different `contentId`: + +1. **Integrity check**: recompute SHA-256 of the `resources` array; abort with `bundle-integrity-failure` on mismatch — prior generation is unchanged. +2. **Diff**: compute `(type, name)` diff between the persisted resource store entries where `managedBy: configuration` and the new canonical configured set. +3. **Added or changed resources**: create new resources; update spec of existing `managedBy: configuration` resources and set their `configurationGeneration`. If a resource with the same `(type, name)` already exists with `managedBy: controller` or `managedBy: api`, the configuration service records a `configuration-name-conflict` error for that resource, marks that generation activation item `Degraded/name-conflict`, and leaves the existing resource untouched. It never seizes ownership by changing `managedBy`. +4. **Absent resources**: set `deletionRequestedAt` on every persisted `managedBy: configuration` resource absent from the new configured set. Deletion proceeds through owner-child/finalizer-safe ordering; the Zone runtime does not force-delete or skip finalizers. +5. **Unchanged resources**: resources whose spec is byte-identical to the persisted spec have their `configurationGeneration` updated to the current generation; no controller reconcile is triggered. + +### Activation does not block + +New generation activation does **not** wait for cleanup completion: + +- `Zone.status.observedGeneration` advances to the new bundle generation immediately after the diff is applied. +- `Zone.status.phase` transitions to `Degraded` with a `pending-cleanup` condition while any prior-generation `managedBy: configuration` resources are still completing deletion. +- `Zone.status.phase` returns to its normal steady state once all pending cleanup completes. +- Providers registered in the new generation start immediately; their controllers may begin creating controller-created children against the new spec without waiting. + +### Prior generation retention + +The Zone daemon retains prior bundle files at `/etc/d2b/zones//resources..json`. The number of retained prior bundles is configurable per Zone in the range 1..16 (default: 3). A prior bundle cannot be pruned while any of the following hold for resources that were removed in the transition from that generation: + +1. Any removed `managedBy: configuration` resource has not yet reached `phase: Deleted`. +2. Any `IncidentHold` condition applies to a removed resource. +3. A pending rollback depends on the prior generation. + +There is no time-based TTL for prior generation retention; retention is count-bounded only. The Zone daemon also retains prior bundles in the resource store for rollback. Rollback replays the prior bundle through the same diff/activate path and is subject to the same integrity-check, diff, and cleanup semantics. + +### Owner-controller child reconciliation + +When a `managedBy: configuration` Provider receives a generation-diff Delete: + +1. Zone runtime sets `deletionRequestedAt` on the Provider resource; Provider controller receives a watch event. +2. Controller's reconcile handler cleans up controller-created children in dependency order: stop component Processes → await mount finalizers → delete Volumes → release Credential leases. +3. Only after all resources with `ownerRef: Provider/` reach `phase: Deleted` does the controller remove its own finalizer. +4. Zone runtime removes the Provider from the resource store and excludes it from future bundle diffs. + +A Provider controller must complete finalizer cleanup within `maxFinalizerDurationSeconds` declared in its descriptor. Exceeding the bound transitions the Provider to `Degraded/finalizer-timeout` and emits the corresponding audit event; operator intervention resolves. + +### Cleanup status, audit, and OTEL + +The following events extend the audit event table in [Status, audit, and OTEL](#status-audit-and-otel): + +| Event kind | Trigger | Payload | +| --- | --- | --- | +| `zone-generation-activated` | Bundle diff applied | zone, generation, content-id, prior-generation | +| `zone-generation-pending-cleanup` | One or more prior-gen resources still deleting | zone, generation, pending-count | +| `zone-generation-cleanup-complete` | All absent resources deleted | zone, generation | +| `zone-generation-rollback` | Prior bundle replayed | zone, from-generation, to-generation | +| `resource-configuration-delete` | Async delete of a prior-gen `managedBy: configuration` resource | zone, resource-type, resource-name, configuration-generation | +| `configuration-name-conflict` | Bundle `(type, name)` collides with existing `managedBy: controller` or `managedBy: api` resource | zone, resource-type, resource-name, generation | +| `provider-finalizer-timeout` | Controller exceeded `maxFinalizerDurationSeconds` | zone, provider-name, elapsed-seconds | +| `bundle-integrity-failure` | `contentId` mismatch on bundle load | zone, generation | + +The following metrics extend the OTEL metric table in [Status, audit, and OTEL](#status-audit-and-otel): + +| Metric | Unit | Labels | +| --- | --- | --- | +| `d2b_zone_generation_activation_total` | Counter | outcome (`applied` \| `integrity-failure`) | +| `d2b_zone_generation_cleanup_pending_resources` | Gauge | (none) | +| `d2b_zone_generation_cleanup_duration_ms` | Histogram | (none) | + +### Required tests for removed-resource cleanup + +Required by ADR046-pstate-010: + +1. **Absent Volume**: new bundle omits a `managedBy: configuration` Volume → Volume receives `deletionRequestedAt`; new generation `status.observedGeneration` advances immediately; `pending-cleanup` condition present; Volume destroyed after mount finalizers clear; `zone-generation-cleanup-complete` audit event. +2. **Absent Provider, children retained**: new bundle omits a `managedBy: configuration` Provider → Provider controller cleans up controller-created children in order; only `managedBy: configuration` Volumes owned by that Provider receive the generation-diff Delete; unrelated controller-created resources in the same Zone are untouched. +3. **Incident hold defers cleanup**: absent `managedBy: configuration` Volume has `IncidentHold` → `deletionRequestedAt` set; `pending-cleanup` condition persists until hold is explicitly cleared; prior generation retained per count-based policy (not prunable while this resource is undeleted). +4. **Bundle integrity failure**: `contentId` in bundle does not match recomputed hash → activation aborts; prior generation unchanged; `bundle-integrity-failure` audit event; no `status.observedGeneration` advance. +5. **Rollback**: prior generation replayed → diff re-creates absent resources; controller-created children with `ownerRef` pointing to restored Provider are retained without spurious deletion. +6. **Finalizer timeout**: Provider controller does not remove finalizer within `maxFinalizerDurationSeconds` → `Degraded/finalizer-timeout` condition set; `provider-finalizer-timeout` audit event. Timeout is stall detection only; no finalizer is force-cleared; operator intervention resolves the stall. +7. **Eval credential-ref guard**: `config` field with `credentialRef: true` receives a raw string → NixOS eval fails with `credential-value-must-be-ref`; no bundle is emitted; build derivation is never entered. +8. **Name conflict**: new bundle contains a resource `(type, name)` that collides with an existing `managedBy: controller` or `managedBy: api` resource → that activation item is `Degraded/name-conflict`; the existing resource and its `managedBy` are unchanged; a `configuration-name-conflict` audit event is emitted; all non-conflicting resources in the same generation activate normally. +9. **Metric identity absence**: every Volume-state and generation metric + descriptor rejects `vm`, `zone`, `zone_id`, `zone_uid`, and every + resource-name-derived label key; a Zone/resource-name canary is absent from + emitted label values while `d2b.zone` remains in OTEL resource attributes. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | Baseline v3 (`fd5b0067`): `packages/d2bd/src/supervisor/state.rs` (`RunnerSnapshotRecord`, keyed by `WorkloadId` + `RunnerRole`); `packages/d2b-contracts/src/broker_wire.rs` (`RunnerRole`: CloudHypervisor, Virtiofsd, Swtpm, SwtpmFlush, Gpu, QemuMedia); `packages/d2b-core/src/processes.rs` (`VmProcessDag`, `ProcessNode`, `ProcessRole`: Swtpm, Virtiofsd, StoreVirtiofsPreflight, CloudHypervisorRunner, etc.); `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` (TPM dir/marker, per `WorkloadId` path); `nixos-modules/store.nix` (per-VM hardlink farm, `WorkloadId`-keyed); `packages/d2b-core/src/storage.rs` (`StorageRoot`, `StoragePathSpec`, `StorageRootClass`, `StorageAuthority`), `sync.rs`, `storage_lifecycle.rs`; `packages/d2b-realm-core/src/ids.rs` (`WorkloadId`, `RealmId`, `RealmPath`, `ProviderId`); `packages/d2b-realm-core/src/workload.rs` (`WorkloadProviderKind`: LocalVm, QemuMedia, ProviderManaged, UnsafeLocal; `IsolationPosture`); `packages/d2b-realm-core/src/realm.rs` (`EntrypointMode`: HostResident, GatewayBacked; `RealmControllerPlacement`: GatewayVm, CloudFullHost, ProviderController, etc.); `packages/d2b-realm-core/src/identity_store.rs` (`RealmIdentityStore`, `EnrollmentRecord`, `ControllerGenerationMetadata`); `packages/d2b-realm-provider/src/provider.rs` (`RuntimeProvider`, `WorkloadProvider`, `HostSubstrateProvider`); `packages/d2b-realm-router/src/session_lifecycle.rs` (`SessionPhase`: Allocating → Running → Stopped); `nixos-modules/unsafe-local-helper.nix`; `nixos-modules/gateway-vm.nix`; `packages/d2b-state/` (main branch 6faa5256 only; absent from v3 baseline) | +| Terminology note | Current v3 uses `WorkloadId`/`Workload` (→ target `Guest/` for VM/sandbox/cloud; `WorkloadProviderKind::UnsafeLocal` → user-only Host, NOT Guest), `Realm`/`RealmId`/`RealmPath` (→ target `Zone`), `ProcessRole`/`ProcessNode`/`VmProcessDag` (→ target `Process` or `EphemeralProcess` resource), `RunnerRole` (→ specific Process resource under Guest/device Provider), `EntrypointMode::GatewayBacked`/`RealmControllerPlacement::GatewayVm` (→ `Guest` VM under `Provider/runtime-cloud-hypervisor`), `StorageRoot`/`StoragePathSpec` (→ Volume layout entries), `StorageAuthority::Daemon/Broker/NixModule` (→ Volume `providerRef` and broker-maintained/Nix-managed layout), `WorkloadProviderKind::LocalVm` (→ `Provider/runtime-cloud-hypervisor`), `WorkloadProviderKind::ProviderManaged` (→ `Provider/runtime-azure-container-apps` or similar). Main branch (6faa5256) uses ADR 0046 target names; none of those names exist in the v3 baseline. | +| Evidence class | `d2b-core` `StorageRoot`/`StoragePathSpec`/`StorageRootClass`/`StorageAuthority` (StorageJson): `generated-or-eval-contract`; swtpm tamper-guard marker (`swtpm_dir.rs`, per `WorkloadId`): `implemented-and-reachable`; daemon `RunnerSnapshotRecord` per `WorkloadId`/`RunnerRole`: `implemented-and-reachable`; ACA/Relay `SessionPhase` lifecycle (`d2b-realm-router/src/session_lifecycle.rs`): `implemented-and-reachable` (gateway paths only); `RealmIdentityStore` enrollment/key-rotation metadata: `implemented-and-reachable`; `RuntimeProvider`/`WorkloadProvider` traits: `implemented-and-reachable` (ACA/Relay/gateway), `test-only-or-preview` (host adapters); `VmProcessDag`/`ProcessRole`/`ProcessNode` generated contracts: `generated-or-eval-contract`; Volume `stateSchema` extension, `ProviderStateSet` query concept, migration EphemeralProcess, staging Volume, sealing, snapshots, relocation, incident hold: `ADR-only` | +| Behavior retained | Anchored path resolution (`AnchoredDir`/`AnchoredResource`, main 6faa5256; equivalent `openat2` pattern in v3 swtpm_dir.rs), OFD locks (`LockGuard`/`LockSet`, main), atomic rename/fsync (`AtomicFilesystem`, main), tamper-guard identity markers (v3 swtpm_dir.rs), fail-closed on missing/replaced state (v3 baseline), exact-owner ACL enforcement (`StoragePathSpec.owner/mode`, v3 baseline), no raw-path in public error envelopes (v3 baseline), generation-bound state envelopes (`StateEnvelope`, main), per-`WorkloadId` (→ per-`Guest`) storage scope, `ProcessRole`-keyed (→ component-descriptor-keyed) state namespace, `StorageLifecycle::Persistent`/`BootScopedReadoptable` classes mapped to Volume `persistenceClass` | +| Required delta | `stateSchema` Volume spec extension, `ProviderStateSet` query (owner-index based), broker-maintained marker root outside Volume tree, migration EphemeralProcess lifecycle, cross-component prepare/commit/rollback, staging Volumes, sealing credential integration, incident hold condition, relocation EphemeralProcess, unclaimed GC, OTEL metric set; `RunnerSnapshotRecord` pattern eliminated (replaced by cgroup-leaf scanning in Process Providers) | +| Reuse path | `d2b-state` from main (6faa5256): copy `AtomicFilesystem`, `AnchoredDir`, `AnchoredResource`, `LeafName`, `RelativePath`, `OfdTransfer`/`LockGuard`/`LockSet` into `d2b-provider-volume-local`; adapt `StateEnvelope`/`CanonicalJson`/`QuarantineRecord` in `d2b-contracts`; adapt `AuditAppender` for Zone audit stream; swtpm_dir.rs marker algorithm (v3 baseline) adapted into volume-local Provider marker module | +| Replacement/deletion | `d2b-core/src/storage.rs` (`StorageRoot`/`StoragePathSpec`) StorageJson, `sync.rs` SyncJson, `storage_lifecycle.rs`, and daemon `RunnerSnapshotRecord` (per `WorkloadId`/`RunnerRole`) removed only after owning Provider Volume specs are live; `VmProcessDag`/`ProcessRole` role branches in daemon removed after Process Provider successors pass parity tests; ACA/Relay `WorkloadProvider`/`RuntimeProvider` adapter removed after `Provider/runtime-azure-container-apps` and `Provider/transport-azure-relay` integrations pass; `RealmIdentityStore` in-memory state is superseded by Zone enrollment credential plane (not a Volume; separate scope from Provider payload state); `d2b-state` crate on main remains until v3 callers migrate | +| Feasibility proof | Pre-acceptance spike: provision one host-local Volume with a stateSchema (using adapted swtpm_dir.rs marker pattern), run a migration EphemeralProcess, verify marker identity check, mount a worker-read view, verify domain-isolation rejection | +| Future owner | ADR046-pstate-001 through ADR046-pstate-011 below | + +## Provider state migration map + +The table below maps each current state root in the v3 tree to its future Volume declaration in the d2b 3.0 Provider model. + +**Terminology note.** In the current v3 baseline (`fd5b0067`), VM execution units are `Workload`/`WorkloadId` (`d2b-realm-core/src/ids.rs`) and `d2b.vms.` Nix options. Current per-VM runner processes are typed by `ProcessRole` (`d2b-core/src/processes.rs`) and `RunnerRole` (`d2b-contracts/src/broker_wire.rs`). In ADR 0046: `WorkloadId`/`Workload` → `Guest/` (for VMs); `Realm`/`RealmId`/`RealmPath` → `Zone`; `ProcessRole`/`RunnerRole` → `Process`/`EphemeralProcess` resources under specific Providers; `WorkloadProviderKind::UnsafeLocal` → user-only Host (not Guest). Paths containing `` use the current `WorkloadId` label; the same label becomes the `Guest/` ResourceName in v3. + +| Current state | Current authority and symbol | v3 persistence | Future Volume owner | Future Volume name pattern | Notes | +| --- | --- | --- | --- | --- | --- | +| `/var/lib/d2b/daemon-state//runtime.json` (`RunnerSnapshotRecord`; `` = `WorkloadId`; role = `RunnerRole` in `d2b-contracts/src/broker_wire.rs`) | Daemon (`d2bd`, `packages/d2bd/src/supervisor/state.rs`) | Superseded | — | — | In v3, Process Providers (system-minijail for `RunnerRole::CloudHypervisor`/`Virtiofsd`/`Swtpm`; system-systemd for `ProcessRole::HostReconcile`) adopt running processes via cgroup-leaf scanning + fresh pidfd; the persisted PID snapshot pattern is eliminated. `WorkloadId` → `Guest/`; each `RunnerRole` variant → a Process resource owned by its Guest/device Provider. No Volume needed. | +| `/var/lib/d2b/vms//swtpm/` (TPM NVRAM + EK seed; `` = `WorkloadId`; launched as `ProcessRole::Swtpm`/`RunnerRole::Swtpm`) | Broker (`d2b-priv-broker/src/ops/swtpm_dir.rs`) | `persistent` | Provider/device-tpm | `device-tpm--nvram--` (`WorkloadId` → `Guest/` name) | Broker-maintained identity marker; fail-closed replacement detection; `RunnerRole::Swtpm` → Process resource under Guest owned by device-tpm Provider | +| `/var/lib/d2b/swtpm-markers/` (TPM tamper-guard marker; `` = `WorkloadId`) | Broker | `persistent` | Provider/device-tpm | (marker root: broker-maintained, outside Volume root) | Marker root pattern adapts directly to broker-maintained `identityMarker.class: broker-maintained` | +| `/var/lib/d2b/vms//store/` (Nix hardlink farm; `` = `WorkloadId`; served via `ProcessRole::Virtiofsd`/`RunnerRole::Virtiofsd`; preflight at `ProcessRole::StoreVirtiofsPreflight`) | NixOS activation (`nixos-modules/store.nix`) | `persistent` | Provider/runtime-cloud-hypervisor | `runtime-cloud-hypervisor--nix-store--` (one per Guest; `WorkloadId` → `Guest/`) | `persistenceClass: config`; layout managed by NixOS activation; no `stateSchema` migration; `ProcessRole::Virtiofsd`/`StoreVirtiofsPreflight` → virtiofsd Process and preflight EphemeralProcess under Guest | +| `/var/lib/d2b/vms//store-meta/` (generation pins + GC roots; `` = `WorkloadId`) | NixOS activation (`nixos-modules/store.nix`) | `persistent` | Provider/runtime-cloud-hypervisor | `runtime-cloud-hypervisor--nix-store-meta--` | `persistenceClass: config` | +| Unsafe-local user session state (helper runtime + shell supervisor; `WorkloadProviderKind::UnsafeLocal` in `d2b-realm-core/src/workload.rs`) | User daemon (`nixos-modules/unsafe-local-helper.nix`) | `ephemeral` user-domain | Provider/system-core (user-only Host, NOT Guest) | `system-core--unsafe-local-session--user-` | User-domain Volume; no cross-uid access; no system-domain mount; maps from `WorkloadProviderKind::UnsafeLocal` → user-only Host ExecutionPolicy (D042) | +| Gateway realm VM config (`nixos-modules/gateway-vm.nix`; current `EntrypointMode::GatewayBacked`, `RealmControllerPlacement::GatewayVm` from `d2b-realm-core/src/realm.rs`; `WorkloadProviderKind::LocalVm`) | NixOS activation | `persistent` | Provider/runtime-cloud-hypervisor (gateway as Guest) | `runtime-cloud-hypervisor--gateway-config--` (`WorkloadId` → `Guest/`) | `persistenceClass: config`; gateway VM OS/data state only; current `RealmIdentityStore` enrollment records are Zone-level credential/enrollment data (not in this Volume; separate mapping below) | +| `d2b-core/src/storage.rs` `StorageJson`/`SyncJson` generated contract rows (per `ProcessRole`/`StorageRoot`/`StoragePathSpec.scope`) | Broker/daemon | varies by `StorageLifecycle` enum | Respective Provider per row (derived from `StorageAuthority` + `ProcessRole` → Provider owner mapping) | Named per Provider component descriptor `stateNamespace.id` | Full reset; no row-level import; `StoragePathSpec.scope` (current ProcessRole-keyed ID) → component-descriptor `stateNamespace.id`; `StorageRootClass::Persistent` → `persistenceClass: persistent`; `StorageRootClass::Config` → `persistenceClass: config`; `StorageLifecycle::BootScopedReadoptable` → `persistenceClass: ephemeral`; `StorageAuthority::Broker` → broker-maintained marker; `StorageAuthority::NixModule` → Nix-managed layout | +| ACA workload/display session lifecycle (`d2b-realm-router/src/session_lifecycle.rs` `SessionPhase`; `WorkloadProvider`/`RuntimeProvider` in `d2b-realm-provider/src/provider.rs`; live for ACA/Relay gateway paths) | ACA/Relay Provider adapters (`d2b-realm-router`) | In-memory | Not Volume — maps to Process/EphemeralProcess status conditions | — | `SessionPhase` (Allocating → TokenMinting → RelayConnecting → DisplayOpening → Running → Stopping → Stopped) is in-memory session state; no persistent file Volume. Maps to EphemeralProcess status under `Provider/runtime-azure-container-apps` or `Provider/transport-azure-relay`. `WorkloadProviderKind::ProviderManaged` → `Guest` under `Provider/runtime-azure-container-apps`. | +| Realm enrollment/key-rotation metadata (`d2b-realm-core/src/identity_store.rs` `RealmIdentityStore`; `EnrollmentRecord`, `ControllerGenerationMetadata`, `RevocationList`; `WorkloadId` in teardown directives) | Realm controller (in-memory, `d2b-realm-core`) | In-memory | Not Provider payload Volume — maps to Zone enrollment credential resources | — | Pure in-memory enrollment, generation, revocation, and recovery metadata; opaque refs and fingerprints only; no key material. In v3, controller static private key arrives via systemd credential (`d2b-controller-static-v2`); enrollment/recovery journal maps to Zone-level `Credential` resources. `RealmId`/`RealmPath` → `Zone`; `WorkloadId` in teardown → `Guest/`. | +| Provider/workload registry (`d2b-realm-core/src/registry.rs` `ProviderRegistryEntry`, `WorkloadPlacement`; in-memory in realm controller) | Realm controller (in-memory) | In-memory | Not Volume — maps to Zone resource store Provider/Guest resources | — | Provider registry entries and workload placement are in-memory routing metadata. In v3, this becomes `Provider/` resource status and `Guest/` spec with `providerRef` in the Zone resource store. No persistent file Volume. | + +All file-backed migrations are destructive v3 resets; no v2 state is imported. Persistent identity data (TPM NVRAM) must be backed up by the operator before the host is reset to v3. + +## d2b-state reuse plan + +Main-branch `d2b-state` (at commit `6faa5256`) is the primary reuse source for volume-local Provider filesystem primitives. This code does not exist in the v3 baseline. The following symbols are selected for copy/adaptation: + +| Main symbol | Main path | Reuse action | v3 destination | Adaptation notes | +| --- | --- | --- | --- | --- | +| `AtomicFilesystem`, `RealAtomicFilesystem`, `AtomicWrite`, `CanonicalJson`, `DurableState`, `QuarantineRecord`, `GenerationPolicy`, `MetadataExpectation`, `ReadPolicy`, `WritePolicy` | `packages/d2b-state/src/atomic.rs` | adapt | `packages/d2b-provider-volume-local/src/atomic.rs` | Replace ADR 0045 `v2_state` contract imports with v3 `StateEnvelope`; retain all filesystem + fsync semantics unchanged | +| `AnchoredDir`, `AnchoredResource`, `LeafName`, `RelativePath` | `packages/d2b-state/src/path.rs` | copy-unchanged | `packages/d2b-provider-volume-local/src/path.rs` | No ADR 0045 contract dependencies; path resolution logic is self-contained | +| `LockGuard`, `LockSet`, `OfdTransfer`, `Cancellation`, `Clock`, `NeverCancelled`, `SystemClock` | `packages/d2b-state/src/lock.rs` | adapt | `packages/d2b-provider-volume-local/src/lock.rs` | Replace `v2_state` LockSpec/ResourceId with v3 typed lock IDs; retain OFD lock/CLOEXEC/fd-transfer semantics | +| `LeaseStatus`, `grant_lease`, `revoke_lease`, `validate_lease` | `packages/d2b-state/src/lease.rs` | adapt | `packages/d2b-contracts/src/v3/state_lease.rs` | Map to v3 Volume Credential rotation protocol; retain expiry/revocation semantics | +| `AuditAppender`, `AuditRecordInput`, `SegmentBuilder`, `checkpoint`, `decide_retention`, `detect_gap`, `read_audit_segment` | `packages/d2b-state/src/audit.rs` | adapt | `packages/d2b-provider-volume-local/src/audit.rs` | Adapt to Zone audit stream interface; retain segment builder, hash chain, and gap detection; replace stream/actor types with v3 equivalents | +| Integration tests (atomic write, OFD lock, quarantine, audit segment, lease) | `packages/d2b-state/tests/state.rs`, `async_state.rs` | adapt | `packages/d2b-provider-volume-local/tests/state.rs` | Port exact test scenarios; replace ADR 0045 contract setup with v3 Volume/StateEnvelope setup; retain all fault-injection and order-of-operations coverage | + +The following ADR 0045 assumptions in `d2b-state` are explicitly excluded: + +- `d2b-contracts::v2_state` envelope schema and authority refs; +- `AuthorityRef`/`OwnershipEpoch`/`v2_state::ResourceId` semantics; +- ADR 0045 broker-operation IDs embedded in `LockSpec.resource_id`; +- the `tokio_api` feature's dependency on the ADR 0045 broker transport. + +## Bus / ComponentSession cross-reference + +Provider state controllers and the volume-local Provider implementation depend on the d2b-bus / ComponentSession stack, but that stack's architecture, reuse plan, and implementation work items are owned by `ADR-046-componentsession-and-bus`. This spec does not duplicate or re-specify them. + +The following state-relevant symbols from main commit `a1cc0b2d` are called out here because they appear directly in provider-state work items: + +- **`Cancellation` / `RequestRegistry`** (`packages/d2b-session/src/cancellation.rs`, main `a1cc0b2d`): copy-unchanged into d2b-bus; used by migration EphemeralProcess coordination (ADR046-pstate-004) and sealing credential operations (ADR046-pstate-005) for atomic shutdown propagation. +- **`OwnedAttachment` / `AttachmentPayload`** (`packages/d2b-session/src/attachment.rs`, main `a1cc0b2d`): copy-unchanged into d2b-bus; volume-local Provider uses file-descriptor attachments to deliver a worker subview dirfd to requesting Processes (see [Worker subviews](#worker-subviews)). The descriptor-payload pairing and `validate_descriptor`-after-decrypt pattern are preserved unchanged. +- **`Fixture`, `FakeProvider`, `DeterministicClock`** (`packages/d2b-provider-toolkit/src/fixture.rs`, main `a1cc0b2d`): adapt into v3 toolkit; used by ADR046-pstate-009 integration tests as the fake Zone runtime without a live daemon. ADR 0045 credential lease formats excluded. +- **`StorageEffectPort` injection pattern** (`packages/d2b-provider-storage-local/src/lib.rs`, `packages/d2bd/src/provider_effects.rs`, main `a1cc0b2d`): adapt into `packages/d2b-provider-volume-local/src/effect_port.rs`; the Provider receives opaque Volume IDs and forwards filesystem operations to an injected async `VolumeEffectPort`; the daemon binds the port via `DaemonEffectAdapters`, never calling the Provider directly for filesystem operations (ADR046-pstate-003). ADR 0045 `v2_provider` operation types and plan/handle/lease protocol excluded. + +## Provider crate layout + +Every `packages/d2b-provider--/` crate created by this or any other spec must include the following four paths. Absence of any path fails the workspace and package policy check (see ADR046-pstate-011): + +| Path | Contents | +| --- | --- | +| `src/` | Implementation modules and binary entry points; colocated `#[cfg(test)]` unit tests for individual functions | +| `tests/` | Hermetic Cargo integration tests: ResourceType conformance tests, controller reconcile logic, fault-injection scenarios that do not require a live daemon (use `Fixture`/`FakeProvider`/`DeterministicClock` from the controller toolkit) | +| `integration/` | Heavier scenario tests requiring a container, real Host/Guest processes, cross-process coordination, or provider-system fixtures; invoked by the existing `make test-integration` / `make heavy-test-integration` orchestration rather than bare `cargo test` | +| `README.md` | Documents: Provider identity and artifact ID; config schema (`spec.config.*` with types and defaults); ResourceTypes owned; controllers/services/workers/binaries and their roles; placement requirements (Host/Guest/user); dependencies and RBAC; security model; state surfaces; telemetry surfaces; build, test, and integration command reference; and future standalone-repo usage guide | + +`integration/` tests are not run by `cargo test` alone. Scenarios requiring a live daemon or a real mounted Volume belong in `integration/`; scenarios that can run against a fake Zone runtime belong in `tests/`. Both test trees are required; an empty `integration/` directory is not acceptable — at minimum it must contain a placeholder scenario and a `README.md` noting future scenarios. + +## Implementation work items + +### ADR046-pstate-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-001` | +| Dependency/owner | ADR046-primitives-001; v3 contracts owner | +| Current source | `packages/d2b-core/src/storage.rs` (`StoragePathSpec` with `scope: ContractId` currently keyed by `ProcessRole`/Workload; `SensitivityClass`; `StorageLifecycle`; `StorageRootClass`); `packages/d2b-state/src/atomic.rs` (main, 6faa5256; absent from v3 baseline) | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/volume_state.rs` | +| Detailed design | `VolumeStateSchema` struct (`schemaId`, `schemaVersion`, `schemaDigest`, `migrationPolicy`); `PersistenceClass` and `SensitivityClass` enums; `VolumeStateStatus` extension (`stateSchemaPhase`, `installedSchemaVersion`, `markerStatus`, `sealingStatus`, `quotaUsage`, `lastMigrationAt`); `StateEnvelope` (replaces v2 `StateEnvelope`); canonical JSON serde and digest helpers Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Volume spec and status structs embed these types; Provider descriptor component stateNamespace declaration uses the same types | +| Data migration | Full v3 reset; no v2 state schema import | +| Validation | Schema golden vectors; phase/reason round-trip; StateEnvelope digest tests | +| Removal proof | `d2b-core/src/storage.rs` StoragePathSpec/SensitivityClass removed only after all Provider descriptor consumers are on v3 Volume spec | + +### ADR046-pstate-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-002` | +| Dependency/owner | ADR046-pstate-001; Provider contracts owner | +| Current source | `packages/d2b-core/src/processes.rs` (`VmProcessDag`, `ProcessNode`, `ProcessRole`: each current `ProcessRole` variant maps to a `Process` or `EphemeralProcess` resource under its owning Provider; `ProcessRole::Swtpm`/`Virtiofsd`/`CloudHypervisorRunner` → Process resources under `Provider/device-tpm`/`Provider/volume-virtiofs`/`Provider/runtime-cloud-hypervisor`); Provider descriptor component model from ADR046-provider-001 | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/provider.rs` (component descriptor `stateNamespaces` field) | +| Detailed design | Add `stateNamespaces: Vec` to the component descriptor (zero or more entries; a component declares an entry only when a payload passes the storage-need test; stateless components declare none); each entry includes `id`, `kind` (always `state`), `schemaId` (non-null), `schemaVersion`, `schemaDigest`, `persistenceClass` (must be `persistent`; `ephemeral`/`cache` rejected), `sensitivityClass`, `migrationPolicy`, `quotaBytes` (nonzero; minimum 4096), `storageNeed` (`secret` \| `large-binary` \| `private-unsafe-for-status` \| `revision-unsuitable`), `sealingRequired`, `placementMode` (`guest-local` or `host-backed-guest` for Guest-targeted; omitted for Host-targeted), `hostCustodyPermitted` (required `true` for `host-backed-guest`; absent/false for `guest-local`), and `views`; there is no empty-payload (`schemaId: null`) namespace | +| Integration | Provider package build emits component descriptors with state namespaces; Provider controller creates Volumes from descriptors at install time | +| Data migration | Full reset | +| Validation | Descriptor schema golden vectors; descriptor-Volume consistency property test; stateless-component-declares-no-namespace round-trip; storage-need justification enforcement (namespace whose payload is derivable from spec/status/core ledger/external observation → `component-state-not-justified`); `kind != state` → `component-kind-invalid`; `persistenceClass: ephemeral` → `component-persistence-class-forbidden`; `quotaBytes: 0` or `1024` → `component-quota-too-small`; base `quota.maxBytes == quotaBytes` and `quota.maxInodes > 0`; Guest-targeted with `placementMode: guest-local` → source.executionRef=Guest; Guest-targeted with `host-backed-guest` + `hostCustodyPermitted: true` → source on Host, Export created; `host-backed-guest` without `hostCustodyPermitted: true` → `placement-host-custody-violation`; credential/audit schema with `host-backed-guest` → `guest-local-required`; `placementMode` change → descriptor version increment enforced | +| Removal proof | Not applicable (new) | + +### ADR046-pstate-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-003` | +| Dependency/owner | ADR046-pstate-001; volume-local Provider owner | +| Current source | `packages/d2b-state/src/atomic.rs`, `path.rs`, `lock.rs` (main, 6faa5256); `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` (marker algorithm, v3 baseline) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-volume-local/` (new crate, full scaffold required): `src/{atomic.rs, path.rs, lock.rs, marker.rs, effect_port.rs}`; `tests/volume_local.rs` (marker missing/replaced/mismatch, domain-isolation rejection, quota enforcement); `integration/volume_local.rs` (real Host filesystem provision, broker-maintained marker check, domain-isolation rejection cross-process); `README.md` | +| Crate layout | Creates full `d2b-provider-volume-local/` scaffold. `src/` owns filesystem primitives and effect port; `tests/` owns hermetic fault-injection and conformance tests (no live daemon); `integration/` owns real-Host provision, marker verification, and cross-process domain-isolation scenarios; `README.md` documents volume-local Provider identity, `spec.config.*`, Volume ResourceType ownership, broker placement requirements, OFD-lock security model, state and telemetry surfaces, and build/test/integration commands | +| Detailed design | Anchored Volume root provision, identity marker write/check, quota soft-check on write, domain-isolation validation, fd-relative layout creation/repair/cleanup, broker-maintained marker root protocol; layout `ownerRef`/`groupRef` must reference a Nix-preprovisioned User principal or bounded system pool — Volume admission rejects runtime-created principals; `VolumeEffectPort` returns opaque IDs and named view dirfds only — no raw host path returned by any EffectPort operation; volume-local must support `source.executionRef: Guest/` for `guest-local` placement (controller running inside the Guest): when executing in a Guest domain, volume-local may not create, read, or hold dirfds/paths for Volumes sourced in another domain; `host-backed-guest` placement creates a `virtiofs.d2bus.org.Export` child per attachment entry and validates `hostCustodyPermitted: true` in the signed descriptor Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged (path.rs) / adapt (atomic.rs, lock.rs) / adapt (swtpm_dir.rs marker algorithm). | +| Integration | `d2b-priv-broker` calls `volume_local::marker::provision_marker` at broker-maintained Volume creation; `d2b-provider-volume-local` controller calls `marker::verify_marker` on every daemon restart via reconcile startup relist | +| Data migration | New marker written for each Volume at v3 first-boot; TPM marker path adapted from current swtpm-markers root | +| Validation | Marker missing/replaced/mismatch tests; domain-isolation rejection tests; quota enforcement tests; crash at every provision step | +| Removal proof | `swtpm_dir.rs` marker implementation retired only after device-tpm Provider Volume is live and marker-check parity is confirmed | + +### ADR046-pstate-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-004` | +| Dependency/owner | ADR046-reconcile-001, ADR046-pstate-003; volume-local Provider and controller-toolkit owners | +| Current source | `packages/d2b-core/src/storage_lifecycle.rs` (`StorageLifecycleReport` issue detection); `packages/d2b-state/src/atomic.rs` (main, 6faa5256) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-volume-local/src/migration.rs`; `packages/d2b-provider-volume-local/tests/migration_unit.rs` (hermetic staging Volume and prepare/commit/rollback); `packages/d2b-provider-volume-local/integration/migration.rs` (real Host crash-injection at each migration step, N-Volume cross-component coordination); `packages/d2b-controller-toolkit/src/state_migration.rs` | +| Crate layout | See [Provider crate layout](#provider-crate-layout). Migration crash-injection tests (OS-level kill between rename steps) and staged filesystem verification require real Host processes → `integration/migration.rs`; pure protocol and EphemeralProcess dispatch logic → `tests/migration_unit.rs` | +| Detailed design | Pre-launch migration EphemeralProcess template, staging Volume create/destroy lifecycle, prepare/commit/rollback protocol implementation, roll-forward on restart detection, migration idempotency | +| Integration | Provider controller's reconcile handler calls toolkit `state_migration::plan` and dispatches EphemeralProcess via ResourceClient; volume-local Provider reports `stateSchemaPhase` transitions | +| Data migration | None (new protocol) | +| Validation | Migration with and without crash at each step; rollback after failed EphemeralProcess; roll-forward after interrupted commit; cross-component N-Volume coordination | +| Removal proof | `StorageLifecycleReport` and storage contract validation in `d2b-core` removed only after v3 Volume migration path is live | + +### ADR046-pstate-005 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-005` | +| Dependency/owner | ADR046-pstate-003; volume-local Provider and Credential Provider owners | +| Current source | `packages/d2b-state/src/lease.rs` (main, 6faa5256); Credential ResourceType from ADR046-primitives-001 | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-volume-local/src/sealing.rs`; `packages/d2b-provider-volume-local/tests/sealing_unit.rs` (hermetic seal/rotation state machine); `packages/d2b-provider-volume-local/integration/sealing.rs` (real Credential lease flow, interrupted key rotation with live Credential Provider) | +| Crate layout | See [Provider crate layout](#provider-crate-layout). Credential lease acquisition and re-encryption under rotation require a live Credential Provider process → `integration/sealing.rs`; pure sealing state-machine logic → `tests/sealing_unit.rs` | +| Detailed design | Envelope encryption on write using Credential lease key material; key rotation EphemeralProcess re-encrypt; no raw key on disk; `sealingStatus` transitions; `rotation-failed` fail-safe | +| Integration | Volume controller reads Credential status/lease before provisioning; sealing wraps `StateEnvelope` writes in atomic.rs | +| Data migration | Sealed Volumes are new (no existing sealing to migrate) | +| Validation | Seal/read/rotation tests; rotation interrupted at commit; credential revoked during runtime | +| Removal proof | Not applicable (new) | + +### ADR046-pstate-006 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-006` | +| Dependency/owner | ADR046-pstate-003, ADR046-pstate-004; volume-local Provider and snapshot toolkit owners | +| Current source | `packages/d2b-state/src/atomic.rs` (main, 6faa5256): `AtomicFilesystem` read snapshot; no existing snapshot infrastructure in v3 | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-volume-local/src/snapshot.rs`; `packages/d2b-provider-volume-local/tests/snapshot_unit.rs` (hermetic `snapshotPolicy` enforcement, retention count and TTL logic); `packages/d2b-provider-volume-local/integration/snapshot.rs` (real Host filesystem snapshot creation, retention expiry, pre-migration auto-snapshot with interrupted migration) | +| Crate layout | See [Provider crate layout](#provider-crate-layout). Real-filesystem snapshot byte-equality verification and pre-migration crash recovery require real Host processes → `integration/snapshot.rs`; policy logic and EphemeralProcess lifecycle → `tests/snapshot_unit.rs` | +| Detailed design | Snapshot EphemeralProcess; bounded `.snapshots/` sub-tree; `snapshotPolicy` enforcement; retention count and TTL cleanup; snapshot status tracking in Volume status | +| Integration | Provider controller creates snapshot EphemeralProcess before migration and relocation based on `snapshotPolicy.triggerOnMigration` and `triggerOnRelocation`; status populated via Volume status update | +| Data migration | No existing snapshots; new infrastructure only | +| Validation | Create/read/expire/retention-limit tests; snapshot before interrupted migration; snapshot list in Volume status | +| Removal proof | Not applicable (new) | + +### ADR046-pstate-007 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-007` | +| Dependency/owner | ADR046-pstate-003; volume-local Provider owner | +| Current source | `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` (v3, marker pattern); `packages/d2b-state/src/path.rs` (main) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-volume-local/src/relocation.rs`; `packages/d2b-provider-volume-local/tests/relocation_unit.rs` (hermetic finalizer set/clear, commit/failure state machine); `packages/d2b-provider-volume-local/integration/relocation.rs` (real Host-to-Host anchored file copy, crash at copy midpoint, virtiofs source re-point after successful relocation) | +| Crate layout | See [Provider crate layout](#provider-crate-layout). Crash-at-midpoint relocation and virtiofs source re-point require real Host/Guest processes → `integration/relocation.rs`; finalizer and commit protocol logic → `tests/relocation_unit.rs` | +| Detailed design | Relocation EphemeralProcess; source finalizer; anchored copy; source Volume (volume-local) relocation for components with Guest attachment (the attachment Volume backed by volume-virtiofs is re-pointed to the new source after copy; see `ADR-046-primitive-resource-composition` Volume attachment spec); commit/failure handling | +| Integration | Provider controller adds `Relocating` finalizer before creating relocation EphemeralProcess; removes finalizer after successful destination Volume activation | +| Data migration | Not applicable | +| Validation | Relocation with crash at copy midpoint; failed relocation source preservation; virtiofs source relocation test | +| Removal proof | Not applicable (new) | + +### ADR046-pstate-008 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-008` | +| Dependency/owner | ADR046-pstate-001; Zone audit stream, OTEL provider owners | +| Current source | `packages/d2b-state/src/audit.rs` (main, 6faa5256): `AuditAppender`, `AuditRecordInput`, `SegmentBuilder`, `checkpoint`, `decide_retention`, `detect_gap`, `read_audit_segment`; Zone audit stream interface from ADR046-bus contracts (see ADR-046-componentsession-and-bus); OTEL cardinality model from `packages/d2b-provider-observability-local/src/` (main, a1cc0b2d) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-volume-local/src/audit.rs`; `packages/d2b-provider-volume-local/src/otel.rs`; `packages/d2b-provider-volume-local/tests/audit_unit.rs` (hermetic audit golden records, OTEL cardinality label tests) | +| Crate layout | See [Provider crate layout](#provider-crate-layout). Hermetic golden-record and cardinality tests → `tests/audit_unit.rs`; live Zone audit stream emission and OTEL export against a running observability Provider → `integration/audit.rs` (added by ADR046-pstate-009) | +| Detailed design | Volume-state audit event types and Zone audit emission; OTEL metric definitions with closed semantic label sets and no Zone or resource-name-derived label keys; Zone identity remains in the `d2b.zone` OTEL resource attribute | +| Integration | Every state lifecycle transition calls `audit::emit_volume_event`; OTEL metrics exported via `observability-otel` Provider | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Audit event golden records; no content/path/credential in audit payload; structural OTEL label-policy tests assert exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys and preserve `d2b.zone` as a resource attribute | +| Removal proof | Not applicable | + +### ADR046-pstate-009 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-009` | +| Dependency/owner | ADR046-pstate-001 through ADR046-pstate-008; integration test owner | +| Current source | `packages/d2b-state/tests/state.rs`, `async_state.rs` (main, 6faa5256): atomic, lock, quarantine, audit, lease tests; `packages/d2b-provider-toolkit/src/fixture.rs` (main, a1cc0b2d): `Fixture`, `FakeProvider`, `DeterministicClock`, `ProviderValues`, `Redacted`, `Secret`, `sample_lease_request` | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-volume-local/tests/state.rs` (ported hermetic atomic/lock/quarantine/lease tests); `packages/d2b-provider-volume-local/tests/migration.rs` (ported migration fault-injection, cross-component N-Volume coordination); `packages/d2b-provider-volume-local/integration/provider_state.rs` (end-to-end: live daemon, real Host Volume mount, cross-process worker subview, full audit stream); `packages/d2b-provider-volume-local/integration/audit.rs` (live Zone audit stream emission and OTEL export) | +| Crate layout | See [Provider crate layout](#provider-crate-layout). This work item populates both `tests/` (hermetic ported d2b-state tests using `FakeProvider`/`DeterministicClock`) and `integration/` (end-to-end provider-system scenarios requiring a live daemon + real Volume mount); must include a populated `integration/README.md` describing scenario prerequisites | +| Detailed design | Port all d2b-state integration tests replacing ADR 0045 contract setup with v3 Volume/StateEnvelope; add provider-state-specific migration, marker, quota, sealing, relocation, snapshot, incident-hold, and unclaimed-GC tests; include cross-component N-Volume coordination test | +| Integration | Tests run against the real volume-local Provider over a fake Zone runtime (no live daemon required) using the standard controller-toolkit fake clients | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | All ported tests pass under v3 contracts; test coverage includes every fault-injection scenario listed in d2b-state/tests/state.rs plus new provider-state cases; stateless-component-declares-no-Volume test passes; shared-Volume attempt rejected; `guest-local` Volume creation inside Guest domain (source.executionRef=Guest, no Export created, Host volume-local holds no dirfd/path); `host-backed-guest` Volume creation (source on Host, Export created, Export reaches Ready, Guest Process mounts source Volume view); `host-backed-guest` without `hostCustodyPermitted: true` → `placement-host-custody-violation`; credential/audit schema with `host-backed-guest` → `guest-local-required`; cross-domain isolation: Guest-local volume-local does not create or observe Host-domain Volumes | +| Removal proof | `d2b-state` crate retired from workspace only after every caller migrates to v3 Volume state helpers and all ported tests pass | + +### ADR046-pstate-010 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-010` | +| Dependency/owner | ADR046-pstate-001; Zone resource bundle owner, NixOS module owner, `d2b-core-controller` owner (ADR-046-core-controllers) | +| Current source | `nixos-modules/manifest.nix` (current `manifest.json` emitter, v3 baseline `fd5b0067`); `packages/d2b-core/src/storage.rs` (`StorageAuthority::NixModule`-owned rows); `packages/xtask/src/main.rs` (`gen-schemas` command, same baseline) | +| Reuse action | adapt | +| Destination | `nixos-modules/zone-resources.nix` (per-Zone bundle emitter NixOS module); `packages/d2b-core/src/v3/zone_bundle.rs` (shared bundle DTOs: `ZoneResourceBundle`, `BundleResource`, `contentId` computation); `packages/d2b-core-controller/src/configuration.rs` (diff/apply loop, name-conflict detection, `pending-cleanup` Zone status, `maxFinalizerDurationSeconds` stall detection — NOT in `d2b-provider-volume-local`); `packages/d2b-core-controller/tests/configuration.rs` (hermetic bundle diff, absent-resource Delete dispatch, name-conflict `Degraded/name-conflict` item, integrity-failure abort); `packages/d2b-core-controller/integration/configuration.rs` (container-based generation activation with running Providers, absent-resource cleanup, rollback, finalizer-timeout stall detection) | +| Crate layout | Configuration-publication diff/apply handler and its tests belong in `packages/d2b-core-controller` (ADR-046-core-controllers), not in `d2b-provider-volume-local` and not in the pre-ADR45 `d2bd` binary. `d2b-provider-volume-local` owns only Volume-behavior code (provision, marker, migration, sealing, snapshot, relocation, unclaimed GC). See [Provider crate layout](#provider-crate-layout) for Volume-behavior crate requirements. | +| Detailed design | Generic `d2b.zones..resources. = { type = "…"; spec = { …exact ResourceTypeSpec fields… }; }` attrset; `metadata.name` derived from attr key, `metadata.zone` from Zone key, `apiVersion` defaulted; `status` omitted (read-only); Nix option types for `spec.*` generated from committed `ResourceTypeSchema` for each `type`; Nix option types for `spec.config.*` generated from the Provider artifact's config schema module (resolved at eval time via `spec.artifactId` from `d2b.artifacts`); credential-ref guard (`credentialRef: true` schema fields accept only `Credential/[a-z][a-z0-9-]*`); build-phase full JSON Schema validation of rendered `spec` against Provider manifest; canonical sorted bundle emission with `contentId` (SHA-256 of sorted `resources` array); configuration service sets `metadata.managedBy: configuration` and `configurationGeneration` in the resource store when persisting activated bundle resources (not in the bundle input; user authors only `type` + `spec`); diff compares new configured set against persisted resource store entries where `managedBy: configuration` (not against the prior bundle file); name-conflict detection: `(type, name)` collision with existing `managedBy: controller` or `managedBy: api` resource → `Degraded/name-conflict` activation item, existing resource untouched, `managedBy` never seized; unchanged-spec resources receive updated `configurationGeneration` with no controller reconcile triggered; absent-resource async Delete with owner-child/finalizer ordering; `Degraded/pending-cleanup` Zone status condition; per-Zone prior bundle count retention (range 1..16, default 3; no TTL); `maxFinalizerDurationSeconds` stall detection and `Degraded/finalizer-timeout` condition (no force-clear) | +| Integration | NixOS build emits `/etc/d2b/zones//resources.json`; Zone daemon watches path via systemd path unit; reconcile loop runs generation diff on change; Provider controller receives `deletionRequestedAt` watch event when configuration-owned Provider is absent from new bundle | +| Data migration | `nixos-modules/manifest.nix` provider-registration and storage-authority rows superseded by `zone-resources.nix`; prior `manifest.json` format retired after Zone daemon migration to bundle format | +| Validation | All eight removed-resource cleanup tests enumerated in [Required tests for removed-resource cleanup](#required-tests-for-removed-resource-cleanup); eval credential-ref guard (test: raw value in `credentialRef` field → NixOS eval error, no bundle emitted); Provider schema conformance golden vector (test: unknown `config` key → build fails); `contentId` determinism (test: two independent builds of identical Nix inputs produce byte-identical bundles) | +| Removal proof | `nixos-modules/manifest.nix` provider-registration rows retired only after all Provider registrations use the bundle format and all consumers (broker, Zone daemon) complete bundle-format migration | + +### ADR046-pstate-011 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-011` | +| Dependency/owner | ADR046-pstate-003; workspace policy and xtask owners | +| Current source | `packages/xtask/src/main.rs` (`gen-schemas` and workspace-policy checks, v3 baseline `fd5b0067`); `tests/unit/gates/drift-check.sh` (schema drift gate, same baseline); `packages/d2b-contract-tests/` (contract-test policy, frozen per AGENTS.md) | +| Reuse action | adapt | +| Destination | `packages/xtask/src/provider_crate_policy.rs`; `tests/unit/gates/provider-crate-layout-check.sh` | +| Crate layout | Not applicable (this work item implements the policy gate itself) | +| Detailed design | `cargo xtask check-provider-crate-layout` command: walks every workspace member matching `d2b-provider-*`; for each, asserts presence of `src/`, `tests/`, `integration/`, and `README.md`; asserts `integration/` contains at least one `.rs` file and a `README.md`; fails closed with a typed `missing-provider-crate-path` error listing every absent path; wired into `make test-policy` via `tests/unit/gates/provider-crate-layout-check.sh`; output is machine-readable JSON (`{ "crate": "…", "missing": ["integration/"] }` per violation) Primary reuse disposition: `adapt`. Preserved source-plan detail: extend. | +| Integration | `make test-policy` runs `cargo xtask check-provider-crate-layout`; GitHub CI runs `make test-policy` on every PR; `make check` includes `test-policy` as a required Layer-1 shard; workspace policy tests in `packages/d2b-contract-tests/` are extended with a static manifest check that asserts `provider_crate_layout` policy version is current | +| Data migration | Not applicable | +| Validation | Policy gate detects missing `src/` → error; missing `tests/` → error; missing `integration/` → error; missing `README.md` → error; empty `integration/` (no `.rs` files) → error; all four paths present and non-empty → pass; existing non-provider `d2b-*` crates not flagged; gate is idempotent across re-runs | +| Removal proof | Not applicable (permanent gate) | + +### ADR046-pstate-012 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pstate-012` | +| Dependency/owner | ADR046-pstate-001, ADR046-pstate-002; Zone runtime owner (`d2b-core-controller`), volume-local Provider owner | +| Current source | `packages/d2b-core/src/status.rs` (v3 common status/observedGeneration/conditions); Provider descriptor state-namespace declaration from ADR046-pstate-002 | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/optional_state_admission.rs` (storage-need admission: reject a declared namespace whose payload is derivable from spec/status/core ledger/external observation with `component-state-not-justified`; only declared namespaces produce a Volume; stateless components produce none); `packages/d2b-core-controller/tests/optional_state_admission.rs` (hermetic: stateless component → no Volume; declared `storageNeed` variants accepted; unjustified namespace rejected; status-first restart revalidation — controller re-derives observed state from status/core ledger/external observation after restart and never treats a status field as authority); `packages/d2b-provider-volume-local/tests/status_bounds.rs` (hermetic: total canonical serialized status cap and provider-specific detail cap enforced; oversize status write → typed rejection; status carries no secret/path/argv/PID/unit/stream/ring content) | +| Crate layout | See [Provider crate layout](#provider-crate-layout). Hermetic admission and status-bound round-trips → `tests/`; Zone-startup restart revalidation requiring real processes → `integration/`. | +| Detailed design | Optional state-Volume admission is a fixed step in Core ProviderDeployment: for each component, if it declares no `stateNamespaces` entry it gets no Volume; for each declared entry, verify the `storageNeed` justification (`secret` \| `large-binary` \| `private-unsafe-for-status` \| `revision-unsuitable`) and reject a namespace whose payload is fully derivable from spec/status/core ledger/external observation with `component-state-not-justified`. Fixed bootstrap components (`system-core`, `system-minijail`, first `volume-local` instance) declare no state Volume and reach Ready using resource `status`, the core Operation ledger, and external observation only; there is no bootstrap-storage mechanism. Status-bound enforcement: reject a status write whose total canonical serialized size exceeds the single canonical status cap, or whose provider-specific detail exceeds the detail cap, or whose condition/count/list/map entries exceed the bounded limits, with the typed status-oversize rejection; status writes occur only on material change. | +| Integration | `d2b-core-controller` runs optional state-Volume admission before creating any declared Volume and before launching a component Process; the status-bound check is applied on every status subresource write in the resource store. | +| Data migration | New; no prior bootstrap artifacts to migrate | +| Validation | Stateless component → no Volume created; each `storageNeed` variant accepted with a declared Volume; unjustified namespace → `component-state-not-justified`; status-first restart: controller re-derives observed state and reverifies against external reality, never treating status as authority; oversize/over-detail/over-cardinality status write → typed rejection; status contains no secret/path/argv/PID/unit/stream/ring content | +| Removal proof | Not applicable (permanent admission + status-bound enforcement) | diff --git a/docs/specs/ADR-046-reset-and-cutover.md b/docs/specs/ADR-046-reset-and-cutover.md new file mode 100644 index 000000000..2df7a23c6 --- /dev/null +++ b/docs/specs/ADR-046-reset-and-cutover.md @@ -0,0 +1,1855 @@ +# ADR 0046 reset and cutover contract + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-reset-and-cutover` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | Zone runtime bootstrap owner, `d2b` CLI cutover owner, storage/broker integrator, `d2b-resource-store-redb` owner | +| Depends on | `ADR-046-decision-register`, `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-store-redb`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-provider-model-and-packaging`, `ADR-046-provider-state`, `ADR-046-components-processes-and-sandbox`, `ADR-046-primitive-resource-composition`, `ADR-046-resources-volume`, `ADR-046-resources-device`, `ADR-046-resources-network`, `ADR-046-resources-host-guest-process-user`, `ADR-046-resources-credential`, `ADR-046-resources-zone-control`, `ADR-046-zone-routing`, `ADR-046-componentsession-and-bus`, `ADR-046-nix-configuration`, `ADR-046-cli-and-operations`, `ADR-046-telemetry-audit-and-support`, `ADR-046-core-controllers`, `ADR-046-current-code-migration-map` | +| Supersedes | [ADR 0034](../adr/0034-storage-lifecycle-restart-and-synchronization.md) "Migration decision" section for the d2b 3.0 cutover; the current `d2b host migrate-storage` verb (retired per `ADR-046-current-code-migration-map` §6 and `ADR-046-cli-and-operations` "v2 command surface removed at 3.0 clean break": it served the one-time v1→v2 storage layout cutover only and has no v3 successor) | + +## Purpose + +This spec is the exhaustive normative contract for taking one running d2b host +from its current pre-ADR-0046 state — the daemon-only/realm-native control +plane described by the repository `AGENTS.md` and evidenced at baseline +`b5ddbed6` — to a live ADR 0046 Zone/Provider control plane. d2b 3.0 has **no +v2 compatibility layer and no in-place protocol migration**: `Realm`, +`Workload`, `d2b-realm-router` PeerSession, the legacy public wire +(`WorkloadOp`, `RealmMethod`), and every v2 CLI verb stop working the moment +the new Zone runtime activates. This is a **destructive cutover**: the +control-plane data plane (redb Zone resource store, ResourceType rows, +revisions, watches) has no import path from any prior representation. + +This spec is not a second implementation surface. It is the authoritative +sequencing, consent, inventory, disposition, safety, rollback-boundary, and +verification contract that every future cutover work item, CLI command, and +Provider dossier migration procedure must satisfy. Every other ADR 0046 spec's +"Data migration" column that reads "Full reset" or "Destructive v3 bootstrap" +is scoped to the **resource-store control-plane rows only**. This spec governs +what happens to the **durable host-owned bytes** — TPM identity, SSH keys, +disk images, durable Volumes, audit history, and credentials — that must +survive the cutover of the control plane sitting on top of them. + +## Cross-reference and evidence corrections + +Two existing ADR 0046 artifacts reference content that this spec supplies +canonically. Recording the correction here (rather than editing those files, +which are out of this work item's scope) keeps the set internally consistent +without an open decision: + +- `ADR-046-resources-zone-control` §9.4 ("Recovery") reads "the out-of-band + safety path (see §12.6)". That file's own §12 ends at §12.4; no §12.6 + exists in that document. **This spec is the out-of-band destructive-reset + procedure §9.4 refers to.** See [Full Zone reset](#full-zone-reset-vs-provider-reset-vs-guest-reset) + below, which is built directly on top of the exact primitive + `ADR-046-resources-zone-control` §2.6 and §9.4 already define (`core.zone-drain` + finalizer, `deletionRequestedAt`, reverse-dependency-order child deletion, + final `phase=Deleted` event, store close, uid=0/local-OS-authenticated + re-entry into bootstrap). The dangling `§12.6` citation should be corrected + to point at this spec's Spec ID in a future non-content-bearing editorial + pass; it is not a foundation decision and does not block acceptance of + either document. +- `ADR-046-provider-volume-local` work item `ADR046-vl-005` ("TPM Volume") + records "Data migration: None (full v3 reset; TPM NVRAM must be backed up + by operator)". Read in isolation this appears to conflict with + `ADR-046-provider-device-tpm` §17.3 ("Migration from current form"), which + requires the existing `/var/lib/d2b/vms//swtpm/` directory and its + provisioning marker to be **migrated, not destroyed**, via a one-time + migration `EphemeralProcess` that re-keys the marker. Both statements are + correct and non-contradictory once scoped: `ADR046-vl-005`'s "full v3 + reset" describes the **volume-local Volume ResourceType implementation** — + there is no row-level import of a prior `Volume` resource because no + `Volume` resource existed before v3. "TPM NVRAM must be backed up by + operator" is an **independent defense-in-depth recommendation**, not a + license for this cutover to destroy TPM state. This spec is the binding + authority: [Never wipe TPM identity or durable Volumes silently](#never-wipe-tpm-identity-or-durable-volumes-silently) + makes host-file preservation of TPM NVRAM and its marker a **mandatory, + automated, fail-closed** disposition of the cutover tool, on top of which + an operator-performed backup remains good practice but is never assumed as + the sole safety net. + +## Terminology + +| Term | Meaning | +| --- | --- | +| **Cutover** | The one-time, host-scoped, destructive procedure that replaces the pre-ADR-0046 control plane (daemon-only `d2bd`/`d2b-priv-broker`, Realm/Workload types, legacy Nix option namespace) with a live ADR 0046 Zone runtime. There is exactly one cutover per physical host (or per execution target that will host its own Zone runtime — see [Full Zone reset vs Provider reset vs Guest reset](#full-zone-reset-vs-provider-reset-vs-guest-reset)). | +| **Cutover snapshot** | The immutable, integrity-pinned, point-in-time capture of every current-baseline artifact this spec's [inventories](#authoritative-inventories) enumerate, taken at the start of [Preflight](#preflight-and-immutable-snapshot) before any mutation. Identified by a `checkpoint_id`. | +| **Disposition** | The single closed-set classification (`Adopt`, `Preserve`, or `Destroy`) this spec assigns to every current-baseline path, unit, process, and artifact. See [Disposition framework](#disposition-framework). | +| **Adopt** | The current artifact's durable bytes are moved, hardlinked, or re-keyed into a new v3-owned location (a Volume, a Zone bundle path, a re-rooted audit segment directory) while preserving identity, content, and any fail-closed provisioning marker. The old path is retired only after the new owner verifies adoption succeeded. | +| **Preserve** | The current artifact is left exactly where it is, unmodified, and continues to be read (never written) by legacy or transitional code until its owning ADR 0046 work item supplies a live successor. Nothing under Preserve is touched by cutover `apply`. | +| **Destroy** | The current artifact is deleted. Destroy is permitted **only** for regenerable, ephemeral, or cache-class data (§ [Old artifact/unit/schema removal gates](#old-artifactunitschema-removal-gates) enumerates the exact gate each Destroy candidate must clear). Destroy is never the default disposition; every row in the [migration/disposition matrix](#migrationdisposition-matrix) states its disposition explicitly — there is no implicit fallback to Destroy for an unlisted path. | +| **Full Zone reset** | The destructive re-initialization of one Zone's redb resource store: every non-`Zone` resource is deleted in reverse-dependency order, the store is closed, and Zone runtime re-enters compiled bootstrap authorization for a fresh initialization. Defined by `ADR-046-resources-zone-control` §2.6/§9.4; this spec is its authoritative out-of-band operational procedure (see [Full Zone reset vs Provider reset vs Guest reset](#full-zone-reset-vs-provider-reset-vs-guest-reset)). | +| **Provider reset** | Deletion and re-creation of exactly one `Provider/` resource, its entire `ProviderStateSet` (every Volume it owns), and its component Process/EphemeralProcess children. Does not touch the Zone, other Providers, Hosts, Guests, or unrelated resources. | +| **Guest reset** | Deletion and re-creation of exactly one `Guest/` resource and its owned children (its runtime Processes, its store-view/TPM Volumes unless explicitly retained). Does not touch the Zone, Providers, or other Guests. | +| **Cutover checkpoint** | One phase-boundary durable record in the [cutover journal](#crashpower-lossretryidempotency-journals), written with the ADR 0034 atomic-persistence sequence (temp file, `fsync`, rename, parent `fsync`). | +| **Incident hold (cutover-wide)** | An operator-set hold that blocks every destructive disposition step and the entire [old artifact/unit/schema removal gate](#old-artifactunitschema-removal-gates) sequence, built on the same `IncidentHold` condition semantics `ADR-046-provider-state` defines for a Volume, extended here to apply Zone-wide during a cutover window. | +| **Gateway Guest** | A Guest whose runtime Provider hosts a nested child Zone (a "gateway guest" in ADR 0032 terms). The child Zone authors and stores its one local uplink `ZoneLink`, whose `spec.childZoneName` matches that child Zone's self-name; the compiler-only `parentZone` setting selects the provisioning allocator. The parent keeps only sealed allocator/route state, with no reciprocal parent-store `ZoneLink` or parent-side `ZoneLink` handler, and never holds the gateway Guest's Credential or audit — see [Gateway Guest credential/audit custody](#gateway-guest-credentialaudit-custody). | + +Every term above composes with, and does not redefine, the shared vocabulary +in `ADR-046-terminology-and-identities` (Zone, ResourceRef, Provider, Host, +Guest, ExecutionPolicy, AuthenticatedSubjectContext) and the common resource +envelope in `ADR-046-resource-object-model` (metadata/spec/status, +`deletionRequestedAt`, finalizers, phase enum). + +## Relationship to prior art + +This spec is not a green-field design. It formalizes and generalizes an +existing, evidenced precedent: + +| Prior art | What it establishes | What this spec adds | +| --- | --- | --- | +| [ADR 0034](../adr/0034-storage-lifecycle-restart-and-synchronization.md) "Migration decision" | One-time planned-downtime cutover permitted to move an existing host into a new storage layout; preserves swtpm NVRAM/markers, SSH keys, store-view state/gcroots, daemon adoption metadata, degraded/audit history, disk images; dry-run/apply modes; checkpoint id + rollback command printed before any apply step | Generalizes this exact preserve-list/checkpoint/dry-run model from "storage layout only" to the entire host control-plane cutover (daemon, wire protocol, Nix option namespace, resource plane, Providers, audit chain) | +| `packages/d2b/src/lib.rs` `cmd_host_migrate_storage`/`build_storage_migration_plan` (evidence class: `implemented-and-reachable`, retired per `ADR-046-cli-and-operations`) | Concrete `StorageMigrationPlan` JSON shape: `checkpointId`, `rollbackCommand`, `preflightRequirements`, `preserve`, `cutoverOnlyCleanup`, `failClosedHazards`, `applyStatus` | This spec's [CLI UX/exit codes/JSON plan](#cli-uxexit-codesjson-plan) reuses this exact JSON shape (renamed fields, extended scope) for `d2b host cutover plan` | +| `packages/d2b/src/lib.rs` `cmd_host_destroy` (evidence class: `implemented-and-reachable`) | Dry-run-first destructive host verb gated by `require_explicit_mutation_flag`, Tier-0-legacy refusal, daemon-down refusal | Reused verbatim as the `--apply` admission precondition pattern for `d2b host cutover apply` and `d2b host reset` | +| `ADR-046-resources-zone-control` §2.6, §9.4 | `core.zone-drain` finalizer; reverse-dependency-order child deletion; compiled bootstrap re-entry; uid=0/local-OS-authenticated out-of-band destructive reset | Adopted verbatim as the mechanism behind [Full Zone reset](#full-zone-reset-vs-provider-reset-vs-guest-reset) | +| `ADR-046-provider-state` "No bootstrap state Volume", "Cross-component migration coordination", "Incident hold", "Unclaimed Volume GC" | No bootstrap state-Volume exception (D086, superseded by D087); migration `EphemeralProcess` prepare/stage/commit/precommit-rollback/roll-forward-after-crash algorithm; per-Volume `IncidentHold` condition; unclaimed-Volume GC policy | Reused as the exact mechanical pattern for every Adopt-disposition row in the [migration/disposition matrix](#migrationdisposition-matrix) that moves bytes into a new Volume (TPM, store-view, disk images, unsafe-local scope state) | +| `ADR-046-nix-configuration` "Prior generation retention and pruning"; D066 | Per-Zone `retainedGenerations` (default 3, range 1..16), count-based (no TTL) pruning, eval-enforced bound | Reused verbatim for [Backup/retention count 1-16, no TTL](#backupretention-count-1-16-no-ttl) applied to cutover snapshots | +| `ADR-046-telemetry-audit-and-support` `d2b-audit` crate design | SHA-256 hash-chain JSONL, segment rotation, 30-day compaction, `AuditWriteClass`, `d2b zone audit export` | Reused for [Audit chain closure/opening](#audit-chain-closure-and-opening) | +| `tests/tools/preflight-disk-space.sh` (10 GiB floor, cited by `AGENTS.md` "Disk hygiene contract") | Fail-closed disk-space guard ordered before toolchain bootstrap | Reused as the model for [Disk-space/GC safety](#disk-spacegc-safety) | + +## Cutover phases overview + +```text +Phase 0 Preflight (read-only; §Preflight and immutable snapshot) +Phase 1 Consent + dry-run plan (read-only; §Explicit operator consent and dry-run plan) +Phase 2 Authoritative inventories (read-only; §Authoritative inventories) +Phase 3 Old daemon/unit/process drain(mutating: stop only; §Old daemon/unit/process drain) +Phase 4 Disposition execution (mutating: Adopt/Preserve/Destroy; §Disposition framework) +Phase 5 Resource-store initialization(mutating: redb bootstrap; §Resource-store initialization) +Phase 6 Provider install/topological start (mutating; §Provider install/topological start) +Phase 7 Zone/ZoneLink cutover (mutating; §Zone/ZoneLink cutover) +Phase 8 Guest/runtime/network/store view activation (mutating; §Guest/runtime/network/store view activation) +Phase 9 Post-cutover verification (read-only; §Post-cutover verification) +Phase 10 Old artifact/unit/schema removal gate (mutating: Destroy only; §Old artifact/unit/schema removal gates) +``` + +Phases 0-2 are always read-only and safe to run repeatedly. Phase 3 is the +**point where the ADR 0034 continuation invariant is deliberately suspended**: +normal daemon/process restarts never sweep runtime state, but this one-time +cutover explicitly quiesces every legacy process before Phase 4 mutates +storage. Phase 4 is the last phase inside the [rollback boundary](#rollback-boundary): +`nixos-rebuild switch --rollback` remains a safe undo through the end of +Phase 4. From Phase 5 onward the resource-store bootstrap and Provider install +sequence commit durable state with no v2 representation to roll back to; the +only recovery path past that point is restoring from the [cutover +snapshot](#preflight-and-immutable-snapshot) via the [rollback boundary](#rollback-boundary) +contract. Phase 10 never runs automatically; it requires its own separate +consent and the [old artifact/unit/schema removal gates](#old-artifactunitschema-removal-gates) +to individually clear. + +## Preflight and immutable snapshot + +`d2b host cutover preflight` is read-only. It: + +1. Refuses to run if a prior cutover snapshot for this host already has a + `phase >= 5` checkpoint recorded (this host has already cut over; direct + the operator to `d2b host reset` or `d2b host cutover doctor` instead). +2. Confirms baseline shape: `Realm`/`Workload` Nix option namespace present + (`d2b.realms.*`), `d2bd.socket`/`d2bd.service`/`d2b-priv-broker.socket`/ + `d2b-priv-broker.service` units installed, no `d2b.zones.*` Zone runtime + unit present yet. +3. Runs the [disk-space guard](#disk-spacegc-safety) before touching anything + else, fail-closed on insufficient space, mirroring + `tests/tools/preflight-disk-space.sh`'s ordering before toolchain + bootstrap. +4. Builds the complete [authoritative inventories](#authoritative-inventories) + in memory (read-only; no lock is taken yet). +5. Computes the cutover `checkpoint_id`: + + ```text + checkpoint_id = "cutover-v1-" + hex(sha256( + "cutover-v1\n" + + sorted(guest_names).join("\n") + "\n" + + sorted(storage_json_ids).join("\n") + "\n" + + sorted(sync_json_ids).join("\n") + "\n" + + sorted(swtpm_marker_ids).join("\n") + "\n" + + sorted(framework_key_fingerprints).join("\n") + "\n" + ))[..12] + ``` + + This follows the exact digest-of-sorted-names pattern + `storage_migration_checkpoint_id` already uses in + `packages/d2b/src/lib.rs`, extended to cover every inventory class in + [Authoritative inventories](#authoritative-inventories) rather than VM + names alone. +6. Writes the **immutable cutover snapshot**: a single JSON document at + `/var/lib/d2b/cutover//snapshot.json`, created `0640 + root:d2bd`, containing every inventory row (path, owner, mode, size, + content digest where the artifact is a regular file bounded under 4 MiB, + or a directory-tree digest for larger trees such as disk images and the + store-view farm), the resolved manifest digest, and the exact NixOS + system-path being activated. The snapshot file is written with the ADR + 0034 atomic-persistence sequence (temp file in the same directory, + `fsync`, rename over target, `fsync` parent directory) and is made + immutable at the filesystem level (`chattr +i` where the filesystem + supports it; otherwise `0440 root:root` with no writer principal granted + write access) immediately after the atomic rename. +7. Prints the `checkpoint_id` and the exact snapshot path before returning. + Nothing before this point mutates host state. + +The snapshot is retained under the count-based retention policy in +[Backup/retention count 1-16, no TTL](#backupretention-count-1-16-no-ttl); it +is never referenced by content again except by the [rollback boundary](#rollback-boundary) +and [Failure/quarantine/manual recovery](#failurequarantinemanual-recovery) +procedures. + +### Config/artifact/schema validation + +Before the plan is offered to the operator, preflight independently validates +two configurations, because the cutover changes both the host's NixOS +configuration and the resource plane it activates: + +1. **Legacy Nix configuration is currently green.** `nix flake check` (or the + already-activated system's `bundle-.json` presence, whichever is + available) must currently validate without error. Cutting over a host + whose current configuration does not evaluate is refused with + `cutover-precondition-failed` — this spec never repairs a pre-existing + broken configuration. +2. **The candidate v3 Zone bundle validates independently.** The new + `d2b.zones..*` Nix configuration that will replace + `d2b.realms.*`/`d2b.vms.*` is evaluated and its generation bundle built + (per `ADR-046-nix-configuration` "Bundle and generation emission") + *without activating it*. Schema validation includes: every + `ResourceTypeSchema`/Provider settings schema referenced by the candidate + bundle is present and its digest matches the signed catalog; every + `Provider.spec.artifactId` resolves in `/etc/d2b/artifact-catalog.json`; + every `Host`/`Guest`/`Volume`/`Network`/`Device` resource's cross-refs + resolve inside the candidate bundle (no dangling `*Ref`); the canonical + sorted rendered bundle's generation id is deterministic and reproducible + (two evaluations of the same tree produce the same generation id, per + `ADR-046-nix-configuration` build-time invariant). A bundle that fails any + of these checks refuses preflight with `bundle-schema-mismatch` or + `bundle-integrity-failure` and prints the exact offending field — + preflight never proceeds on a bundle it cannot fully validate. +3. **Provider trust preflight.** Every Provider named by the candidate + bundle's `Provider.spec.artifactId` values is checked against the trust + rules in `ADR-046-provider-model-and-packaging` §"Trust": exact digest, + trusted publisher/root epoch, valid signature, no emergency deny, accepted + provenance/SBOM/license/vulnerability policy, exact package/API + conformance attestation. A Provider that fails trust preflight blocks + `cutover plan` from proceeding with that Provider installed; the operator + may re-run preflight after removing or replacing the offending Provider + from the candidate bundle. + +## Explicit operator consent and dry-run plan + +`d2b host cutover plan [--zone ] [--json | --human]` is always a +dry-run: it never mutates state. It re-runs preflight (reusing an existing +unexpired `checkpoint_id` if inventories are unchanged, else computing a new +one) and renders the complete plan: + +```json +{ + "command": "host cutover plan", + "mode": "dry-run", + "checkpointId": "cutover-v1-4f2a9c7b1e83", + "snapshotPath": "/var/lib/d2b/cutover/cutover-v1-4f2a9c7b1e83/snapshot.json", + "rollbackCommand": "d2b host cutover rollback --checkpoint cutover-v1-4f2a9c7b1e83", + "consentPhrase": "I understand this destructively replaces the d2b control plane and accept the disposition table below", + "guestCount": 2, + "guests": ["corp-vm", "work-vm"], + "preflightRequirements": [ + "legacy Nix configuration currently evaluates cleanly", + "candidate v3 Zone bundle validates independently (schema, artifact catalog, cross-refs, deterministic generation id)", + "every named Provider passes trust preflight", + "10 GiB free under /var/lib/d2b and the store-view farm filesystem", + "all Guests stopped", + "d2bd.service and d2b-priv-broker.service stopped", + "net VMs stopped; guest routing and dependent bridge traffic will be interrupted", + "no incident hold currently active for this host" + ], + "disposition": { + "adopt": ["... see migration/disposition matrix ..."], + "preserve": ["... see migration/disposition matrix ..."], + "destroy": ["... see migration/disposition matrix, Phase 10 only ..."] + }, + "failClosedHazards": [ + "symlink or path traversal inside any adopted path", + "foreign ownership markers on a d2b-managed path", + "recursive operations traversing hardlink farms or mutating shared /nix/store inodes", + "missing swtpm marker for a previously provisioned TPM Guest", + "missing framework SSH host key for a previously provisioned Guest", + "any candidate outside the generated storage/inventory root set", + "any open d2b daemon, broker, runner, net VM, or Guest file descriptor", + "any attempt to unlink a lock file rather than leaving it for reboot/tmpfs cleanup", + "insufficient disk space for the largest disk-image or store-view adopt operation" + ], + "rollbackBoundary": "safe through end of Phase 4 (disposition execution); destructive and unrecoverable except via snapshot restore from Phase 5 onward", + "applyStatus": "requires --consent matching consentPhrase and --apply" +} +``` + +`d2b host cutover apply --consent "" [--zone ] +[--json | --human]` requires the operator to pass the **exact, +byte-for-byte** `consentPhrase` string printed by the preceding `plan` +invocation (bound to the plan's `checkpointId`, so a stale consent phrase from +an earlier plan is rejected with `cutover-consent-required` and a fresh +`checkpoint_id`). This mirrors `require_explicit_mutation_flag`'s existing +dry-run/apply precondition pattern in `packages/d2b/src/lib.rs`, extended +with a content-bound consent string because this operation is destructive +across an entire host rather than one flag-gated command. There is no +`--force`, no environment-variable bypass, and no non-interactive default +that supplies consent implicitly. + +## Authoritative inventories + +Phase 2 builds seven closed inventories. Every disposition row in the +[migration/disposition matrix](#migrationdisposition-matrix) is drawn from +exactly one of these enumerations; the matrix is not permitted to reference a +path this phase does not walk. + +| Inventory | Source | Contents | +| --- | --- | --- | +| **Guest inventory** | Parsed manifest (`ManifestDocument::vms()`) | Every declared VM/net-VM name, env, `is_net_vm`, enabled component set (graphics/tpm/usbip/audio), `WorkloadProviderKind` | +| **Storage inventory** | `storage.json` (per ADR 0034, read via `BundleResolver.storage`) | Every declared storage id, path template, kind, owner/group/mode, persistence class, restart class | +| **Sync inventory** | `sync.json` (per ADR 0034, read via `BundleResolver.sync`) | Every declared lock id, path template, lock kind, allowed holders, acquisition order | +| **TPM inventory** | `/var/lib/d2b/vms//swtpm/` + `/var/lib/d2b/swtpm-markers/` for every Guest with `tpm.enable = true` | Marker presence/content digest, NVRAM directory tree digest, `previously-provisioned` fail-closed state | +| **Key inventory** | `/_ed25519{,.pub}` for every Guest; `/vms//host-keys/{host.pub,user-authorized-keys}` | Fingerprint, mode/owner, staged host-keys content | +| **Disk-image inventory** | Guest-declared disk images and writable store-overlay images under `/var/lib/d2b/vms//` | Path, size, content digest (tree digest for large images uses a sampled/streaming digest, never a full read into memory) | +| **Network inventory** | Declared host bridges, TAP naming intent, nftables `inet d2b` table ownership markers, NetworkManager/`systemd-networkd` coexistence markers | Bridge names, ownership-marker `comment "d2b managed: "` values, NM unmanaged-config presence | +| **Audit/degraded inventory** | `daemon-events-*.jsonl` (daemon), broker `audit.rs` segments, `d2b-gateway-runtime` `audit_jsonl.rs` segments (gateway-backed realms only), daemon degraded ledger | Segment file list, last `record_hash`, degraded-ledger entry count by class | +| **Credential/identity inventory** | `realm-controllers.json`, `realm-identity.json`, `~/.local/state/d2b/unsafe-local-scopes.json` per user | Realm controller placement records, identity/key-rotation metadata, unsafe-local scope ledger entries | + +Every inventory entry that this spec's [migration/disposition +matrix](#migrationdisposition-matrix) does not explicitly list is treated as +**Preserve by default** for Phase 4 (never mutated by this cutover) and is +flagged in the plan output under a `unclassified` array so the operator sees +it before consenting — there is no silent Destroy for anything outside the +matrix. + +## Old daemon/unit/process drain + +Phase 3 is the only phase permitted to suspend the ADR 0034 continuation +invariant ("do not clear runtime state just because a daemon process +restarted"). It executes, in order, and refuses to proceed past any step that +does not reach the expected quiesced state within its bounded deadline +(default 120 s per step, operator-configurable, never silently extended): + +1. **Stop every Guest and net VM.** Uses the existing graceful-shutdown path + ([ADR 0040](../adr/0040-graceful-vm-shutdown.md)); a Guest that does not + reach a stopped state within its shutdown deadline aborts the drain with + `cutover-precondition-failed` naming the stuck Guest — cutover never force + kills a Guest that might hold unflushed durable Volume state. +2. **Stop `d2bd.service`.** The daemon relinquishes `public.sock`. +3. **Stop `d2b-priv-broker.service`.** The broker relinquishes `broker.sock` + and any adoptable-runner delegated cgroup leaves it still supervises. +4. **Stop any gateway-backed realm's local `d2bd`/broker pair** (ADR 0032 + gateway guests are themselves Guests and are already stopped by step 1; + this step additionally confirms no residual `d2b-gateway-runtime` process + remains attached to a stopped gateway Guest's vsock CID). +5. **Verify quiescence.** Re-read `/proc` for any process matching a + `d2b--*` cgroup leaf under `d2b.slice`; any live process here after + steps 1-4 aborts the drain with `cutover-precondition-failed` rather than + being killed — a live, unaccounted-for process is exactly the "any open + d2b daemon, broker, runner, net VM, or Guest file descriptor" fail-closed + hazard from the dry-run plan. +6. **Boot-scoped runtime cleanup, cutover-only.** Only after step 5 confirms + quiescence: remove boot-scoped runtime socket files under `/run/d2b-gpu`, + `/run/d2b-video`, `/run/d2b-wlproxy`, and `/var/lib/d2b/guest-control-` + (these are the same `cutoverOnlyCleanup` candidates + `build_storage_migration_plan` already names). Lock files under `/run/d2b` + are **never** unlinked here — they are left for the normal reboot/tmpfs + cleanup path, exactly as ADR 0034's `fail_closed_hazards` already + requires. + +After step 6, this host has no live d2b process. Phase 4 begins. + +## Disposition framework + +Every path, unit, process, and artifact this spec's inventories enumerate +receives exactly one of three dispositions. The framework — not any single +matrix row — is the normative contract; the [migration/disposition +matrix](#migrationdisposition-matrix) is its application to the concrete +baseline evidence. + +### Adopt + +Adopt moves durable bytes into a new v3-owned location while preserving +content, identity, and any fail-closed provisioning marker. Every Adopt +disposition: + +1. is executed by a dedicated, idempotent migration `EphemeralProcess` using + the exact prepare/stage/commit/precommit-rollback/roll-forward-after-crash + algorithm `ADR-046-provider-state` "Cross-component migration coordination" + already defines — this spec introduces no second migration state machine; +2. never deletes the source path until the destination Volume/artifact + reports `phase: Ready` (or the equivalent terminal success state for a + non-Volume artifact, such as an audit segment directory) **and** the + migration `EphemeralProcess` itself reports `Succeeded`; +3. re-validates the source's provisioning marker (where one exists, such as + the swtpm marker) before adoption and refuses the specific adopt step with + a fail-closed condition rather than silently re-provisioning if the marker + is absent, replaced, or fails identity verification — the marker check is + identical to the existing `previously-provisioned-swtpm-state-missing` + detection in `d2b-priv-broker/src/ops/swtpm_dir.rs`, generalized to every + marker-bearing Adopt candidate; +4. writes the new marker/identity record at the destination **before** + reporting the migration `EphemeralProcess` `Succeeded`, so a crash between + destination-write and old-marker-removal always resolves toward "old path + still present, new path present and valid" (safe to re-run) rather than + "neither path has a valid marker" (data loss); +5. leaves the source path in place, unmodified, until the corresponding + [old artifact/unit/schema removal gate](#old-artifactunitschema-removal-gates) + clears — Adopt is never combined with an immediate Destroy of its own + source in the same phase. + +### Preserve + +Preserve leaves an artifact exactly where it is. A Preserved path: + +- is read (never written) by any transitional or legacy code that still + needs it until its owning ADR 0046 work item supplies a live successor; +- is never included in Phase 4's mutation set; +- appears in the cutover snapshot for completeness and audit, but its content + digest is not re-verified after cutover (it was never touched). + +`nixos-modules/host-activation.nix`, `nixos-modules/host-users.nix`, +`/etc/d2b/privileges.json`, and every path the [migration/disposition +matrix](#migrationdisposition-matrix) marks `RETAIN`/`RETAIN-ADAPT` in +`ADR-046-current-code-migration-map` fall under Preserve for the duration of +this cutover: they continue to exist at their current path and are only +edited in place by a later Nix/Rust work item, never by this cutover's Phase 4. + +### Destroy + +Destroy deletes an artifact. Destroy is the disposition of last resort and is +gated as follows: + +- Destroy is **never** assigned to any path this spec classifies as TPM + identity state or `kind: durable` Volume-equivalent user data (see [Never + wipe TPM identity or durable Volumes silently](#never-wipe-tpm-identity-or-durable-volumes-silently)); +- every Destroy candidate must be regenerable (a cache, a stale lock file + already covered by the reboot/tmpfs path, a boot-scoped runtime socket) or + have a live, tested successor already adopted and verified (an old + artifact/unit/schema that a new resource/Provider/systemd-unit has already + replaced); +- Destroy for any current-baseline artifact other than the Phase 3 + boot-scoped runtime sockets is deferred to Phase 10 — see [Old + artifact/unit/schema removal gates](#old-artifactunitschema-removal-gates) + — and never runs inside Phase 4's initial disposition execution; +- every Destroy is logged to the cutover journal with the exact path/unit + name, the gate that cleared it, and the checkpoint id, before the delete + syscall executes. + +## Never wipe TPM identity or durable Volumes silently + +This is the single hardest invariant in this spec, and it is fail-closed in +both directions: cutover must neither destroy this data nor proceed as if it +had been safely handled when it was not. + +1. **TPM identity.** Every Guest with `tpm.enable = true` at baseline has its + swtpm NVRAM tree and provisioning marker (`/var/lib/d2b/vms//swtpm/`, + `/var/lib/d2b/swtpm-markers/`) **Adopted**, never Destroyed, using the + exact one-time migration `EphemeralProcess` `ADR-046-provider-device-tpm` + §17.3 already specifies: the swtpm directory is migrated to the + controller-created TPM Volume path, and the existing provisioning marker + is preserved and re-keyed by `Provider/volume-local` from its old basename + to the new `device_uid`-based name. A missing or already-dropped marker at + preflight time fails cutover `plan` closed with + `cutover-precondition-failed` naming the affected Guest — cutover refuses + to proceed rather than silently re-provisioning a fresh, identity-less TPM + for that Guest. This is the binding resolution of the apparent + `ADR046-vl-005` vs. device-tpm §17.3 tension recorded in [Cross-reference + and evidence corrections](#cross-reference-and-evidence-corrections): the + operator-backup recommendation in `ADR046-vl-005` is additional, not a + substitute for this automated Adopt. +2. **Durable Volumes.** Every current-baseline path this spec's inventories + classify as user-declared persistent data — Guest disk images (including + writable store-overlay images), any path a future `Volume` resource will + declare with `kind: durable` per `ADR-046-resources-volume` §"Kind" — is + **Adopted**, never Destroyed. There is no disposition path in this spec + that deletes a `kind: durable`-equivalent artifact; the [Disposition + framework](#disposition-framework)'s Destroy gate explicitly excludes this + class. +3. **Fail-closed default.** If preflight cannot classify a path with + confidence as ephemeral/cache/regenerable, its default disposition is + **Preserve**, never Destroy. Ambiguity resolves toward keeping data, not + discarding it. +4. **Independent operator backup remains recommended, never assumed.** Every + `plan` output includes an advisory line recommending an out-of-band backup + of TPM NVRAM and any disk image before `apply`, consistent with + `ADR046-vl-005`. Cutover's own automated Adopt behavior does not depend on + the operator having taken that advice; it is defense-in-depth on top of, + not a substitute for, the automated preservation this section mandates. +5. **Verification is mandatory, not advisory.** [Post-cutover + verification](#post-cutover-verification) re-reads every adopted TPM + marker and every adopted durable Volume's content digest against the + cutover snapshot and fails the cutover (without touching the still-present + Preserved source) if any digest does not match. +6. **Entra identity-Guest login state (D093).** The Entrablau-enabled identity + Guest owns its machine credential, TPM binding, enrollment, and + refresh-token/private login state (guest-local, secret/large/private). This + state is treated exactly like TPM identity: it is **Preserved/Adopted**, never + silently wiped. A Guest, Provider, or Zone reset either preserves the identity + Guest's TPM/login state or **explicitly destroys** it under the operator's + authorized destructive disposition — never as an implicit side effect. A + subsequent login re-enrolls only if the operator explicitly destroyed that + state. + +## Gateway Guest credential/audit custody + +ADR 0032 establishes that a gateway-backed realm's relay credentials, remote +node registry, provider configuration, and audit trail live **only** inside +its dedicated gateway guest VM; the host never holds them +(`CredentialCustody::None` for host-local vs. +`CredentialCustody::GatewayGuest` for relay-backed, per +`d2b-realm-router/src/service_v2.rs`, and `ADR-046-zone-routing` +"d2b-realm-router/src/service_v2.rs — RealmServiceServer" evidence). This +cutover preserves that boundary exactly, translated into ZoneLink terms: + +1. A pre-cutover gateway-backed realm becomes, post-cutover, a Guest whose + runtime Provider hosts a **nested child Zone**. That child authors and stores + its one local uplink `ZoneLink/`; `spec.childZoneName` must equal the + child Zone's self-name. The enclosing child Zone's compiler-only + `parentZone` setting selects the provisioning parent allocator, which keeps + only sealed allocator/route state and creates no reciprocal parent-store + resource or parent-side `ZoneLink` handler (per + `ADR-046-resources-zone-control` §3.1/§10.3 and + `ADR-046-nix-configuration` "Zone declaration"/"ZoneLink"). +2. The cutover's [inventories](#authoritative-inventories) and [migration/ + disposition matrix](#migrationdisposition-matrix) **never** enumerate the + gateway guest's internal relay credentials, remote node registry, or + realm audit log as host-side artifacts to Adopt, Preserve, or Destroy from + the parent host's perspective — those live inside the gateway Guest's own + filesystem and are the gateway Guest's own (nested) cutover's + responsibility when that Guest itself boots a v3 Zone runtime internally. + The parent host's cutover Adopts no `ZoneLink` row. It regenerates only the + compiler-only `parentZone` topology as sealed allocator/route state; the + child Zone's separate cutover authors and reconciles its local `ZoneLink` + from ordinary Nix configuration. Neither side treats that configuration as + migrated credential bytes. +3. The parent allocator/route engine owns no `ZoneLink` resource or handler and + never receives, stores, or logs the child Zone's Credential resources or raw + token bytes, matching + `ADR-046-componentsession-and-bus` "Sensitive credential delivery": a + Credential Provider may deliver raw token bytes only over a dedicated KK + ComponentSession to an authorized consumer, and d2b-bus/Zone/relay + intermediaries forward opaque protected records without decrypting them. +4. If the gateway Guest itself is being cut over from a pre-ADR-0046 + `d2b-gateway-runtime` process to a nested v3 Zone runtime, that nested + cutover is a **separate, independent invocation** of this same procedure, + run *inside* the Guest (its own Host in ADR 0046 terms), with its own + snapshot, checkpoint id, and consent — it is never folded into the parent + host's single `checkpoint_id` and never shares a cutover journal with the + parent. +5. Audit authority remains Zone-local: the child Zone's audit stream records + its child-local `ZoneLink` resource/session lifecycle events, while the + parent records only allocator/route-engine decisions and effects that the + parent authoritatively owns. The parent never records a gateway Guest's + internal realm audit events or claims a child `ZoneLink` resource transition + (per `ADR-046-zone-routing` "Audit records" and + `ADR-046-resources-zone-control` §3.1). + +## ProviderStateSet and state schema migration policy + +Every Provider component's durable payload state is governed entirely by +`ADR-046-provider-state`; this spec does not define a second state model. +Provider state Volumes are optional and declared only under the storage-need +test — bounded non-secret operational state lives in resource `status` and the +core Operation ledger by default (D087). Two distinctions matter specifically +for cutover: + +1. **First-bootstrap components never have a "from" schema.** For every + Provider installed during [Provider install/topological start](#provider-installtopological-start), + any *declared* component state Volume is created fresh by Core + ProviderDeployment with `stateSchemaPhase: current` immediately (there is no + `migration-required` phase to resolve, because `installedSchemaVersion` + equals `spec.stateSchema.schemaVersion` at creation by construction). + Cutover never invokes `ADR-046-provider-state` "Schema migration" + §"Pre-launch migration"/"Online migration" for a freshly created Provider; + those mechanisms exist for a **later** Provider software upgrade, not for the + initial cutover. +2. **Cutover-adopted state Volumes.** The TPM Volume and store-view Volume + created during [disposition execution](#disposition-framework) are created + by the Adopt migration `EphemeralProcess` itself, which writes both the + layout content (from the adopted source) and the identity marker before the + Volume is marked `Ready`. Once created, they participate in the exact same + ownership, quota, sealing, snapshot, retention, incident-hold, and + unclaimed-GC machinery as any other declared Provider state Volume — cutover + does not create a special-cased Volume subtype. + +The optional `ProviderStateSet` query itself +(`ProviderStateSet(zone, provider-name) = { v : Volume | v.metadata.zone == +zone && v.metadata.ownerRef == "Provider/" }`) requires no +cutover-specific extension: once Phase 6 installs a Provider, its +`ProviderStateSet` is exactly the set of *declared* Volumes Core +ProviderDeployment (or, for the Adopt cases above, the Adopt migration +`EphemeralProcess`) created for it — possibly empty — queryable the same way at +any later time. + +## Resource-store initialization + +Phase 5 creates the Zone's redb resource store from nothing, per +`ADR-046-resource-store-redb` "Backup, restore, and physical schema upgrade" +and work item `ADR046-store-003` ("Data migration: Destructive v3 bootstrap; +v3-to-v3 logical restore"). There is no logical import of any pre-cutover +daemon state, Realm/Workload representation, or legacy JSON artifact into the +redb store — the store's `store_meta` table is populated fresh: + +```text +store_uuid = freshly generated +zone_name = (from --zone or the candidate bundle's sole Zone) +zone_uid = freshly generated +created_at = now() (RFC 3339 UTC) +schema_version = current physical schema version +current_revision = 0 +compaction_floor = 0 +active_configuration_revision = 0 (set to 1 after Phase 7 activates the first generation) +policy_revision = 0 (set after bootstrap RBAC publishes, per Phase 6) +api_catalog_revision = 0 (set after the initial ResourceType catalog binds) +clean_shutdown = true +backup_generation = none +``` + +Phase 5's only input from the pre-cutover host is the **already-adopted** +bytes from Phase 4 (TPM/store-view/disk-image Volumes) and the **validated +candidate bundle** from preflight — never a raw read of `d2bd`'s in-memory +state, `realm-controllers.json`, or `realm-identity.json` (those remain +Preserved on disk per the [migration/disposition matrix](#migrationdisposition-matrix) +until their owning work item retires them, but their content is not imported +into redb rows). + +Immediately after store creation, Zone runtime follows the exact 9-step +startup sequence `ADR-046-core-controllers` "Startup" already defines: + +1. Zone runtime validates/opens the freshly created store. +2. Resource service and local d2b-bus/ComponentSession endpoint start. +3. Fixed `system-core` and `system-minijail` controller processes start and + authenticate as their exact Provider subjects. +4. Compiled bootstrap authorization (per `ADR-046-resources-zone-control` §9) + grants only the closed verb set to exactly `Provider/system-core` and + `Provider/system-minijail`. +5. Handlers list/recover/checkpoint concurrently (trivially empty on first + boot). +6. Configuration publishes the first candidate bundle as generation 1. +7. `system-core` reconciles the first `Host` and local `User` set. +8. Other Provider controllers/processes launch through resources (Phase 6). +9. Zone readiness publishes after mandatory handlers are current. + +## volume-local reaches Ready without a state Volume + +Step 8 above depends on `Provider/volume-local` being able to create the +*declared* state Volumes for other Providers — but `volume-local`'s own +controller instance declares no state Volume: its bounded non-secret +operational state lives in resource `status` and the core Operation ledger +(D087). Because the fixed bootstrap components (`volume-local`, `system-core`, +`system-minijail`) declare no state Volume, no component needs a Volume before +`volume-local` is Ready, so cutover relies on no bootstrap state-Volume cycle, +no per-execution-target local bootstrap storage mechanism, and no +bootstrap-storage exception (D086, superseded by D087; see "No bootstrap state +Volume" in `ADR-046-components-processes-and-sandbox`): + +- the first `Provider/volume-local` controller instance on the Host reaches + `Ready` from Host-local primitives and its own `status` alone, then begins + creating the declared state Volumes of later Providers; +- a Guest boots its own `volume-local` instance from Guest-local primitives, + without any parent-Host dirfd or resource handle, and that instance likewise + reaches Ready from its own status; +- the TPM/store-view Adopt Volumes described above are created later, in + Phase 4/8, by their owning migration `EphemeralProcess` and Provider + (`device-tpm`, `runtime-cloud-hypervisor`), strictly after `volume-local` + has already reached readiness — they are ordinary + Core-ProviderDeployment-adjacent declared Volumes. + +Cutover's only responsibility here is sequencing: Phase 5 step 8 (Provider +install) must not attempt to create any Provider's declared component state +Volume before `volume-local`'s own controller Process has reached `Ready` on +the target it will serve — this is naturally satisfied because +`Provider/volume-local` is itself the very first non-bootstrap Provider +installed in [Provider install/topological start](#provider-installtopological-start). + +## Provider install/topological start + +After Zone startup step 7 (`system-core` reconciles the first `Host` and +`User` set), step 8 installs every remaining Provider named by the candidate +bundle in dependency order. Provider dependencies are declared as manifest +aliases (`runtime`, `volume`, `network`, `credential`, `transport`, per +`ADR-046-provider-model-and-packaging` "Provider dependencies"); the +configuration-publication handler resolves this into a strict partial order +before any Process is created: + +1. Build a dependency graph: one node per `Provider/` in the candidate + bundle, one edge `A -> B` for every alias in `A`'s manifest that Zone + configuration binds to Provider `B`. +2. Reject the candidate bundle at Phase 5/6 boundary (before any Process + exists) if this graph contains a cycle — `ADR-046-provider-model-and-packaging` + "Provider dependencies" already requires synchronous dependency cycles to + fail configuration; cutover enforces this before Phase 6 begins, not + after a partial install. +3. Install Providers in reverse-topological order (dependencies before + dependents), fixed at these staged priorities so that Providers without + declared cross-dependencies still install deterministically: + + ```text + Stage A (fixed bootstrap; already running before Phase 6 begins) + Provider/system-core + Provider/system-minijail + + Stage B (storage/process substrate; no Provider dependency of its own) + Provider/volume-local + Provider/system-systemd + + Stage C (attachment/fabric Providers; depend only on Stage A/B) + Provider/volume-virtiofs + Provider/network-local + Provider/device-tpm + Provider/device-usbip + Provider/device-security-key + Provider/device-gpu + Provider/transport-unix + Provider/transport-vsock + Provider/transport-azure-relay + Provider/credential-secret-service + Provider/credential-entra + Provider/credential-managed-identity + + Stage D (execution Providers; depend on Stage B/C for volume/network/device/credential/transport aliases) + Provider/runtime-cloud-hypervisor + Provider/runtime-qemu-media + Provider/runtime-azure-container-apps + Provider/runtime-azure-virtual-machine + Provider/activation-nixos + Provider/observability-otel + + Stage E (interaction Providers; depend on Stage D Guests/Hosts being reconcilable) + Provider/display-wayland + Provider/audio-pipewire + Provider/clipboard-wayland + Provider/notification-desktop + Provider/shell-terminal + ``` + + Optional declared dependencies within a stage that are absent from the + candidate bundle produce declared degraded behavior for the dependent + Provider (per `ADR-046-provider-model-and-packaging` "Provider + dependencies"), never an install failure — the fixed staging above is a + deterministic default ordering for Providers with no explicit + cross-dependency edge, not a hard requirement that every stage's Providers + be present. +4. Within a stage, Providers install in parallel; installs across stages are + strictly ordered by Provider lifecycle handler readiness (a Stage C + Provider's `Ready` phase gates Stage D Providers that declare it as a + dependency alias). +5. Each Provider install follows `ADR-046-core-controllers` "Provider + lifecycle" handler algorithm unmodified: verify package/trust/config/ + conformance, validate controller/service/worker graph, create owned + Volume/Process/EphemeralProcess children (subject to [ProviderStateSet + and state schema migration policy](#providerstateset-and-state-schema-migration-policy) + above), wait for required dependencies, publish exported ResourceTypes/ + services only after ready. + +Cutover introduces no new Provider-install algorithm; it only fixes the +deterministic default stage order above so that a cutover `plan` can print a +complete, reproducible install sequence before `apply` runs. + +## Zone/ZoneLink cutover + +1. The candidate bundle's Zone becomes the `Zone/` self resource at + Phase 5 step 1 (redb store creation). There is exactly one Zone per cutover + invocation; a multi-Zone host runs one independent cutover invocation per + Zone (this spec's `checkpoint_id` is always Zone-scoped, never host-wide + when a host is declared to run more than one Zone). +2. Any pre-cutover realm that used `EntrypointMode::GatewayBacked` is + represented after cutover by one local uplink `ZoneLink/` authored and + stored in the nested child Zone during Phase 6/7 of that child's independent + invocation (per [Gateway Guest credential/audit + custody](#gateway-guest-credentialaudit-custody)) — never as migrated + session or credential state. Its `spec.childZoneName` must equal the child + Zone's self-name, and its `transportProviderRef` must name an + already-`Ready` Stage C transport Provider in that same child Zone (per + [Provider install/topological start](#provider-installtopological-start)). + The child Zone's compiler-only `parentZone` selects the provisioning + allocator and is sealed into allocator bootstrap state; it is not emitted + into `Zone.spec` or a resource bundle. The parent stores only the resulting + sealed allocator/route state, never a reciprocal `ZoneLink` row or + parent-side handler (per `ADR-046-resources-zone-control` §3.1/§10.3 and + `ADR-046-nix-configuration` "Zone declaration"/"ZoneLink"). A child-local + `ZoneLink` whose transport Provider is not yet Ready enters `Degraded` with + `ConfigurationCurrent: False` until that Provider reconciles, exactly as + `ADR-046-nix-configuration` "Cross-Zone generation ordering" specifies; the + child Zone's activation is never blocked on the parent Zone's generation or + remote-link readiness. +3. Any pre-cutover realm that used `EntrypointMode::HostResident` becomes an + ordinary `Guest` (VM/sandbox) or a user-only `Host` (unsafe-local, per + D042) in the parent Zone — never a `ZoneLink`, because a host-resident + realm never had a separate resource store to link to. +4. ZoneLink activation completes when the **child Zone's** core + `zone link/delegation` handler (`ADR-046-resources-zone-control` §11.2) + reports its local uplink `Ready` — verified in + [Post-cutover verification](#post-cutover-verification). + +## Guest/runtime/network/store view activation + +Phase 8 brings up every declared `Guest` in dependency order derived from its +own resource refs, following the same reverse-topological principle as +Provider install: + +1. **Network first.** `Provider/network-local` reconciles every declared + `Network` resource (bridges, DHCP/DNS/NAT/firewall) before any Guest that + attaches to it is started. +2. **Volume/store-view/TPM adopt confirmation.** For every Guest whose + store-view or TPM Volume was created in Phase 4's disposition execution, + `Provider/volume-local` must report that Volume `Ready` (marker verified, + quota enforced) before the owning runtime Provider is permitted to start + that Guest's Process — a Guest never starts against a Volume still in + `Pending`/`Degraded`. +3. **Device attachment.** `Provider/device-tpm`, `device-usbip`, + `device-security-key`, `device-gpu` reconcile their declared attachments + for each Guest, gated on the Volume readiness in step 2 where the device + is TPM-backed. +4. **Guest runtime start.** The Guest's runtime Provider + (`runtime-cloud-hypervisor`, `runtime-qemu-media`, + `runtime-azure-container-apps`, or `runtime-azure-virtual-machine`) starts + the Guest's Process(es) only after steps 1-3 report `Ready` for every + resource that Guest's `ExecutionPolicy` attachment defaults name. +5. **Store view.** The per-Guest `/nix/store` hardlink farm (adopted from + `/var/lib/d2b/vms//store/` in Phase 4) is mounted read-only into the + Guest by the owning virtiofsd Process, which `Provider/volume-local` + supervises per `ADR-046-provider-volume-local` ADR046-vl-004 — unchanged + from the current same-filesystem hardlink-farm contract in + `nixos-modules/store.nix`/ADR 0027, only its activation path moves from + Nix activation to the Volume controller. + +Any Guest that fails to reach `Ready` within its per-Guest deadline during +Phase 8 does not block other Guests — per `ADR-046-resource-reconciliation` +"Process fast path", independent resources reconcile/start concurrently. That +Guest's `Degraded`/`Failed` status is surfaced individually in [Post-cutover +verification](#post-cutover-verification) and does not abort the cutover for +every other Guest. + +## Post-cutover verification + +`d2b host cutover verify [--zone ] [--json | --human]` is read-only and +checks, in order, refusing to report success unless every check passes: + +| # | Check | Failure condition | +| --- | --- | --- | +| 1 | Zone `status.phase == Ready` | Any mandatory core-controller handler not `Ready` | +| 2 | Every Stage A/B/C Provider from the candidate bundle reports `status.phase == Ready` | Any mandatory Provider `Degraded`/`Failed`/`Unknown` | +| 3 | Every declared `Guest` reports `status.phase == Ready` (or the operator-accepted subset, if `--allow-degraded-guests ` was explicitly passed to `apply`) | Any Guest not `Ready` outside the accepted subset | +| 4 | Every adopted TPM marker's content digest matches the cutover snapshot's recorded digest | Digest mismatch on any TPM Volume | +| 5 | Every adopted durable Volume's (disk image, store-view) content digest matches the cutover snapshot's recorded digest | Digest mismatch on any durable Volume | +| 6 | Every declared child-local `ZoneLink` reports `Ready` or an accepted `Degraded/waiting-on-remote` (per Cross-Zone generation ordering), and no parent Zone store contains a reciprocal row | A child-local `ZoneLink` is in any other Degraded/Failed condition, or a reciprocal parent-store row exists | +| 7 | Every declared `Network` reports `Ready` | Any Network Degraded/Failed | +| 8 | No orphaned pre-cutover process remains under any `d2b--*`/`d2b.slice` cgroup leaf outside the current Zone's own cgroup partition | Any orphan found | +| 9 | The new audit chain's first record verifies against the closure record of the old chain (per [Audit chain closure and opening](#audit-chain-closure-and-opening)) | Hash-chain break or missing closure record | +| 10 | `d2b host cutover doctor` reports zero entries in the [degraded-state ledger](#failurequarantinemanual-recovery) tagged `cutover-quarantined` | Any such entry present | + +`verify` failing any check does **not** automatically roll back — the +[rollback boundary](#rollback-boundary) has already closed by this point +(verification only runs after Phase 8). A failed `verify` transitions the +host into [Failure/quarantine/manual recovery](#failurequarantinemanual-recovery) +instead. + +## Rollback boundary + +Cutover has exactly one rollback boundary, and it is stated once here so no +other section needs to restate it: + +| Point | What "rollback" means | Mechanism | +| --- | --- | --- | +| Before Phase 1 (`plan`) | No state changed; nothing to roll back | N/A | +| End of Phase 3 (drain complete) | Configuration/binary rollback: undo the NixOS activation that installed the new units, restart the old `d2bd`/`d2b-priv-broker` units, restart Guests | `nixos-rebuild switch --rollback` to the pre-cutover system generation, then `systemctl start d2bd.service d2b-priv-broker.service`, then normal Guest start | +| End of Phase 4 (disposition execution complete) | **Last safe rollback point.** Adopted bytes exist at both old and new locations (old path not yet removed by any gate); configuration/binary rollback is still possible because the old daemon/unit set has not been destroyed, only stopped | Same as above; the cutover journal (below) records that Phase 4 completed so a resumed/re-run cutover does not re-adopt already-adopted paths | +| Phase 5 onward | **No rollback.** The redb resource store now holds committed state with no v2 representation to revert to; Provider installs and Guest starts have created new process/cgroup/Volume state that a binary rollback cannot cleanly unwind | Recovery is **restore from the cutover snapshot**, not rollback: stop the new Zone runtime, `d2b host cutover rollback --checkpoint ` re-runs Phase 3's drain against the *new* control plane, restores the pre-cutover NixOS generation, and restarts the old daemon/broker against the still-Preserved and still-Adopted (never Destroyed, because Phase 10 has not run) durable paths | +| After Phase 10 clears any gate | **Fully irreversible** for whatever that gate destroyed. `d2b host cutover rollback` after Phase 10 refuses with `cutover-rollback-window-closed` and directs the operator to the cutover snapshot for forensic recovery only (the snapshot's content digests, not a live restore, since the artifact itself is gone) | N/A — this is why Phase 10 requires its own separate consent and never runs automatically | + +`d2b host cutover rollback --checkpoint [--json | --human]` +is only valid while the journal (below) shows the named checkpoint's last +completed phase is `<= 4`. It: + +1. Re-drains the new Zone runtime using the same algorithm as [Old + daemon/unit/process drain](#old-daemonunitprocess-drain), applied to the + new units instead of the old; +2. Restores the pre-cutover NixOS system generation + (`nixos-rebuild switch --rollback`, or the equivalent `activation-nixos` + rollback once that Provider itself is live for a later, non-initial + cutover); +3. Restarts `d2bd.service`/`d2b-priv-broker.service`; +4. Restarts every Guest from its Preserved/Adopted-but-not-yet-Destroyed + state — since Phase 10 has not run, every old path this rollback needs is + still present. + +Rollback past Phase 4 (i.e., against a checkpoint whose last completed phase +is `>= 5`) is refused with `cutover-rollback-window-closed`; the typed error +message names the exact phase reached and points at the cutover snapshot path +for manual forensic recovery. + +## Removed configuration async finalizer cleanup + +The candidate v3 Zone bundle activated in Phase 5 step 6 is generation 1 for +this Zone — there is no "prior generation" to clean up on the very first +activation. This section exists to state precisely that the [generation-based +cleanup contract](../specs/ADR-046-nix-configuration.md#resource-cleanup-contract) +(`managedBy=configuration` diff-and-async-Delete, `PendingCleanup`/`Degraded` +status, finalizer-safe, non-blocking activation, count-based +`retainedGenerations` retention) applies to this cutover's Zone bundle +**exactly the same way it applies to every later configuration change** — +cutover does not special-case generation 1: + +- generation 1's `managedBy=configuration` resource set is exactly the + candidate bundle validated in preflight; +- there is no absent-resource cleanup to enqueue on generation 1's own + activation (there is no generation 0 to diff against); +- the very next `nixos-rebuild switch` after cutover that changes the Zone's + Nix configuration activates generation 2 and runs the ordinary cleanup + contract unmodified; +- `retainedGenerations` (default 3, range 1..16, per D066) begins counting + from generation 1 exactly as it would for any Zone. + +This section is deliberately short: its entire content is "the existing +contract applies unmodified starting from generation 1," which is itself the +normative statement removing any ambiguity about whether cutover needs a +separate cleanup path for its own bundle. It does not. + +## Crash/power-loss/retry/idempotency journals + +Every phase boundary writes one durable **cutover journal checkpoint** using +the ADR 0034 atomic-persistence sequence, at +`/var/lib/d2b/cutover//journal.jsonl` (append-only, one JSON +object per line, `O_APPEND | O_CREAT`, `fsync` after every append): + +```json +{"phase": 4, "step": "adopt-tpm-corp-vm", "outcome": "succeeded", "at": "2026-07-22T00:03:11Z"} +``` + +Idempotency rules, applied uniformly to every phase: + +1. **Re-running `apply` after a crash resumes, never restarts.** On start, + `apply` reads the journal for the named `checkpoint_id`, determines the + highest phase/step with a recorded `succeeded` outcome, and resumes + immediately after it. A step recorded `started` but never `succeeded`/ + `failed` (crash mid-step) is re-executed from its own beginning — every + Adopt step's migration `EphemeralProcess` is required (per [Disposition + framework](#disposition-framework)) to be idempotent and safe to re-run + for exactly this reason, mirroring `ADR-046-provider-state` "Migration + operator requirements": deterministic, idempotent, roll-forward only. +2. **Power loss during an Adopt step's atomic file-level operation** resolves + the same way `ADR-046-provider-state` "Roll-forward after interrupted + commit" already specifies: on restart, if the destination marker/content + is already valid, the step is marked `succeeded` retroactively (the write + completed; only the journal append did not) and the source is left in + place for the normal removal gate; if the destination marker is absent or + invalid, the step re-executes from its own beginning against the + still-present source. +3. **Phase 5 (resource-store initialization) is the one phase that is not + naturally idempotent** — creating a redb store twice would create two + `store_uuid` values. Cutover's journal therefore records the + `store_uuid`/`zone_uid` immediately after store creation; a resumed + `apply` that finds Phase 5 already `succeeded` in the journal opens the + existing store by that recorded identity rather than creating a second + one, and fails closed with `cutover-precondition-failed` if the store at + the expected path does not match the recorded identity (this is the same + "previously provisioned database that is missing, replaced, bound to + another Zone/UID... fails closed" invariant `ADR-046-resource-store-redb` + "Store identity" already defines for the store in general). +4. **Retry budget.** Any individual step retries up to 3 times with bounded + backoff before the whole `apply` invocation aborts with a typed failure + naming the exact step, phase, and journal path — cutover never retries + indefinitely or silently degrades a failed step into a skipped one. +5. **The journal is retained forever for a given checkpoint_id** (it is small + and append-only); it is pruned only when its `checkpoint_id`'s snapshot is + pruned under [Backup/retention count 1-16, no TTL](#backupretention-count-1-16-no-ttl). + +## Old artifact/unit/schema removal gates + +Phase 10 (`d2b host cutover finalize --consent "" +[--json | --human]`) is the **only** phase permitted to Destroy anything +beyond the Phase 3 boot-scoped runtime sockets. It requires its own separate +consent phrase (bound to the same `checkpoint_id`, distinct wording from +`apply`'s), and each candidate must independently clear its own gate — there +is no bulk Destroy: + +| Candidate | Gate that must clear before Destroy | Removal proof | +| --- | --- | --- | +| `d2bd.service`, `d2bd.socket`, `d2b-priv-broker.service`, `d2b-priv-broker.socket` unit files | New fixed Zone runtime units installed and `verify` check 1 passed at least once since the units were stopped | `tests/host-integration/cutover-unit-retirement.nix` boots with only new units present and passes `d2b host cutover verify` | +| `/etc/d2b/realm-controllers.json`, `/etc/d2b/realm-identity.json` | Every Zone self-resource exists, the compiler-only `parentZone` topology is present only in sealed allocator bootstrap state, and every declared child-local `ZoneLink` reports `Ready`/accepted-`Degraded` with no reciprocal parent-store row per `verify` check 6 | `ADR046-nix-008`/`ADR046-nix-009` parity tests pass against the live Zone | +| `nixos-modules/options-realms*.nix`, `nixos-modules/options-vms.nix` | Every VM/realm declaration has an equivalent `d2b.zones..resources.*` declaration that produced a `Ready` Guest/Host in `verify` | `tests/unit/nix/cases/realm-to-zone-parity.nix` | +| `/var/lib/d2b/vms//swtpm/`, `/var/lib/d2b/swtpm-markers/` (source side of an Adopt) | `verify` check 4 (TPM digest match) passed **and** the destination TPM Volume has survived at least one full Guest restart cycle post-cutover, proving the adopted marker is load-bearing in practice, not merely digest-equal | `integration/swtpm_marker.rs` adapted; `tests/host-integration/tpm-adopt-retirement.nix` | +| `/var/lib/d2b/vms//store/` (source side of an Adopt) | `verify` check 5 (store-view digest match) **and** a live Guest boot successfully mounts the new store-view Volume path | `tests/store_view.rs`; `integration/store_view.rs` | +| Legacy `d2b-realm-router` PeerSession/MuxSession, `WorkloadOp`/`RealmMethod` wire types | Every v3 successor route (`d2b-bus`, `ResourceOp`, ZoneLink) is compiled in and `verify` passes with the new wire exclusively | `packages/d2b-contracts/tests/` wire-type removal compile check | +| Legacy CLI verbs (`d2b vm *`, `d2b realm *`, `d2b up/down/...`) | v3 successor verb implemented and dispatch-wired per `ADR-046-cli-and-operations` "Removal notes" | Compilation failure if any legacy `cmd_vm_*`/`cmd_realm_*` dispatch entry is reintroduced (policy lint, per existing `policy_*` test convention) | +| `d2b-unsafe-local-helper` binary, `DaemonToUnsafeLocalHelper` wire protocol | Process Provider supervisor ticket migration for the user-only Host is live and passes conformance | Per `ADR-046-current-code-migration-map` row for `d2b-unsafe-local-helper` | + +Every row above stays at **Preserve** until its gate clears, independent of +every other row — clearing the TPM gate does not imply the store-view gate is +also clear. `d2b host cutover finalize` reports, per candidate, which gate is +outstanding and refuses to Destroy that candidate until it clears; it never +partially destroys a candidate (e.g., it never removes only the marker file +while leaving the swtpm directory, or vice versa). + +## Data export/import where selected + +Cutover itself imports no row-level data into the resource store (see +[Resource-store initialization](#resource-store-initialization)). Two +operator-selected export/import affordances exist, both optional and +explicitly requested — never automatic: + +1. **Pre-cutover audit export.** `d2b audit export` (the current, retained + verb) or, once the Zone is live, `d2b zone audit export` may be run by the + operator *before* Phase 3 drain to produce an NDJSON export of the legacy + hash chain for archival outside `/var/lib/d2b`. This is purely advisory + and has no effect on cutover's own behavior — the legacy audit segments + are Preserved regardless (see next section). +2. **Guest configuration export/import.** For a Guest whose declarative + configuration cannot be mechanically derived from its pre-cutover Nix + declaration (rare; only applies to hand-edited `/etc/nixos` drift outside + the merged d2b PR, per the ADR 0034 "Repository landing remains separate + from host adoption" precedent), the operator may explicitly pass + `--export-guest-config ` to `preflight`, which writes that Guest's + resolved configuration to the cutover snapshot directory for manual + reconciliation into the candidate v3 Zone bundle before `apply`. This is + the only case where cutover reads configuration content for a purpose + other than direct 1:1 Nix-option translation, and it is always + operator-initiated per Guest, never automatic for the whole host. + +No other export/import path exists. In particular, there is no bulk +"export the daemon's in-memory state" or "import realm-controllers.json rows +into the resource store" affordance — those artifacts are Preserved read-only +evidence, not import sources, consistent with [Resource-store +initialization](#resource-store-initialization). + +## Audit chain closure and opening + +The pre-cutover audit surface is a set of independent SHA-256 hash chains: +daemon (`daemon-events-*.jsonl`), broker (`AuditWriteClass`-rate-limited +segments), and, for any gateway-backed realm, `JsonlGatewayAudit` segments. +Cutover treats each chain identically: + +1. **Close.** During Phase 3 drain, immediately before stopping the owning + process, a terminal **closure record** is appended to each chain: + `{"event": "chain-closed", "reason": "adr0046-cutover", "checkpoint_id": "", "last_record_hash": "", "at": ""}`, + computed with that chain's own existing `record_hash`/`prev_hash` + algorithm so the closure record is itself a verifiable final link. +2. **Preserve, never delete.** Every closed segment file is **Preserved** + forever under its current path — it is authoritative historical evidence + and is never a Destroy candidate at any Phase 10 gate. (This is + consistent with `ADR-046-current-code-migration-map` and + `ADR-046-telemetry-audit-and-support`'s `audit_segments_preserved_on_provider_delete` + test convention: audit segments are never deleted as a side effect of + anything this spec does.) +3. **Open.** The first write to the new Zone's `d2b-audit` chain (per + `ADR-046-telemetry-audit-and-support` `ADR046-audit-*` work items) is a + **genesis record**: `{"event": "chain-opened", "reason": "adr0046-cutover", "checkpoint_id": "", "closed_chain_refs": [], "at": ""}`. + This is the only place a v3 audit record references pre-cutover evidence, + and it references it only by path and closure hash — never by copying + pre-cutover record content into the new chain. +4. **Verification.** [Post-cutover verification](#post-cutover-verification) + check 9 confirms the new chain's genesis record's `closed_chain_refs` + match the actual closure records written in step 1, byte for byte. +5. **No merge.** The old and new chains are never merged, re-hashed together, + or presented as one continuous chain to `d2b zone audit export` — an + operator who needs pre-cutover history reads the old segment files + directly (still present, per step 2); `d2b zone audit export` only ever + serves the new chain. + +## Reset of stateless components and fresh state + +The large majority of Provider components installed during cutover are +stateless: their bounded non-secret operational state lives in resource +`status` and the core Operation ledger (D087), so they declare no state Volume +and none is created. For the minority of components that declare an optional +state Volume under the storage-need test, a freshly installed Provider's +*declared* state Volume is created fresh by Core ProviderDeployment exactly as +`ADR-046-provider-state` "Volume creation and ownership" specifies, with a +valid `stateSchema`. This is worth naming explicitly here only to make clear +that cutover: + +- creates a state Volume for a freshly installed Provider **only** for a + declared namespace that passed the storage-need test — never an empty, + identity-only Volume, and never a Volume for a stateless component (per the + revised D076); +- never invents synthetic prior state for a freshly installed Provider — + `stateSchemaPhase: current` and `installedSchemaVersion` equal to + `spec.stateSchema.schemaVersion` are set at creation, not derived from any + cutover-specific migration path; +- treats this identically for a component on a Host and a component on a + Guest — the placement rules in `ADR-046-provider-state` "State placement + under Host/Guest/user execution" apply unmodified during cutover. + +## Incident hold (cutover-wide) + +An operator may declare a cutover-wide incident hold at any point from the +end of Phase 2 (inventories built) through the end of Phase 9 (verification) +with `d2b host cutover hold --reason "" +[--json | --human]`. While active: + +1. Phase 4 (disposition execution), Phase 8 (Guest/runtime activation past + what has already started), and Phase 10 (removal gates) refuse to proceed + — a hold blocks every destructive or state-creating step the same way a + per-Volume `IncidentHold` condition blocks `deletionRequestedAt` + processing and migration commit in `ADR-046-provider-state` "Incident + hold"; +2. read-only phases (Preflight, `plan`, `verify`, `doctor`) continue to work + normally — a hold never blocks observation; +3. any Adopt migration `EphemeralProcess` already in flight when the hold is + declared completes its current atomic step (it does not abort mid-write, + which would violate the idempotency contract in [Crash/power-loss/retry/ + idempotency journals](#crashpower-lossretryidempotency-journals)) and then + pauses before starting its next step; +4. the hold is recorded in the cutover journal as a phase-independent + annotation (not tied to any single phase/step) and is cleared only by the + same administrative Role that set it, via + `d2b host cutover hold --clear --reason "<...>"`; +5. once every per-Volume `IncidentHold` condition this cutover created + (Volumes it adopted state into) is individually cleared by their owning + Provider controller's normal reconcile per `ADR-046-provider-state`, the + cutover-wide hold is a separate, independent hold and must be cleared on + its own — clearing one does not implicitly clear the other. + +## Full Zone reset vs Provider reset vs Guest reset + +These three destructive operations are related but strictly nested in scope. +None of them is the cutover this spec otherwise describes — they are the +post-cutover recovery levers this spec defines as the "full Zone reset," +"Provider reset," and "Guest reset" scopes the rest of the ADR 0046 set +(system-minijail, volume-local, and other dossiers) already reference by name +without defining their mechanism. + +### Not a reset: disruptive upgrade/recycle (D091) + +An in-place disruptive **upgrade/recycle** (`d2b upgrade [--recursive] +--apply`, D091) is distinct from and never a substitute for these destructive +resets. An upgrade preserves the Resource UID and spec identity and preserves +durable/state/secret Volumes and TPM identity (`preserveState: true`), recycling +only the resource's realization and owned ephemeral Processes/endpoints, and its +dependency-aware planner drains dependents first. `Replace` of a resource-row +identity is used only when explicitly required and is planned with ownership and +state transfer so durable/state Volumes and TPM identity move to the replacement +rather than being wiped. Owned `Endpoint` resources (D092) recycle with their +producer (bumping `endpointGeneration` so consumers re-resolve) and are deleted +child-first with the producer/owner; they carry no raw locator, so recycling an +endpoint never leaks or persists a path/address/fd. A full factory reset (below) +is the separate, explicitly-destructive lever; upgrade/recycle never silently +deletes durable state, and reset never masquerades as an upgrade. + +### Full Zone reset + +`d2b host reset --scope zone --target Zone/ [--dry-run | --apply] +[--json | --human]` executes exactly the out-of-band destructive procedure +`ADR-046-resources-zone-control` §2.6 and §9.4 already define, and which this +spec is the authoritative operational owner of (see [Cross-reference and +evidence corrections](#cross-reference-and-evidence-corrections)): + +1. Core adds the `core.zone-drain` finalizer to `Zone/`. +2. `metadata.deletionRequestedAt` is set on the Zone; core stops admitting new + resource/service requests. +3. Every non-`Zone` resource receives a delete request in reverse dependency + order under normal finalizer protocol (Guests/Processes first, then their + owning Providers, then Volumes, then Networks/Devices/Credentials, with + authored qualified Bindings removed/retargeted before their import-owned + projection Services, then ResourceImport/ResourceExport rows, the Zone's + child-local ZoneLink, and finally Role/RoleBinding/Quota/EmergencyPolicy). +4. After every other resource is deleted, `core.zone-drain` is cleared and a + final transaction emits the Zone's own `phase=Deleted` event and closes + the store. +5. Zone runtime re-enters compiled bootstrap authorization (§9 of + `ADR-046-resources-zone-control`) for a fresh initialization — equivalent + to re-running this spec's Phase 5 onward against a new, empty store at the + same store path. +6. Authentication for this operation is OS-level (uid=0 or the local `d2b` + group's `SO_PEERCRED` admission, matching the existing local lifecycle + authorization surface) — it is never reachable remotely or through + d2b-bus, exactly as `ADR-046-resources-zone-control` §9.4 requires. +7. **Full Zone reset destroys every Volume in the Zone, including + `kind: durable` Volumes, unless `--preserve-durable-volumes` is passed**, + in which case durable Volumes are relocated (per `ADR-046-provider-state` + "Relocation") to a holding area outside the Zone before store deletion and + are eligible for re-attachment to a freshly reconciled Guest/Provider + after the reset completes. This flag defaults to **on** (durable Volumes + are preserved by default) — a Full Zone reset that would destroy durable + Volumes requires the operator to explicitly pass + `--destroy-durable-volumes` and the same exact-consent-phrase pattern as + cutover `apply`. + +### Provider reset + +`d2b host reset --scope provider --target Provider/ +[--destroy-volumes] [--dry-run | --apply] [--json | --human]`: + +1. Deletes `Provider/` through the normal resource API delete path + (`deletionRequestedAt`, finalizer-ordered child deletion, `Deleted` event); + the Provider's and its children's `status` — the default surface for bounded + non-secret operational state (D087) — disappears with the resource row and + its revision, requiring no separate state disposition; +2. Any declared Volume in that Provider's (possibly empty) `ProviderStateSet` + is deleted **only** if `--destroy-volumes` is explicitly passed; by default, + Volumes with `persistenceClass: persistent` are detached (ownerRef cleared + to `null` pending operator disposition) rather than deleted, surfaced as + `Unclaimed` per `ADR-046-provider-state` "Unclaimed Volume GC" — an + operator must explicitly delete an unclaimed Volume; it is never + automatically swept by a Provider reset; +3. Re-creating `Provider/` afterward (a fresh `apply` of the same + `artifactId`) goes through the normal Provider install algorithm and, if + any Volume from the prior instance is still present and unclaimed with a + matching component-state schema, the new instance's Core ProviderDeployment + does **not** automatically re-adopt it — the operator must explicitly + re-attach it, because an automatic re-adopt across a Provider identity + change is exactly the kind of implicit ownership inference + `ADR-046-resource-object-model` "Deletion" already forbids ("a + deleted/recreated object with the same type/name has a different UID and + does not silently inherit old ownership/operation state"). +4. Scope: never touches the Zone, other Providers, or any Host/Guest that + merely depends on this Provider (they transition to `Degraded/provider-unavailable` + until the Provider is reinstalled or its dependency is satisfied + otherwise). + +### Guest reset + +`d2b host reset --scope guest --target Guest/ [--destroy-volumes] +[--dry-run | --apply] [--json | --human]`: + +1. Deletes `Guest/` through the normal resource API delete path; +2. The Guest's owned Process/EphemeralProcess children are deleted + child-first under normal finalizer protocol; +3. The Guest's store-view and TPM Volumes follow the exact same + `--destroy-volumes`-gated preserve-by-default rule as Provider reset above + — **never** destroyed by default, consistent with [Never wipe TPM + identity or durable Volumes silently](#never-wipe-tpm-identity-or-durable-volumes-silently); +4. Re-creating `Guest/` afterward re-attaches a preserved Volume only + when the operator explicitly names it in the new Guest's declaration + (never automatically); +5. Scope: never touches the Zone, the Guest's runtime Provider, or any other + Guest. + +### ResourceExport/ResourceImport reset behavior (D096) + +All reset scopes tear down cross-Zone sharing child-first: + +1. `ResourceExport` reset stops new advertisements, revokes every active lease + through the Provider export adapter, waits for bounded revoke/deadline + completion, and then drops the advertisement before the export row is + deleted. It never deletes the owner Service or its local backing. +2. `ResourceImport` reset marks its same-qualified-type projection Service + draining/revoked and refuses new sessions. Matching Binding controllers stop + their owned Process/Endpoint children. +3. Binding is operator/Nix-owned desired consumer intent, never import-owned; + observed realization belongs only in status. A scoped reset that includes + the Binding deletes it normally; a narrower import reset waits with + `BindingReferencesRemain` until each Binding is explicitly deleted or + retargeted. The import controller never auto-deletes Binding. +4. After no Binding references remain, the import releases the remote lease, + deletes only the core-owned projection Service + (`ownerRef: ResourceImport/`) and remaining provider-owned children, + then deletes the import row. +5. Loss of the Zone's child-local ZoneLink during reset is treated as + revoke/degrade, not as retained authority. Reconnect after a reset must + revalidate generation and schema plus factory fingerprint before any new + lease is admitted. +6. Full factory reset removes export/import rows, authored Bindings in scope, + projection Services, internal leases/sessions/streams, and advertisements. + No cross-Zone authority, capability grant, stream credit, or import session + generation survives. Owner Service backing follows its own explicit reset + disposition; no backing is silently wiped. + +Reset/recreate preserves the qualified semantic Service/Binding type exactly; +it does not derive a type from the selected Provider or persist/copy a remote +`spec.provider` extension. A replacement Provider must accept the canonical +minimal base and bind the same semantic factory metadata. PipeWire, CTAPHID, +OTEL, and USBIP implementation state is disposed only under that Provider's +explicit reset policy and never becomes semantic base status or surviving +remote authority. + +### Comparison table + +| Property | Full Zone reset | Provider reset | Guest reset | +| --- | --- | --- | --- | +| Scope | Entire Zone (all resources) | One Provider + its ProviderStateSet | One Guest + its children | +| Authentication | OS-level (uid=0/local `d2b` group), never remote/d2b-bus | Normal resource API RBAC | Normal resource API RBAC | +| Durable Volume default | Preserved (relocated out of Zone) unless `--destroy-durable-volumes` | Detached/Unclaimed unless `--destroy-volumes` | Detached/Unclaimed unless `--destroy-volumes` | +| ResourceExport/ResourceImport | Revoke leases; remove authored Bindings in scope, then projection Services/imports/exports; preserve backing by explicit disposition | Revoke affected Service exports/imports; wait for Bindings to delete/retarget; never cascade Binding or backing | Guest-targeted Bindings and owned children delete first; imports wait on any remaining Bindings; sibling exports unaffected | +| Re-entry after reset | Compiled bootstrap authorization | Normal Provider install | Normal Guest declaration | +| Effect on siblings | None (other Zones on the host, if any, are unaffected) | Dependents degrade; not deleted | None | + +## CLI UX/exit codes/JSON plan + +### Command surface + +```text +d2b host cutover preflight [--zone ] [--json | --human] +d2b host cutover plan [--zone ] [--json | --human] +d2b host cutover apply --consent "" [--zone ] [--allow-degraded-guests [,...]] [--json | --human] +d2b host cutover verify [--zone ] [--json | --human] +d2b host cutover hold --reason "" | --clear --reason "" [--json | --human] +d2b host cutover finalize --consent "" [--zone ] [--only [,...]] [--json | --human] +d2b host cutover rollback --checkpoint [--json | --human] +d2b host cutover doctor [--zone ] [--read-only] [--json | --human] + +d2b host reset --scope zone --target Zone/ [--destroy-durable-volumes] [--dry-run | --apply] [--json | --human] +d2b host reset --scope provider --target Provider/ [--destroy-volumes] [--dry-run | --apply] [--json | --human] +d2b host reset --scope guest --target Guest/ [--destroy-volumes] [--dry-run | --apply] [--json | --human] +``` + +Every mutating verb (`apply`, `finalize`, `rollback`, `hold`, `reset +--apply`) follows the existing `require_explicit_mutation_flag` precondition +pattern from `packages/d2b/src/lib.rs`: a bare invocation with neither +`--dry-run` nor `--apply` (for `reset`) or without `--consent` (for `apply`/ +`finalize`) refuses with exit code 2 and a usage message rather than silently +defaulting to either mode. + +### New stable error classes + +| Error class | Meaning | +| --- | --- | +| `cutover-precondition-failed` | A preflight/drain/adopt precondition (Guest not stopped, marker missing, bundle invalid, store identity mismatch) failed | +| `cutover-consent-required` | `--consent` absent, mismatched, or bound to a stale `checkpoint_id` | +| `cutover-already-complete` | `apply`/`preflight` invoked against a host whose journal already shows Phase 5+ complete | +| `cutover-checkpoint-not-found` | Named `checkpoint_id` has no snapshot/journal on disk | +| `cutover-rollback-window-closed` | `rollback` requested against a checkpoint whose last completed phase is `>= 5` | +| `cutover-incident-hold-active` | A mutating verb refused because a cutover-wide incident hold is active | +| `zone-reset-scope-invalid` | `--scope`/`--target` combination does not name a resolvable resource of the expected type | +| `zone-reset-consent-required` | `--apply` for `host reset --scope zone` without the required consent phrase | + +### Exit codes + +Every verb above uses the existing stable exit-code table in +`ADR-046-cli-and-operations` unmodified: + +| Exit code | Used for | +| --- | --- | +| 0 | Success (dry-run plan rendered, apply/finalize/rollback/reset completed, verify all-clear) | +| 1 | `cutover-precondition-failed`, `zone-unavailable`, `provider-unavailable`, or any operational failure | +| 2 | Usage error (missing `--apply`/`--dry-run`/`--consent`, invalid `--scope`/`--target`) | +| 3 | Cancelled (SIGINT/SIGTERM during a long-running `apply`/`finalize`) | +| 78 | `not-implemented` (reserved; this spec's verbs are ADR-only until their implementation work items land, per D024) | + +`verify` uses exit 0 only when every check in [Post-cutover +verification](#post-cutover-verification) passes; exit 1 with the first +failing check named otherwise (it does not require every check to run before +reporting the first failure, but it does run every independent check it can +before returning so a single invocation surfaces as many problems as +possible). + +### JSON plan schema + +`plan`'s JSON shape is given in full in [Explicit operator consent and +dry-run plan](#explicit-operator-consent-and-dry-run-plan) above. `apply`, +`finalize`, and `reset --apply` share one result envelope shape: + +```json +{ + "command": "host cutover apply", + "checkpointId": "cutover-v1-4f2a9c7b1e83", + "phasesCompleted": [0, 1, 2, 3, 4, 5, 6, 7, 8], + "verifyRecommended": true, + "degradedGuests": [], + "warnings": [] +} +``` + +`--human` output mirrors the existing `host migrate-storage`/`host destroy` +text convention: one summary line, then a bulleted list per section +(preflight requirements, preserve, disposition, hazards), with no manual +`chmod`/`chown`/`setfacl` remediation text anywhere in the human or JSON +output — consistent with the existing `host migrate-storage` documentation +note that such instructions are never an acceptable recovery path. + +## NixOS activation sequencing + +Cutover spans two distinct NixOS activation contexts, and this spec is +explicit about which one applies where: + +1. **The one-time bootstrap `nixos-rebuild switch`.** Before any Zone runtime + exists, the operator runs a normal `nixos-rebuild switch` against a NixOS + configuration that has replaced `nixos-modules/options-realms*.nix`/ + `options-vms.nix` with `d2b.zones..*`. This activation: + - installs the new fixed Zone runtime systemd unit set (the + `Provider/system-core`/`system-minijail` bootstrap processes' owning + unit, per `ADR-046-core-controllers` "Process model") **without + starting it** — `system.activationScripts` orders the new unit's + installation `Before=` the point where old units are stopped, but the + unit itself is declared `wantedBy = []`/not auto-started at this + activation, so the physical host boots with both the old units present + (already stopped by a prior `d2b host cutover` Phase 3 drain, if this + is a re-activation) and the new unit installed-but-dormant; + - does **not** delete `d2bd.service`/`d2b-priv-broker.service` unit files + yet — those remain Preserved until [Old artifact/unit/schema removal + gates](#old-artifactunitschema-removal-gates) clears them in Phase 10; + - is itself rollback-safe via the normal NixOS generation mechanism for as + long as this spec's [rollback boundary](#rollback-boundary) remains + open (through end of Phase 4). +2. **`d2b host cutover apply`** is then run interactively by the operator + (never from an activation script — it requires the exact consent phrase + from a human-read `plan` output) and is what actually starts the new Zone + runtime unit and executes Phases 3-8. +3. **After cutover completes**, ordinary NixOS rebuilds resume their normal + role: they change the Zone's `d2b.zones..*` Nix configuration and + activate new configuration generations through the ordinary + `ADR-046-nix-configuration` "Bundle and generation emission" contract — + the `activation-nixos` Provider (once installed, per [Provider + install/topological start](#provider-installtopological-start) Stage D) + takes over ordinary Host/Guest NixOS generation plan/apply/adopt/rollback + from that point forward; it is never used for the initial bootstrap + switch itself, because a Provider cannot exist before the Zone runtime + that hosts it exists — this is the same chicken-and-egg boundary the + [volume-local reaches Ready without a state Volume](#volume-local-reaches-ready-without-a-state-volume) + section already establishes for Volume creation, applied here to NixOS + activation instead. +4. **Ordering invariant.** The new fixed Zone runtime unit is declared + `After=` and `Requires=` nothing that depends on Zone runtime already + being up (it is the process that brings Zone runtime up); it is declared + `Before=` nothing that the old `d2bd.service`/`d2b-priv-broker.service` + units are declared `After=`, so the two unit sets never race for the same + socket path during the window where both are installed but only one is + running. + +## Backup/retention count 1-16, no TTL + +Cutover snapshots use the identical retention model +`ADR-046-nix-configuration` D066 already establishes for configuration +generations, applied to `/var/lib/d2b/cutover//` directories +instead of `bundle/generation-.json` files: + +```nix +d2b.site.cutoverSnapshotRetention = 3; # default 3; range 1..16 +``` + +- an eval assertion enforces `1 <= cutoverSnapshotRetention <= 16`, mirroring + the existing `retainedGenerations` assertion exactly; +- retention is **count-based only** — there is no TTL/age-based expiry of a + cutover snapshot, matching the existing "Generations within retention count + retained (no TTL)" test invariant already proven for configuration + generations; +- pruning removes only snapshot directories beyond the retention count, + oldest first, and only for snapshots whose checkpoint shows Phase 10 fully + cleared (a snapshot for an incomplete or rolled-back cutover is never + auto-pruned, regardless of age, until the operator explicitly discards it + with `d2b host cutover doctor --discard-checkpoint `); +- pruning a snapshot never touches the live adopted Volumes/Providers/Guests + it describes — only the snapshot's own JSON/journal files under + `/var/lib/d2b/cutover//` are removed. + +## Disk-space/GC safety + +Cutover's disk-space guard runs **before** every other preflight step, +mirroring `tests/tools/preflight-disk-space.sh`'s placement "after the orphan +reapers but BEFORE the rust toolchain bootstrap so the fail-closed guard +cannot be bypassed by disk-consuming setup": + +1. Compute required headroom: the sum of every Adopt candidate's size (TPM + NVRAM trees, disk images, store-view farm) that will be **copied** rather + than **hardlinked/renamed** in place, plus a fixed 10 GiB floor matching + the existing project-wide disk-hygiene convention, plus the cutover + snapshot's own estimated size (bounded; snapshot JSON never embeds full + file contents, only digests and metadata, so this is small). +2. If free space under `/var/lib/d2b` (or wherever the target Volume root + will live, if separately configured) is below this computed requirement, + `preflight` fails closed with `cutover-precondition-failed` and prints the + exact shortfall in bytes — it never proceeds with a partial Adopt that + could run out of space mid-copy. +3. Adopt operations that use same-filesystem `rename`/hardlink (store-view + farm, matching the existing ADR 0027 hardlink-farm contract) require zero + additional headroom beyond directory-entry overhead and are excluded from + the "copied" sum above — cutover's disk-space estimate distinguishes + same-filesystem-cheap Adopt from cross-filesystem-expensive Adopt exactly + as `ADR 0034` already requires for the hardlink-sensitive store-view path + ("same-filesystem/cross-mount invariants... recursive chmod/chown/setfacl + over a hardlink farm remains forbidden"). +4. GC safety: cutover snapshot pruning (previous section) and the ordinary + Zone generation GC (`d2b activation gc`, once `activation-nixos` is + installed) are independent budgets — pruning a cutover snapshot never + counts against or interferes with `retainedGenerations` pruning, and vice + versa. + +## Failure/quarantine/manual recovery + +Cutover reuses the ADR 0034 degraded-state ledger model, extended with one +new closed reason class: + +| Class | When it is set | Recovery | +| --- | --- | --- | +| `cutover-quarantined` | Any Phase 4-8 step whose ambiguity cannot be resolved automatically (e.g., a marker exists at the destination but does not match the source's recorded digest; a redb store exists at the target path but its `zone_uid` does not match the journal's recorded identity) | `d2b host cutover doctor` surfaces the exact quarantined step, affected path/resource, and a static remediation id; the privileged broker never repairs a quarantined path from trusted paths/owners/modes in the ledger — only from the trusted bundle/journal, matching the existing ADR 0034 invariant that "repairs never trust paths, owners, modes, ACLs, or commands from the ledger" | +| `adoption-quarantined` (reused from ADR 0034) | An Adopt migration `EphemeralProcess` finds multiple, ambiguous, or mismatched candidates for a marker-bearing source (e.g. two swtpm directories claim the same Guest identity) | Operator resolves the ambiguity manually (removing the stale candidate) and re-runs the specific Adopt step; cutover never guesses which candidate is authoritative | +| `restart-required` (reused) | A Provider/Guest process needs a restart to pick up newly adopted state (e.g., `volume-local` restarted mid-Adopt) | `d2b host cutover doctor` names the exact process; operator restarts it directly | +| `storage-drift` (reused) | A path's on-disk owner/mode/ACL does not match its declared storage contract entry after Adopt | Broker repair resolves only the trusted storage id from the bundle, never a raw path from the ledger | + +`d2b host cutover doctor [--zone ] [--read-only] [--json | --human]` +is read-only (its `--read-only` flag is accepted for symmetry with `d2b host +doctor` but is always effectively on for this verb — `doctor` never mutates). +It reports: + +1. every open journal checkpoint and its last completed phase/step; +2. every degraded-ledger entry tagged with any class above; +3. the exact remediation command for each (never a raw chmod/chown/setfacl + suggestion); +4. whether an incident hold is currently active and its reason text; +5. whether the [rollback boundary](#rollback-boundary) is still open for the + named checkpoint. + +Manual recovery for a `cutover-quarantined` entry always follows the same +shape as the existing ADR 0034 degraded-ledger recovery: the operator +resolves the *specific* ambiguity named by `doctor` (never a broad +chmod/chown/setfacl instruction), then re-runs the exact `apply`/`finalize` +invocation, which resumes from the journal per [Crash/power-loss/retry/ +idempotency journals](#crashpower-lossretryidempotency-journals) rather than +restarting the whole cutover. + +## Migration/disposition matrix + +This is the exhaustive application of the [Disposition +framework](#disposition-framework) to every artifact this spec's +[inventories](#authoritative-inventories) enumerate. Every row is tied to its +`ADR-046-current-code-migration-map` evidence (§ and evidence class) and, for +Provider-owned destinations, the exact Provider dossier and work item that +owns the destination. + +### Daemon, broker, and process substrate + +| Current artifact | Evidence (migration-map) | Disposition | Target | Owning work item / dossier | +| --- | --- | --- | --- | --- | +| `d2bd.service`, `d2bd.socket` | §7, `production-reachable` | Preserve until Phase 10 gate clears (§ [Old artifact/unit/schema removal gates](#old-artifactunitschema-removal-gates)), then Destroy | Fixed Zone runtime unit | `ADR046-core-001` | +| `d2b-priv-broker.service`, `d2b-priv-broker.socket` | §7, `production-reachable` | Preserve until Phase 10 gate clears, then Destroy | Zone-local privileged broker (`ADR046-provider-003`) | `ADR046-provider-003` | +| `/run/d2b/d2bd.sock`, `/run/d2b/broker.sock` | §7 | Destroy (Phase 3, boot-scoped) | `/run/d2b/z-/...` fresh sockets | `ADR046-core-controllers` "Process model" | +| `/run/d2b/allocator.sock` | §7, config-ref/schema-only, engine not live | Destroy (Phase 3; never adopted — no live allocator process to quiesce) | No successor socket; provisioning integrates into fixed core controllers | `ADR046-core-001` | +| `d2b-realm-router` PeerSession/MuxSession/`WorkloadOp`/`RealmMethod` wire | multiple rows, `dead-reachable`/`production-reachable` | Preserve (compiled into binary; Destroy only when the binary itself is retired at Phase 10) | ComponentSession/d2b-bus/`ResourceOp` | `ADR046-session-001`, `ADR046-bus-001`, `ADR046-api-001` | +| `d2b-unsafe-local-helper` binary, `DaemonToUnsafeLocalHelper` protocol | §7, `production-reachable` | Preserve until Process Provider supervisor ticket migration lands, then Destroy | User-only `Host` Process supervisor | `ADR046-primitives-003` | +| `d2b-guest-shell-runner` | `production-reachable` | Preserve until user-only Host shell Process parity, then Destroy | `Process` child of user-only Host | `ADR046-primitives-003` | +| `~/.local/state/d2b/unsafe-local-scopes.json` | §7 evidence, per-user scope ledger | Adopt into a declared user-only Host state Volume — this ledger is real private per-user content that passes the storage-need test, not derivable from status/core ledger — via a per-user migration `EphemeralProcess` | User-only Host declared component state Volume | `ADR046-primitives-003` | + +### Storage/restart/synchronization contract (ADR 0034) + +| Current artifact | Evidence | Disposition | Target | Owning work item / dossier | +| --- | --- | --- | --- | --- | +| `storage.json` | ADR 0034 generated artifact | Preserve (read by legacy code until every storage id has a live Volume/resource successor), then Destroy per-id as each successor lands | Per-artifact `Volume`/resource storage declaration | `ADR046-store-003` | +| `sync.json`/`locks.json` | ADR 0034 generated artifact | Preserve, then Destroy per-id as each lock's successor (OFD-lock-owning resource/controller) lands | Internal controller/transaction lock mechanics (not a ResourceType, per `ADR-046-resource-object-model` "Folded implementation detail") | `ADR046-store-001` | +| Daemon degraded-state ledger | ADR 0034 | Preserve as historical evidence; new degraded conditions post-cutover use the Zone resource `status.conditions` model instead | `Resource.status.conditions` | `ADR046-core-001` | +| OFD lock files under `/run/d2b` | ADR 0034 | Destroy only via normal reboot/tmpfs cleanup — never unlinked directly by cutover (explicit fail-closed hazard) | N/A (mechanism, not a resource) | N/A | + +### TPM (device-tpm Provider dossier) + +| Current artifact | Evidence | Disposition | Target | Owning work item / dossier | +| --- | --- | --- | --- | --- | +| `/var/lib/d2b/vms//swtpm/` | migration-map row (TPM/swtpm section), `production-reachable` | **Adopt** (never Destroy until Phase 10 gate; never silently re-provisioned) | Controller-created TPM `Volume` per Guest | `ADR-046-provider-device-tpm` §17.3; `ADR046-device-tpm-004`; `ADR046-vl-005` | +| `/var/lib/d2b/swtpm-markers/` | migration-map row | **Adopt**, re-keyed from `` basename to `device_uid`-based name by `volume-local` | TPM Volume identity marker (`broker-maintained` class) | `ADR-046-provider-device-tpm` §17.3 | +| `SwtpmArgvInput`/`SwtpmIoctlFlushInput` (`d2b-host/src/swtpm_argv.rs`) | `production-reachable` | Preserve until extracted, then Destroy at Phase 10 | `d2b-provider-device-tpm/src/` | `ADR046-device-tpm-001` | +| `PrepareSwtpmDir` broker op | `production-reachable` | Preserve (still invoked, now only by `volume-local`) | Same op, narrower caller | `ADR-046-provider-device-tpm` §18 | +| `components/tpm.nix` | `nix-emitted` | Preserve until Device Nix declaration (§17.1 of the dossier) is authored for every Guest, then Destroy | `d2b.zones..resources.` Device declaration | `ADR-046-provider-device-tpm` §17.3 | + +### Store-view hardlink farm and disk images (volume-local Provider dossier) + +| Current artifact | Evidence | Disposition | Target | Owning work item / dossier | +| --- | --- | --- | --- | --- | +| `/var/lib/d2b/vms//store/` | migration-map §7, `production-reachable` | **Adopt** (same-filesystem hardlink-farm rename; zero-copy) | Store-view `Volume` per Guest | `ADR046-vl-004` | +| `/var/lib/d2b/vms//store-meta/`, `store-view/` | migration-map §7, legacy-recovery-artifact (optional per `ownership_preflight.rs`) | Preserve if present (optional legacy recovery artifact; not required post-cutover) | Superseded by store-view Volume metadata | `ADR046-vl-004` | +| VM disk images, including writable store-overlay images | ADR 0034 "preserves critical persistent data" | **Adopt** (copy or same-filesystem move depending on the declared source policy) into a `kind: durable` block-image Volume | `Volume` with `source.settings.kind: block-image` | `ADR-046-resources-volume` §"Sources"; `ADR046-vl-006` | +| `nixos-modules/store.nix` (`share.source == "/nix/store"` sentinel) | `nix-emitted` | Preserve until store-view Volume controller is live and passes parity tests, then Destroy | `Provider/volume-virtiofs` reconciled virtiofsd Process | `ADR046-vl-004` | + +### Networking + +| Current artifact | Evidence | Disposition | Target | Owning work item / dossier | +| --- | --- | --- | --- | --- | +| Declared host bridges, TAP naming intent | `nixos-modules/network.nix`, `net.nix` | Preserve (bridges keep serving traffic through Phase 8 Network-Provider reconciliation; never torn down mid-cutover) | `Network` resource reconciled by `Provider/network-local` | `ADR-046-current-code-migration-map` §"nixos-modules/network.nix" row | +| `inet d2b` nftables table ownership markers (`comment "d2b managed: "`) | ADR 0013 | Preserve; ownership markers re-validated, never re-created with a new marker scheme during cutover | Same marker scheme, now written by `network-local`'s broker effect adapter | `ADR-046-provider-network-local` | +| NetworkManager/`systemd-networkd` coexistence markers | ADR 0013 | Preserve | Unmodified | `ADR-046-provider-network-local` | + +### Keys, credentials, and identity + +| Current artifact | Evidence | Disposition | Target | Owning work item / dossier | +| --- | --- | --- | --- | --- | +| `/_ed25519{,.pub}` | `nixos-modules/host-keys.nix` | Preserve (framework SSH keys are never regenerated by cutover) | Unchanged; continues to be consumed the same way by Guest boot | N/A — out of ADR 0046 Provider scope per activation-nixos dossier §1.2 ("SSH key lifecycle... belongs to a separate identity Provider") | +| `/vms//host-keys/{host.pub,user-authorized-keys}` | `nixos-modules/host-keys.nix` | Preserve | Unchanged | Same as above | +| `realm-controllers.json`, `realm-identity.json` | migration-map §"Current-code fit" rows, `implemented-and-reachable`/live | Preserve until the compiler-only `parentZone` topology is sealed, every child-local `ZoneLink`/Credential successor is `Ready`, and the no-reciprocal-parent-row check passes (Phase 10 gate), then Destroy | Runtime-created `Zone` self resource + sealed allocator topology + child-local `ZoneLink` transport/route state; Credential `scope`/`audience`/`allowedOperations` fields | `ADR046-nix-008`, `ADR046-nix-009` | +| Gateway guest realm relay credentials/audit (ADR 0032 `CredentialCustody::GatewayGuest`) | ADR 0032 evidence | **Never enumerated by the parent host's inventory** — see [Gateway Guest credential/audit custody](#gateway-guest-credentialaudit-custody) | Nested child Zone's own Credential resources | N/A — parent host cutover has no authority here | + +### Audit and telemetry + +| Current artifact | Evidence | Disposition | Target | Owning work item / dossier | +| --- | --- | --- | --- | --- | +| `daemon-events-*.jsonl` | `packages/d2bd/src/daemon_audit.rs`, `implemented-and-reachable` | Preserve forever (closed with a terminal record; never a Destroy candidate at any gate) | New chain opened per [Audit chain closure and opening](#audit-chain-closure-and-opening) | `ADR046-audit-*` (telemetry-audit-and-support work items) | +| Broker `audit.rs` segments | `implemented-and-reachable` | Preserve forever | Same | Same | +| `JsonlGatewayAudit` segments (gateway-backed realms only) | `implemented-and-reachable` (gateway paths) | Preserve forever, and never read by the parent Zone (custody boundary) | Nested child Zone's own audit chain | Same | +| `packages/d2bd/src/metrics.rs` hand-rolled Prometheus registry | `implemented-and-reachable` | Preserve until OTEL SDK metrics reach parity, then Destroy | `observability-otel` Provider native OTLP | `ADR-046-telemetry-audit-and-support` work items | + +### CLI surface + +| Current artifact | Evidence | Disposition | Target | Owning work item / dossier | +| --- | --- | --- | --- | --- | +| `d2b vm *`, `d2b realm *`, `d2b up/down/restart/list/status`, `d2b usb *`, `d2b keys *`, `d2b build/switch/boot/test/rollback/gc/migrate/config *` | `ADR-046-cli-and-operations` "v2 command surface removed at 3.0 clean break" | Preserve (compiled dispatch) until v3 successor is wired, then Destroy at Phase 10 (compile-time removal, verified by policy lint) | `d2b guest/zone/device/exec/shell/activation *` | `ADR-046-cli-and-operations` per-verb work items | +| `d2b host migrate-storage` | `ADR-046-cli-and-operations` "Removal notes" | Destroy at Phase 10 with **no v3 successor** — the layout cutover it served (v1→v2) is unrelated to this cutover and is not re-implemented | None (retired) | N/A | +Any current-baseline path, unit, or artifact not named in any table above is, +per [Authoritative inventories](#authoritative-inventories), classified +**Preserve by default** and surfaced in the `plan` output's `unclassified` +array for explicit operator review before `apply` — this matrix is exhaustive +over every category this spec's inventories walk, but a future Provider +dossier revision that introduces a new current-code row must add a +corresponding row here before its disposition may be anything other than the +Preserve default. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | ADR 0034 "Migration decision" (planned-downtime storage cutover, preserve list, checkpoint/rollback UX); `packages/d2b/src/lib.rs` `cmd_host_migrate_storage`/`build_storage_migration_plan`/`storage_migration_checkpoint_id` (retired verb, reused shape); `cmd_host_destroy`/`require_explicit_mutation_flag` (dry-run/apply precondition pattern); `packages/d2bd/src/storage_lifecycle.rs`/`ownership_preflight.rs` (bundle-versioned contract checks, legacy-recovery-artifact optionality); `ADR-046-resources-zone-control` §2.6/§9.4 (destructive reset primitive); `ADR-046-provider-state` (migration/incident-hold/unclaimed-GC machinery) | +| Evidence class | The CLI dry-run planning precedent (`host migrate-storage`, `host destroy`) is `implemented-and-reachable`; the ADR 0034 preserve-list/checkpoint contract is `implemented-and-reachable` as design but its `--apply`/`--rollback` are themselves `test-only-or-preview` (fail closed in the current build); the Zone/Provider/Volume destinations this spec adopts into are `ADR-only` | +| Behavior retained | Dry-run-before-apply with a printed checkpoint id and exact rollback command; preserve-list-first design (swtpm NVRAM/markers, SSH keys, store-view state/gcroots, disk images, audit/degraded history never silently destroyed); fail-closed hazards enumerated explicitly rather than left implicit; broker-mediated path-safe mutation (no manual chmod/chown/setfacl); atomic persistence sequence (temp file, fsync, rename, parent fsync) for every durable record this spec's own journal/snapshot writes | +| Required delta | The entire Zone/Provider/redb resource-store bootstrap this spec's Phase 5-8 execute has no current-baseline equivalent at all — ADR 0034's cutover only ever moved a storage *layout*, never replaced the daemon/wire protocol/resource model sitting on top of it; the Full/Provider/Guest reset scopes, the cutover-wide incident hold, and the gateway-custody-aware child-local ZoneLink translation are new | +| Reuse path | Copy the exact `StorageMigrationPlan` JSON shape (renamed fields) for the new `plan`/`apply` JSON envelopes; copy `require_explicit_mutation_flag`'s precondition gate for every new mutating verb; copy `storage_migration_checkpoint_id`'s digest-of-sorted-names pattern extended over every inventory class; copy the ADR 0034 preserve list verbatim as the seed of the [migration/disposition matrix](#migrationdisposition-matrix)'s TPM/keys/disk-image/audit rows; copy `ADR-046-provider-state`'s prepare/stage/commit/rollback/roll-forward algorithm unmodified as every Adopt step's mechanism | +| Replacement/deletion | `d2b host migrate-storage` is retired with no successor (it served an unrelated v1→v2 cutover); every other current-baseline artifact this spec's matrix names is Preserved until its own [Old artifact/unit/schema removal gate](#old-artifactunitschema-removal-gates) clears — nothing is removed by this spec's own authoring, only by its future implementation work items after their gates clear | +| Feasibility proof | A disposable end-to-end cutover rehearsal fixture (single-Guest, single-TPM, single-store-view host) proving: preflight snapshot digest reproducibility; drain quiescence detection; Adopt idempotency across an injected crash at every step boundary; Phase 5 redb bootstrap against the rehearsal fixture's adopted Volumes; Provider install topological order determinism; `verify` catching an injected digest mismatch; rollback within the boundary and refusal past it; Full/Provider/Guest reset scope isolation (resetting one does not affect siblings) | +| Future owner | Work items below | + +## Tests + +Every test type below follows the taxonomy `tests/AGENTS.md` defines; this +spec introduces no new top-level `tests/*.sh` gate (the closed-set rule +applies here exactly as everywhere else in the repository). + +### Type 1 — eval cases (`tests/unit/nix/cases/`) + +| Case | Asserts | +| --- | --- | +| `cutover-snapshot-retention-bounds.nix` | `d2b.site.cutoverSnapshotRetention` eval-rejects values outside `1..16`; defaults to `3` | +| `cutover-candidate-bundle-validation.nix` | A candidate `d2b.zones..*` configuration with a dangling `*Ref` fails eval with a structured error naming the offending field | +| `zone-reset-scope-target-parsing.nix` | `--scope`/`--target` combinations for `zone`/`provider`/`guest` parse to the correct `ResourceRef` type; a `Zone/` target under `--scope provider` is rejected | + +### Type 2 — unit tests (`packages//src/**`) + +| Test | Asserts | +| --- | --- | +| `checkpoint_id` digest determinism | Same sorted inventory produces the same `checkpoint_id`; any single differing inventory element changes it | +| Journal resume logic | Given a journal with phases 0-4 `succeeded` and phase 5 `started` (no `succeeded`/`failed`), a resumed `apply` re-executes phase 5 from its own beginning, not phase 0 | +| Disposition framework invariant | Every disposition table entry compiles to exactly one of `Adopt`/`Preserve`/`Destroy`; a path absent from every table defaults to `Preserve` | +| TPM/durable-Volume Destroy exclusion | Property test: no code path can assign `Destroy` to a path tagged `kind: durable`-equivalent or TPM-marker-bearing in the inventory | + +### Type 3 — integration tests (`packages//tests/*.rs`) + +| Test | Asserts | +| --- | --- | +| `cutover_preflight_refuses_dirty_flake_check` | `preflight` refuses with `cutover-precondition-failed` when the legacy configuration does not currently evaluate | +| `cutover_apply_requires_exact_consent_phrase` | `apply` refuses with `cutover-consent-required` for a missing, mismatched, or stale-checkpoint consent string | +| `cutover_drain_refuses_on_live_process` | Drain refuses with `cutover-precondition-failed` naming the exact stuck Guest/process rather than force-killing it | +| `cutover_rollback_window_closes_after_phase_5` | `rollback` succeeds for a checkpoint at phase `<=4` and refuses with `cutover-rollback-window-closed` at phase `>=5` | +| `host_reset_scope_isolation` | A `Provider reset` does not mutate the Zone resource, other Providers, or unrelated Guests; a `Guest reset` does not mutate the Zone, Providers, or other Guests | + +### Type 4 — contract tests (`packages/d2b-contract-tests/tests/*.rs`) + +| Test | Asserts | +| --- | --- | +| `cutover_snapshot_schema_matches_doc` | The rendered `snapshot.json`/`journal.jsonl` shapes match this spec's documented fields exactly (drift gate) | +| `cutover_plan_json_schema_v1` | `plan`'s JSON envelope matches the documented schema, frozen at version 1 | + +### Type 5 — policy lints (`packages/d2b-contract-tests/tests/policy_*.rs`) + +| Test | Asserts | +| --- | --- | +| `policy_no_destroy_without_gate` | Every `Destroy`-classified matrix row has a corresponding entry in [Old artifact/unit/schema removal gates](#old-artifactunitschema-removal-gates) | +| `policy_legacy_cli_verbs_absent_after_gate` | Compilation fails if any retired `cmd_vm_*`/`cmd_realm_*` dispatch entry is reintroduced after its Phase 10 gate is recorded clear | +| `policy_no_manual_chmod_chown_setfacl_text` | No cutover CLI human-output string contains `chmod`/`chown`/`setfacl` remediation text | + +### Type 6 — flake checks (`tests/unit/smoke/`) + +| Check | Asserts | +| --- | --- | +| `smoke-eval-cutover-candidate-bundle` | The candidate v3 Zone bundle for the repository's own example configurations (`examples/minimal`, `examples/multi-env`) evaluates and builds under the new `d2b.zones.*` namespace | + +### Type 9 — container (`tests/integration/containers/`, `make test-integration`) + +| Test | Asserts | +| --- | --- | +| `cutover-rehearsal-container.sh` | A rootless-podman fixture proves the Adopt migration `EphemeralProcess` pattern (prepare/stage/commit/rollback) against a synthetic swtpm-directory/store-view fixture without requiring a real Guest boot | + +### Type 10 — VM/host-KVM (`tests/host-integration/*.nix`, `make test-host-integration`) + +| Test | Asserts | +| --- | --- | +| `cutover-full-rehearsal.nix` | A real NixOS VM with one TPM-enabled Guest, one store-view Guest, and framework SSH keys: full Preflight→Verify cutover rehearsal, `verify` digest checks pass, old units retired only after their Phase 10 gates clear | +| `cutover-crash-resume.nix` | Kills the `apply` process mid-Phase-4 (mid-Adopt) and confirms a re-run resumes idempotently with no data loss and no duplicate Volume creation | +| `zone-provider-guest-reset-isolation.nix` | Full/Provider/Guest reset scope isolation proven against a live multi-Guest, multi-Provider Zone | +| `tpm-adopt-retirement.nix` | Phase 10 TPM gate: adopted TPM Volume survives a full Guest restart cycle before its source directory may be Destroyed | + +### Type 11 — live-host (`tests/integration/live/`, `D2B_LIVE=1`, manual, never CI) + +| Test | Asserts | +| --- | --- | +| `live/cutover-real-host.sh` | Full cutover against a real deployed pre-ADR-0046 host (never CI); requires explicit operator sign-off and a pre-existing full disk/VM backup outside this spec's own snapshot mechanism | +| `live/cutover-real-host-cloud-guest.sh` (manual cloud) | Cutover of a host with a live `runtime-azure-container-apps`/`runtime-azure-virtual-machine` Guest against real Azure resources; proves the gateway-custody boundary holds with a real relay-backed realm | + +### Type 12 — hardware (`tests/host-integration/hardware/`, manual, real devices) + +| Test | Asserts | +| --- | --- | +| `hardware/cutover-real-tpm.sh` | Cutover against a host with a real hardware-backed TPM passthrough Guest (not swtpm), proving the TPM Adopt path handles both the swtpm and hardware-TPM device-tpm Provider variants identically from the cutover's perspective | +| `hardware/cutover-real-usbip-security-key.sh` | Cutover against a host with a live USBIP/security-key attachment, proving Phase 8 device attachment reconciliation does not require detaching a physically-present device | + +## Implementation work items + +### ADR046-reset-001 — Inventory and snapshot engine + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reset-001` | +| Dependency/owner | W0 shared contract root; storage/broker integrator | +| Current source | `packages/d2bd/src/storage_lifecycle.rs` (`run_startup_contract_check`, bundle-versioned contract validation pattern); `packages/d2bd/src/ownership_preflight.rs` (`EntrySpec`, legacy-recovery-artifact optionality); `packages/d2b/src/lib.rs` `build_storage_migration_plan`/`storage_migration_checkpoint_id` | +| Reuse source | None from main; this is a v3-only cross-cutting concern with no main-branch equivalent | +| Reuse action | adapt | +| Destination | `packages/d2b-cutover/src/{inventory,snapshot,checkpoint}.rs` | +| Detailed design | Implement the seven closed inventories of [Authoritative inventories](#authoritative-inventories); the `checkpoint_id` digest algorithm of [Preflight and immutable snapshot](#preflight-and-immutable-snapshot); the atomic snapshot-write sequence (temp file, fsync, rename, parent fsync, post-rename immutability) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | `d2b host cutover preflight`/`plan` CLI commands consume this crate exclusively; no other crate re-implements inventory walking | +| Data migration | New; no prior inventory/snapshot format exists | +| Validation | `checkpoint_id` determinism property test; snapshot atomic-write crash-injection test; `cutover_preflight_refuses_dirty_flake_check` | +| Removal proof | Not applicable (net-new capability) | + +### ADR046-reset-002 — Config/artifact/schema validation + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reset-002` | +| Dependency/owner | ADR046-reset-001; `d2b-resource-store-redb` owner; `d2b-provider` catalog owner | +| Current source | `nixos-modules/bundle-artifacts.nix`, `nixos-modules/assertions.nix` (existing eval-time validation precedent); `ADR-046-nix-configuration` "Bundle and generation emission" | +| Reuse source | None from main | +| Reuse action | adapt | +| Destination | `packages/d2b-cutover/src/{bundle_validate,trust_preflight}.rs` | +| Detailed design | Independent legacy-flake-check gate; candidate v3 bundle schema/cross-ref/determinism validation per [Config/artifact/schema validation](#configartifactschema-validation); Provider trust preflight per `ADR-046-provider-model-and-packaging` "Trust" | +| Integration | Invoked by `preflight` before the snapshot is written; failures block `plan` from being offered | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | `cutover-candidate-bundle-validation.nix`; trust-preflight rejection tests for each of digest/publisher/signature/deny/provenance/conformance failure modes | +| Removal proof | Not applicable | + +### ADR046-reset-003 — Consent, drain, and disposition executor + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reset-003` | +| Dependency/owner | ADR046-reset-001, ADR046-reset-002; Process/Guest lifecycle owner | +| Current source | `packages/d2b/src/lib.rs` `require_explicit_mutation_flag`, `cmd_host_destroy` (dry-run/apply precondition pattern); [ADR 0040](../adr/0040-graceful-vm-shutdown.md) graceful shutdown path | +| Reuse source | None from main | +| Reuse action | adapt | +| Destination | `packages/d2b-cutover/src/{consent,drain,disposition}.rs` | +| Detailed design | Exact-consent-phrase gate bound to `checkpoint_id`; Phase 3 drain algorithm (§ [Old daemon/unit/process drain](#old-daemonunitprocess-drain)); the [Disposition framework](#disposition-framework)'s Adopt/Preserve/Destroy executor, delegating every Adopt to ADR046-reset-004 Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | `d2b host cutover apply` orchestrates drain then disposition execution then hands off to Phase 5 (ADR046-reset-005) | +| Data migration | Destructive; this is where Phase 3/4 boundary-of-no-return-approach begins (rollback still open through end of Phase 4) | +| Validation | `cutover_apply_requires_exact_consent_phrase`; `cutover_drain_refuses_on_live_process` | +| Removal proof | Not applicable | + +### ADR046-reset-004 — Adopt migration EphemeralProcess integration + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reset-004` | +| Dependency/owner | ADR046-reset-003; `ADR-046-provider-state` owner; `device-tpm`/`volume-local` Provider owners | +| Current source | `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` (marker fail-closed pattern); `packages/d2b-host/src/hardlink_farm.rs` (same-filesystem rename pattern) | +| Reuse source | None from main | +| Reuse action | adapt | +| Destination | `packages/d2b-cutover/src/adopt.rs`, thin wrapper invoking `ADR-046-provider-state`'s migration `EphemeralProcess` prepare/stage/commit/rollback machinery with cutover-specific source paths | +| Detailed design | One Adopt invocation per matrix row tagged `Adopt` in the [migration/disposition matrix](#migrationdisposition-matrix); marker re-validation before every step; new-marker-before-old-removal ordering; idempotent re-run safety per [Crash/power-loss/retry/idempotency journals](#crashpower-lossretryidempotency-journals) | +| Integration | Called by ADR046-reset-003's disposition executor for every Adopt row; writes to the state Volumes ADR046-device-tpm-004/ADR046-vl-004/ADR046-vl-006 define | +| Data migration | This work item *is* the data migration mechanism for TPM/store-view/disk-image/unsafe-local-scope bytes | +| Validation | Crash-injection at every step boundary (Type 10 `cutover-crash-resume.nix`); TPM/durable-Volume Destroy-exclusion property test | +| Removal proof | Not applicable (the mechanism is retained permanently for later Full/Provider/Guest reset relocation use, not retired after first use) | + +### ADR046-reset-005 — Resource-store bootstrap and Provider install sequencer + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reset-005` | +| Dependency/owner | ADR046-reset-004; `d2b-resource-store-redb` owner (`ADR046-store-003`); core-controller owner (`ADR046-core-001`) | +| Current source | None (bootstrap sequencing over Zone runtime startup, which is itself ADR-only) | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-cutover/src/{store_bootstrap,provider_sequence}.rs` | +| Detailed design | Phase 5 store creation per [Resource-store initialization](#resource-store-initialization); Phase 6 topological Provider install per [Provider install/topological start](#provider-installtopological-start), including the fixed staged default order and cycle-rejection check | +| Integration | Invoked immediately after ADR046-reset-003/004 complete; hands off to Phase 7 (ADR046-reset-006) | +| Data migration | Destructive v3 bootstrap; no v2 resource import (per `ADR046-store-003`, `ADR046-object-001`) | +| Validation | Provider install topological-order determinism test; cycle-rejection test; store-identity mismatch fail-closed test | +| Removal proof | Not applicable | + +### ADR046-reset-006 — Child-local ZoneLink/Guest activation and gateway custody boundary + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reset-006` | +| Dependency/owner | ADR046-reset-005; `ADR-046-zone-routing` owner; `ADR-046-resources-zone-control` owner | +| Current source | ADR 0032 gateway guest custody evidence (`d2b-realm-router/src/service_v2.rs` `CredentialCustody`); frozen target model in `ADR-046-resources-zone-control` §3.1/§10.3 and `ADR-046-nix-configuration` "Zone declaration"/"ZoneLink" | +| Reuse source | None from main | +| Reuse action | adapt | +| Destination | `packages/d2b-cutover/src/{zonelink_cutover,guest_activation}.rs` | +| Detailed design | Phase 7 translation from `EntrypointMode::GatewayBacked`/`HostResident` per [Zone/ZoneLink cutover](#zonezonelink-cutover): the child authors/stores one local uplink with self-matching `childZoneName`; compiler-only `parentZone` selects the allocator; the parent retains only sealed allocator/route state and owns no reciprocal row or ZoneLink handler. Phase 8 follows Network→Volume→Device→Guest ordering per [Guest/runtime/network/store view activation](#guestruntimenetworkstore-view-activation). The parent inventory never enumerates gateway-guest-internal credential/audit state | +| Integration | Consumes Providers installed by ADR046-reset-005; hands off to ADR046-reset-007 (verification) | +| Data migration | None (the child-local ZoneLink is ordinary Nix-authored configuration, `parentZone` is recompiled into sealed allocator state, and neither is migrated credential material) | +| Validation | Gateway-custody-boundary test asserting the parent inventory never contains a gateway-guest-internal path; child-local ZoneLink test asserting one uplink, self-matching `childZoneName`, and child-store ownership; compiler test asserting `parentZone` selects the allocator but appears only in sealed bootstrap state; no-reciprocal-parent-row/no-parent-handler test; child-local ZoneLink `Degraded/waiting-on-remote` non-blocking test | +| Removal proof | Not applicable | + +### ADR046-reset-007 — Verification, doctor, and degraded-ledger integration + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reset-007` | +| Dependency/owner | ADR046-reset-006; telemetry-audit-and-support owner (`d2b-audit`) | +| Current source | ADR 0034 degraded-state ledger taxonomy and repair-never-trusts-ledger-paths invariant | +| Reuse source | None from main | +| Reuse action | adapt | +| Destination | `packages/d2b-cutover/src/{verify,doctor,degraded}.rs` | +| Detailed design | The ten `verify` checks in [Post-cutover verification](#post-cutover-verification); the `cutover-quarantined` degraded class and `doctor` reporting in [Failure/quarantine/manual recovery](#failurequarantinemanual-recovery); audit chain closure/genesis-record cross-check (check 9) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | `d2b host cutover verify`/`doctor` CLI commands; consumed by the Phase 10 finalize gate table | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Injected-digest-mismatch test for TPM/durable-Volume verify checks; audit-genesis-cross-check test; `cutover-full-rehearsal.nix` | +| Removal proof | Not applicable | + +### ADR046-reset-008 — Old artifact/unit/schema removal gate engine (Phase 10) + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reset-008` | +| Dependency/owner | ADR046-reset-007; owner of each retiring artifact's ADR 0046 successor work item | +| Current source | `ADR-046-cli-and-operations` "Removal notes" (live-successor-before-deletion criterion) | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-cutover/src/finalize.rs` | +| Detailed design | Per-candidate independent gate evaluation exactly as tabled in [Old artifact/unit/schema removal gates](#old-artifactunitschema-removal-gates); separate consent phrase from `apply`; never partial-destroys a candidate | +| Integration | `d2b host cutover finalize` CLI command; reads gate status from ADR046-reset-007's verify results plus each named policy-lint/integration test's pass/fail recorded in CI | +| Data migration | This work item is where every previously-Preserved legacy artifact is finally Destroyed, one gate at a time | +| Validation | `policy_no_destroy_without_gate`; `policy_legacy_cli_verbs_absent_after_gate`; `tpm-adopt-retirement.nix` | +| Removal proof | Each candidate's own row in [Old artifact/unit/schema removal gates](#old-artifactunitschema-removal-gates) states its exact removal proof | + +### ADR046-reset-009 — Rollback, journal resume, and incident hold + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reset-009` | +| Dependency/owner | ADR046-reset-003 through ADR046-reset-006; `ADR-046-provider-state` incident-hold owner | +| Current source | ADR 0034 "dry-run and preflight output print the checkpoint id and exact rollback command before any apply step begins" | +| Reuse source | None from main | +| Reuse action | adapt | +| Destination | `packages/d2b-cutover/src/{journal,rollback,hold}.rs` | +| Detailed design | Append-only journal per [Crash/power-loss/retry/idempotency journals](#crashpower-lossretryidempotency-journals); [Rollback boundary](#rollback-boundary) enforcement (`cutover-rollback-window-closed` past phase 4); cutover-wide incident hold per [Incident hold (cutover-wide)](#incident-hold-cutover-wide) | +| Integration | `d2b host cutover rollback`/`hold` CLI commands; consulted by ADR046-reset-003's disposition executor and ADR046-reset-008's finalize gate before every mutating step | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | `cutover_rollback_window_closes_after_phase_5`; incident-hold-blocks-destructive-step test | +| Removal proof | Not applicable | + +### ADR046-reset-010 — Full/Provider/Guest reset CLI and scope isolation + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reset-010` | +| Dependency/owner | ADR046-reset-005 through ADR046-reset-007; `ADR-046-resources-zone-control` owner (§2.6/§9.4) | +| Current source | `ADR-046-resources-zone-control` §2.6 (`core.zone-drain` finalizer algorithm), §9.4 (out-of-band destructive reset, uid=0 authentication) | +| Reuse source | None from main | +| Reuse action | adapt | +| Destination | `packages/d2b-cutover/src/reset_scope.rs`; `d2b host reset` CLI command | +| Detailed design | The three reset scopes and their comparison table in [Full Zone reset vs Provider reset vs Guest reset](#full-zone-reset-vs-provider-reset-vs-guest-reset); durable-Volume preserve-by-default with explicit `--destroy-durable-volumes`/`--destroy-volumes` opt-in; OS-level authentication for the zone scope only | +| Integration | Standalone from the cutover Phases 0-10 above; usable at any later time as a recovery/maintenance lever once a Zone exists | +| Data migration | None (this is a post-cutover recovery operation, not part of the cutover data migration itself) | +| Validation | `host_reset_scope_isolation`; `zone-provider-guest-reset-isolation.nix`; durable-Volume-preserved-by-default property test for both Provider and Guest scopes | +| Removal proof | Not applicable | + +### ADR046-reset-011 — Live-host and hardware validation + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reset-011` | +| Dependency/owner | ADR046-reset-001 through ADR046-reset-010, fully landed | +| Current source | `tests/integration/live/` conventions; `tests/host-integration/hardware/` conventions | +| Reuse source | None | +| Reuse action | create | +| Destination | `tests/integration/live/cutover-real-host.sh`, `tests/integration/live/cutover-real-host-cloud-guest.sh`, `tests/host-integration/hardware/cutover-real-tpm.sh`, `tests/host-integration/hardware/cutover-real-usbip-security-key.sh` | +| Detailed design | Manual, `D2B_LIVE=1`/hardware-gated validation scripts described in [Tests](#tests) Type 11/12 rows; never run in CI; require operator sign-off and an independent out-of-band backup before execution | +| Integration | Run manually by an operator against a real host/device before the reset-and-cutover implementation is declared production-ready | +| Data migration | None (validation only) | +| Validation | Manual pass/fail sign-off recorded per the project's existing live-host/hardware validation conventions | +| Removal proof | Not applicable | diff --git a/docs/specs/ADR-046-resource-api-and-authorization.md b/docs/specs/ADR-046-resource-api-and-authorization.md new file mode 100644 index 000000000..ce48a9d5d --- /dev/null +++ b/docs/specs/ADR-046-resource-api-and-authorization.md @@ -0,0 +1,586 @@ +# ADR 0046 resource API and authorization + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-resource-api-and-authorization` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-resource-api`, Zone authorization engine | +| Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-store-redb` | +| Supersedes | None | + +## Service + +`d2b.resource.v3` is a language-neutral protobuf/ttrpc service routed through +d2b-bus over ComponentSession and a d2b transport. + +The resource API is asynchronous end-to-end. Rust clients expose async methods; +synchronous redb/filesystem work remains behind bounded blocking adapters in +the Zone runtime. + +## Methods + +| Method | Semantics | +| --- | --- | +| `Get` | Exact ResourceRef; returns current envelope/revision | +| `List` | Exact ResourceTypes and bounded filters/pagination; returns one MVCC snapshot revision | +| `Watch` | Exact authorized filters after revision; returns named stream | +| `Create` | Create-absent precondition and complete envelope spec | +| `UpdateSpec` | Full spec replacement with expected revision | +| `UpdateStatus` | Full status replacement with expected revision; controller/status-owner only | +| `UpdateMetadata` | Bounded labels/annotations/ownerRef/deletion-neutral metadata with expected revision | +| `UpdateFinalizers` | Exact finalizer add/remove with expected revision and ownership checks | +| `Delete` | Request deletion with expected revision/preconditions | +| `CommitBatch` | Atomic bounded set of the above in one Zone transaction | +| `ResolveRef` | Validate canonical ref/type/UID and return bounded identity metadata | +| `InspectSchema` | Read bound ResourceType schema/export/Provider identity | +| `Upgrade` | Authorized `assess_update`/`plan_upgrade`/`execute_upgrade` on a ResourceRef; plan-by-default, apply with explicit intent; optional `--recursive` owned/dependency planning (D091) | + +`Create`, `UpdateSpec`, and `Delete` accept an authorized expedited option +`waitForReconcile` with a bounded deadline (D090; see § Expedited reconcile). + +There is no arbitrary path/header/query/table API, JSON patch, server-side +apply, direct redb access, exec/port-forward, secret byte read, or generic +Provider command. + +## Expedited reconcile (D090) + +`Create`/`UpdateSpec`/`Delete` may set `waitForReconcile` with a bounded +deadline. Under one **mutation ticket** carrying an `operationId` and the +deadline, Core admission plus the reserved-revision redb transaction run in +parallel with the owning controller's preflight/plan: + +1. The controller MAY validate and plan before commit but performs **no** + external effect, finalizer release, or status mutation until Core supplies a + typed `CommittedRevisionProof { resourceUid, generation, revision, operationId }`. +2. A DB failure sends `Abort`; no effect occurs. The API returns success only if + the initial resource mutation durably committed. A durable commit is never + rolled back because reconcile later fails or times out. +3. Commit enqueues the ordinary reconcile hint; the expedited request enters a + bounded priority lane into the SAME per-resource single-flight reconciler + (`ADR-046-resource-reconciliation`). Any normally queued reconcile stays + queued and runs after the expedited pass. All effect IDs/idempotency keys + derive from `(UID, generation, revision, operationId)`; a normal re-entry + observes converged/progressing state and no-ops/rejoins, never duplicating. + +The API waits in parallel for durable commit plus **one expedited reconcile +pass** and returns: + +```yaml +resource: # base spec + committed metadata +status: # universal + resource + optional provider +disposition: Converged | Progressing | Blocked | UpgradeRequired | Failed +statusPersistence: pending | committed +lastPersistedStatusRevision: +reconcileProjection: +``` + +Status persistence is asynchronous; the response need not wait for the status +write, so no uncommitted status is represented as durable. `Delete` returns an +event-only Deleted projection / not-found outcome. Expedited completion means +one pass reached a `Converged|Progressing|Blocked|UpgradeRequired|Failed` +disposition, not long-running external Ready. A timeout/cancel after commit +returns a typed committed-but-reconcile-pending response while the ordinary +queue continues. Priority quotas/fairness prevent starvation/DoS. Only +authorized UX mutations and core may request expedited mode; the admin +`resource reconcile` CLI action reuses the same lane. + +## Request context + +The caller cannot provide authenticated identity or authorization outcome. +d2b-bus/ComponentSession supplies the shared `AuthenticatedSubjectContext`. +The API derives request attributes from it plus: + +- operation/idempotency/correlation/trace IDs; +- issue/deadline/cancellation; +- policy/API/config/controller revisions. + +The resource payload supplies only target refs, desired body, preconditions, +pagination/watch filters, and method-specific options. + +## ResourceType schemas and API exports + +Every Provider resource binds: + +- package/signature/trust identity; +- exported ResourceType names/versions; +- exact ResourceTypeSchema digests; +- controller descriptors and supported methods; +- maximum permission claims; +- service descriptors. + +The Zone self resource binds accepted API exports and short ResourceType names. +Binding: + +1. verifies Provider/package/schema signatures; +2. rejects a short ResourceType collision; +3. validates compatibility/fingerprint; +4. intersects permission claims with Zone policy; +5. installs the schema into api_schemas; +6. advances API catalog revision. + +Providers cannot mutate api_schemas directly. + +### ResourceApiBinding base-schema conformance (D089) + +Each Provider `ResourceApiBinding` for a ResourceType declares and MUST +implement the exact ResourceType **base spec** and **base status** schema +version/fingerprint and pass the base lifecycle/status/error/finalizer +conformance suite. Binding: + +1. records the declared base schema version/fingerprint and rejects a binding + whose declared fingerprint does not match the installed ResourceType base + schema; +2. records the Provider's signed `spec.provider` extension schema + (`schemaId`/`schemaVersion`/`settings` JSON Schema) and the aligned + `status.provider` extension schema, both deny-unknown, bounded, and digested; +3. records the Provider's signed **standard capability matrix** — the exact set + of optional base capabilities it does and does not support. + +A bound Provider MUST accept the canonical minimal valid base Spec for the +ResourceType. It MAY reject an optional base capability only when its signed +capability matrix declares that capability unsupported, returning a typed, +provider-neutral `unsupported-capability` result naming the base capability; it +MUST NOT ignore, reinterpret, rename, duplicate, or weaken the bounds of any +base field, and MUST NOT require `spec.provider` data for base-required +behavior. Generic API/CLI/controllers author and read only the base spec and +base status; `spec.provider`/`status.provider` are provider-scoped and never +required for a generic Get/List/Watch/UpdateSpec of the base. + +`UpdateSpec` validates the canonical `spec.provider` envelope +(`{ schemaId, schemaVersion, settings }`) against the installed Provider named +by `spec.providerRef`: an unregistered/version-mismatched `schemaId`/ +`schemaVersion` is rejected with `spec-provider-schema-invalid`, an unknown +field in `settings` is denied, a `settings` that restates/overrides a base field +is rejected with `spec-provider-shadow`, and an over-limit envelope is rejected +with the spec bounds error. The same validation runs at Nix build time. + +### Endpoint resource resolution (D092) + +`Endpoint` is a standard ResourceType and uses the ordinary +`Get`/`List`/`Watch`/`UpdateSpec`/`UpdateStatus`/`Delete`/`Upgrade` verbs and +Role/RoleBinding authorization. Its spec/status never carry a raw locator, so a +`Get` returns only closed class/transport/locality/purpose values and bounded +fingerprints. Resolving an `Endpoint` to a live transport/FD is **not** a public +API verb: Core/ProviderSupervisor performs it privately through the +EffectPort/LaunchTicket path when wiring an authorized consumer Process. A +consumer that is not authorized (by RoleBinding and the Endpoint's +`consumerPolicy`) to resolve an `Endpoint` is denied with `endpoint-resolve-denied` +and receives no locator. A producer restart bumps `status.update`/ +`endpointGeneration`, firing the consumer's `dependency-changed` reconcile. +Endpoint `visibility` is the closed enum `owner | provider | zone`; examples and +schemas use no other token. It is only a coarse ceiling. `consumerPolicy` +provides finer exact consumer bounds, and both visibility and consumer policy +must allow resolution. + +### ResourceExport and ResourceImport (D096) + +`ResourceExport` and `ResourceImport` are standard ResourceTypes and use the +ordinary `Create`/`Get`/`List`/`Watch`/`UpdateSpec`/`UpdateStatus`/`Delete`/ +`Upgrade` verbs plus Role/RoleBinding authorization. Cross-Zone advertisement +and import are still per-hop: native RBAC, the export's consumer-Zone policy, +the ZoneLink relationship, and the export capability ceiling must all allow the +operation. + +For every exportable capability, the installed Provider descriptor supplies a +signed projection factory binding the exact qualified semantic/provider-neutral +`*Service` and `*Binding` types, allowed owner-Service backing ref types, allowed +Binding target ref types, the strict projection-Service schema/fingerprint, and +an aggregate semantic factory fingerprint. Admission fails closed when the +metadata is absent, unsigned, or mismatched. Provider/adapter identity is not +part of the semantic fingerprint: local `providerRef` independently selects the +implementation. Authored owner Services and Bindings may carry that +implementation's strict `spec.provider`; a Core-generated projection never +does. Its route derives from the signed local Provider descriptor, +`providerRef`, and ResourceImport record, and implementation observation may +appear only in `status.provider`. +`*State`, `stateType`, and `allowedStateTargetRefTypes` are not compatibility +aliases; strict schema admission rejects them. + +Core enforces all of these rules before advertisement, lease creation, or local +projection: + +1. `ResourceExport.resourceRef` resolves in the export Zone to the owner + qualified `*Service` declared by the factory. A Device, Endpoint, Binding, or + any other resource is rejected, even if it is a Service backing. +2. `ResourceImport` contains only a local `zoneLinkRef`, bounded `exportKey`, + expected qualified Service type, and expected projection/factory + fingerprints; it contains no remote Ref. +3. The advertised, expected, and locally installed factory values match exactly, + the Provider accepts the canonical minimal base without `spec.provider`, and + `requestedCapabilities` is within the export ceiling. +4. Core creates exactly one same-qualified-type local projection Service with + `ownerRef: ResourceImport/`, `providerRef`, and only semantic + base/import fields. It rejects `spec.provider` and never creates a Device, + Endpoint, or Binding projection. +5. An authored matching Binding is admitted only in the same Zone, with + `serviceRef` targeting that Service and its consuming `Guest`, `User`, or + `Zone` target type allowed by the factory. Binding spec contains desired + intent only; observed realization is written only to status. Binding is + non-exportable and cannot claim remote authority. + +RoleBindings separately authorize export/import mutation, Service use, Binding +creation, and target use. Possessing a local projection-Service Ref does not +grant access to its remote authority or stream; the current lease/capability +check remains mandatory. Leases, ceremonies, sessions, and streams are internal +records rather than API resources. + +Admission also rejects a semantic base schema/status, condition, error, or +fingerprint containing implementation-specific behavior or protocol fields. +The base `providerRef` is the sole opaque implementation selector; PipeWire, +CTAPHID, OTEL, and USBIP details belong only to that selected Provider's +registered strict extension, never the base API. + +### Authority index admission (D097) + +Core keeps a unique **authority index** keyed by `(Zone/scope, authorityClass, +opaqueKeyDigest)` derived from each authority Resource/Process's signed +`AuthorityDescriptor`. On `Create`/`UpdateSpec` of an authority-bearing Resource +(or launch of an authority owner Process), admission consults the index **before +any external effect** and rejects a second claimant for an `exactly-one`/ +`zero-or-one` authority — or one exceeding a `bounded-many` bound — with the +typed `duplicateConflict` error naming the exact incumbent owner digest. The +`authorityKey` is internal and non-authorizing: it is never an authorization +principal and never appears as a locator in spec/status/audit. Authorization for +authority operations still flows through native Role/RoleBinding; the +`resource authorities` read surface (list authorities/holders and any conflict) +requires ordinary `get`/`list` verbs on the owning ResourceType. + +## Native RBAC resources + +### Bootstrap authorization + +Before a reset/empty store has Role/RoleBinding resources, the Zone runtime has +one compiled, non-configurable bootstrap policy: + +- exact subjects: Provider/system-core and Provider/system-minijail; +- exact local ComponentSession purposes/services; +- only store recovery, schema/config publication, initial Host/User/Provider/ + Role/RoleBinding creation, and first Process-controller launch verbs; +- no wildcard Provider/resource/runtime authority; +- no config field can widen it; +- normal stored RBAC governs all non-bootstrap work after publication. + +Every bootstrap action remains structurally validated/audited. A different +subject, remote route, Provider generation, or method fails closed. + +### Role + +Role spec contains bounded rules: + +```yaml +rules: + - resourceTypes: [Process, Volume] + verbs: [get, list, watch, create, update-spec] + sessionVerbs: [] + subresources: [] + resourceNames: [] + zones: [dev] + executionRefs: [Host/host-system] +``` + +Rules use exact values; no implicit wildcard is granted. A reviewed explicit +wildcard may exist only for fixed core-controller roles and remains narrowed by +Zone/Provider/controller structural checks. + +### RoleBinding + +RoleBinding spec contains: + +- `roleRef: Role/`; +- exact subjects as canonical same-Zone refs: Zone, User, Provider, Host, Guest, + or Process; a trusted external-principal selector binds the exact adjacent + enrolled `Zone/*` transport subject for a core-generated relay binding; +- optional authenticated external-principal selector generated by trusted + enrollment/config; +- bounded scope narrowing. + +RoleBinding has no expiry field. Revocation uses normal spec update or deletion. +A request body cannot select/override its subject. + +## Authorization attributes + +Every decision evaluates: + +```text +Zone +subject +ResourceType +subresource/service +verb +resource name +executionRef/domain/userRef scope +Provider/controller generation +``` + +Resource verbs are the exact closed set: + +- get; +- list; +- watch; +- create; +- update-spec; +- update-status; +- update-metadata; +- update-finalizers; +- delete; +- use-credential; and +- admin-credential. + +`use-credential` is valid only for `Credential` rules with one or more exact +operation subresources from the Credential contract. `admin-credential` is +valid only for `Credential` rules with exact `create`, `update-spec`, or +`delete` subresources and is supplemental to the matching ordinary CRUD verb; +it grants no CRUD action by itself. + +Runtime/session verbs are the exact closed set `connect`, `invoke`, +`open-stream`, `relay`, `attach`, `cancel`, `observe`, `audit-export`, and +`support-bundle`. They are mapped through the same engine but are not resource +mutations. `audit-export` binds only +`d2b.audit.v3.AuditService/Export`; `support-bundle` binds only +`d2b.support.v3.SupportService/GenerateBundle`. Both are admin-only, +session-only grants and imply no `get`, `list`, or other resource authority. +`relay` permits only an already-authenticated ZoneLink/transport subject to +forward an already-admitted invocation or stream to one route-selected next +hop. It grants no resource CRUD, identity mapping, capability widening, +attachment, credential, or local lifecycle authority. + +Every forwarding hop evaluates two independent permissions: `relay` for the +authenticated adjacent-Zone transport subject and the forwarded operation's +target verb under the exact local Role/RoleBinding scope. Named methods carry +one immutable resource name. Nameless `List` and `Watch` carry no synthetic +name: their exact ResourceType, non-empty authorized `resourceNames` allowlist, +and bounded filters are evaluated as a set, and every hop preserves those +filters byte-for-byte. A filter that could select a name outside the local +intersection is denied rather than widened. The final target evaluates the +same target verb and selector again. Neither permission implies the other; a +missing grant or unavailable policy state fails closed. Relay-bearing +Roles/Bindings are core-generated and ZoneLink-scoped by default. Admission +rejects wildcard, self-asserted, Provider-authored, or ordinary +operator-authored relay grants unless an already-authorized local administrator +explicitly permits the exact bounded grant through durable admin policy. + +Native RBAC allow is necessary but not sufficient. Core structural checks also +enforce: + +- correct Zone/session/route; +- installed Provider/API binding; +- ResourceType/controller/status-owner match; +- ownerRef/UID/ref integrity; +- Host/Guest executionRef/domain/user placement; +- generation/revision; +- budget/quota/cardinality; +- process/sandbox/resource policy; +- broker/FD/locality constraints. + +Structural checks may narrow an allow and never override a deny. + +## Status ownership + +Each ResourceType schema identifies: + +- spec writer roles/controllers; +- status owner Provider/controller; +- finalizer owners; +- fields core alone sets. + +Only the current exact controller lease/generation may update status. Status +updates carry expected revision and observedGeneration. A Host/Guest/link/ +controller disconnect cannot write success; status becomes Unknown through the +authorized observer/core rule. + +Status is the default durable observation and recovery surface for bounded +non-secret operational state (D087) and has a frozen three-layer shape (D088): +the universal `ResourceStatus` base, the ResourceType-common `status.resource` +object, and an optional Provider-specific `status.provider` extension. The +owning controller writes all present layers in one `UpdateStatus` mutation with +a single expected revision; the layers never diverge across separate writes. + +`UpdateStatus` is bounded and validated per layer: the resource store rejects a +status replacement whose total canonical serialized size exceeds 64 KiB, whose +`status.resource` typed detail exceeds 32 KiB, whose `status.provider.details` +exceeds 32 KiB, or whose condition/list/map cardinality exceeds the frozen +limits (see `ADR-046-resource-object-model` § Status bounds) with a typed +`status-oversize` error. A `status.provider` whose `providerRef`/`schemaId`/ +`schemaVersion` is not registered for the installed Provider, or whose `details` +carries an unknown field, is rejected with `status-provider-schema-invalid`; the +write changes nothing and the caller re-reads and retries. A `status.provider` +that restates, overrides, or duplicates a universal or `status.resource` field +is rejected with `status-provider-overlap`. + +Generic API/CLI/controllers request and depend on a **base-only projection** +(the universal base plus `status.resource`); the optional `status.provider` +extension is ignored by base-only consumers and never required for a Watch or +Get to succeed. Status never carries secrets, authority-conferring handles, +private path/argv/environment/PID/unit data, terminal/clipboard/CTAP bytes, raw +cloud error bodies, or high-frequency streams; those stay in their owning +surfaces. Controllers write status only on a material change in observed state, +and never treat status as a host-mutation or repair authority. + +## OwnerRef authorization + +Create/reparent requires: + +- permission on the child; +- get/use permission on the owner; +- scope compatibility; +- no cycle/depth violation; +- permission for both old/new owners on reparent. + +A child mutation's owner hint is generated by core after commit and cannot be +suppressed by the child. + +## Authorization cache + +Positive decisions may be cached only under: + +- exact subject; +- exact authorization attributes; +- Role/RoleBinding/Provider/API/Zone policy revisions; +- short expiry. + +Relevant resource revisions invalidate caches immediately after durable commit. +Denials may be cached briefly but never become allows. + +If authorization/store state is unavailable: + +- no new resource/runtime operation is admitted; +- admitted bounded operations retain their original context until deadline; +- long-lived streams require short authorization leases and close on expiry; +- local emergency disable remains available through the fixed out-of-band + safety path. + +## Parent/child Zone access + +A parent: + +- authenticates over a ZoneLink ComponentSession; +- maps to a child-local subject/RoleBinding; +- when it forwards again, requires both `relay` and the requested target verb + for that authenticated adjacent-Zone subject; +- calls the child d2b.resource.v3 service; +- receives only child-authorized data/status. + +The child commits to its own store. The parent receives no database handle, +credential, token, or cross-Zone ResourceRef. + +A disconnected child may record a bounded outbound intent in its child-local +ZoneLink but cannot claim the parent resource changed. On reconnect the parent +reauthorizes and applies/rejects against current revision. + +## Limits + +The API spec freezes bounds for: + +- request/response/batch bytes; +- batch mutation/resource count; +- ResourceType/name/ref depth/length; +- list page/filter count; +- watch count/filter complexity/rate/credit; +- Role rules/bindings/subjects; +- conditions/status/error strings; +- finalizers/owner depth; +- concurrent reads/writes per principal/controller; +- deadlines and retry-after. + +Over-limit input is rejected before redb mutation or Provider invocation. + +## Errors + +Stable classes include: + +- resource-not-found; +- resource-already-exists; +- resource-conflict; +- resource-schema-invalid; +- resource-ref-invalid; +- resource-owner-cycle; +- resource-owner-depth; +- resource-finalizer-denied; +- resource-provider-unavailable; +- resource-controller-mismatch; +- resource-status-owner-mismatch; +- status-oversize; +- status-provider-schema-invalid; +- status-provider-overlap; +- spec-provider-schema-invalid; +- spec-provider-shadow; +- unsupported-capability; +- expedited-not-authorized; +- expedited-quota-exceeded; +- expedited-reconcile-pending; +- upgrade-required; +- endpoint-resolve-denied; +- relay-denied; +- role-relay-grant-restricted; +- authorization-denied; +- revision-expired; +- backpressure; +- timeout; +- cancelled; +- resource-plane-unavailable; +- internal-integrity-failure. + +Error messages are bounded/redacted. Conflict returns current revision but does +not return an unauthorized resource body. + +## Audit + +Audit records: + +- subject/Zone; +- ResourceType/name or bounded digest; +- verb/subresource; +- expected/current/result revision; +- authorization decision and policy revisions; +- operation/correlation; +- fixed outcome/error/retry class. + +It excludes spec/status payloads, Provider diagnostics, host paths, credentials, +process data, and terminal bytes. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | Public daemon/broker seqpacket auth; `d2b-daemon-access` admission types; `d2b-realm-router` principal/capability/idempotency checks; Realm access resolver; strict DTOs | +| Evidence class | Local daemon auth is reachable; daemon-access/Realm peer abstractions are partly unwired; native RBAC/API are ADR-only | +| Behavior retained | SO_PEERCRED/local identity, typed denials, positive capabilities, no relay-to-local auth, strict bounds/unknown-field rejection | +| Required delta | Entire resource API, Provider API schemas/bindings, Role/RoleBinding engine, status ownership, parent resource routing | +| Reuse path | Extract exact admission/error/id/ref validators and router authorization derivation | +| Replacement/deletion | Old public wire remains until CLI/controllers consume new services | +| Feasibility proof | Multi-process local/vsock/Zone resource calls, immediate revocation, conflict/no-leak tests | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-api-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0; resource API integrator | +| Current source | `packages/d2b-contracts/src/public_wire.rs`, `broker_wire.rs`; `d2b-daemon-access/src/lib.rs`; `d2b-realm-router/src/lib.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/proto/d2b-resource-v3.proto`, `packages/d2b-resource-api/src/service.rs`, `client.rs` | +| Detailed design | Async methods, contexts, preconditions, limits, errors, status/finalizer separation, batch API Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | d2b-bus exact service → Zone auth → redb actor | +| Data migration | None; v3 clean break | +| Validation | Protocol vectors; malformed/oversize/conflict/status-owner tests | +| Removal proof | Old command/resource-equivalent paths removed only per integration wave | + +### ADR046-api-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-api-001; authorization integrator | +| Current source | `d2bd` public admission; `d2b-daemon-access` policy evidence; `d2b-realm-core/src/access.rs`, `audit.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-resource-api/src/authz.rs`, `packages/d2b-core-controller/src/rbac.rs` | +| Detailed design | Role/RoleBinding schemas/evaluator/cache/revision invalidation, canonical resource/session verb enums including ZoneLink-scoped `relay`, ComponentSession subject mapping, parent Zone access, and independent per-hop relay plus target-verb admission | +| Integration | Every resource/runtime method invokes one native evaluator before structural checks | +| Data migration | Generate initial Roles/Bindings from Nix v3 config | +| Validation | Decision matrix/property tests; closed-enum and relay-origin/scope rejection; relay-missing and target-verb-missing fail-closed vectors; revocation/cache/outage/parent-child tests | +| Removal proof | Legacy auth remains until every v3 route is covered | diff --git a/docs/specs/ADR-046-resource-object-model.md b/docs/specs/ADR-046-resource-object-model.md new file mode 100644 index 000000000..8bfa95375 --- /dev/null +++ b/docs/specs/ADR-046-resource-object-model.md @@ -0,0 +1,741 @@ +# ADR 0046 resource object model + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-resource-object-model` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-contracts`, Zone resource API/store | +| Depends on | `ADR-046-decision-register`, `ADR-046-terminology-and-identities` | +| Supersedes | None | + +## Universal envelope + +Every resource has: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Host +metadata: + name: host-system + zone: dev + uid: + generation: 1 + revision: + ownerRef: null + finalizers: [] + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:00:00Z +spec: {} +status: + observedGeneration: 0 + phase: Pending + conditions: [] + lastReconciledAt: null + startedAt: null + completedAt: null + outcome: null +``` + +`metadata`, `spec`, and `status` are always present. A type with no desired +fields uses `spec: {}`. + +## Metadata + +| Field | Rules | +| --- | --- | +| `name` | Required ResourceName; unique for the bound ResourceType in this Zone | +| `zone` | Required plain Zone name; must equal the store's `Zone/` self resource | +| `uid` | Immutable, generated at create, never caller-selected | +| `generation` | Starts at 1; increments only when spec changes | +| `revision` | Opaque Zone-local latest-commit/watch token | +| `ownerRef` | Optional singular canonical same-Zone ResourceRef | +| `ownerUid` | Internal store binding, not caller-writable/portable JSON input | +| `finalizers` | Bounded unique typed finalizer IDs owned by installed controllers | +| `deletionRequestedAt` | Core-set RFC 3339 UTC timestamp; null before delete | +| `createdAt` | Core-set RFC 3339 UTC timestamp | +| `updatedAt` | Core-set RFC 3339 UTC timestamp on every mutation | + +Internal non-user-writable management metadata also records: + +- `managedBy = configuration|controller|api`; +- optional `configurationGeneration` for Nix-owned roots; +- controller/Provider generation for dynamic children. + +These fields are authority inputs set by the configuration/resource service, +not labels/annotations or caller-selected spec values. + +`api` is assigned to resources created directly by an authorized API client; +they persist until explicit API deletion and are never swept by configuration +generation cleanup. + +Labels/annotations are optional bounded presentation metadata. They never +select authorization, provider/controller ownership, path, process identity, +or implicit relationships. A ResourceType may declare a closed set of indexed +exact-match metadata fields. + +## Spec + +Spec is the desired state. Rules: + +- strict schema with unknown fields denied unless the signed ResourceType + explicitly declares a bounded vendor extension object; +- deterministic defaults applied before storage; +- canonical JSON representation and digest; +- all references use canonical ResourceRef values; +- no secrets, raw credentials, or authority decisions; +- provider-specific settings are schema-bound to the exact installed Provider; +- spec mutation requires expected revision; +- full replacement is the default; no field-level server-side apply or silent + merge; +- a successful spec replacement increments generation and revision. + +### Three-layer spec shape (D089) + +`spec` mirrors the D088 three-layer status shape with symmetric frozen names. +Generic API/CLI/controllers author and reconcile only Layers 1 and 2; the +Layer-3 Provider extension builds on — never shadows, overrides, renames, or +duplicates — the fields below it. + +| Layer | Location | Owner | Consumers | +| --- | --- | --- | --- | +| 1. Universal Resource envelope/metadata | `apiVersion`, `type`, `metadata`, `spec` presence rules | every resource | all generic tooling | +| 2. ResourceType base spec | top-level `spec.*` (incl. `spec.providerRef`) | the ResourceType schema (provider-neutral) | all implementations + generic tooling | +| 3. Provider-specific extension | optional `spec.provider` | the selected Provider (signed schema) | that Provider's implementation only | + +```yaml +spec: + # Layer 2 — ResourceType base spec (provider-neutral; includes providerRef) + providerRef: Provider/ + # ...exact typed base fields frozen by the ResourceType schema... + # Layer 3 — optional canonical selected-Provider extension envelope + provider: + schemaId: .d2bus.org//spec + schemaVersion: "1.0" + settings: + # strict, bounded, versioned, deny-unknown implementation-only desired settings + ... +``` + +`spec.provider = { schemaId, schemaVersion, settings }` is the one canonical +provider-extension envelope. It replaces the former Host/Guest/Credential +`providerSettings`, Device `settings`, and every ad hoc per-ResourceType +extension. It is symmetric to `status.provider`: `spec.provider.settings` (the +desired implementation settings) mirrors `status.provider.details` (the observed +implementation state), and `spec.provider` omits `providerRef` and +`observedProviderGeneration` because `spec.providerRef` is a base field and spec +is desired, not observed. The selected `spec.provider` schema and the +`status.provider` schema align for the same Provider. + +**Base-schema conformance.** Every Provider `ResourceApiBinding` declares and +MUST implement the exact ResourceType base spec schema version/fingerprint and +pass the base lifecycle/status/error/finalizer/conformance suite. It MUST accept +the canonical minimal valid base Spec. It MAY reject an optional base capability +only via its signed standard capability matrix and a typed, provider-neutral +`unsupported-capability` result; it MUST NOT ignore, reinterpret, rename, +duplicate, or weaken the bounds of any base field, and MUST NOT require +extension data for base-required behavior. + +**Provider extension.** `spec.provider` is optional unless the selected +Provider's signed schema marks it required. Its `settings` object is strict +deny-unknown, size/cardinality bounded, versioned/digested, and validated +against `spec.providerRef` at Nix build and at API admission. It carries only +implementation-only desired settings and MUST NOT shadow, restate, or override a +universal or ResourceType-base field. Any field or semantic shared across two or +more implementations is promoted to the ResourceType base and never carried in +`spec.provider`. + +**Generic tooling.** Generic CLI and controllers operate on the base spec (and +base status); they never author or require `spec.provider`. Qualified +Provider-defined ResourceTypes carry the universal envelope, their own type-base +spec, and an optional implementation extension under the same `spec.provider` +envelope. + +**Provider self-description exception.** The `Provider` resource's spec remains +`{ artifactId, config }` (D075) — the one documented exception to the +`providerRef`-plus-`spec.provider` shape, because a `Provider` cannot carry a +non-circular `spec.providerRef` to itself. + +**Update policy (D091).** The base spec carries a provider-neutral +`spec.updatePolicy` (universal or ResourceType base): disruptive upgrades default +to `manual` (the controller reports `UpgradeRequired` and waits for an explicit +`resource upgrade ... --apply`), while automatic application of non-disruptive +changes MAY be permitted. A `spec.provider` extension MAY add implementation +knobs but MUST NOT bypass or weaken the base disruption policy. + +## Status + +Status is observed state and is a separately authorized subresource. It is the +**default durable observation and recovery surface** for a resource's bounded +non-secret operational state (D087): reconcile stage, opaque non-authorizing +external handles/IDs/digests, adoption observations after restart, bounded +counters, closed-enum detail, dependency-readiness observations, and last +successful checkpoints. Status is revisioned, optimistic-status-writer +controlled (every status write carries an expected revision), RBAC-readable, +redacted, and reverified against external reality by the owning controller +after a restart. The spec remains the desired-state authority; status is +observation only and is never a host-mutation or repair authority. + +A controller keeps bounded non-secret operational state in `status` whenever +possible. Separate durable state storage (a Provider-owned Volume, see +`ADR-046-provider-state`) is used only when a payload is a secret or sensitive +private datum, is large/binary/file content, or is otherwise unsuitable for the +revisioned status API. + +### Three-layer status shape (D088) + +Every resource's `status` has a frozen three-layer shape. Generic API, CLI, and +controllers depend only on Layers 1 and 2; the Layer-3 Provider extension builds +on — never replaces, overrides, or duplicates — the fields below it. + +| Layer | Location | Owner | Consumers | +| --- | --- | --- | --- | +| 1. Universal `ResourceStatus` base | top-level `status.*` common fields | every resource | all generic tooling | +| 2. ResourceType-common | `status.resource` | the ResourceType schema (provider-neutral) | all implementations + cross-resource/provider consumers | +| 3. Provider-specific extension | optional `status.provider` | the installed Provider (signed schema) | that Provider's own tooling only | + +```yaml +status: + # Layer 1 — universal ResourceStatus base (present on every resource) + observedGeneration: 4 + phase: Ready + conditions: [ ... ] + lastReconciledAt: 2026-07-23T00:00:01Z + startedAt: 2026-07-23T00:00:00Z + completedAt: null + outcome: null + update: # universal currency object (D091) + state: Current + reasons: [] + observedGeneration: 4 + targetGeneration: 4 + disruption: None + preserveState: true + operationId: null + lastAssessedAt: 2026-07-23T00:00:01Z + owned: { count: 0, refs: [] } + dependencies: { count: 0, refs: [] } + # Layer 2 — ResourceType-common, provider-neutral (required across all implementations) + resource: + # exact typed fields frozen by the ResourceType schema + ... + # Layer 3 — optional Provider-specific extension + provider: + providerRef: Provider/ + schemaId: .d2bus.org//status + schemaVersion: "1.0" + observedProviderGeneration: 7 + details: + # strict, bounded, redacted, unknown-field-denied implementation observation + ... +``` + +### Layer 1: universal ResourceStatus base + +The universal base is present on every resource at `status` top level: + +| Field | Rules | +| --- | --- | +| `observedGeneration` | Numeric spec generation accounted for by this status | +| `phase` | `Pending`, `Ready`, `Succeeded`, `Degraded`, `Failed`, `Deleted`, or `Unknown` | +| `conditions` | Bounded latest condition set keyed by condition type | +| `lastReconciledAt` | RFC 3339 UTC; set on completed reconcile attempt | +| `startedAt` | Optional RFC 3339 UTC; resource/effect start | +| `completedAt` | Optional RFC 3339 UTC; terminal completion | +| `outcome` | Optional latest bounded outcome | +| `update` | Universal provider-neutral currency object (D091); present on every resource | + +### Universal currency object `status.update` (D091) + +Every resource carries a bounded, provider-neutral `status.update` object in the +universal base. Because it is universal (or, for a ResourceType-specific +refinement, `status.resource`), its shared fields are never duplicated in a +`status.provider` extension (D088): + +| Field | Rules | +| --- | --- | +| `state` | `Current`, `UpdateAvailable`, `UpgradeRequired`, `Upgrading`, `Blocked`, or `Unknown` | +| `reasons` | Bounded list from the closed enum `CoreGenerationChanged`, `ProviderGenerationChanged`, `ArtifactChanged`, `ImageOrSystemGenerationChanged`, `SpecChanged`, `DependencyChanged`, `SecurityPolicyChanged` | +| `observedGeneration` / `targetGeneration` | Numeric; and/or bounded non-secret observed/target digest IDs | +| `disruption` | `None`, `Reload`, `Restart`, `Recycle`, or `Replace` | +| `preserveState` | Bool: whether the planned upgrade preserves durable/state/secret Volumes and identity | +| `operationId` | Optional in-flight upgrade Operation id (core Operation ledger) | +| `lastAssessedAt` | RFC 3339 UTC of the last `assess_update` | +| `owned` | `{ count, refs }` bounded/truncated aggregate currency of owned resources | +| `dependencies` | `{ count, refs }` bounded/truncated aggregate currency of dependencies | + +A controller assesses currency via `assess_update` on core/Provider-generation, +artifact/image/NixOS-generation, immutable-spec, dependency, or +security-policy change (D091). It MUST report `UpgradeRequired` rather than apply +a disruptive in-place change; non-disruptive changes reconcile normally. Core +aggregates self/owned/dependency currency for list/get. The latest bounded +upgrade plan/result lives here; the core Operation ledger owns +operation/idempotency/progress (never a second ledger in status). + +Condition: + +```yaml +type: Ready +status: "True" # True | False | Unknown +reason: process-ready +message: bounded redacted operator detail +observedGeneration: 3 +lastTransitionAt: 2026-07-22T00:00:01Z +``` + +Outcome: + +```yaml +code: process-exited +exitCode: 1 +message: bounded redacted error detail +retryable: true +retryAfter: 5s +occurredAt: 2026-07-22T00:00:01Z +``` + +`code` and `reason` are stable lower-kebab-case machine values. `message` may +contain actionable Provider detail but is bounded, UTF-8/control-character +validated, and must not contain secrets, tokens, credential material, terminal +bytes, argv/environment, state contents, or host/provider paths. + +### Layer 2: ResourceType-common status (`status.resource`) + +`status.resource` is the provider-neutral, typed object frozen by the +ResourceType schema. It holds the observation fields that every implementation +of that ResourceType and every cross-resource/provider consumer must be able to +read — for example Guest runtime readiness/capabilities, the Device claim base, +Credential lease metadata, or the Volume attachment base. It extends the +universal base (Layer 1) and never restates it. Generic API/CLI/controllers +depend only on the universal base plus `status.resource`. Any field shared +across two or more implementations MUST be promoted here and MUST NOT be copied +into individual Provider extensions. + +### Layer 3: Provider-specific extension (`status.provider`) + +`status.provider` is optional and carries implementation-only observation that +is not shared across implementations: + +| Field | Rules | +| --- | --- | +| `providerRef` | `Provider/` of the writing Provider | +| `schemaId` | Qualified, immutable status-extension schema ID (per the D080 grammar, e.g. `.d2bus.org//status`) | +| `schemaVersion` | Semver `MAJOR.MINOR` of the extension schema | +| `observedProviderGeneration` | Numeric `Provider/` resource generation this observation reflects | +| `details` | Strict typed object: unknown-field-denied, size/cardinality bounded, redacted/non-secret | + +The `status.provider.details` schema is signed into and registered with the +Provider package (see `ADR-046-provider-model-and-packaging`). A `status.provider` +whose `schemaId`/`schemaVersion` is not registered for the installed Provider, or +whose `details` carries an unknown field, is rejected. `status.provider` builds +on Layers 1 and 2 and MUST NOT replace, override, or duplicate any universal or +`status.resource` field. + +**Atomic layered write.** The owning controller writes all present layers +(universal base, `status.resource`, and any `status.provider`) in one status +mutation with a single expected revision; the layers never diverge across +separate writes. + +**Status-first state mapping (D087 + D088).** State shared across +implementations goes in `status.resource`; implementation-specific bounded +non-secret observation goes in `status.provider.details`; secret, large, or +private state goes in an optional Volume (`ADR-046-provider-state`), never in +status. + +ResourceType schemas add typed `status.resource` fields and register any +`status.provider` extension; they do not replace the universal base. + +### Status bounds + +Status is a bounded observation surface, not a stream. The resource store +enforces the following caps on every status subresource write and rejects an +over-limit write with a typed `status-oversize` error (the write changes +nothing and the caller may re-read and retry with a smaller status): + +| Bound | Limit | +| --- | --- | +| Total canonical serialized status object (all three layers) | 64 KiB | +| `status.resource` ResourceType-common typed detail | 32 KiB | +| `status.provider.details` Provider-specific extension | 32 KiB | +| `conditions` entries | 32 | +| Any status list or map field | 64 entries | +| Any single bounded status string (`message`, opaque handle, digest) | 4 KiB | + +A controller writes status only on a **material change** in observed state. +Status never carries high-frequency byte streams, logs, metrics, command +output, or ring buffers; those stay in their owning surfaces (OTEL for +metrics/traces, the authoritative audit stream for security history, owning +process memory for content streams). Watches, revision compaction, and +backpressure remain bounded per `ADR-046-resource-store-redb`. + +### Status prohibitions + +Status MUST NOT contain secrets, raw tokens/keys/PSKs, any credential source +handle that confers authority, private endpoint/path/argv/environment/PID/unit +data, terminal/clipboard/CTAP bytes, raw cloud error bodies, large binary blobs, +unbounded collections, or any content whose churn would bloat revision history. +An opaque handle may appear in status only if it is bounded, non-secret, +non-authorizing, safe for authorized API readers, and independently revalidated +by the owning controller against external reality. + +Status retains only the latest conditions/outcome. Prior status versions remain +in revision_log until compaction. + +### Phase use + +- `Pending`: desired state exists but is not yet ready/terminal. +- `Ready`: long-lived resource is healthy and current. +- `Succeeded`: one-shot/finite desired work completed successfully. +- `Degraded`: usable but one or more declared conditions are impaired. +- `Failed`: current desired generation cannot complete under the current + retry/terminal policy. +- `Deleted`: final status event after finalizers, immediately before row/index + removal. +- `Unknown`: owning controller/Host/Guest/link cannot currently prove state. + +Starting, reconciling, retrying, draining, and deleting are condition/reason +details, not additional common phases. + +## Ownership and child-triggered reconciliation + +Each resource has zero or one ownerRef. Create/reparent: + +1. resolves canonical owner type/name in the same Zone; +2. stores owner UID binding; +3. rejects self/cycles/excessive depth; +4. updates owner_index atomically with the resource and revision event. + +Every committed child spec/status/metadata/finalizer/delete mutation produces a +coalesced `owned-resource-changed` hint for the current owner. The owner +controller relists owner_index, compares children with desired state, creates +missing children, corrects drift through expected-revision writes, and removes +no-longer-desired children under finalizer policy. + +Child status updates still trigger the owner even when a controller's own +status-only event would otherwise be suppressed. + +## Generation and revision + +- create: generation 1, one Zone revision; +- status-only/metadata/finalizer update: generation unchanged, revision changes; +- spec update: generation increments exactly once, revision changes; +- a multi-resource mutation batch receives one Zone revision and ordered + per-resource ordinals; +- stale expected revision changes nothing and returns conflict/current revision. + +## Deletion + +1. Authorized delete sets deletionRequestedAt and emits a revision. +2. Controllers complete exact finalizers child-first. +3. Final transaction emits a `phase=Deleted` change event and removes the + resource plus indexes immediately. +4. GET returns not found. revision_log is the only deletion history. + +No retained resource tombstone exists. + +### Removed Nix configuration + +After a newly validated Zone configuration generation activates, core diffs its +canonical configured resource set against the prior active set. Every prior +`managedBy=configuration` resource omitted from the new set receives normal +asynchronous Delete: + +- activation succeeds without waiting for cleanup; +- generation status becomes Degraded/pending-cleanup while removals remain; +- owner children/finalizers complete through normal reconciliation; +- controller-created resources are never swept merely because absent from Nix; +- prior generation remains retained until cleanup/rollback policy permits + pruning; +- failures are visible/audited and never reported as deleted. + +## Minimal standard ResourceType catalog + +Core control: + +- Zone; +- ZoneLink; +- Provider; +- Role; +- RoleBinding. +- Quota; +- EmergencyPolicy. + +Standard execution/shared: + +- Host; +- Guest; +- Process; +- EphemeralProcess; +- Volume; +- Network; +- Device; +- User; +- Credential; +- Endpoint. + +Cross-Zone sharing (D096): + +- ResourceExport; +- ResourceImport. + +There are 19 standard ResourceTypes (Endpoint added by D092; ResourceExport and +ResourceImport added by D096). Qualified semantic ResourceTypes and other +Provider-bound extension types may extend the set through signed schemas/API +bindings. They use this same envelope/status/ownership contract. + +### Qualified Service/Binding pairs for cross-Zone capabilities (D096) + +`Service` and `Binding` are suffixes for **qualified semantic, provider-neutral +ResourceTypes**, not Provider/implementation namespaces and not additional +standard ResourceTypes. Every exportable semantic capability declares exactly +one `*Service` type and matching `*Binding` type in the shared API contract. +Provider descriptors bind implementations to those types; Providers do not +rename or vendor-qualify them. The frozen initial semantic pairs are: + +| Semantic capability | Service type | Binding type | Initial Provider implementation | +| --- | --- | --- | --- | +| audio | `audio.d2bus.org.AudioService` | `audio.d2bus.org.AudioBinding` | `Provider/audio-pipewire` | +| security key | `security-key.d2bus.org.SecurityKeyService` | `security-key.d2bus.org.SecurityKeyBinding` | `Provider/device-security-key` | +| telemetry | `telemetry.d2bus.org.TelemetryService` | `telemetry.d2bus.org.TelemetryBinding` | `Provider/observability-otel` | +| USB | `usb.d2bus.org.UsbService` | `usb.d2bus.org.UsbBinding` | `Provider/device-usbip` (policy-gated) | + +The cross-cutting contract fixes their roles: + +- the owner-Zone `*Service` is the one real authority. Its Provider schema may + reference only same-Zone backing `Device`, `Endpoint`, or qualified semantic + backend resources allowed by the signed projection factory; +- `ResourceExport.resourceRef` MUST reference that owner `*Service`. A + `Device`, `Endpoint`, `*Binding`, or other backing is never an export target; +- one `ResourceImport` causes core to create exactly one same-qualified-type + projection `*Service` in the consumer Zone with + `metadata.ownerRef: ResourceImport/`. Core never projects a `Device`, + `Endpoint`, or `*Binding`; +- an operator or Nix configuration authors one or more same-Zone matching + `*Binding` resources. A Binding is desired consumer intent only: it references + the local `serviceRef` and an allowed consuming `Guest`, `User`, or `Zone` + target ref. All observations belong only in resource `status`. The Provider + controller owns any resulting `Process` and `Endpoint` children; +- the import controller never creates, owns, or deletes `*Binding`. Session, + stream, ceremony, transfer, and lease records that churn per connection stay + internal opaque records under the owning controller. + +Consequently the standard catalog remains exactly 19 types. A qualified +`*Service`/`*Binding` pair is admitted only when its installed Provider supplies +matching signed projection-factory metadata; absence or any type/schema/ +fingerprint mismatch fails closed. There is no `*State` compatibility spelling +or alias: a Provider type ending in `State` does not satisfy this pattern, and +the obsolete `stateType`/`allowedStateTargetRefTypes` metadata keys are unknown +fields rejected by strict admission. + +Every semantic Service and Binding base spec/status is implementation-independent +and D089-conformant. `providerRef` selects an installed implementation and the +optional strict `spec.provider` envelope carries only implementation-specific +settings. The canonical minimal base spec MUST work without `spec.provider`. +Export/import preserves the semantic Service ResourceType exactly; it never +rewrites it to an implementation namespace. PipeWire, OTEL, USBIP, CTAPHID, and +other implementation/protocol details are forbidden in semantic base behavior, +conditions, errors, fingerprints, and status. The base `providerRef` is the one +opaque implementation selector; it does not change the semantic type or base +contract. + +## Entity promotion test and opaque-ID classification (D092) + +A stable managed identity MUST be a proper ResourceType (not an opaque `*Id`/ +`Handle`) when **any** of these holds: + +- it is stable across a reconcile pass or restart; +- it is independently lifecycle/readiness/finalizer/lease managed; +- it is referenced by another resource, controller, or Provider; +- authorization/policy/audit applies to it independently; +- it is visible in resource status or CLI; +- the ownership/dependency graph needs it. + +An opaque ID or handle is permitted **only** when it is confined to one +controller/process, not API/status-visible as a locator, has no independent +lifecycle/authorization, and/or is per-connection/high-churn. Every retained +public `*Id`/`Handle` documents its rationale. The frozen permitted-opaque set: + +| Opaque handle | Why it is not a resource | +| --- | --- | +| pidfd | Process-local, non-persistent, never crosses the bus (D022) | +| fd index / inherited-fd slot | Per-launch LaunchTicket-local; not API/status-visible | +| named stream id (per ComponentSession) | Per-session/high-churn; internal to one session | +| transport byte-stream handle (`OwnedTransport`) | Per-session carriage handle held in one process (D081) | +| reconcile operation token / `operationId` | In-flight, single-controller, core Operation ledger owned | +| content/schema digest | A value, not a managed entity | + +Stable cross-boundary endpoint identities do **not** qualify as opaque and are +the `Endpoint` ResourceType. + +## Authority and cardinality (D097) + +Every scarce or singleton backing — a physical device, a singleton external +service, a per-Zone runtime service, a per-Host/user/seat service, a fixed +listener/store, or a globally-unique policy — is governed by a signed +**`AuthorityDescriptor`**. It is a provider-neutral declaration attached to the +owning typed Resource and its owning Provider (not a new opaque public ID, and +**not** a new ResourceType unless an audit proves no existing type can own the +lifecycle). Exactly one authority owns/opens each backing; duplicate opens or +effects are rejected before they happen. + +**`AuthorityDescriptor` schema:** + +| Field | Type | Notes | +| --- | --- | --- | +| `authorityScope` | enum | `global\|zone\|host\|guest\|user\|seat\|physical-device\|external-service` | +| `authorityKey` | opaque class | Canonical opaque key **class** (a digest input); never a raw path/serial/address; internal and non-authorizing (never a locator or authz principal) | +| `cardinality` | enum + bound | `exactly-one\|zero-or-one\|bounded-many` (with numeric bound for bounded-many) | +| `arbitration` | enum | `exclusive\|shared\|multiplexed\|partitioned` | +| `authorityRef` | ResourceRef | The single authority Resource; for D096 this is the qualified owner `*Service`, whose controller owns the backing-opening Process/Endpoint children | +| `duplicateConflict` | error class | Typed, deterministic conflict returned to a second claimant | +| `ownerProof` | opaque | Adoption/restart owner proof by process/resource identity | +| `updateStrategy` | object | Drain-then-recycle policy (D091) | +| `exportability` | enum | `forbidden\|explicit-export` (cross-Zone only via D096) | +| `quota` | object | Quota/fairness policy for shared/multiplexed/partitioned arbitration | + +**Core authority index.** Core maintains an authority index and rejects a +conflicting authority Resource or Process **before any external effect**. The +index key is scoped to the level at which a collision can actually occur: +`zone`-scoped authorities are keyed by `(Zone, authorityClass, opaqueKeyDigest)`, +but `host`, `physical-device`, `seat`, and `external-service` authorities — where +**two Zones on the same host can collide over one physical/kernel backing** (a +GPU, physical TPM, USB device, `/dev/kvm`, a passthrough NIC, a globally-unique +vsock CID, or a fixed listener port) — are keyed **Host-global** by +`(Host, authorityClass, opaqueKeyDigest)`, not merely Zone-local. A Host-global +authority admits exactly one owner across **all** Zones on that host; a second +Zone claiming the same physical backing is a `duplicateConflict`. `authorityKey` +is internal; the stable authority itself is the existing typed Resource +(`Device`/`Network`/`Provider`/`Endpoint`/`Host`/`Guest`/`User`/`Zone`/…). One +authority service opens/owns the physical/singleton backing; consumers get local +Service projections/leases/streams, and cross-Zone sharing is only via D096 +ResourceExport/ResourceImport targeting the qualified owner Service (a shared/ +multiplexed backing still has exactly one authority owner; no duplicate opens). + +**Shared physical USB backing.** After trusted physical-USB identity resolution, +Core derives one non-reversible `opaqueKeyDigest` using the +`physical-usb-backing/v1` domain and indexes the exact Host-global tuple +`(Host, physical-usb-backing, opaqueKeyDigest)`. Every USB or security-key +Provider backed by that device must acquire this tuple before any open, +withhold, bind, module, relay, or attachment effect. A Provider-supplied label, +selector digest, service-specific authority, or transport authority cannot +replace it. The shared deterministic conflict is +`physical-usb-backing-conflict`; no raw identity or digest enters public spec or +status. + +**Status/spec base.** The universal status base exposes bounded provider-neutral +authority state — `available`, current holder count, queue depth, arbitration, +and update currency — with **no raw identity**; provider extension carries +implementation detail only. The spec base (or provider capability) declares the +requested share mode and **cannot bypass** the descriptor. + +**Lifecycle.** Duplicate config/API requests produce a deterministic conflict +with no second effect; config activation goes `Degraded` naming the exact owner +digest. Restart adopts the exact authority by process/resource identity; +ambiguity quarantines. D091 upgrade drains consumers then recycles the authority; +reset preserves or destroys the underlying state per an explicit per-authority +disposition. + +### Standard ResourceType authority classification + +Provider-neutral classification for the 19 standard ResourceTypes (per-Provider +qualified authorities are classified in each Provider dossier; conservative +existing-behavior defaults apply until refined by evidence). "Exportability" +means cross-Zone sharing via D096. + +| ResourceType | Default `authorityScope` | Default `cardinality` | Typical `arbitration` | Exportability | +| --- | --- | --- | --- | --- | +| `Zone` | zone | exactly-one (self) | exclusive | forbidden (singleton, not cross-Zone) | +| `ZoneLink` | child zone (local uplink edge) | zero-or-one per edge | exclusive | forbidden | +| `Provider` | zone | bounded-many (per controller cardinality) | partitioned | forbidden | +| `Role` / `RoleBinding` | zone | bounded-many | n/a (policy) | forbidden | +| `Quota` | zone/scope | exactly-one per scope | exclusive (scope uniqueness) | forbidden | +| `EmergencyPolicy` | zone/scope | exactly-one per scope | exclusive (scope uniqueness) | forbidden | +| `Host` | host | exactly-one substrate authority | exclusive (allocator/effect) | forbidden | +| `Guest` | guest | exactly-one per Guest | exclusive | forbidden | +| `Process` | host/guest | bounded-many | partitioned | forbidden | +| `EphemeralProcess` | host/guest | bounded-many | partitioned | forbidden | +| `Volume` | zone | zero-or-one writer authority | exclusive-write / shared-read | forbidden (state stays local) | +| `Network` | zone | exactly-one net-VM DHCP/DNS/NAT authority per Network | exclusive | forbidden | +| `Device` | physical-device | zero-or-one per physical backing | exclusive (full/DRM/TPM/key/USB) or shared (render-node) | forbidden as a direct target; an approved semantic `*Service` may mediate it (D096) | +| `User` | user/seat | exactly-one per user | exclusive | forbidden | +| `Credential` | zone | zero-or-one | exclusive | forbidden (D093 default) | +| `Endpoint` | scope of producer | zero-or-one per stable listener/port | exclusive (fixed listener) | forbidden as a direct target; an approved semantic `*Service` may mediate it (D096) | +| `ResourceExport` | owner Zone | zero-or-one per exported backing | per exported arbitration | n/a (is the export) | +| `ResourceImport` | consumer Zone | bounded-many consumers | per lease | n/a (is the import) | + +Singleton-but-not-cross-Zone-exportable core authorities (audit chain, broker, +resource store, resource API, bus, core controller, config publisher, artifact +catalog) are `exportability: forbidden` and are detailed in the Zone-control +spec; physical/scarce and per-user/session Provider authorities (mic/speaker, +security key, GPU/render-node, video decoder, TPM/swtpm, NIC/uplink, Wayland +portal, clipboard, notification sink, PipeWire mediator, Secret Service, +systemd-user manager, Entra login authority, shell supervisor, SigNoz ingest, +cloud subscription control) carry their qualified `AuthorityDescriptor` in the +owning Provider dossier. + +Qualified semantic `*Service` types carry the `explicit-export` descriptor when +approved. Their matching `*Binding` types are always non-exportable. The initial +approved families are audio, security-key, observability, and policy-gated +USBIP; every other Provider family remains non-exportable unless a later +reviewed dossier and signed projection factory add it. + +## Folded implementation detail + +The following are not standalone ResourceTypes: + +- budgets/cgroups; +- sandbox/namespace/seccomp/capability profiles; +- files/directories/ACLs/views/mounts outside Volume; +- process ports and telemetry bindings (stable endpoints are the `Endpoint` + ResourceType, D092); +- controller instances; +- pidfds and per-session named-stream/transport handles; +- locks/leases internal to transactions/controllers; +- syscalls/broker operations. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `d2b-realm-core/src/ids.rs`, `workload.rs`, `allocator.rs`; `d2b-core/src/storage.rs`, `processes.rs`; daemon operation/readiness/status DTOs | +| Evidence class | Current DTOs are mixed reachable/generated; universal resource envelope is ADR-only | +| Behavior retained | Strict serde, bounded IDs/messages, typed status/error enums, generation-bound exec/shell attach, storage owner/repair metadata | +| Required delta | Universal metadata/spec/status, ResourceType schemas, owner index/triggers, revisions, conditions/outcome, native deletion | +| Reuse path | Extract validators/redaction/error constants and storage lifecycle fields named in work items | +| Replacement/deletion | Existing manifest/process/storage DTOs remain until owning ResourceType integrations are live | +| Feasibility proof | Schema golden vectors, owner property tests, optimistic conflicts, status redaction, deletion event/removal | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-object-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0 shared contract root; `d2b-contracts` | +| Current source | `packages/d2b-realm-core/src/ids.rs`, `workload.rs`, `error.rs`; `packages/d2b-core/src/storage.rs`, `processes.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/resource.rs`, `resource_status.rs`, `resource_schema.rs` | +| Detailed design | Implement strict ResourceEnvelope, metadata, the three-layer spec shape (universal envelope + ResourceType base `spec.*` incl. `spec.providerRef` + optional canonical `spec.provider` `{ schemaId, schemaVersion, settings }`), the three-layer status shape (universal base + `status.resource` + optional `status.provider` with `providerRef`/`schemaId`/`schemaVersion`/`observedProviderGeneration`/`details`), phase/condition/outcome, canonical JSON, per-layer bounds/redaction, ownerRef/UID fields Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Store/API/SDK/Nix/codegen consume one contract | +| Data migration | Full d2b 3.0 reset; no v2 resource import | +| Validation | Golden JSON/protobuf vectors; serde unknown-field; three-layer spec shape round-trip; canonical minimal base-spec acceptance; base-schema version/fingerprint conformance; `spec.provider` deny-unknown/version-mismatch/shadow rejection and providerRef-binding; three-layer status shape round-trip; base-only projection (universal + `status.resource`) ignores/omits `status.provider`; `status.provider` unknown-field/version-mismatch rejection; status redaction/size/time/phase tests; `status.update` currency object round-trip (state/reasons/disruption/preserveState/owned+dependency refs bounded); `spec.updatePolicy` base round-trip | +| Removal proof | Old DTOs removed per owning ResourceType wave only after rendered/runtime consumers move | + +### ADR046-object-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-object-001; native resource store | +| Current source | `packages/d2b-realm-core/src/allocator_engine.rs`, `d2b-realm-router/src/lib.rs` shared ownership/idempotency precedents | +| Reuse action | adapt | +| Destination | `packages/d2b-resource-store-redb/src/ownership.rs`, `packages/d2b-controller-toolkit/src/owner_hints.rs` | +| Detailed design | Singular ownerRef resolution/UID binding, cycle/depth property checks, reverse index, owner hints, child-first deletion | +| Integration | Every store mutation updates owner index and hint dispatcher atomically | +| Data migration | None after reset | +| Validation | Property tests for cycles/reparent/name reuse; integration tests for child drift repair and owner cascades | +| Removal proof | Not applicable | diff --git a/docs/specs/ADR-046-resource-reconciliation.md b/docs/specs/ADR-046-resource-reconciliation.md new file mode 100644 index 000000000..cdb67748d --- /dev/null +++ b/docs/specs/ADR-046-resource-reconciliation.md @@ -0,0 +1,491 @@ +# ADR 0046 resource reconciliation + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-resource-reconciliation` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | Core hint dispatcher, Provider controller toolkit | +| Depends on | `ADR-046-resource-object-model`, `ADR-046-resource-store-redb`, `ADR-046-resource-api-and-authorization` | +| Supersedes | None | + +## Ownership + +Provider controller processes own: + +- watch subscription; +- local pending/running map; +- per-resource serialization; +- cross-resource async concurrency; +- reconcile/observe/finalize handlers; +- retry/requeue decision; +- status/mutation batches. + +Core owns: + +- watch-plan validation; +- ResourceType/provider/controller ownership; +- type/owner/dependency reverse indexes; +- change filtering; +- converged/self-status suppression; +- owner triggers; +- coalesced high-water hints; +- controller lease/generation; +- fair API/watch budgets; +- durable revision/watch delivery. + +Core does not execute Provider domain logic. Providers do not poll the store or +open broad watches. + +## Controller descriptor + +Each signed controller descriptor declares: + +- Provider/component/controller ID and generation; +- ResourceTypes and versions; +- Host/Guest Provider capabilities supported; +- Process domains supported; +- spec/status/finalizer verbs; +- exact watch selectors; +- explicit dependency selectors; +- owner-child triggers consumed; +- reconcile/observe concurrency; +- maximum pending resources; +- observe/resync policy; +- finalizers owned; +- service/schema fingerprints; +- ResourceType-specific deadlines/retry classes. + +Runtime registration must match the installed Provider descriptor and +authenticated Process/Host/Guest identity. + +## Async interface + +Language-neutral semantic interface: + +```text +async describe() -> ControllerDescriptor +async validateSpec(context, resource) -> ValidationResult +async plan(context, resource, dependencies) -> ReconcilePlan +async reconcile(context, resource, dependencies) -> ReconcileResult +async observe(context, resource) -> ObservationResult +async finalize(context, deletingResource) -> FinalizeResult +async health() -> ControllerHealth +async drain(deadline) -> DrainResult +``` + +The official Rust toolkit exposes async traits and an async ResourceClient. +Non-Rust SDKs implement the same vectors/state machine. + +No handler holds a redb transaction or blocking kernel/systemd/filesystem call +across an await. Blocking effects use explicit bounded adapters. + +## Reconcile context + +Context contains: + +- controller/Provider/Process/Host/Guest/Zone identity and generations; +- target ResourceRef/UID/revision/generation; +- trigger reason set and high-water revision; +- operation/idempotency/correlation/trace IDs; +- attempt; +- monotonic deadline/cancellation; +- policy/API/config/controller revisions; +- capability-limited async ResourceClient. + +It contains no database handle, direct broker socket, reusable credential, raw +route table, or authority supplied by the resource payload. + +## Reconcile results + +Result contains: + +- processed revision/generation; +- zero or one ResourceMutationBatch; +- latest status/conditions/outcome; +- one disposition: + - `converged`; + - `pending`; + - `degraded`; + - `failed-retryable`; + - `failed-terminal`; + - `requeue-at`; + - `finalized`; +- next observe/requeue time where applicable. + +A stale mutation conflict discards the result; the toolkit re-reads and lets the +controller retry under policy. Core never merges stale Provider output. + +## Async loop + +1. Register descriptor/watch plan. +2. List resources requiring initial work and receive snapshot revision. +3. Open watch after snapshot revision. +4. Dedicated async receiver continuously consumes hints. +5. If a resource is idle, dispatch immediately. +6. If already queued/running, replace pending high-water revision and union + non-droppable reasons. +7. Each resource has one running handler; independent resources run in parallel + under semaphore/budget. +8. Handler reads one fresh resource/dependency snapshot. +9. Handler may asynchronously write Pending/starting status. +10. Handler starts effects in its own task/blocking adapter. +11. Receiver continues reading and dispatching other ready resources. +12. Handler commits mutation/status with expected revisions. +13. Toolkit acknowledges/checkpoints after commit or terminal no-mutation + outcome. +14. Disconnect/revision-expired relists and rebuilds the queue. + +There is no fixed polling interval, debounce window, or sleep between ready +resources. + +## Expedited reconcile pass (D090) + +An expedited (`waitForReconcile`) mutation enters a **bounded priority lane** +into the SAME per-resource single-flight reconciler; it does not create a second +executor and never bypasses per-resource serialization: + +1. The controller may preflight/plan in parallel with Core admission/commit but + starts no external effect, finalizer release, or status mutation until it + receives the typed `CommittedRevisionProof { resourceUid, generation, + revision, operationId }` from Core; an `Abort` for that `operationId` means + no effect. +2. On proof, the expedited request is dispatched ahead of normally-queued work + for that resource; any already-queued ordinary reconcile stays queued and + runs after the expedited pass completes. +3. All effect IDs/idempotency keys derive from `(UID, generation, revision, + operationId)`. The later ordinary re-entry observes the converged/progressing + state and no-ops or rejoins the in-flight operation — it never duplicates an + effect. +4. The pass returns a bounded `ReconcileProjection` and one **disposition**: + `Converged`, `Progressing`, `Blocked`, `UpgradeRequired`, or `Failed` + (these map onto the ordinary result dispositions; expedited completion is one + pass reaching a disposition, not long-running external Ready). +5. Status persistence is asynchronous: the controller returns the projected + layered status candidate; the actual `UpdateStatus` write is a normal later + mutation. A timeout/cancel after commit yields a committed-but-reconcile- + pending outcome and the ordinary queue continues. + +Priority quotas and fairness bound the expedited lane so it cannot starve +ordinary reconciles or be used for DoS. Only authorized UX mutations/core (and +the admin `resource reconcile` action) use this lane. + +## Currency and disruptive upgrade (D091) + +Every controller additionally implements `assess_update`, `plan_upgrade`, and +`execute_upgrade` alongside ordinary reconcile, serialized through the same +per-resource single-flight so a reconcile and an upgrade never run concurrently +for one resource: + +- **assess_update** runs on core/Provider-generation, artifact/image/NixOS- + generation, immutable-spec, dependency, or security-policy change and writes + the bounded `status.update` currency object (state/reasons/observed+target + IDs/disruption/preserveState/owned+dependency aggregates). A controller MUST + report `UpgradeRequired` for a disruptive change rather than apply it in + place; non-disruptive changes reconcile normally. +- **plan_upgrade** produces a bounded plan (disruption class, preserveState, + affected owned/dependent set); **execute_upgrade** applies it. The core + Operation ledger persists the upgrade operation/idempotency/progress and + resumes after crash/restart; `status.update` carries only the latest bounded + plan/result, never a second ledger. +- Upgrade **preserves** the Resource UID and spec identity where possible and + recycles only the realization and owned ephemeral Processes/endpoints; durable + and state/secret Volumes and TPM identity are preserved (`preserveState: + true`). `Replace` of the resource-row identity is used only when explicitly + required and planned with ownership/state transfer; full factory reset is a + separate destructive path (`ADR-046-reset-and-cutover`). +- A **dependency-aware planner** topologically drains, recycles, and restarts + affected owned/dependent resources. Example: a GPU Device marks itself + `UpgradeRequired`/`Blocked` while applications depend on it; the planner + drains dependent Processes/Guests, recycles the GPU realization, then restarts + the dependents — no surprise disruption. Core invokes dependency/owner + triggers and aggregates self/owned/dependency currency for list/get. + +## Trigger reasons + +Closed common reasons: + +- spec-generation-changed; +- owned-resource-changed; +- dependency-changed; +- dependency-ready; +- deletion-requested; +- finalizer-required; +- controller-generation-changed; +- Provider-generation-changed; +- policy-changed; +- security-policy-changed; +- artifact-or-image-changed; +- execution-status-changed; +- scheduled-observe; +- assess-update-due; +- upgrade-requested; +- expedited-mutation; +- retry-due; +- manual-reconcile; +- startup-relist. + +Reasons are coalesced without dropping owner/deletion/finalizer/policy/ +generation causes. + +## Core suppression + +Core may suppress: + +- unbound ResourceTypes/scopes; +- irrelevant metadata/status fields; +- controller's own status-only event when no owner/dependency consumer needs + it; +- object whose generation equals observedGeneration, controller generation is + current, no dependency/owner/delete/observe/retry cause exists, and conditions + do not require work. + +Core may not suppress: + +- any child mutation's ownerRef trigger; +- deletion/finalizer; +- policy/Provider/controller generation; +- explicit dependency; +- due retry/observe; +- Unknown state requiring observation. + +## Owner reconciliation + +On any child mutation: + +1. store emits owned-resource-changed after durable commit; +2. owner hint includes child ref/UID/revision/event; +3. owner controller relists owner_index; +4. it compares complete desired children with observed children; +5. it creates missing children; +6. it repairs drift through expected-revision writes; +7. it requests deletion for children no longer desired; +8. child finalizers/status remain owned by their controllers. + +Propagation to ancestors is acyclic, depth/budget bounded, and coalesced. + +### ResourceImport projection ownership (D096) + +A `ResourceImport` owns exactly one local projection **Service** through +`metadata.ownerRef: ResourceImport/`. Its ResourceType is the same +qualified semantic/provider-neutral `*Service` type as the remote owner Service, +as bound by the signed projection factory. The consumer's local `providerRef` +selects the conformant implementation; the projection does not copy the +owner's `spec.provider`. Core rejects a missing/mismatched factory or any +semantic-type rewrite and never projects a Device, Endpoint, or `*Binding`. + +Operator/Nix-authored matching same-Zone `*Binding` resources reference the +projection's `serviceRef` and an allowed consuming Guest/User/Zone. They are not +owned by the import. Binding spec contains desired consumer intent only; all +observations belong in status. Their Provider controller creates and reconciles +owned Process/Endpoint children. The import controller never creates, exports, +or deletes Binding; per-session leases/streams remain internal records. + +Status and D091 update currency propagate owner Service → export → import → +projection Service → Binding → owned children. `ResourceExport` removal or +ZoneLink loss revokes the lease and marks the projection Service degraded/ +revoked; Binding controllers then stop children in topological order. Import +finalization marks the projection draining, rejects new sessions, and waits for +all referencing Bindings to be deleted or retargeted. It then releases the remote +lease, deletes the projection Service and remaining provider-owned children, and +clears its own finalizer. `BindingReferencesRemain` is visible pending cleanup; +there is no implicit Binding cascade. + +Service and Binding base reconcile inputs and status are +implementation-independent, and every selected Provider accepts the canonical +minimal base. PipeWire, CTAPHID, OTEL, and USBIP observations may appear only in +their registered bounded `status.provider` extension; they never affect the +semantic dependency keys or base conditions/errors. + +### Authority adoption, quarantine, and drain-recycle (D097) + +An authority owner (Resource or owner service Process for a scarce/singleton +backing) is adopted across a restart by its signed `ownerProof` (process/resource +identity), never by re-opening the backing speculatively. The reconciler +revalidates the authority index entry `(Zone/scope, authorityClass, +opaqueKeyDigest)` against the recovered owner: + +- exact identity match → adopt in place, no re-open, no second effect; +- ambiguity (two candidates, or an index entry with no verifiable owner) → + **quarantine** the authority (no effect, `Degraded` naming the incumbent owner + digest) until an operator or a deterministic tiebreak resolves it; +- a duplicate config/API claimant → deterministic `duplicateConflict`, no + second open. + +A D091 upgrade of an authority **drains its consumers first** (leases/projections +released or migrated in topological order) and then recycles the authority owner; +it never recycles a backing while consumers still hold live leases. Reset +preserves or destroys the underlying backing state per the authority's explicit +per-authority disposition. + +## Process fast path + +When a Process or EphemeralProcess durable commit completes and dependencies +are ready: + +- post-commit dispatcher pushes a matching hint immediately; +- p95 handler start is <=5 ms; +- Process Provider validates/starts launch attempt in a background task; +- p95 commit-to-launch-attempt is <=20 ms; +- status is written asynchronously; +- watch receiver continues reading; +- next independent Process may start before prior readiness/completion; +- only per-resource ordering, true dependencies, configured concurrency, or + backpressure may delay dispatch. + +The benchmark proves 1/10/100 ready Process resources and records queue/ +event-loop responsiveness. + +## Process status + +Typical transitions: + +```text +Pending (Queued/Starting condition) + -> Ready + -> Degraded | Failed | Unknown +``` + +Starting/retrying/draining are conditions/reasons, not common phases. + +EphemeralProcess: + +```text +Pending -> Succeeded | Failed | Unknown +``` + +It includes startedAt/completedAt/outcome/exitCode and: + +- successfulTtl default 1h; +- failedTtl default 24h; +- cleanupEligibleAt; +- finalizer/incident-hold-safe deletion. + +## Finalization + +The exact finalizer owner receives deletion-requested. It returns: + +- complete; +- pending with requeue-at; +- blocked with typed condition; +- ambiguous with no false success. + +A controller clears only its finalizer. Core removes the resource only after all +finalizers and owned-child deletion complete. + +## Resync and external drift + +Controllers do not poll by default. A ResourceType whose external state can +drift declares a bounded observe interval. Core schedules exactly that +reconcile reason. Missed watch events are recovered by revision replay/relist. + +## Status-first recovery after restart + +Resource `status` is the default durable observation and recovery surface for +bounded non-secret operational state (D087): reconcile stage, opaque +non-authorizing external handles/IDs/digests, adoption observations, bounded +counters, closed-enum detail, dependency readiness, and last successful +checkpoints. It is written only on a material change and never carries secrets, +authority-conferring handles, private path/argv/environment/PID/unit data, or +high-frequency streams (see `ADR-046-resource-object-model` § Status bounds). + +Status has the frozen three-layer shape (D088): the universal `ResourceStatus` +base, the ResourceType-common `status.resource` object, and an optional +Provider-specific `status.provider` extension. A controller writes all present +layers in one status mutation with a single expected revision; the layers never +diverge. Cross-resource and cross-provider reconcilers depend only on the +universal base plus `status.resource` (a base-only projection) and never read a +peer's `status.provider.details`; any field a second implementation needs is +promoted to `status.resource`. + +Symmetrically, desired `spec` has the frozen three-layer shape (D089): the +universal envelope, the ResourceType base spec at `spec.*` (including +`spec.providerRef`), and an optional canonical `spec.provider = +{ schemaId, schemaVersion, settings }`. Generic controllers reconcile from the +base spec and base status only; a Provider controller additionally reads its own +`spec.provider.settings` and writes its own `status.provider`. A Provider that +cannot honor an optional base capability reports the provider-neutral +`unsupported-capability` outcome rather than ignoring or reinterpreting the base +field. + +On a Zone or controller restart a controller re-reads its owned resources' +status and treats every field as **observation, not authority**. Before +relying on any recovered observation it reverifies against external reality — +re-discovering running processes from declared cgroup leaves, opening fresh +pidfds, and revalidating opaque external handles and markers against the live +external system — and quarantines or degrades any ambiguity. Status never +substitutes for that reverification and never carries or stands in for a +privileged effect. This makes a durable payload Volume unnecessary for a +component whose operational state is fully derivable from spec, status, the +core Operation ledger, and independent external observation. + +## Backpressure and fairness + +- bounded watch stream credit; +- bounded pending map; +- per-controller/Provider/Host/Guest concurrency; +- fair ResourceClient queues; +- reserved health/cancel/status capacity; +- typed backpressure, never silent drop of an admitted mutation; +- latest high-water coalescing for the same resource; +- no cross-resource eviction. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `d2b-realm-router` shared OperationRouter/mux/session lifecycle; d2bd DAG/topological executor/readiness/pidfd; role-specific state machines | +| Evidence class | Current route/DAG logic is tested; generic controller loop/hints are ADR-only | +| Behavior retained | Deterministic ordering, capability denial, idempotency, bounded queues, cancellation, fail-fast typed errors, pidfd adoption | +| Required delta | Async controller SDK/loop, store watches/hints, owner triggers, cross-resource concurrency, status batches | +| Reuse path | Extract pure state machines/limits and deterministic test clocks; replace role branches with ResourceType controllers | +| Replacement/deletion | DAG/role path remains until each successor controller/Process graph is integrated | +| Feasibility proof | Real multi-process controller over d2b-bus; latency/load/conflict/owner/finalizer tests | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-reconcile-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0/W1a; controller toolkit owner | +| Current source | `packages/d2b-realm-router/src/lib.rs`, `mux_session.rs`, `session_lifecycle.rs`; `packages/d2bd/src/supervisor/dag.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-controller-toolkit/src/lib.rs`, `runner.rs`, `queue.rs`, `context.rs`, `result.rs` | +| Detailed design | Async ResourceReconciler, watch receiver, coalescing, per-resource serialization, parallel tasks, retry/checkpoint/finalize; expedited priority lane and `CommittedRevisionProof`-gated effects (D090); `assess_update`/`plan_upgrade`/`execute_upgrade` methods serialized in the same single-flight (D091) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Provider controller binaries wrap handlers with toolkit | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Golden state-machine vectors, deterministic clocks, conflict/restart/queue tests; D090: commit-fails/Abort → no effect, controller finishes-before-commit gated on proof, effects-gate, status-write-delayed (`statusPersistence: pending`), normal-queued no-op/rejoin, concurrent mutation, delete event-only projection, expedited timeout committed-but-pending, restart re-entry no duplicate; D091: current/non-disruptive/each-trigger assess, UpgradeRequired-not-in-place, dependency propagation/topological drain-recycle-restart, GPU blocking, state/TPM preservation, crash/re-entry resume, single-flight reconcile-vs-upgrade serialization | +| Removal proof | Current per-role orchestration removed only after ResourceType successors | + +### ADR046-reconcile-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | Store/API + ADR046-reconcile-001; core controller | +| Current source | `d2b-realm-core/src/route_engine.rs`, `allocator_engine.rs`; `d2b-realm-router/tests/transport_topology_harness.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/hints.rs`, `dependencies.rs`, `owner_reconcile.rs` | +| Detailed design | Watch-plan validation, indexes, suppression, owner/dependency hints, leases, startup relist, fair admission | +| Integration | Store post-commit dispatcher → d2b-bus controller streams | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Owner/dependency chains, suppression/no-loss, restart/relist, lease withdrawal | +| Removal proof | Not applicable | + +### ADR046-reconcile-003 + +| Field | Value | +| --- | --- | +| Dependency/owner | Process Providers + benchmark owner | +| Current source | `d2bd/src/supervisor/dag.rs`, `pidfd.rs`, unsafe-local blocked supervisor, guest exec runner | +| Reuse action | adapt | +| Destination | `packages/d2b-controller-toolkit/benches/reaction.rs`, Process Provider integration tests | +| Detailed design | Commit-to-handler/launch fast path, nonblocking watch, parallel ready resources | +| Integration | Resource store → bus/session → controller → Process effect/status | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hard <=5 ms/<=20 ms p95 gates and 1/10/100 Process concurrency | +| Removal proof | Not applicable | diff --git a/docs/specs/ADR-046-resource-store-redb.md b/docs/specs/ADR-046-resource-store-redb.md new file mode 100644 index 000000000..934d37578 --- /dev/null +++ b/docs/specs/ADR-046-resource-store-redb.md @@ -0,0 +1,388 @@ +# ADR 0046 redb Zone resource store + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-resource-store-redb` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | Zone runtime, `d2b-resource-store-redb` | +| Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model` | +| Supersedes | None | + +## Ownership and process boundary + +Every Zone runtime embeds exactly one redb database and one resource service. +The database belongs to that Zone only. + +The generated storage contract gives one broker/Host/Guest-local storage owner +authority to create/validate: + +- database inode; +- identity marker; +- owner/group/mode/link count; +- filesystem/locking support; +- parent-directory fsync; +- replacement/missing-state detection. + +The owner passes one already-open regular `File` to the Zone runtime. The +pinned redb API must use `FileBackend::new(File)` or an equivalently reviewed +fd-backed API. The Zone runtime does not resolve a caller-controlled store +path. + +Providers/controllers never receive a redb handle, database file/dir fd, path, +table access, or direct store client. Only the resource service/store actor +touches redb. + +## Store identity + +One closed `store_meta` table binds: + +| Key | Value | +| --- | --- | +| `store_uuid` | Random immutable store identity | +| `zone_name` | Must match `Zone/` self resource | +| `zone_uid` | Immutable UID of self resource | +| `created_at` | RFC 3339 UTC | +| `schema_version` | Internal physical schema version | +| `current_revision` | Latest committed Zone revision | +| `compaction_floor` | Earliest replayable revision | +| `active_configuration_revision` | Active Nix/root configuration | +| `policy_revision` | Current authorization policy revision | +| `api_catalog_revision` | Current bound ResourceType catalog | +| `clean_shutdown` | Clean/crash-open marker | +| `backup_generation` | Latest validated logical backup | + +A previously provisioned database that is missing, replaced, bound to another +Zone/UID, newer than the binary schema, or internally inconsistent fails closed. +The runtime never silently creates an empty replacement. + +## Physical tables + +| Table | Key | Value | +| --- | --- | --- | +| `store_meta` | closed metadata key | Closed versioned scalar | +| `api_schemas` | canonical API/schema identity digest | Signed ResourceTypeSchema/ResourceApiExport, validator fingerprint, compatibility/provenance | +| `resources` | `(bound ResourceType, resource_name)` | Complete strict ResourceEnvelope plus internal owner UID | +| `type_index` | `(bound ResourceType, resource_name)` | Immutable resource UID | +| `owner_index` | `(owner UID, child UID)` | Child ResourceType/name and latest revision | +| `producer_index` | `(producer UID, Endpoint UID)` | Endpoint producerRef reverse index (D092): resolves the `Endpoint` resources a `Process`/`Device`/`Guest`/`Host`/qualified producer realizes | +| `controller_index` | `(controller binding ID, ResourceType, name)` | Resource UID | +| `revision_log` | monotonically increasing u64 revision | Ordered bounded ChangeBatch | +| `operations` | OperationId | Idempotency/request digest, resources, phase/outcome, accepted/finished revisions, retention | +| `zone_link_cursors` | peer Zone UID | Link epoch and last sent/acked/received/applied revisions | + +Keys use versioned length-prefixed binary tuples, never delimiter-joined +caller strings. Dynamic spec/status is canonical JSON validated against the +exact signed schema before storage. Envelope/index/operation/change values use +one versioned deterministic encoding owned by d2b-contracts. + +`ResourceExport` and `ResourceImport` rows are stored and indexed like any other +bound ResourceType through `resources`, `type_index`, `owner_index`, and +`controller_index`. The local typed projection that core creates for a +`ResourceImport` is a normal local resource with +`metadata.ownerRef: ResourceImport/`, so it appears in the same indexes as +any Provider-created resource. No cross-Zone rows are stored in a Zone database; +imports carry only their local `zoneLinkRef` plus bounded `exportKey`, and +ZoneLink cursor state remains the only cross-Zone store state. + +Spec has the frozen three-layer shape (D089): the universal Resource +envelope/metadata, the ResourceType base spec at top-level `spec.*` (including +`spec.providerRef`), and an optional canonical `spec.provider = +{ schemaId, schemaVersion, settings }` extension. A spec write is validated +against the ResourceType base schema and, when `spec.provider` is present, +against the installed Provider's registered, signed, digested extension schema +(`schemaId`/`schemaVersion`) with strict unknown-field denial and spec bounds; +a mismatch fails closed with `spec-provider-schema-invalid`, a `settings` that +restates a base field with `spec-provider-shadow`, before any redb mutation. + +Status is the default durable observation surface for bounded non-secret +operational state (D087) and is stored inside the resource envelope, not in a +side stream. Status has the frozen three-layer shape (D088): the universal +`ResourceStatus` base, the ResourceType-common `status.resource` object, and an +optional Provider-specific `status.provider` extension. A status write is +validated per layer before storage — total canonical serialized status ≤ 64 KiB, +`status.resource` typed detail ≤ 32 KiB, `status.provider.details` ≤ 32 KiB, and +bounded condition/list/map cardinality — and a `status.provider` is validated +against the installed Provider's registered, signed extension schema +(`schemaId`/`schemaVersion`) with strict unknown-field denial. An over-limit +write fails closed with `status-oversize`, an unregistered/unknown-field +extension with `status-provider-schema-invalid`, and a duplicated +universal/`status.resource` field with `status-provider-overlap`, all before any +redb mutation (see `ADR-046-resource-object-model` § Status). All present layers +are committed atomically in one status mutation. Controllers write status only +on a material change, so status churn cannot outpace revision compaction; there +are no high-frequency byte streams, logs, metrics, or ring buffers in the +store. + +Unknown table/encoding/schema versions fail closed. + +## Async storage adapter + +redb is synchronous. The Zone runtime exposes only async resource APIs. + +- one bounded fair async write queue feeds one dedicated blocking store actor; +- read requests execute as short-lived blocking MVCC read transactions through + a bounded adapter pool; +- async executor threads never call blocking redb/filesystem APIs; +- read transactions cannot survive an await or watch lifetime; +- per-principal/controller fair admission prevents one caller monopolizing the + writer; +- queue saturation returns typed backpressure before opening a transaction. + +The writer may perform bounded group commit. It takes a small immediately +available batch from the fair queue without sleeping/debouncing: + +- preserves per-principal fairness and per-resource order; +- validates each mutation/result independently in one write transaction; +- includes only non-conflicting/dependency-compatible mutations; +- assigns one Zone revision and ordered per-mutation ordinals; +- performs one crash-safe commit/fsync; +- returns each caller its own success/conflict/error; +- emits ordered ChangeBatch entries and controller hints after commit. + +A mutation that depends on/conflicts with another queued mutation is ordered +explicitly or committed separately. Group commit never silently changes atomic +batch semantics or shares authorization outcomes. + +## Write transaction + +For every mutation/bounded group: + +1. authenticate/admit request before queueing; +2. begin one redb write transaction; +3. recheck policy/API/controller generations; +4. resolve target refs/UIDs and expected revisions; +5. validate ResourceType schema, owner graph, finalizers, quotas, and controller + ownership; +6. reject conflicts with no mutation; +7. update resources plus every affected index; +8. update durable operation/idempotency state; +9. allocate exactly `current_revision + 1`; +10. append one ordered ChangeBatch; +11. update store metadata; +12. commit with the selected crash-safe redb durability; +13. only after commit, swap in-memory indexes and push matching watch/reconcile + events directly to d2b-bus. + +No success, status, watch event, reconcile hint, or effect starts before durable +commit returns. + +Two callers using the same expected resource revision cannot both succeed. The +first valid durable commit wins; the other gets `resource-conflict` with the +current revision and may re-read/retry. + +### Expedited commit proof (D090) + +For an expedited (`waitForReconcile`) `Create`/`UpdateSpec`/`Delete`, the writer +reserves the target revision under one mutation ticket (`operationId`) while the +owning controller runs preflight/plan in parallel. The controller performs no +external effect, finalizer release, or status mutation until the writer emits, +only after step 12 (durable commit), a typed +`CommittedRevisionProof { resourceUid, generation, revision, operationId }`. If +the transaction fails or is rejected at any step, the writer emits `Abort` for +that `operationId` and no effect occurs. A durable commit is authoritative and +is never rolled back because the subsequent expedited reconcile pass fails or +times out. The commit still emits the ordinary ChangeBatch/hint (step 13); the +expedited request additionally enters the priority reconcile lane. Status +written by the expedited pass is a normal later asynchronous status mutation +with its own revision, never part of the spec/create commit. + +## Revision model + +Zone revision: + +- is a monotonically increasing u64 inside one Zone; +- increments once per successful write transaction; +- is not wall time or cross-Zone causal order; +- orders changes within a ChangeBatch by bounded ordinal. + +Resource metadata.revision is the Zone revision of its latest change. List +returns a consistent MVCC snapshot plus snapshot revision. + +## ChangeBatch + +Each event contains only: + +- revision and ordinal; +- ResourceType/name/UID; +- event `Created|SpecUpdated|StatusUpdated|MetadataUpdated|DeleteRequested|Deleted`; +- old/new generation where applicable; +- current ownerRef/owner UID; +- payload digest; +- complete bounded ResourceEnvelope when watch authorization permits; +- operation/correlation IDs. + +No secret, credential byte, terminal data, raw Provider state, pidfd, host path, +or process argv/environment enters revision_log. + +## Watches + +Watch is application-owned because redb has no native changefeed. + +1. Caller supplies exact authorized ResourceTypes/filters and `afterRevision`. +2. Service replays matching revision_log entries. +3. Under the watch coordinator it registers live delivery and rechecks the + high-water revision, preventing a replay/live gap. +4. Live committed changes are pushed immediately through d2b-bus named streams. +5. Caller acknowledges fully processed revisions. +6. Disconnect resumes from the last acknowledged revision. + +There is no fixed polling, debounce, or compaction-tick delivery delay. + +The log is bounded by bytes, count, and age. Slow clients cannot pin it forever. +Compaction advances a durable floor and deletes old batches in bounded write +transactions. A cursor below the floor receives `revision-expired` plus current +revision and must list/re-watch. + +## Owner triggers + +Every child mutation updates owner_index/ChangeBatch in the same transaction. +After commit, the dispatcher emits `owned-resource-changed` for the singular +owner with child ref/UID/revision/event. + +- repeated child changes coalesce only while owner is queued/running; +- child status-only updates still trigger the owner; +- propagation follows the validated acyclic owner chain; +- strict depth/work budgets prevent amplification; +- parent relists owner_index and reasserts its complete child set. + +## Deletion + +Delete flow: + +1. set metadata.deletionRequestedAt and trigger controller/finalizers; +2. reconcile children/finalizers child-first; +3. final transaction creates a `Deleted` change event, removes resource/type/ + owner/controller indexes, and commits; +4. GET returns not found immediately after commit. + +No resource tombstone is retained. revision_log is the deletion history. + +## EphemeralProcess cleanup + +Terminal EphemeralProcess status includes completedAt and cleanupEligibleAt. + +- Succeeded defaults successfulTtl to `1h`. +- Failed defaults failedTtl to `24h`. +- Pending/Ready/Unknown/Degraded never age out. +- explicit bounds are enforced by schema/policy; +- finalizers and incident holds block cleanup; +- cleanup uses expected revision and the normal delete transaction; +- owner deletion/reconcile notification is preserved. + +## Backup, restore, and physical schema upgrade + +Backup uses a bounded consistent logical read snapshot containing: + +- store identity/schema and revision boundary; +- API schemas/catalog; +- resources; +- operations; +- Zone-link cursors; +- index checksums/rebuild inputs. + +An open database file is never copied unless the pinned redb version explicitly +documents that operation as crash-safe. + +Restore/upgrade: + +1. stop Zone mutation admission; +2. validate source identity/digest/schema; +3. build a staged new redb database; +4. validate resources, refs, owners, indexes, revisions, operations; +5. fsync staged file and parent; +6. atomically publish while retaining prior file for the bounded rollback + window; +7. reopen/rebuild indexes before readiness. + +Corruption or ambiguous publication quarantines the Zone resource plane. It +does not create a fresh store or claim partial success. + +## Performance contract + +On the pinned reference host/release profile: + +| Metric | Hard target | +| --- | --- | +| Normal readiness | <=500 ms | +| Aggregate Zone resource service/store + fixed system-core and system-minijail controllers idle RSS | <=64 MiB | +| p95 local Get/bounded List | <=2 ms | +| p95 crash-safe single-resource mutation | <=10 ms | +| p95 durable commit → matching controller handler start | <=5 ms | +| p95 ready Process commit → launch-attempt start | <=20 ms | + +Benchmark fixtures include: + +- empty store; +- 10,000 resources; +- 100 live watches; +- 1/10/100 concurrently ready Process resources; +- expected-revision conflict storm; +- owner-trigger fan-in/chain; +- revision compaction; +- forced crash at every commit boundary; +- backup/restore/internal schema upgrade; +- repeated open/close and long-reader rejection. + +Failure to meet a hard target changes the Proposed design. Durability, +authorization, or audit cannot be weakened to pass. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | No generic store. Reuse `d2b-core/src/storage.rs`, `sync.rs`; daemon snapshot/operation records; `d2b-realm-router` idempotency; broker fd/path safety | +| Evidence class | Current storage/locks/ledgers are mixed reachable/generated; redb store is ADR-only | +| Behavior retained | Single repair owner, no-follow/fd-relative safety, atomic rename/fsync, OFD locks, bounded records, pidfd non-persistence, idempotency/quarantine | +| Required delta | Entire redb schema, store actor, revisions, indexes, watches, conflicts, backup/upgrade | +| Reuse path | Extract exact storage/atomic/idempotency validators named below; redb only supplies ACID B-trees | +| Replacement/deletion | No existing state file/ledger is removed until its owning resource/operation migration lands | +| Feasibility proof | Disposable redb crash/performance/watch/backup spike with exact pinned version | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-store-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0; store integrator | +| Current source | `packages/d2b-core/src/storage.rs`, `sync.rs`; `packages/d2bd/src/supervisor/state.rs`, `daemon_audit.rs`; `d2b-realm-router/src/lib.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-resource-store/src/lib.rs`, `packages/d2b-resource-store-redb/src/lib.rs`, `schema.rs`, `keys.rs`, `transaction.rs` | +| Detailed design | redb tables/encodings, fd backend, store identity, fair actor, MVCC reads, atomic indexes/revisions/operations/conflicts Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Zone runtime owns store; resource API is sole caller | +| Data migration | Full reset; logical backup only for v3 stores | +| Validation | Unit/property/fault tests and hard benchmark | +| Removal proof | Existing ledgers removed only by owning future work items | + +### ADR046-store-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-store-001; watch/reconciliation integrator | +| Current source | `packages/d2b-realm-core/src/mux.rs`, `d2b-realm-router/src/mux_session.rs`, `route_engine.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-resource-store-redb/src/revision_log.rs`, `packages/d2b-resource-api/src/watch.rs` | +| Detailed design | replay/live no-gap watch, cursors, owner hints, compaction floor, expired relist | +| Integration | d2b-bus named streams; controller toolkit | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | deterministic watch/compaction/disconnect/fan-in tests | +| Removal proof | Not applicable | + +### ADR046-store-003 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-store-001; storage/broker integrator | +| Current source | `nixos-modules/storage-json.nix`, `packages/d2b-priv-broker/src/ops/storage_contract.rs`, existing marker/ownership tests | +| Reuse action | adapt | +| Destination | `packages/d2b-resource-store-redb/src/backup.rs`, `migration.rs`; generated v3 storage row | +| Detailed design | fd-backed provision/open, marker identity, logical backup, staged restore/upgrade, corruption quarantine | +| Integration | Broker/Host/Guest storage owner passes File to Zone runtime | +| Data migration | Destructive v3 bootstrap; v3-to-v3 logical restore | +| Validation | marker replacement, crash publication, backup/restore/upgrade tests | +| Removal proof | Not applicable | diff --git a/docs/specs/ADR-046-resources-credential.md b/docs/specs/ADR-046-resources-credential.md new file mode 100644 index 000000000..2f612be58 --- /dev/null +++ b/docs/specs/ADR-046-resources-credential.md @@ -0,0 +1,2100 @@ +# ADR 0046 Credential ResourceType + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-resources-credential` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-provider-credential-secret-service`, `d2b-provider-credential-entra`, `d2b-provider-credential-managed-identity`, Credential controller, Nix Credential compiler | +| Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-provider-model-and-packaging`, `ADR-046-primitive-resource-composition`, `ADR-046-componentsession-and-bus` | +| Supersedes | Current v3 `CredentialProvider` trait, `CredentialStatus`, `CredentialPlane` model in `d2b-realm-provider/src/credential.rs` and `provider.rs` | + +## Purpose + +This spec defines the `Credential` ResourceType, its opaque lease/typed +operation model, its placement and scope constraints, its zero-secret-bytes +invariant for all persistent and observable surfaces (resource store, status, +audit, OTEL, logs), its end-to-end sensitive output delivery contract via dedicated +Noise ComponentSession, the three initial Credential Provider dossiers +(`credential-secret-service`, `credential-entra`, +`credential-managed-identity`), and every work item required to take these from +the v3 baseline to a running d2b 3.0 controller. + +## Core design principle: persistent and observable surfaces are zero-secret + +`spec`, `status`, the resource store, revision/WAL, bus routing DTOs, error +messages, audit records, OTEL spans, metrics, and all log lines **never +contain secret material** in any field or byte. This invariant is unconditional +for those surfaces: + +- `spec` contains only non-secret declarative fields (providerRef, ownerRef, + scope/audience selectors, rotation/expiry policy, and allowed operation + classes). +- `status` contains only opaque lease identifiers, generation counters, expiry + timestamps, and phase/condition/outcome values. It never contains token bytes, + key material, passwords, connection strings, or byte buffers. +- The resource store row, redb WAL, and revision log never contain secret bytes. +- Resource API routing DTOs, d2b-bus request/response envelopes, and + ResourceRef handles never carry secret bytes. The sole exception is the + dedicated credential-delivery ComponentSession record described in + §Credential-delivery endpoint contract: token bytes (`AcquireToken`, + `RefreshToken`) and signature bytes (`SignChallenge`) are delivered + exclusively through end-to-end Noise-encrypted records between the + Credential Provider and the authorized consumer Provider process. + d2b-bus authorizes the route and forwards the opaque Noise-protected + records without terminating or decrypting them. +- Error/outcome messages are bounded, redacted, and must not echo request + arguments. +- Audit records carry only the stable code plus the opaque lease handle + digest. They never carry payload content, token prefix/suffix, or + provider-internal diagnostics. +- OTEL spans and metrics never label secret bytes, provider-internal identity, + or token scope values. + +Credential resource identity is non-secret but sensitive, high-cardinality +metadata. Among observable outputs, the only permitted destination for +`metadata.name`, the canonical Credential ResourceRef, the Credential UID, or +any digest derived from one of them is an authorization-controlled, bounded +Zone audit record. The audit schema below is stricter: it retains only +`resource_name_digest`, never the raw name, ResourceRef, or UID. Credential +identity is never copied into status (including `status.update` or +`status.provider`), errors, logs/Debug output, OTEL Resource attributes, span +attributes, or metric labels. This restriction does not remove identity from +the resource envelope or from authenticated internal routing/session bindings +that require the ResourceRef and UID and are not observable output surfaces. + +The injected credential client/port in the Credential Provider process acquires +the secret from the external service. Token delivery to the authorized consumer +Provider Process uses the end-to-end Noise channel defined in +§Credential-delivery endpoint contract; no intermediate process terminates +or stores that channel's content. + +## Credential resource schema + +### Spec + +#### Three-layer spec shape (D089) + +D089 freezes Credential spec as three layers. Layer 1 is the universal Resource +envelope and metadata. Layer 2 is the Credential base spec at top-level +`spec.*`, including `spec.providerRef`; the lease, scope, audience, rotation, +expiry, revocation, consumer, and allowed-operation fields documented here are +base fields. Layer 3 is the optional canonical selected-Provider extension +`spec.provider = { schemaId, schemaVersion, settings }`; it is the only +Provider-specific desired extension and replaces the former `providerSettings` +shape. It omits `providerRef` and +`observedProviderGeneration`: `spec.providerRef` is base, and spec is desired +rather than observed. + +**D091 update policy.** The universal base spec carries `spec.updatePolicy` for +every Credential: disruptive changes default to manual, while automatic +non-disruptive upgrades are permitted by policy. A `spec.provider` extension MAY +add provider-specific knobs, but MUST NOT bypass or weaken base +`spec.updatePolicy`. + +Every Credential Provider `ResourceApiBinding` MUST implement the exact +Credential base spec schema version and fingerprint, accept the canonical +minimal valid base Spec, and pass base lifecycle/status/error/finalizer +conformance. A Provider MAY reject an optional base capability only through its +signed standard capability matrix and a typed provider-neutral +`unsupported-capability` error; it MUST NOT ignore, reinterpret, rename, +duplicate, weaken, or require extension data for base-required behavior. +`spec.provider.settings` is strict deny-unknown, bounded, schema-versioned and +digested, validated against `spec.providerRef` at Nix build and API admission, +and fails with `spec-provider-schema-invalid` or `spec-provider-shadow` when +invalid or shadowing/restating/overriding/renaming/duplicating a base field. +Shared Credential semantics are promoted to the Credential base spec and never +live in `spec.provider`; generic CLI/controllers operate on base spec plus base +status. For the same Provider, the `spec.provider` and `status.provider` schemas +align. + +Provider resource dossiers below retain the D075 Provider self-description shape +(`spec.artifactId`, `spec.config`) because a Provider has no non-circular +`spec.providerRef`. + +**Non-exportable by default (D096).** Credential and token resources are +non-exportable by default: no `ResourceExport` may name a `Credential` and no +credential/token bytes cross a Zone. Per D093, Entra ID identity stays a +same-Zone identity Guest (`identityGuestRef`); there is no cross-Zone export of +Entra credentials unless a future, explicitly reviewed export capability is +added. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Credential +metadata: + name: work-entra + zone: dev + uid: + generation: 1 + revision: + ownerRef: null # optional; see §Ownership + finalizers: [] + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:00:00Z +spec: + providerRef: Provider/credential-entra + scope: + executionRef: Guest/work-vm # optional Host or Guest placement restriction + domainFilter: user # optional: system | user + userRef: User/alice # optional; required when domainFilter=user + audience: azure-resource-manager # Provider-validated opaque audience token + consumerRef: Provider/display-wayland # optional; restricts which Provider may acquire + allowedOperations: + - acquire-token + - refresh-token + - revoke-token + - inspect-metadata + rotation: + policy: proactive # on-expiry | proactive | on-demand + proactiveWindowMs: 300000 # rotate when remaining lifetime < this value + maxLeaseLifetimeMs: 3600000 # maximum lease lifetime cap + expiry: + hardDeadlineMs: 28800000 # hard maximum from issue time; 0 = provider default + revocation: + onOwnerDelete: immediate # immediate | drain-leases + onProviderGeneration: immediate # immediate | drain-leases + provider: + schemaId: credential-entra.d2bus.org/spec + schemaVersion: "1.0" + settings: {} # non-secret Provider-specific desired settings +status: + observedGeneration: 1 + phase: Ready + conditions: + - type: CredentialReady + status: "True" + reason: lease-active + message: "" + observedGeneration: 1 + lastTransitionAt: 2026-07-22T00:00:01Z + - type: RotationDue + status: "False" + reason: within-window + observedGeneration: 1 + lastTransitionAt: 2026-07-22T00:00:01Z + lastReconciledAt: 2026-07-22T00:00:01Z + startedAt: 2026-07-22T00:00:01Z + completedAt: null + outcome: null + credential: + leaseHandle: + leaseState: Active # Active | Expired | Revoked | Unknown + rotationGeneration: 1 + sourceVersion: + expiresAtUnixMs: 1753228801000 + issuedAtUnixMs: 1753225201000 + lastRefreshedAt: 2026-07-22T00:00:01Z + lastRotatedAt: null + placementBinding: user-agent # user-agent | host-system | guest-agent +``` + +### Spec field reference + +| Field | Type | Required | Rules | +| --- | --- | --- | --- | +| `providerRef` | ResourceRef | Yes | Resolves `Provider/credential-*` in same Zone; must be Ready | +| `scope.executionRef` | ResourceRef | No | `Host/` or `Guest/`; restricts which execution context may acquire a lease; null = any execution context in Zone | +| `scope.domainFilter` | enum | No | `system` or `user`; restricts process domain allowed to acquire; null = any domain allowed by execution context | +| `scope.userRef` | ResourceRef | No | `User/`; required when `scope.domainFilter=user`; restricts acquiring user identity | +| `audience` | string | Yes | Provider-validated non-secret audience token (e.g. Azure resource URI prefix, Secret Service collection alias, IMDS audience ID); max 256 chars; charset restricted to reject secret shapes (same rules as `OpaqueAzureRef`) | +| `consumerRef` | ResourceRef | No | `Provider/`; restricts which Provider may invoke credential-bound methods; the consumer Provider's signed component descriptor and Role/RoleBinding selects the exact receiving component and Process within that Provider; null = any subject authorized by RBAC; must be in same Zone; no arbitrary component fallback | +| `allowedOperations` | enum[] | Yes | Bounded non-empty closed set of typed operation classes; see §Operation classes | +| `rotation.policy` | enum | Yes | `on-expiry` (rotate only after expiry), `proactive` (rotate before expiry window), or `on-demand` (never auto-rotate; consumer drives refresh explicitly) | +| `rotation.proactiveWindowMs` | u64 | No | Required when policy=proactive; rotate when remaining lifetime is below this threshold; max 1 800 000 ms (30 min); must be less than `rotation.maxLeaseLifetimeMs / 2` | +| `rotation.maxLeaseLifetimeMs` | u64 | No | Cap on Provider-granted lease lifetime; 0 = use Provider default cap; max `MAX_PROVIDER_LEASE_LIFETIME_MS` (7 days, per main contract constant) | +| `expiry.hardDeadlineMs` | u64 | No | Hard maximum total lifetime from issue; 0 = use Provider default; must not exceed `rotation.maxLeaseLifetimeMs` if both are non-zero | +| `revocation.onOwnerDelete` | enum | Yes | `immediate` (revoke all active leases before finalizer completes) or `drain-leases` (allow active leases to expire naturally) | +| `revocation.onProviderGeneration` | enum | Yes | `immediate` (revoke all active leases on Provider generation change) or `drain-leases` | +| `identityGuestRef` | ResourceRef | Conditional | For interactive/identity-Guest-grounded providers (e.g. `credential-entra`, D093): required same-Zone `Guest/` whose NixOS system composes the identity implementation (Entrablau) and hosts the login/token service; login/token acquisition is grounded in this Guest, never a Host login | +| `loginEndpointRef` | ResourceRef | Conditional | For interactive providers: required `Endpoint/` (D092) in the identity Guest implementing the standard typed login/token service; `scope.executionRef` and consumer placement must be same-Zone-consistent with it | + +Provider-specific config fields are permitted only in bounded +`spec.provider.settings` validated against the signed Provider schema selected by +`spec.providerRef`. No provider-specific field may accept secret bytes. + +### Interactive login base operations and status (D093) + +Interactive, identity-Guest-grounded credential providers share a +ResourceType-common (base) interactive-login contract so generic CLI/controllers +work identically across implementations. The Credential base exposes three +operations: + +| Operation | Semantics | +| --- | --- | +| `BeginLogin` | Open an authenticated end-to-end session to the login service in `identityGuestRef` (via `loginEndpointRef`) and start an interactive login; returns a bounded projected status, never a token | +| `ObserveLogin` | Stream/poll the bounded non-secret login-session state and progress metadata | +| `CancelLogin` | Cancel an in-flight login session by generation | + +Base `status.resource` interaction fields (non-secret; on top of the universal +base including `status.update`): + +| Field | Values | +| --- | --- | +| `interactionState` | `NotRequired`, `Required`, `Starting`, `AwaitingUser`, `Authenticated`, `Failed`, or `Unknown` | +| `loginSessionGeneration` | Numeric; increments per `BeginLogin` | +| `loginDeadline` | RFC 3339 UTC deadline for the in-flight interactive login | +| `challengeMetadata` | Bounded, non-secret, closed-enum challenge/progress descriptors only | + +Status/audit/OTEL MUST NOT carry a device code that confers authority, a login +URL, a cookie, a token, or any user PII. The interactive byte/UI stream is a +named ComponentSession stream established **direct to the Guest login service**, +never proxied as status. The provider extension (`status.provider`) carries only +Entra-specific non-secret modes/reasons. Expedited reconcile (D090) may return +`interactionState: Required`/`Starting` after create/update but never blocks on +human login past `loginDeadline`. + +#### Three-layer status shape (D088) + +D088 freezes `Credential` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `Credential`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +**D091 update currency.** Every Credential includes universal `status.update` +with `state` +(`Current|UpdateAvailable|UpgradeRequired|Upgrading|Blocked|Unknown`), `reasons` +(`CoreGenerationChanged|ProviderGenerationChanged|ArtifactChanged|ImageOrSystemGenerationChanged|SpecChanged|DependencyChanged|SecurityPolicyChanged`), +bounded non-secret observed/target generation and digest IDs, `disruption` +(`None|Reload|Restart|Recycle|Replace`), `preserveState`, optional +`operationId`, `lastAssessedAt`, and bounded/truncated `owned:{count,refs}` and +`dependencies:{count,refs}`. Credential-specific currency refinements live in +`status.resource` and never in `status.provider`; controllers set +`status.update` via `assess_update` on core/provider/artifact/spec/dependency/ +security-policy triggers and MUST report `UpgradeRequired` for disruptive +changes rather than applying them in place. Credential currency is limited to +provider/artifact generation and bounded non-secret digests; rotation is a +credential operation, not an upgrade, and no secret material appears in +`status.update`. + +The existing `status.credential` sub-object is carried within `status.resource` +as `status.resource.credential` by the mapping convention. `Credential` has +multiple implementations (`credential-secret-service`, `credential-entra`, and +`credential-managed-identity`). Non-secret lease metadata, including expiry, +issue/refresh/rotation timestamps, opaque non-authorizing lease handles, +source/audience-derived bounded identifiers, and placement binding, is frozen in +`status.resource` and MUST be identical across all implementations. Secret bytes +MUST NOT appear in status. Implementation-specific observation belongs only in +that implementation's `status.provider.details`; shared fields MUST NOT be +duplicated there. + +Credential identity remains in the resource envelope, not in status: +`metadata.name`, the Credential ResourceRef/UID, `resource_name_digest`, and any +other value derived from them MUST NOT be copied into the universal status +base, `status.update`, `status.resource`, or `status.provider.details`. + +| Field | Rules | +| --- | --- | +| `leaseHandle` | Opaque, bounded (max 256 chars), non-secret handle assigned by the provider process; stable across refreshes within one rotation generation; suitable for d2b-bus method routing; never a token or partial token | +| `leaseState` | `Active`, `Expired`, `Revoked`, or `Unknown` | +| `rotationGeneration` | Monotonic counter incremented on rotation; bounded u64 | +| `sourceVersion` | Opaque, bounded, non-secret credential source version from the Provider port | +| `expiresAtUnixMs` | RFC 3339-equivalent Unix milliseconds of lease expiry; 0 when lease is absent | +| `issuedAtUnixMs` | Unix milliseconds of last successful lease acquisition | +| `lastRefreshedAt` | RFC 3339 UTC of last successful lease refresh | +| `lastRotatedAt` | RFC 3339 UTC of last successful rotation cycle | +| `placementBinding` | Closed stable code: `user-agent`, `host-system`, or `guest-agent` | + +### Status conditions + +| Condition type | Meaning | +| --- | --- | +| `CredentialReady` | `True` when leaseState=Active and lease is not within rotation window | +| `RotationDue` | `True` when policy=proactive and remaining lifetime < proactiveWindowMs | +| `ProviderUnavailable` | `True` when the credential provider process cannot be reached | +| `LeaseRevoked` | `True` when leaseState=Revoked and no replacement has been issued | + +## Operation classes + +`allowedOperations` uses a closed, non-secret typed enum. It never encodes +audience bytes, scopes, claims, or credential content: + +| Class | Meaning | +| --- | --- | +| `acquire-token` | Consumer may request the provider process to use the credential to obtain a service token | +| `refresh-token` | Consumer may request the provider process to refresh an existing token | +| `revoke-token` | Consumer may request the provider process to revoke an existing token | +| `sign-challenge` | Consumer may request the provider process to sign a challenge without token export | +| `inspect-metadata` | Consumer may inspect opaque lease metadata without acquiring a token | + +Operation-class validation: + +- a `consumerRef` that is not authorized for a requested class fails closed; +- operation class sets are not extensible at runtime; new classes require a + spec generation change and Provider schema update; +- the `inspect-metadata` class alone never grants token acquisition; +- every credential-bound service method maps to exactly one of these classes; + base interactive methods reuse the matching exact class and introduce no + Role-subresource alias. + +## Placement model: User, Host, and Guest + +A Credential resource belongs to one Zone. Its placement within that Zone is +governed by `scope.executionRef` and `scope.domainFilter`. + +### User-domain Credential + +```yaml +spec: + providerRef: Provider/credential-secret-service + scope: + executionRef: Host/host-system + domainFilter: user + userRef: User/alice +``` + +The credential provider process runs in the user domain of `Host/host-system` +as `User/alice`. Only processes running as `User/alice` in the user domain of +that Host may invoke credential-bound methods. The Provider process is a +user-domain Process resource under the same Host. + +### Host-system Credential + +```yaml +spec: + providerRef: Provider/credential-managed-identity + scope: + executionRef: Host/host-system + domainFilter: system +``` + +The credential provider process runs as a system-domain Process under +`Host/host-system`. Only system-domain processes on that Host with an +authorized `consumerRef` may acquire a lease. + +### Guest Credential + +```yaml +spec: + providerRef: Provider/credential-entra + scope: + executionRef: Guest/work-vm + domainFilter: user + userRef: User/alice +``` + +The Credential is served by a provider process running inside the Guest VM. +The provider process is a user-domain Process under `Guest/work-vm`. Cross-Zone +consumption is not permitted in the initial contract. + +### Placement binding derivation + +The controller derives `status.credential.placementBinding` from: + +- `user-agent` when `scope.domainFilter=user` (regardless of Host or Guest); +- `host-system` when `scope.executionRef` resolves to a Host and `domainFilter=system`; +- `guest-agent` when `scope.executionRef` resolves to a Guest and + `domainFilter=system`. + +Scope validation at create/update: + +- `scope.executionRef` must resolve to a Ready Host or Guest in the same Zone; +- `scope.userRef` must resolve to a Ready User in the same Zone; +- `userRef` is required when `domainFilter=user`; +- the resolved Provider must support the derived placement binding; +- the resolved consumerRef (if present) must be in the same Zone and be + compatible with the placement. + +## Ownership and finalizer model + +### ownerRef + +`metadata.ownerRef` is optional. Common ownership patterns: + +- a Guest resource (e.g. `Guest/work-vm`) owns its Credentials; VM + controller creates and manages them; +- a Provider resource (e.g. `Provider/display-wayland`) owns Credentials it + declares in its root config/descriptor; +- a standalone Credential has no ownerRef and is managed directly. + +Any committed child mutation of the owning resource triggers the owner's +controller reconcile loop. The Credential controller does not implicitly +become a child of its credential Provider. + +### Finalizers + +| Finalizer ID | Owner | Meaning | +| --- | --- | --- | +| `credential.d2bus.org/provider-revoke` | Credential controller | Revoke all active leases before deletion proceeds; honors `revocation.onOwnerDelete` policy | +| `credential.d2bus.org/consumer-drain` | consumerRef controller | Drain in-flight operations before Provider releases the lease handle | + +Finalizer execution order: `consumer-drain` completes before +`provider-revoke`. A missing consumerRef removes the `consumer-drain` finalizer +automatically. + +A terminal revocation (`leaseState=Revoked`) or a confirmed expiry +(`leaseState=Expired`) satisfies the `provider-revoke` finalizer immediately +without further network calls. An ambiguous or unknown state requires one +revoke attempt before the controller reports `blocked` with a `provider-unavailable` +condition and sets a bounded `requeue-at`. + +## RBAC + +The Credential ResourceType uses the ordinary resource verbs plus the canonical +Credential-specific resource verbs `use-credential` and `admin-credential`. +They are resource verbs in the native Role engine, not `sessionVerbs` and not +ad hoc service-method verbs: + +| Verb | Who | Meaning | +| --- | --- | --- | +| `get` | Any authorized subject | Read Credential metadata/spec/status (no secret bytes present) | +| `list` | Any authorized subject | List Credentials matching filters | +| `watch` | Any authorized subject | Watch Credential events | +| `create` | Deployer/system-core configuration controller | Create a new Credential resource; also requires `admin-credential` on subresource `create` | +| `update-spec` | Deployer/system-core configuration controller | Replace Credential spec; also requires `admin-credential` on subresource `update-spec` | +| `update-status` | Credential controller only | Update Credential status | +| `update-finalizers` | Credential controller, consumerRef controller | Add/remove owned finalizers | +| `delete` | Deployer/system-core configuration controller | Request Credential deletion; also requires `admin-credential` on subresource `delete` | +| `use-credential` | Consumer subject authorized via consumerRef | Invoke an admitted credential operation under the exact `allowedOperations` value used as the Role subresource | +| `admin-credential` | Authorized operator/configuration controller | Supplement ordinary Credential CRUD under the exact matching `create`, `update-spec`, or `delete` subresource; it grants no CRUD action by itself | + +Credential Roles use the existing `subresources` rule field. No alternate +Credential-operation Role field exists. A rule granting either +Credential-specific verb MUST name exact, non-empty Credential subresources. A +consumer Role rule is: + +```yaml +rules: + - resourceTypes: [Credential] + verbs: [use-credential] + subresources: [acquire-token, refresh-token] + resourceNames: [work-entra] + zones: [dev] + executionRefs: [Guest/work-vm] + sessionVerbs: [] +``` + +A separate deployer Role rule is: + +```yaml +rules: + - resourceTypes: [Credential] + verbs: [create, update-spec, delete] + subresources: [] + resourceNames: [work-entra] + zones: [dev] + executionRefs: [] + sessionVerbs: [] + - resourceTypes: [Credential] + verbs: [admin-credential] + subresources: [create, update-spec, delete] + resourceNames: [work-entra] + zones: [dev] + executionRefs: [] + sessionVerbs: [] +``` + +The closed admission mapping is: + +| Credential action | Required resource verb | Required Role `subresources` value | +| --- | --- | --- | +| `AcquireToken`, `BeginLogin` | `use-credential` | `acquire-token` | +| `RefreshToken` | `use-credential` | `refresh-token` | +| `RevokeToken`, `CancelLogin` | `use-credential` | `revoke-token` | +| `SignChallenge` | `use-credential` | `sign-challenge` | +| `InspectMetadata`, `ObserveLogin` | `use-credential` | `inspect-metadata` | +| Resource create | `admin-credential` in addition to ordinary `create` | `create` | +| Resource spec update | `admin-credential` in addition to ordinary `update-spec` | `update-spec` | +| Resource delete | `admin-credential` in addition to ordinary `delete` | `delete` | + +For each service method, the operation class is both the exact +`spec.allowedOperations` value and the exact `use-credential` Role subresource. +Effective permission is their intersection, further narrowed by `consumerRef`, +scope, and structural Provider/component checks. The closed use subresource set +is therefore exactly `acquire-token`, `refresh-token`, `revoke-token`, +`sign-challenge`, and `inspect-metadata`. An empty/wildcard/unknown +subresource, a noncanonical method-name alias, or a verb/subresource mismatch +fails admission. Administrative admission separately requires the matching +ordinary CRUD verb and `admin-credential` subresource; only exact `create`, +`update-spec`, and `delete` pairs are valid. + +Status ownership: only the Credential controller's exact registered process +generation may call `UpdateStatus` on a Credential resource. + +## Credential-bound service methods + +The `d2b.credential.v3` protobuf/ttrpc service is served by the credential +provider process and routed through d2b-bus over ComponentSession. It is not +served directly by the Zone runtime. + +Route key example: + +```text +( + Zone=dev, + service=d2b.credential.v3, + method=AcquireToken, + target=Credential/work-entra, + schema_fingerprint=, + provider_generation=, + controller_generation= +) +``` + +### Service methods + +| Method | Required exact operation class | Required Role permission | Outer DTO fields | Token bytes | +| --- | --- | --- | --- | --- | +| `AcquireToken` | `acquire-token` | `use-credential/acquire-token` | `AcquireTokenResponse`: leaseHandle, sourceVersion, rotationGeneration, expiresAtUnixMs | raw token bytes in a dedicated sensitive ComponentSession record (see §Credential-delivery endpoint contract) | +| `RefreshToken` | `refresh-token` | `use-credential/refresh-token` | `RefreshTokenResponse`: leaseHandle, sourceVersion, rotationGeneration, new expiresAtUnixMs | raw token bytes in a dedicated sensitive ComponentSession record | +| `RevokeToken` | `revoke-token` | `use-credential/revoke-token` | `RevokeTokenResponse`: closed revocation result (Revoked or AlreadyRevoked), revokedAtUnixMs | none | +| `SignChallenge` | `sign-challenge` | `use-credential/sign-challenge` | `SignChallengeResponse`: outcome code | signature bytes in a dedicated sensitive ComponentSession record (same channel as token delivery) | +| `InspectMetadata` | `inspect-metadata` | `use-credential/inspect-metadata` | `InspectMetadataResponse`: leaseState, rotationGeneration, sourceVersion, expiresAtUnixMs | none | + +The `RevokeToken` and `InspectMetadata` response DTOs are non-secret: they carry +only opaque identifiers, outcome codes, and timestamps. `AcquireToken`, +`RefreshToken`, and `SignChallenge` additionally deliver secret bytes in a +dedicated sensitive ComponentSession record (see §Credential-delivery endpoint +contract). Every method: + +- rejects a request where the authenticated subject is not the declared + `consumerRef` (when consumerRef is set); +- rejects before Provider/Endpoint dispatch unless the Role engine allows the + exact resource verb and mapped `subresources` value above; +- rejects an operation class not in `spec.allowedOperations`; +- returns a stable closed error code rather than provider-internal diagnostics; +- carries operation/idempotency/correlation IDs from d2b-bus context; +- enforces a per-call deadline propagated from the d2b-bus context. + +Token bytes and signature output travel end-to-end in the Noise-encrypted +sensitive ComponentSession record between the Credential Provider and the +authorized consumer Provider Process; they never enter the outer DTO, the +resource spec/status, the store, or any audit/log surface. The provider process's +injected credential client/port acquires the token from the external service and +delivers it through this bounded record; there is no ambient SDK chain or fallback +path. The consumer Provider's signed component descriptor and Role/RoleBinding +determines the exact receiving Process; no arbitrary component is permitted. + +## Credential-delivery endpoint contract + +Token and signature delivery uses a dedicated end-to-end Noise ComponentSession +established directly between the Credential Provider process and the authorized +consumer Provider process. d2b-bus authorizes the route and then forwards opaque +Noise-protected records between the two endpoints; it never terminates, decrypts, +or stores the delivery channel's content. This channel is used for all three +sensitive-output methods: `AcquireToken`, `RefreshToken`, and `SignChallenge`. + +### Session profile + +The delivery channel MUST use `Noise_KK_25519_ChaChaPoly_SHA256`. The NN +profile is forbidden for sensitive output delivery; any anonymous-channel attempt +is rejected immediately. Both parties require enrolled static keys. + +- **Credential Provider** key: registered at Provider installation; the bus + holds only the public key. +- **Consumer Provider** key: extracted from the consumer Provider's signed + component descriptor. The consuming component/Process identity is derived from + the consumer Provider's Role/RoleBinding, not from an arbitrary component + selection. + +### Binding contract + +Each delivery session binds: + +| Field | Description | +| --- | --- | +| `credentialRef` | Canonical Credential ResourceRef: `Credential/` | +| `credentialUID` | Credential resource UID (stable across spec updates) | +| `credentialGeneration` | Credential resource generation at time of delivery | +| `consumerProviderRef` | `Provider/` matching `spec.consumerRef` | +| `consumerComponentGeneration` | Consumer Provider component generation (from signed descriptor) | +| `audience` | Closed `spec.audience` value (opaque; not echoed in logs or spans) | +| `operationClass` | Closed operation class (`acquire-token`, `refresh-token`, or `sign-challenge`) | +| `expiryUnixMs` | Absolute expiry of this delivery session; clipped to `spec.rotation.maxLeaseLifetimeMs` | +| `deadlineUnixMs` | Hard session close deadline; must be ≤ `expiryUnixMs` | +| `routeDigest` | Digest of the bus-authorized route parameters (Zone, consumer, provider) | +| `schemaVersion` | Fixed version of this binding contract | +| `maxTokenBytes` | Closed upper bound on sensitive output size (token or signature bytes) for this delivery session | +| `transcriptDigest` | Noise transcript digest after handshake completion, before any record | + +The binding is constructed by d2b-bus during route authorization and conveyed to +both endpoints in the Noise handshake prologue. Both parties MUST verify the +full binding before accepting records. + +### Security requirements for the delivery session + +1. **Enrolled keys only**: Both static keys must be enrolled and verified at + session initiation. Any NN/NX/N pattern or raw-token bootstrap attempt is + rejected immediately; the session is closed and zeroized. + +2. **Replay-safe sequence**: Each delivery session carries a monotonically + increasing per-credential-UID sequence number. A replay of a prior session's + ciphertext at the same or lower sequence number is rejected. + +3. **Bounded output size**: The sensitive output record (token or signature bytes) + MUST NOT exceed `maxTokenBytes` (delivery-session bound). Any record exceeding + this size is rejected; the channel is closed and zeroized immediately. + Fragmentation is not permitted unless the protocol explicitly bounds each + fragment and the reassembled record does not exceed `maxTokenBytes`. + +4. **Zeroizing buffers**: The delivery record's plaintext MUST be zeroed in memory + immediately after the consumer extracts it. The Credential Provider MUST zero + the plaintext source after encryption. All intermediate buffers involved in + serialization and deserialization are zeroizing types. + +5. **Redacted Debug**: All credential-bearing Rust types involved in delivery + (request, response, record wrapper, buffer) MUST implement `Debug` via a + redacted hand-written impl that emits only the type name and a placeholder + value. Derived `Debug` is forbidden for these types. + +6. **No automatic success-shaped replay**: After any ambiguous delivery outcome + (timeout, partial write, disconnection before confirmation), the Credential + Provider MUST NOT automatically retry with the same record. The consumer + must re-initiate via `AcquireToken`, `RefreshToken`, or `SignChallenge`, + which establishes a new delivery session with a fresh sequence number. + +7. **Immediate close/zeroize**: After the delivery record is confirmed received + (consumer ACKs the delivery session record), the Credential Provider closes + the delivery channel and zeroizes all session key material. The consumer + similarly closes and zeroizes after extraction. The channel is not reused + across multiple deliveries. + +### RBAC enforcement point + +d2b-bus performs the following checks before authorizing the route: + +- RBAC `use-credential` for the authenticated consumer Provider subject and + Credential ResourceRef under the exact operation-class Role subresource; +- `spec.consumerRef` matches the consumer Provider identity from the signed + component descriptor; +- `spec.allowedOperations` and the Role `subresources` both include the same + exact operation class; +- The current lease state permits the operation class (Active/RotationDue for + `refresh-token`; no existing active lease required for `acquire-token` + subject to `maxLeases`); +- No Role/RoleBinding or Provider generation change has revoked the + authorization since the last audit checkpoint. + +After these checks pass, d2b-bus forwards opaque Noise-encrypted records +between the two endpoints until the delivery session closes. Bus never buffers +or stores the records. + +## Noise session binding for credential-bound calls + +Non-secret credential operations (`RevokeToken`, `InspectMetadata`) traverse +the standard ComponentSession/d2b-bus stack defined in +`ADR-046-componentsession-and-bus`: + +1. The consumer process authenticates to d2b-bus using a local + `Noise_NN_25519_ChaChaPoly_SHA256` ComponentSession over a Unix socketpair + or seqpacket transport. The transport provides `SO_PEERCRED` and process + identity evidence mapping the consumer to an exact Zone subject. + +2. d2b-bus checks native RBAC `use-credential` for the authenticated subject, + target Credential ResourceRef, exact operation-class Role subresource, and + current Role/RoleBinding/Provider revisions, then requires that same exact + class in `spec.allowedOperations`. + +3. For non-secret methods, d2b-bus routes the call to the exact credential + provider process using an enrolled `Noise_KK_25519_ChaChaPoly_SHA256` + session. The credential provider's static public key is registered at + Provider installation; the bus holds only the public key. + +4. The credential provider process responds with the non-secret outer DTO. + The `AuthenticatedSubjectContext` carried by the bus routing includes + `credentialRef`, `operationClass`, `consumerRef`, and the session + generation/transcript digest for audit. + +For secret-returning methods (`AcquireToken`, `RefreshToken`, `SignChallenge`), +d2b-bus additionally initiates the credential-delivery endpoint contract +(§Credential-delivery endpoint contract): it authorizes the route, then +forwards opaque Noise-encrypted records between the Credential Provider and +the consumer Provider without terminating or decrypting the delivery channel. +The outer routing channel uses KK; the delivery channel is a separate nested +KK session whose plaintext is never accessible to the bus. + +A credential-bound ComponentSession carries its authorization lease revision. +When the lease expires or is invalidated by a Role/RoleBinding or Provider +generation change, d2b-bus closes the stream and refuses new requests on the +existing session. The consumer must re-establish both the ComponentSession and +re-request the credential-bound operation. + +## Rotation, expiry, and revocation state machine + +```text +Absent (no active lease) + |-- AcquireToken ---------> Active (leaseState=Active, CredentialReady=True) + +Active + |-- proactive window ------> RotationDue (CredentialReady=True, RotationDue=True) + |-- RefreshToken ----------> Active (rotationGeneration unchanged, new expiresAtUnixMs) + |-- [rotation] ------------> Active (rotationGeneration+1, new leaseHandle) + |-- Provider gen change ---> [revocation.onProviderGeneration=immediate] -> Revoked + | [revocation.onProviderGeneration=drain-leases] -> Expired on deadline + |-- RevokeToken -----------> Revoked (leaseState=Revoked, CredentialReady=False) + |-- expiry deadline -------> Expired (leaseState=Expired, CredentialReady=False) + +RotationDue + |-- rotation attempt ------> Active (new rotationGeneration) + |-- rotation fails --------> RotationDue/Degraded (bounded retry) + |-- final retry fails -----> Failed (CredentialReady=False, outcome=rotation-failed) + +Expired + |-- AcquireToken ----------> Active (new lease; rotationGeneration+1) + +Revoked + |-- AcquireToken ----------> Active if policy permits re-acquisition + |-- resource delete -------> provider-revoke finalizer satisfied immediately +``` + +### Rotation algorithm + +For `policy=proactive`: + +1. Controller sets `RotationDue=True` condition when + `now + proactiveWindowMs >= expiresAtUnixMs`. +2. Controller issues a new AcquireToken request to the provider process with + `requested_expiry_unix_ms` = `now + maxLeaseLifetimeMs` (capped by provider + maximum). +3. On grant: increment `rotationGeneration`, store new `leaseHandle` and + `expiresAtUnixMs`, commit status, clear `RotationDue`. +4. Old lease is valid until the new one is active. A second AcquireToken with + the same `idempotency_key` returns the same grant. +5. On failure: bounded retry under `requeue-at` disposition; degrade after + final retry. + +For `policy=on-demand`: controller does not auto-rotate. Consumer explicitly +calls `RefreshToken` or `RevokeToken`+`AcquireToken`. Controller only monitors +expiry for status conditions. + +### Revocation on Provider generation change + +When the Credential provider Process is replaced (new Provider generation): + +- `immediate` policy: controller immediately calls `RevokeToken` against the + old provider process before the generation changes. If the old process is + unreachable, the controller marks `leaseState=Revoked` and writes a bounded + audit record. +- `drain-leases` policy: controller does not actively revoke. Active leases + expire by their natural deadline. Status remains `Active` until expiry. + +## Async reconciliation + +The Credential controller handles async reconciliation following the +`ADR-046-resource-reconciliation` loop model with these Credential-specific +behaviors: + +**D090 expedited reconcile.** Authorized Credential `Create`, `UpdateSpec`, and +`Delete` calls MAY set `waitForReconcile`. Under one mutation ticket, +`operationId`, and deadline, Core admission and the reserved-revision redb commit +run in parallel with controller preflight/plan, but the controller MUST NOT +perform external effects, finalizer release, or status mutation until Core +supplies `CommittedRevisionProof {resourceUid, generation, revision, +operationId}`; DB failure aborts with no effect. The API returns the committed +object plus one-pass projected layered status, `disposition` +(`Converged|Progressing|Blocked|UpgradeRequired|Failed`), `statusPersistence` +(`pending|committed`), and the last persisted status revision. The durable +commit is never rolled back on reconcile timeout or failure; effect idempotency +keys derive from `(UID,generation,revision,operationId)`, and the expedited pass +uses a bounded priority lane in the same per-resource single-flight. + +### Controller descriptor + +```yaml +providerId: Provider/credential- +controllerType: Credential +resourceTypes: [Credential] +watchSelectors: + - resourceType: Credential + providerRefFilter: Provider/credential- + - resourceType: Provider + nameFilter: credential- +dependencySelectors: + - resourceType: Provider + relationship: providerRef + - resourceType: Host + relationship: scope.executionRef + - resourceType: Guest + relationship: scope.executionRef + - resourceType: User + relationship: scope.userRef +ownerChildTriggers: [owned-resource-changed] +reconcileConcurrency: 8 +maxPendingResources: 256 +finalizers: [credential.d2bus.org/provider-revoke] +observeInterval: 30s # check expiry/rotation due; no external drift +``` + +### Reconcile lifecycle + +**Create**: controller acquires a lease if `allowedOperations` includes +`acquire-token`; sets `leaseState=Active`, writes status, sets +`CredentialReady=True`. + +**Spec update**: if `providerRef` changes, revoke old lease under old policy, +then acquire new lease under new provider. If scope/audience changes and the +provider requires re-acquisition, revoke old and acquire new. + +**Dependency-ready**: when the credential Provider transitions to Ready after a +Pending state, the controller re-attempts any pending lease acquisition. + +**Scheduled-observe**: every `observeInterval`, the controller calls +`InspectMetadata` on the provider to confirm `leaseState=Active` and check +whether rotation is due. This detects out-of-band revocations. + +**Deletion-requested**: trigger `provider-revoke` finalizer. On completion, +clear finalizer and allow core to emit `phase=Deleted`. + +**Provider-generation-changed**: apply `revocation.onProviderGeneration` +policy as described in §Rotation, expiry, and revocation. + +**Unknown state**: when the credential provider process is unreachable or +returns an error that cannot be resolved without external action, set +`phase=Degraded`, `ProviderUnavailable=True`, and `CredentialReady=False`. +Bounded retry under `requeue-at`. After retry exhaustion, set `phase=Failed`. + +### Idempotency + +Every AcquireToken/RefreshToken/RevokeToken call carries a stable +`idempotency_key` derived from the Credential UID, current `rotationGeneration`, +and the operation class. A duplicate acquire with the same key returns the +existing grant without double-issuing. + +## Errors + +Stable Credential-specific error codes: + +| Code | Meaning | +| --- | --- | +| `credential-not-found` | Credential resource does not exist in this Zone | +| `credential-provider-unavailable` | Credential provider process unreachable or not Ready | +| `credential-lease-expired` | Lease is past its expiry deadline | +| `credential-lease-revoked` | Lease was explicitly revoked | +| `credential-operation-denied` | Operation class not in `allowedOperations` or RBAC denied | +| `credential-consumer-mismatch` | Requesting subject does not match `consumerRef` | +| `credential-placement-mismatch` | Request execution context/domain does not match `scope` | +| `credential-rotation-failed` | Proactive rotation attempt failed after bounded retries | +| `credential-invariant-failure` | Provider returned a response failing invariant checks | +| `credential-schema-invalid` | Spec field fails validation at create/update | +| `credential-queue-pressure` | Provider lease table at capacity; retry after backpressure | + +All error messages are bounded (max 240 UTF-8 chars), stripped of control +characters, and must not contain token bytes, URLs, UUIDs, provider diagnostics, +host paths, connection string shapes, a Credential resource name/ResourceRef/ +UID, or any digest derived from Credential identity. + +## Audit + +Audit records for Credential operations: + +| Event | Fields retained | +| --- | --- | +| Credential resource create/update/delete | Zone, subject digest, `resource_name_digest`, ordinary mutation verb, matching `admin-credential` lifecycle-subresource decision, revision result | +| `AcquireToken` | Zone, subject digest, `resource_name_digest`, operation class, `use-credential/acquire-token` decision, `rotationGeneration`, outcome code, idempotency key digest | +| `RefreshToken` | Zone, subject digest, `resource_name_digest`, operation class, `use-credential/refresh-token` decision, `rotationGeneration`, outcome code, idempotency key digest | +| `RevokeToken` | Zone, subject digest, `resource_name_digest`, operation class, `use-credential/revoke-token` decision, `rotationGeneration`, revocation result code | +| `SignChallenge` | Zone, subject digest, `resource_name_digest`, operation class, `use-credential/sign-challenge` decision, outcome code (no signature bytes) | +| `InspectMetadata`, `ObserveLogin` | Zone, subject digest, `resource_name_digest`, `use-credential/inspect-metadata` decision, bounded outcome code | +| `BeginLogin` | Zone, subject digest, `resource_name_digest`, `use-credential/acquire-token` decision, bounded outcome code (no challenge/UI bytes) | +| `CancelLogin` | Zone, subject digest, `resource_name_digest`, `use-credential/revoke-token` decision, bounded outcome code | +| Rotation | Zone, `resource_name_digest`, trigger reason, old `rotationGeneration`, new `rotationGeneration`, outcome code | +| Provider generation change revocation | Zone, `resource_name_digest`, policy applied, outcome code | + +`resource_name_digest` is the existing bounded audit identity: SHA-256 of the +Credential resource name, never the raw name. It is emitted only into the +authorization-controlled Zone audit stream and, for caller-initiated +operations, after the authorization decision. Raw Credential name, ResourceRef, +and UID are excluded even from these records. The digest is audit-only and is +never copied to OTEL Resource attributes, span attributes, metric labels, logs, +status, errors, collector diagnostics, or support summaries. Audit schemas cap +record size and field count; failed/unauthorized requests cannot use audit as an +identity oracle. + +Excluded from all audit records: token bytes, key material, passwords, bearer +strings, provider-internal diagnostics, host paths, connection strings, +audience literals, tenant/subscription/client IDs, endpoint URIs, and +Noise/session key material. + +## OTEL and metrics + +### Spans + +Span names use the pattern `d2b.credential.`: + +- `d2b.credential.acquire_token` +- `d2b.credential.refresh_token` +- `d2b.credential.revoke_token` +- `d2b.credential.sign_challenge` +- `d2b.credential.inspect_metadata` +- `d2b.credential.reconcile` +- `d2b.credential.rotation` + +Required span attributes (closed set): + +| Attribute | Value | +| --- | --- | +| `d2b.credential.provider` | Provider name (e.g. `credential-entra`) | +| `d2b.credential.operation_class` | Closed enum string | +| `d2b.credential.placement_binding` | `user-agent` / `host-system` / `guest-agent` | +| `d2b.credential.outcome` | Stable closed outcome code | +| `d2b.credential.rotation_generation` | Numeric rotation generation | + +Credential telemetry uses only applicable generic OTEL Resource attributes from +the collector's closed allowlist: + +| Resource attribute | Value | +| --- | --- | +| `d2b.zone` | Zone name, re-stamped at trusted ingress | +| `d2b.provider` | Closed Provider name | +| `d2b.component` | Signed component ID | +| `service.name` | Fixed service name | +| `service.namespace` | Fixed service namespace | +| `service.version` | Build version | + +No OTEL Resource attribute or span attribute carries a Credential resource +name, ResourceRef, UID, digest (including `resource_name_digest`), or derived +identity token. Also forbidden: token bytes, audience literals, provider +diagnostics, host paths, resource IDs, tenant/subscription IDs, endpoint URIs, +and correlation IDs that embed secret shapes. + +### Metrics + +| Metric | Type | Labels | +| --- | --- | --- | +| `d2b_credential_operations_total` | Counter | `provider`, `operation_class`, `placement_binding`, `outcome` | +| `d2b_credential_lease_expiry_seconds` | Gauge | `provider`, `placement_binding` | +| `d2b_credential_rotation_total` | Counter | `provider`, `policy`, `outcome` | +| `d2b_credential_provider_health` | Gauge (0/1) | `provider` | +| `d2b_credential_active_leases` | Gauge | `provider`, `placement_binding` | + +Label cardinality is bounded and semantic. The expiry gauge reports the +minimum seconds remaining across active leases in each provider/placement +aggregate (0 when none). Metric labels carry no Credential resource name, +ResourceRef, UID, digest, or derived identity token. Credential identity is +available only as `resource_name_digest` in the authorized bounded audit +records above; it never appears in telemetry. Generic allowlisted OTEL Resource +attributes such as `d2b.zone`, `d2b.provider`, and `d2b.component` remain +available and are not copied into metric labels or span attributes. + +## Nix configuration + +### Zone-level Credential declaration + +```nix +# d2b.zones..resources. = { type = "..."; spec = { ... }; } +# metadata is optional: only ownerRef and presentation metadata +# (labels, annotations) are Nix-authorable. Option types, defaults, +# and docs come from the committed ResourceTypeSchema and Provider schema. +{ + d2b.zones.dev.resources = { + + work-entra = { + type = "Credential"; + # optional: ownerRef and/or presentation labels/annotations only + metadata = { + labels = { "team" = "platform"; }; + }; + spec = { + providerRef = "Provider/credential-entra"; + scope = { + executionRef = "Guest/work-vm"; + domainFilter = "user"; + userRef = "User/alice"; + }; + audience = "azure-resource-manager"; + consumerRef = "Provider/display-wayland"; + allowedOperations = [ "acquire-token" "refresh-token" ]; + rotation = { + policy = "proactive"; + proactiveWindowMs = 300000; + maxLeaseLifetimeMs = 3600000; + }; + revocation = { + onOwnerDelete = "immediate"; + onProviderGeneration = "immediate"; + }; + }; + }; + + local-keyring = { + type = "Credential"; + spec = { + providerRef = "Provider/credential-secret-service"; + scope = { + executionRef = "Host/host-system"; + domainFilter = "user"; + userRef = "User/alice"; + }; + audience = "user-session"; + allowedOperations = [ "acquire-token" "revoke-token" "inspect-metadata" ]; + rotation = { + policy = "on-demand"; + }; + revocation = { + onOwnerDelete = "immediate"; + onProviderGeneration = "immediate"; + }; + }; + }; + + }; +} +``` + +The authoring shape is the same for every ResourceType: +- `d2b.zones..resources.` is the single generic resource attr path; + there is no type-specific sub-namespace. +- `type` is the ResourceType string (`"Credential"`, `"Provider"`, `"Host"`, etc.). +- `metadata` is **optional**. When present it may contain `ownerRef` and/or + presentation metadata (`labels`, `annotations`). No other metadata field is + Nix-authorable. +- `spec` fields mirror the canonical ResourceTypeSchema `spec` object exactly — + the same field names, the same nesting, the same value shapes. There is no + second bespoke Nix vocabulary, no renamed subkey, and no Provider-specific + re-nesting. +- `metadata.name` is derived from the attr key (`work-entra`). +- `metadata.zone` is derived from the Zone attr key (`dev`). +- `apiVersion` defaults to `resources.d2bus.org/v3` and is not authored. +- `status` is omitted from Nix entirely; it is controller-managed and read-only. +- `metadata.uid`, `generation`, `revision`, `managedBy`, `configurationGeneration`, + `createdAt`, `updatedAt` are filled by core; they are never authored in Nix. + +Nix option types, defaults, and inline documentation for every `spec` field are +**generated from the same `ResourceTypeSchema` JSON** (`docs/reference/schemas/v3/credential.json` +and the signed Provider schema for `providerRef`-typed constraints) that drives +build-time validation. No bespoke Nix option module is maintained separately +from the schema. + +Provider-specific config (e.g. `credential-entra` root config such as `tenantId`) +lives under the Provider resource's own `spec`, not on the Credential resource. +The Credential `spec` carries only non-secret selector/policy fields. Any string +field that resembles a secret fails the eval-time secret-shape assertion before +the NixOS build completes. + +### Artifact catalog + +Derivation-valued inputs (Provider binaries, NixOS system closures) are configured +exclusively in a separate named artifact catalog, never inside a ResourceSpec. + +```nix +# d2b.artifacts. = { package = ; type = "provider"|"nixos-system"|...; }; +# IDs are plain bounded identifiers ([A-Za-z0-9_-], max 128 chars). +# Nix builds and includes the derivation, validates catalog type/ID/duplicates/trust, +# and emits a private integrity-pinned artifact catalog mapping each ID to +# type / digest / closure metadata. Store paths are private catalog implementation +# data; they never appear in any resource spec, status, audit record, or log. +{ + d2b.artifacts = { + credential-secret-service-bin = { + package = pkgs.d2b-provider-credential-secret-service; + type = "provider"; + }; + credential-entra-bin = { + package = pkgs.d2b-provider-credential-entra; + type = "provider"; + }; + credential-managed-identity-bin = { + package = pkgs.d2b-provider-credential-managed-identity; + type = "provider"; + }; + }; +} +``` + +Provider resources reference their binary via `spec.artifactId`; Guest system +resources use `spec.systemArtifactId`. Both are plain bounded string IDs — not +`*Ref` fields, because `Artifact` is not a ResourceType and the value does not +serialize as `/`. A missing or wrong-type artifact ID fails +the NixOS build; the error identifies the invalid ID and its catalog entry. + +Artifact catalog rules: +- Each ID is unique within the catalog; duplicate IDs fail the NixOS build. +- The `type` field must match the expected type for the consuming resource field + (`"provider"` for `spec.artifactId` in a Provider resource; `"nixos-system"` + for `spec.systemArtifactId` in a Guest resource). +- Catalog entries are validated and integrity-pinned at build time alongside the + resource bundle; the emitted artifact catalog is co-located with the resource + bundle at `/etc/d2b/zones//artifact-catalog.json` (mode 0640, + root-readable, `d2bd`-readable), never served over the public d2b-bus surface. +- `activation-nixos` verifies both the resource bundle digest and the artifact + catalog digest before creating or updating any resource. +- Credential resource specs (`type = "Credential"`) do not have `artifactId` or + `systemArtifactId` fields; those fields belong on Provider and Guest specs + respectively. + +### Eval-time and build-time validation + +All Credential resource declarations are validated at Nix eval time (before the +build) and at build time (against committed generated schemas). Because Nix option +types, defaults, and constraint docs are generated from the ResourceTypeSchema, +many type/bounds errors are caught by the Nix option type system before +assertions run. + +#### Eval-time assertions (Nix, `nixos-modules/assertions.nix` pattern) + +Applied to every `d2b.zones..resources.` entry whose `type = "Credential"`: + +- `spec.providerRef` resolves a declared `Provider/credential-*` entry in the + same Zone; the Provider's `credentialDomains` must include the declared + `spec.scope.domainFilter`. +- `spec.audience` passes the OpaqueAzureRef-equivalent charset and length validation + (`^[A-Za-z0-9._:/@-]+$`, max 256 chars; rejects `=`, `+`, whitespace, `{}`). +- `spec.rotation.proactiveWindowMs` < `spec.rotation.maxLeaseLifetimeMs / 2` + when both are non-zero. +- `spec.rotation.policy = "proactive"` requires `proactiveWindowMs > 0` and + `maxLeaseLifetimeMs > 0`. +- `spec.consumerRef`, if set, resolves a declared `Provider/` in the same Zone. +- `spec.scope.executionRef` resolves a declared `Guest/` or `Host/` + in the same Zone. +- `spec.scope.userRef`, when set, resolves a declared `User/` in the same + Zone; required when `spec.scope.domainFilter = "user"`. +- `spec.allowedOperations` is a non-empty subset of the Provider's declared + `supportedOperations`; values outside the Provider's set fail the assertion. +- Provider-specific constraints (placement, supported operations) are checked + against the committed Provider schema (`docs/reference/schemas/v3/` or the + generated schema embedded in the Provider package): `credential-secret-service` + rejects `domainFilter = system` and `domainFilter = guest`; `credential-managed-identity` + rejects `domainFilter = user`. +- No `spec` field accepts a credential-shaped value: `contains_sensitive_shape` + guard (adapted from `d2b-realm-provider/src/error.rs`) runs at Nix eval time + on all string fields; the eval fails with a descriptive assertion if any field + matches a secret pattern. +- No Credential resource name (attr key) uses the `sys-` reserved prefix. +- Two Credential resources may not declare the same `(providerRef, scope.executionRef, + scope.userRef, audience)` tuple in the same Zone (duplicate-binding conflict + rule). +- `spec.providerRef` must resolve a Provider resource in the same Zone; that + Provider resource's `spec.artifactId` must resolve an artifact catalog entry + of `type = "provider"`. A missing catalog entry or wrong type fails the eval. + +#### Build-time validation (via `xtask gen-schemas` and drift gate) + +- Each Credential resource `spec` is validated against the committed + `ResourceTypeSchema` JSON at `docs/reference/schemas/v3/credential.json`. + Unknown fields, wrong types, and out-of-bounds values produce a build error. + **This is the same schema that generated the Nix option types; both must agree.** +- The Provider-specific schema for `spec.providerRef` (e.g. + `docs/reference/schemas/v3/provider-credential-entra-config.json`) is + cross-checked: the `audience` charset rules and the `tenantId` + format match the Provider's declared validation constraints. +- The drift gate (`make test-drift`) regenerates all schemas with + `cargo xtask gen-schemas` and asserts `git diff --exit-code`. A committed + schema change that does not match the crate-derived schema fails the gate. + A Nix options module that diverges from the generated schema also fails the + gate. +- Secrets remain as `Credential` refs: any provider-specific config field + declared with `secretRef: true` in the Provider schema must reference + `Credential/`, not an inline string. The build-time validator confirms + this constraint on every Provider-specific config block. +- **Artifact catalog validation**: each `d2b.artifacts.` entry is validated + for type (`"provider"`, `"nixos-system"`, etc.), charset of the ID + (`[A-Za-z0-9_-]+`, max 128 chars), and uniqueness (duplicate IDs fail the + build). Every `spec.artifactId` in a Provider resource must resolve an entry + of `type = "provider"` in the catalog. Store paths and closure metadata in the + emitted artifact catalog are private; they are not present in any resource + spec, resource bundle, status, audit record, or log surface. The artifact + catalog is emitted at `/etc/d2b/zones//artifact-catalog.json` with its + own SHA-256 digest header; `activation-nixos` verifies this digest alongside + the resource bundle digest before any create/update. + +### Canonical ResourceSpec JSON shape + +Nix renders each Credential declaration as a canonical JSON object with +deterministic defaults applied. The `spec` attr maps directly and unchanged to +the `spec` object in the output. The output for the `work-entra` example above: + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Credential", + "metadata": { + "name": "work-entra", + "zone": "dev", + "labels": { + "team": "platform" + }, + "annotations": {}, + "ownerRef": null + }, + "spec": { + "providerRef": "Provider/credential-entra", + "scope": { + "executionRef": "Guest/work-vm", + "domainFilter": "user", + "userRef": "User/alice" + }, + "audience": "azure-resource-manager", + "consumerRef": "Provider/display-wayland", + "allowedOperations": ["acquire-token", "refresh-token"], + "rotation": { + "policy": "proactive", + "proactiveWindowMs": 300000, + "maxLeaseLifetimeMs": 3600000 + }, + "revocation": { + "onOwnerDelete": "immediate", + "onProviderGeneration": "immediate" + } + } +} +``` + +Rules for this output: + +- `metadata.name` is the attr key (`work-entra`); `metadata.zone` is the Zone + attr key (`dev`); `apiVersion` is the fixed default `resources.d2bus.org/v3` and + is a top-level field, not nested inside `metadata`. + None of these are authored in Nix. +- `metadata` in the Nix-rendered output contains only: `name` (derived), `zone` + (derived), and optionally `ownerRef` and presentation metadata (`labels`, + `annotations`) if authored. `finalizers` is omitted from the Nix-rendered + input; core manages finalizers and never accepts them from the Nix bundle. +- `metadata.uid`, `metadata.generation`, `metadata.revision`, `metadata.createdAt`, + `metadata.updatedAt` are not present: these are assigned by the resource store + on create/update and are not Nix-authored fields. +- `status` is not present: status is exclusively controller-managed. +- The `spec` object in JSON is identical in structure to the `spec` attr in Nix. + No field is renamed, re-nested, or transformed by the Nix layer. +- All references use the canonical ResourceRef form (`/`). +- `allowedOperations` is sorted alphabetically and deduplicated. +- String fields are Unicode-normalized (NFC) before serialization. +- Deterministic defaults applied at eval time: + `revocation.onOwnerDelete` defaults to `"immediate"` when absent; + `revocation.onProviderGeneration` defaults to `"immediate"` when absent. +- `metadata.labels` and `metadata.annotations` in the Nix authoring map + directly to the corresponding output fields. They are optional presentation + metadata. No management labels are generated by Nix. +- Core assigns `metadata.managedBy = "configuration"` and + `metadata.configurationGeneration = ` to all configuration-managed + resources at create/update time. These are first-class metadata fields, not + labels, and are never authored in Nix. +- Build validation compares this canonical rendered JSON against the committed + ResourceTypeSchema to confirm exact structural parity. + +### Zone resource bundle and generation emission + +Nix emits all Credential resource declarations for a Zone into the Zone resource +bundle file alongside other ResourceTypes. The bundle is produced by the +`activation-nixos` Provider's Nix configuration compiler: + +```json +{ + "bundleVersion": 1, + "zone": "dev", + "nixConfigGeneration": 42, + "emittedAt": "2026-07-22T00:00:00Z", + "digestAlgorithm": "sha256", + "digest": "", + "resources": [ + { "type": "Credential", "name": "local-keyring", "spec": { ... } }, + { "type": "Credential", "name": "work-entra", "spec": { ... } } + ] +} +``` + +Rules: + +- `resources` is sorted lexicographically by `(type, name)` before digest + computation. The order is deterministic and reproducible. +- `digest` covers the UTF-8 encoding of the canonical JSON of the `resources` + array (without the `digest` field itself). The digest is computed by Nix at + eval time using a pure derivation and committed into the store; the + `activation-nixos` Provider verifies it before applying. +- The bundle file is stored at + `/etc/d2b/zones//resource-bundle.json` by the NixOS activation script. + It is root-readable, `d2bd`-readable, mode 0640. +- The bundle includes all Nix-managed ResourceTypes for the Zone, not only + Credentials. Credential entries appear in the `resources` array sorted with + all other types. +- `nixConfigGeneration` is the NixOS system configuration generation number + (from `config.system.nixos.version` or the NixOS generation number). +- A NixOS build failure (schema validation, secret-shape detection, reference + resolution) prevents the bundle from being emitted. The prior bundle remains + on disk until replaced by a successful build. + +### Resource managedBy classification + +The `activation-nixos` Provider distinguishes three classes of Credential +resources within a Zone by `metadata.managedBy`: + +| Class | Marker | Example | +| --- | --- | --- | +| **Configuration-managed** | `metadata.managedBy = "configuration"` | Any resource declared under `d2b.zones..resources` in Nix | +| **Controller-created** | `metadata.managedBy = "controller"` | Ephemeral lease records, auto-generated proxy credentials created by a Provider controller during reconciliation | +| **API-created** | `metadata.managedBy = "api"` | Credentials created directly via the resource API by an operator or external system; persist until an explicit Delete; never generation-swept | + +Rules: + +- `managedBy` and `ownerRef` are orthogonal. Configuration-managed and + API-created resources may carry an optional same-Zone `ownerRef` and + participate in owner cascade under the standard ownerRef deletion propagation. + The presence of an `ownerRef` does not change their `managedBy` classification. +- Only configuration-managed Credentials are subject to the generation cleanup + contract described below. +- Controller-created Credential resources carry a controller-assigned `ownerRef` + to their parent resource. Their lifecycle is governed by the owner controller's + reconciliation loop and the standard ownerRef deletion propagation. +- API-created Credential resources persist until an explicit Delete is issued. + They are never swept by a generation transition. They may carry an authorized + same-Zone `ownerRef` and participate in owner cascade. +- The `activation-nixos` Provider MUST NOT delete controller-created or + API-created resources merely because they are absent from the new Nix + configuration set. Absence from the Nix bundle is not a delete signal for + resources whose `metadata.managedBy` is `"controller"` or `"api"`. + +### Generation transition and cleanup contract + +When a new NixOS generation is activated, the `activation-nixos` Provider: + +1. **Reads and integrity-verifies** the new resource bundle (SHA-256 digest + check against the committed bundle digest). + +2. **Computes the desired set** of configuration-managed Credential resources + from the bundle (resources whose `metadata.managedBy = "configuration"`). + +3. **Creates or updates** resources in the desired set: + - A resource in the desired set that does not exist in the store is created + with generation 1. + - A resource in the desired set whose stored spec differs from the bundle + spec receives a full spec replace (generation increments; revision + increments). + - A resource in the desired set whose stored spec is identical to the bundle + spec receives no write (idempotent). + +4. **Activates the new generation without blocking on cleanup**: The new + bundle's activation completes and the `activation-nixos` Provider's own + status transitions to `Ready` (or `Degraded/pending-cleanup` — see below) + immediately after step 3. The activation does not wait for removed resources + to finish deletion. + +5. **Issues async Delete for removed resources**: Configuration-managed + resources currently in the store (those with `metadata.managedBy = + "configuration"`) that are **absent from the new desired set** receive an + authorized `delete` (sets `deletionRequestedAt`). These deletes are issued + asynchronously after the activation completes. The Credential controller + processes them through its normal finalizer path: `provider-revoke` finalizer + runs, the credential service calls `RevokeToken` on all active leases, then + the resource transitions to `phase=Deleted`. Resources with + `metadata.managedBy = "controller"` or `metadata.managedBy = "api"` are + never touched by this path. + +6. **Retains prior configuration bundles** up to the configured count (default: + 3; range: 1..16, set via Zone configuration). Prior bundles are stored at + `/etc/d2b/zones//resource-bundle..prev.json`. When the retained + count would be exceeded, the oldest retained bundle is pruned. There is no + time-based retention window. Rollback re-activates a retained prior bundle; + if resources from that bundle were already deleted, they are re-created from + scratch (prior secrets are not restored; fresh leases are acquired by the + credential controller after re-creation). + +#### Zone and Credential status during cleanup + +While removed Credential resources are pending deletion: + +- Each removed Credential resource transitions immediately to + `phase=Degraded` with condition `Cleanup=True`: + + ```yaml + type: Cleanup + status: "True" + reason: nix-generation-removed + message: "credential removed from nix configuration; pending provider-revoke finalizer" + observedGeneration: + lastTransitionAt: + ``` + +- The Zone's `activation-nixos` Provider status reflects a `PendingCleanup` + condition while any removed configuration-managed Credential (or other + configuration-managed resource) is still in the store: + + ```yaml + type: PendingCleanup + status: "True" + reason: removed-resources-pending-deletion + message: "N credential(s) pending deletion from prior nix generation" + ``` + +- The Zone `activation-nixos` Provider transitions to `phase=Ready` (clearing + `PendingCleanup`) only after all removed resources reach `phase=Deleted`. + +- Stalled cleanup (e.g. finalizer blocked on a `ProviderUnavailable` condition) + surfaces as `phase=Degraded` on the Credential resource and on the Provider. + The Credential controller retries with backpressure and reports the stall via + the bounded `ProviderUnavailable` condition on the resource. + +#### Owner-controller child reconciliation + +When a configuration-managed Credential resource is deleted (either by +generation cleanup or by explicit Nix removal), the Credential controller +reconciles and deletes its controller-created children: + +- The Credential controller's `finalize` handler lists all `ownerRef = + Credential/` children in the Zone (e.g., ephemeral lease records). +- Children are deleted child-first in dependency order before the + `provider-revoke` finalizer is released. +- The Credential controller does not broadly delete all resources that happen + to share a related name or label; it deletes only the exact `ownerRef`-bound + children it created. + +### Status, errors, and audit for generation transitions and cleanup + +#### Status fields specific to cleanup + +The `activation-nixos` Provider adds these fields to its own status during a +generation transition: + +```yaml +status: + activationGeneration: 42 # the NixOS generation just activated + pendingCleanupCount: 2 # number of configuration-managed resources awaiting deletion + pendingCleanupResourceRefs: # bounded list (max 64 entries; truncated with note) + - "Credential/old-keyring" + - "Credential/deprecated-entra" + retainedConfigurationCount: 3 # current number of retained prior bundles + retainedConfigurationMax: 3 # configured maximum (default 3, range 1..16) +``` + +#### Errors + +| Code | Meaning | +| --- | --- | +| `nix-bundle-digest-mismatch` | Bundle SHA-256 does not match declared digest; activation aborted | +| `nix-bundle-schema-invalid` | One or more resource specs fail ResourceTypeSchema validation at activation time | +| `nix-bundle-ref-unresolved` | A resource spec carries a `ResourceRef` that does not resolve in the current Zone at activation time | +| `nix-cleanup-stalled` | A removed resource's finalizer has not completed within the stall threshold; the Zone remains `Degraded/pending-cleanup` | +| `nix-cleanup-revoke-failed` | `RevokeToken` returned a permanent error during cleanup finalizer; operator intervention required | +| `nix-rollback-recreation-failed` | A resource from a retained prior bundle could not be re-created during rollback (e.g. Provider unreachable); fresh lease acquisition will retry | + +#### Audit + +| Event | Fields retained | +| --- | --- | +| Bundle activated (new NixOS generation) | Zone, `activationGeneration`, `nixConfigGeneration`, bundle digest, resource create/update/skip counts, removed count | +| Configuration-managed resource removed (async delete issued) | Zone, Credential ResourceRef, `activationGeneration`, `deletionRequestedAt`, reason `nix-generation-removed` | +| Cleanup complete | Zone, Credential ResourceRef, final `phase=Deleted`, `activationGeneration`, `cleanupLatencyMs` | +| Cleanup stalled | Zone, Credential ResourceRef, stall duration, last error code | +| Rollback initiated | Zone, from/to `activationGeneration`, `retainedConfigurationCount` | + +All audit records exclude: token bytes, key material, prior-generation spec +contents, provider diagnostics, and user-identifying path components. + +### Eval/build/runtime tests for Nix configuration and cleanup + +#### Eval/build tests (nix-unit, drift gate) + +| Test | Validates | +| --- | --- | +| `nix-unit` golden JSON envelope for each Credential example | Spec JSON shape, metadata shape (no management labels in Nix output), field ordering, canonical defaults | +| `nix-unit` assertion-failure for secret-shaped `audience` | `contains_sensitive_shape` eval-time rejection | +| `nix-unit` assertion-failure for `providerRef` not resolving | Eval-time reference validation | +| `nix-unit` assertion-failure for `domainFilter=system` on `credential-secret-service` | Provider-specific placement constraint | +| `nix-unit` assertion-failure for `proactiveWindowMs > maxLeaseLifetimeMs / 2` | Rotation bounds | +| `nix-unit` duplicate-binding conflict assertion | Same `(providerRef, executionRef, userRef, audience)` in same Zone | +| `nix-unit` full bundle JSON with two Credentials: sorted by name, digest present | Bundle sort order and digest field | +| `nix-unit` assertion-failure for missing `d2b.artifacts` entry referenced by Provider `artifactId` | Artifact catalog ID resolution | +| `nix-unit` assertion-failure for `d2b.artifacts..type != "provider"` for Provider `artifactId` | Artifact catalog type enforcement | +| `nix-unit` assertion-failure for duplicate `d2b.artifacts` ID | Artifact catalog duplicate-ID rejection | +| `make test-drift` (`cargo xtask gen-schemas`) | Schema drift gate; must pass with zero diff | + +#### Build tests + +| Test | Validates | +| --- | --- | +| Provider-specific schema cross-check: `credential-entra` `audience` charset in bundle | Provider schema validation against generated schema file | +| Bundle digest round-trip: compute digest of sorted resources; verify matches bundle header | Integrity-pinned bundle correctness | +| Artifact catalog round-trip: emitted artifact catalog digest matches header; store paths absent from resource bundle and status | Artifact catalog integrity and store-path privacy | +| Nix store contains no credential-shaped bytes and no store paths in any resource bundle derivation output | Secret-shape and store-path detection covers generated bundle and options modules | + +#### Runtime tests (integration, `tests/host-integration/`) + +| Test | Validates | +| --- | --- | +| `credential-cleanup-basic`: NixOS generation N has `work-entra`; generation N+1 removes it; resource reaches `phase=Deleted` | Async Delete path end-to-end | +| `credential-cleanup-nonblocking`: generation N+1 activation completes (returns Ready status on new resources) before `work-entra` cleanup finalizer finishes | Activation does not block on cleanup | +| `credential-cleanup-pending-status`: during cleanup, removed resource shows `Cleanup=True`/`nix-generation-removed`; Zone Provider shows `PendingCleanup=True` | Status fields during cleanup | +| `credential-cleanup-stalled`: `credential-secret-service` unavailable during cleanup; removed resource shows `Degraded`/`nix-cleanup-stalled`; recovers after Provider returns | Stall detection and recovery | +| `credential-cleanup-controller-children-preserved`: a controller-created lease record with `ownerRef=Credential/work-entra` is deleted by the Credential controller's finalizer, not orphaned or double-deleted | Owner controller child cleanup | +| `credential-cleanup-no-dynamic-deletion`: a controller-created Credential (`metadata.managedBy = "controller"`) and an API-created Credential (`metadata.managedBy = "api"`) with same name pattern as a removed configuration-managed Credential are NOT deleted by activation-nixos cleanup | Configuration-managed vs controller/API isolation | +| `credential-retained-generation-count`: after cleanup completes, up to `retainedConfigurationMax` (default 3) prior bundles are retained; rollback re-creates removed resource from retained bundle; exceeding the count prunes the oldest retained bundle | Count-based retention and retained-generation rollback | +| `credential-bundle-digest-mismatch`: tampered bundle file causes `nix-bundle-digest-mismatch` and aborts activation | Integrity verification | + +## Three-plane credential model and v3 reachability + +The v3 baseline distinguishes three credential planes in +`d2b-realm-provider/src/credential.rs` (`b5ddbed6`): + +| Plane | v3 type (current symbols) | Evidence class | v3 reachability | Target mapping note | +| --- | --- | --- | --- | --- | +| Azure control plane | `AzureControlPlaneRef` (`tenant_id: OpaqueAzureRef`, `subscription_id: OpaqueAzureRef`, `region: OpaqueAzureRef`) in `d2b-realm-provider/src/credential.rs` | `implemented-and-reachable` (tests pass, used in bundle resolver) | Opaque ref data only; no credential acquisition path | Current type retained directly; `OpaqueAzureRef` validation reused in `credential-entra` and `credential-managed-identity` config | +| Container managed identity | `ManagedIdentityRef` (`client_id: OpaqueAzureRef`) in `d2b-realm-provider/src/credential.rs`; `ProviderWorkloadIdentity::ManagedIdentity` in `d2b-realm-provider/src/types.rs:ProviderGuestdBootstrapContract.workload_identity` | `implemented-and-reachable` (tests pass, ACA config path; `ProviderWorkloadIdentity` used in live ACA bootstrap) | Opaque ref data only; IMDS acquisition is inside ACA container | Current types retained; `credential-managed-identity` Provider formalizes this as a Credential resource; `ProviderGuestdBootstrapContract.workload_identity` uses current `WorkloadId`-backed ACA workload concept (target: Guest-based execution context) | +| d2b-internal session | `SessionCredentialBinding` in `d2b-realm-provider/src/credential.rs`; fields import `d2b_realm_core::{RealmPath, WorkloadId, GatewayId, StreamId}` (current v3 names from `d2b-realm-core`): `realm: RealmPath` (current; target terminology: Zone), `workload: WorkloadId` (current; target terminology: Guest execution context for ACA sandbox), `gateway: GatewayId`, `display_stream: StreamId` | `implemented-and-reachable` (tests pass, gateway mux path) | Gateway-internal; never stored as a resource | **Not migrated.** Current names `RealmPath`/`WorkloadId` are v3 Realm-era symbols; their ADR 0046 successors are Zone and Guest. This binding is a per-session ephemeral gateway artifact, not a Credential ResourceType | + +None of the three planes represents a general-purpose Credential ResourceType. +The ADR 0046 Credential ResourceType is a new construct (`ADR-only`) that +replaces the role of the old `CredentialProvider` trait +(`d2b-realm-provider/src/provider.rs:301`, current v3 symbol), which is `implemented-and-reachable` +with only `status()` and `enrollment_valid()`, for the three Credential +Provider families. + +The `SessionCredentialBinding` plane is not migrated to a Credential resource: +it is a per-session ephemeral artifact owned exclusively by the gateway runtime +and has no independent lifecycle. It is excluded from the ADR 0046 Credential +ResourceType catalog. Its current v3 field `realm: RealmPath` uses the current +Realm terminology and does not introduce a Zone Credential resource ref. + +The `no_secrets_or_credentials: bool` invariant in +`packages/d2b-core/src/realm_workloads_launcher.rs:LauncherMetadataInvariants` +(evidence class: `implemented-and-reachable`; current v3 symbol in the +`RealmWorkloadsLauncherV2Json` artifact — current Realm-era name for the Zone +launcher metadata artifact) provides direct v3 evidence for the zero-secret-bytes +design principle applied at the launcher boundary. The target Credential +ResourceType extends this invariant to all resource/store/status/audit/log +boundaries. Token delivery to the authorized consumer uses a dedicated +end-to-end Noise-encrypted ComponentSession (see §Credential-delivery endpoint +contract); neither the bus intermediary nor any persistent surface stores or +decrypts those records. + +The `AzureControlPlaneRef` and `ManagedIdentityRef` planes provide the opaque +non-secret audience reference pattern used by the `credential-entra` and +`credential-managed-identity` Provider specs (§Credential Provider dossiers). + +## Required crate layout for Credential Provider packages + +Every `packages/d2b-provider--/` crate for a Credential +Provider MUST contain all four of the following paths. Missing any path is a +workspace/package policy failure enforced by `make test-policy` (via +`packages/d2b-contract-tests`): + +| Path | Contents | +| --- | --- | +| `src/` | Provider implementation, controller, service handler, binary entry points, and all colocated unit tests (`#[cfg(test)]` modules within `src/` files). One binary per role declared in the dossier's "Process components" table. | +| `tests/` | Hermetic Cargo integration tests (`#[test]` in `tests/*.rs`, no external process or container): ResourceType/controller lifecycle, provider conformance (`d2b-provider-toolkit::conformance::check_provider_conformance`), fault injection (locked/unavailable/interaction-required/generation-mismatch/oversize), secret canary enforcement, Credential name/ResourceRef/UID/digest canaries absent from status/errors/logs and every OTEL Resource/span attribute/metric label (with only authorized bounded audit retaining `resource_name_digest`), delivery-session binding, and placement rejection tests. Run with `cargo test -p d2b-provider-credential-`. | +| `integration/` | Heavier fixtures and scenarios invoked by existing test orchestration (`make test-integration`, `make test-host-integration`): container-backed Provider service, Host/Guest placement, cross-process d2b-bus routing, provider-system startup/restart/drain, and Nix-generation cleanup/rollback. Files are shell scripts, Nix expressions, or container specs consumed by `tests/integration/containers/` or `tests/host-integration/`; they are NOT run by `cargo test`. | +| `README.md` | All sections listed in §Provider README required sections below. | + +### Provider README required sections + +Every Provider `README.md` MUST contain these sections in order: + +1. **Provider identity** — `providerRef`, managed ResourceType(s), provider + generation/versioning policy, Zone placement constraints. +2. **Config schema** — `spec` fields (non-secret only), types, defaults, + constraints, and a worked example using the `d2b.zones..resources` + Nix authoring shape. +3. **ResourceTypes managed** — for each managed ResourceType: lifecycle phases, + status conditions owned, finalizers owned. +4. **Controllers, services, workers, and binaries** — one subsection per + component from the dossier "Process components" table: binary name, role, + domain (user/system), placement constraints. +5. **Placement** — supported `placementBinding` values and rejected values + with their error codes. +6. **Dependencies and RBAC** — required Zone resources (`executionRef`, + `consumerRef`, `userRef`), RBAC verbs consumed, consumer Provider + requirements, cross-resource ordering. +7. **Security, state, and telemetry** — secret isolation model; what the + Provider persists (opaque handles only; no token bytes); audit events + emitted; OTEL spans/metrics emitted; canary enforcement. +8. **Build, test, and integration commands** — exact `cargo`/`make` invocations + for `src/` unit tests, `tests/` integration tests, and `integration/` + scenarios. +9. **Standalone-repo usage** *(mandatory before first release to a sibling + flake)* — how to consume the crate outside the monorepo; flake input + pattern; nixpkgs/toolkit input-follows boilerplate; compatibility + constraints. + +## Credential Provider dossiers + +### Provider: `credential-secret-service` + +| Field | Value | +| --- | --- | +| providerRef | `Provider/credential-secret-service` | +| Implements | `Credential` | +| Crate | `packages/d2b-provider-credential-secret-service/` | +| Required layout | `src/` (impl + unit tests), `tests/` (hermetic Cargo integration/conformance/fault), `integration/` (container/Host/Guest fixtures), `README.md` (all §Provider README required sections) | +| Main reuse source | main `a1cc0b2d`: `packages/d2b-provider-credential-secret-service/src/{lib.rs, tests.rs}` | + +#### Description + +User-session Secret Service credential provider. The provider process runs +exclusively in the user domain (`scope.domainFilter=user`) of a Host. It is +constructed only for the authenticated user-domain Process (ADR 0046 target: +`Process` ResourceType) authorized by `scope.userRef`, and communicates with an +injected `Oo7SecretServicePort` implementation. The `oo7` port owns all +interaction with the FreeDesktop.org Secret Service D-Bus API (GNOME Keyring, +KWallet, or equivalent) and retains all credential material. In the outer +`d2b.credential.v3` RPC path, only bounded opaque lease metadata leaves this +module (no token bytes in outer DTOs, status, audit, or logs). Token bytes are +released exclusively via the dedicated end-to-end `Noise_KK` delivery record +to the authorized consumer (see §Credential-delivery endpoint contract). + +The main-branch reuse source encodes this placement restriction as +`SecretServiceOwner::Userd` (`packages/d2b-provider-credential-secret-service/src/lib.rs`, +evidence class for reuse: main `a1cc0b2d` only). The current v3 baseline crate +`packages/d2b-userd/src/lib.rs` is a **guest exec stub** (service mode exits 78: +`"service mode is not implemented in this build"`; contains only `UserAttachRequest` +and `UserExecSession` guest wire primitives; evidence class: `test-only-or-preview`). +It provides no credential functionality and is not the hosting process for this +Provider. The hosting user-domain Process is fully new work (ADR-only ResourceType). + +There is no system-domain path, no host-daemon path, no keyring file path, and no +environment-fallback. + +#### Provider resource shape for `credential-secret-service` + +`spec.artifactId` is a sibling of `spec.config` on the Provider resource, not +inside `spec.config`. A complete Provider resource declaration for this Provider: + +```nix +d2b.zones.dev.resources.credential-secret-service = { + type = "Provider"; + spec = { + artifactId = "credential-secret-service-bin"; # d2b.artifacts entry; type = "provider" + config = { + collectionAlias = "login"; + maxLeases = 64; + lockPolicy = "fail-closed"; + }; + }; +}; +``` + +#### Root `spec.config` schema (bounded, non-secret) + +```yaml +# Provider.spec.config — runtime config only; artifactId is Provider.spec.artifactId +collectionAlias: login # provider-validated Secret Service collection alias +maxLeases: 64 # maximum concurrent active leases; max 256 +lockPolicy: fail-closed # fail-closed | fail-degraded when keyring is locked +``` + +#### Supported placement bindings + +- `user-agent` only. `host-system` and `guest-agent` are rejected at + Provider-install validation. + +#### Supported operation classes + +- `acquire-token`, `refresh-token`, `revoke-token`, `inspect-metadata`. +- `sign-challenge` is not supported; schema rejects it. + +#### Process components + +| Component | Type | Domain | Binary | +| --- | --- | --- | --- | +| `secret-service-controller` | controller | user | `d2b-provider-credential-secret-service` | + +One controller process per user identity per Zone. It is a user-domain Process +under the Host declared in `scope.executionRef`. + +#### Credential state machine (provider-internal) + +`SecretServiceState`: `Locked | Unlocked`. +`SecretServiceLeaseState`: `Active | Revoked | Expired`. + +When state=Locked: AcquireToken returns `credential-provider-unavailable` +(mapped from `SecretServicePortError::Locked`). The status reflects +`ProviderUnavailable=True`, `leaseState=Unknown`. + +#### Credential-bound method mapping + +| `d2b.credential.v3` method | `Oo7SecretServicePort` call | +| --- | --- | +| `AcquireToken` | `issue_lease(&SecretServiceLeaseRequest)` → `SecretServiceLeaseGrant` | +| `RefreshToken` | `refresh_lease(&SecretServiceLeaseRef)` → `SecretServiceLeaseRenewal` | +| `RevokeToken` | `revoke_lease(&SecretServiceLeaseRef)` → `SecretServiceLeaseRevocation` | +| `InspectMetadata` | `inspect_lease(&SecretServiceLeaseRef)` → `SecretServiceLeaseInspection` | + +No port method accepts or returns a password, secret value, token, endpoint, +path, file descriptor, or byte buffer. The `credential_canary` and +`object_path_canary` fields in the main test (`tests.rs`) demonstrate that +those values remain inside the fake port and never appear in provider output. + +#### Status derivation + +`CredentialReady=True` when `SecretServiceState=Unlocked` and +`leaseState=Active`. `ProviderUnavailable=True` when `SecretServicePortError::Unavailable` +or `SecretServicePortError::Locked` is returned consistently. + +#### v3 current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `d2b-realm-provider/src/provider.rs:CredentialProvider` (status-only trait), `credential.rs` (plane types) | +| Evidence class | Minimal trait is `implemented-and-reachable`; full lease model is `ADR-only` in v3 | +| Main reuse source | main `a1cc0b2d` `d2b-provider-credential-secret-service/src/lib.rs`: `Oo7SecretServicePort`, `SecretServiceLeaseRequest/Ref/Grant/Inspection/Renewal/Revocation`, `SecretServiceCredentialProvider`, `SecretServiceCredentialProviderFactory`; `tests.rs`: `FakeOo7Port`, `credential_canary` enforcement, lease lifecycle and locked-state tests | +| Reuse action | copy and adapt (reversion v3 types; replace v2 `CredentialProvider` trait with v3 `d2b.credential.v3` service; replace v2 provider registry with Provider resource) | +| Required delta | v3 contract names/versions, Provider resource/controller descriptor, d2b-bus routing, Zone/Resource placement/scope, async reconciliation integration | +| Excluded main assumptions | v2 EndpointRole/Realm/userd process model; v2 ProviderFactory/ProviderRegistryBuilder; v2 component-session auth and prologue | +| Destination | `packages/d2b-provider-credential-secret-service/` (reused crate name) | + +--- + +### Provider: `credential-entra` + +| Field | Value | +| --- | --- | +| providerRef | `Provider/credential-entra` | +| Implements | `Credential` | +| Crate | `packages/d2b-provider-credential-entra/` | +| Required layout | `src/` (impl + unit tests), `tests/` (hermetic Cargo integration/conformance/fault), `integration/` (container/Host/Guest fixtures), `README.md` (all §Provider README required sections) | +| Main reuse source | main `a1cc0b2d`: `packages/d2b-provider-credential-entra/src/{lib.rs, tests.rs}` | + +#### Description + +Entra credential provider for an exact co-located cloud consumer. The injected +`EntraCredentialClient` retains all token material and is implemented in the +same provider-agent process as its SDK consumer (e.g., a cloud-connected guest +provider agent). In the outer `d2b.credential.v3` RPC path, only bounded opaque +lease metadata leaves this module (no token or signature bytes in outer DTOs, +status, audit, or logs). Token and signature bytes are released exclusively via +the dedicated end-to-end `Noise_KK` delivery record to the authorized consumer +(see §Credential-delivery endpoint contract). There +is no key-chain, environment, developer-tool, or alternate credential-chain path. + +`EntraCredentialOwner` is `ExactConsumer`: the provider is constructed for a +single exact co-located consumer identified by `consumerRef` in the Credential +spec. + +#### Provider resource shape for `credential-entra` + +`spec.artifactId` is a sibling of `spec.config`, not inside `spec.config`: + +```nix +d2b.zones.dev.resources.credential-entra = { + type = "Provider"; + spec = { + artifactId = "credential-entra-bin"; # d2b.artifacts entry; type = "provider" + config = { + tenantId = "2f8e1c3a-1234-5678-9abc-def012345678"; + authorityUrl = "login.microsoftonline.com"; + maxLeases = 64; + interactionPolicy = "fail-closed"; + }; + }; +}; +``` + +#### Root `spec.config` schema (bounded, non-secret) + +```yaml +# Provider.spec.config — runtime config only; artifactId is Provider.spec.artifactId +tenantId: "2f8e1c3a-1234-5678-9abc-def012345678" # opaque Azure tenant GUID; not a ResourceRef +authorityUrl: "login.microsoftonline.com" # bounded hostname; no secret shape +maxLeases: 64 +interactionPolicy: fail-closed # fail-closed | interaction-required +``` + +`tenantId` uses the `OpaqueAzureRef` charset restriction +(`^[A-Za-z0-9._-]+$`) from `d2b-realm-provider/src/credential.rs:OpaqueAzureRef` +(current v3 source name) to structurally reject secret-shaped values at parse time. +The field name is `tenantId`, not `tenantIdRef`; it is an opaque inline identifier, +not a `/` ResourceRef. + +#### Supported placement bindings + +- `user-agent` (user-domain on Host or Guest); +- `guest-agent` (system-domain on Guest). +- `host-system` is rejected; Entra credentials require a configured + consumer-agent co-location. + +#### Supported operation classes + +All five classes: `acquire-token`, `refresh-token`, `revoke-token`, +`sign-challenge`, `inspect-metadata`. + +#### Process components + +| Component | Type | Domain | Binary | +| --- | --- | --- | --- | +| `entra-controller` | controller | user or system per spec | `d2b-provider-credential-entra` | + +#### Credential-bound method mapping + +| `d2b.credential.v3` method | `EntraCredentialClient` call | +| --- | --- | +| `AcquireToken` | `issue_lease(&EntraLeaseRequest)` → `EntraLeaseGrant` | +| `RefreshToken` | `refresh_lease(&EntraLeaseRef)` → `EntraLeaseRenewal` | +| `RevokeToken` | `revoke_lease(&EntraLeaseRef)` → `EntraLeaseRevocation` | +| `InspectMetadata` | `inspect_lease(&EntraLeaseRef)` → `EntraLeaseInspection` | + +`EntraClientError::InteractionRequired` maps to +`credential-provider-unavailable` (not `credential-operation-denied`): it +signals a transient state, not a policy denial. + +`EntraClientState`: `Ready | InteractionRequired`. + +#### Three-plane relationship + +The `AzureControlPlaneRef` type in v3 +(`d2b-realm-provider/src/credential.rs:AzureControlPlaneRef`) provides the +`tenant_id`, `subscription_id`, and `region` non-secret references used in the +`credential-entra` root config. The `OpaqueAzureRef` parse/validation logic is +reused directly. The config schema for this Provider uses the same charset +restriction without carrying a secret. + +#### v3 current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `d2b-realm-provider/src/credential.rs:AzureControlPlaneRef`, `OpaqueAzureRef` (implemented-and-reachable); `provider.rs:CredentialProvider` (minimal, implemented-and-reachable) | +| Evidence class | Opaque ref model is reachable; full Entra lease provider is `ADR-only` in v3 | +| Main reuse source | main `a1cc0b2d` `d2b-provider-credential-entra/src/lib.rs`: `EntraCredentialClient`, `EntraLeaseRequest/Ref/Grant/Inspection/Renewal/Revocation`, `EntraCredentialProvider`, `EntraCredentialProviderFactory`; `tests.rs`: `FakeEntraClient`, `credential_canary`/`endpoint_canary` enforcement, `interaction-required` and colocated-consumer tests | +| Reuse action | copy and adapt | +| Required delta | v3 contract versions, Provider resource/descriptor, d2b-bus routing, v3 placement/scope, `OpaqueAzureRef` reuse for config validation | +| Excluded main assumptions | v2 AgentPlacementBinding, v2 EndpointRole/Realm, v2 ProviderFactory | +| Destination | `packages/d2b-provider-credential-entra/` (reused crate name) | + +--- + +### Provider: `credential-managed-identity` + +| Field | Value | +| --- | --- | +| providerRef | `Provider/credential-managed-identity` | +| Implements | `Credential` | +| Crate | `packages/d2b-provider-credential-managed-identity/` | +| Required layout | `src/` (impl + unit tests), `tests/` (hermetic Cargo integration/conformance/fault), `integration/` (container/Host/Guest fixtures), `README.md` (all §Provider README required sections) | +| Main reuse source | main `a1cc0b2d`: `packages/d2b-provider-credential-managed-identity/src/{lib.rs, tests.rs}` | + +#### Description + +Host or Guest managed-identity credential provider for an exact co-located SDK +consumer. There is no environment credential chain, no developer-tool fallback, +and no keyring path. The injected `ManagedIdentityCredentialClient` holds all +token material. `ManagedIdentityCredentialOwner` is `ExactSdkConsumer`. + +Managed identity runs on Hosts or Guests where an IMDS-compatible endpoint is +available (e.g. Azure VM instance metadata, ACA sidecar IMDS). The provider +config carries only the opaque `ManagedIdentityRef.client_id` (an +`OpaqueAzureRef`, current v3 source type names) identifying the user-assigned managed identity. + +#### Provider resource shape for `credential-managed-identity` + +`spec.artifactId` is a sibling of `spec.config`, not inside `spec.config`: + +```nix +d2b.zones.dev.resources.credential-managed-identity = { + type = "Provider"; + spec = { + artifactId = "credential-managed-identity-bin"; # d2b.artifacts entry; type = "provider" + config = { + clientId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"; + imdsEndpointAlias = "azure-imds"; + maxLeases = 64; + }; + }; +}; +``` + +#### Root `spec.config` schema (bounded, non-secret) + +```yaml +# Provider.spec.config — runtime config only; artifactId is Provider.spec.artifactId +clientId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" # opaque Azure MI client GUID; not a ResourceRef +imdsEndpointAlias: azure-imds # provider-validated closed alias +maxLeases: 64 +``` + +`clientId` is an opaque inline identifier validated against the `OpaqueAzureRef` +charset (`^[A-Za-z0-9._-]+$`). The field name is `clientId`, not `clientIdRef`. + +#### Supported placement bindings + +- `host-system` and `guest-agent`. The IMDS endpoint is machine-local. +- `user-agent` is rejected: managed identity is not a user-session credential. + +#### Supported operation classes + +`acquire-token`, `refresh-token`, `revoke-token`, `inspect-metadata`. +`sign-challenge` is not supported. + +#### Process components + +| Component | Type | Domain | Binary | +| --- | --- | --- | --- | +| `managed-identity-controller` | controller | system | `d2b-provider-credential-managed-identity` | + +#### Credential-bound method mapping + +| `d2b.credential.v3` method | `ManagedIdentityCredentialClient` call | +| --- | --- | +| `AcquireToken` | `issue_lease(&ManagedIdentityLeaseRequest)` → `ManagedIdentityLeaseGrant` | +| `RefreshToken` | `refresh_lease(&ManagedIdentityLeaseRef)` → `ManagedIdentityLeaseRenewal` | +| `RevokeToken` | `revoke_lease(&ManagedIdentityLeaseRef)` → `ManagedIdentityLeaseRevocation` | +| `InspectMetadata` | `inspect_lease(&ManagedIdentityLeaseRef)` → `ManagedIdentityLeaseInspection` | + +`ManagedIdentityClientState`: `Ready | Unavailable`. + +#### Three-plane relationship + +The `ManagedIdentityRef` type in v3 (`d2b-realm-provider/src/credential.rs:ManagedIdentityRef`) +provides the `client_id` `OpaqueAzureRef` used directly in this Provider's root +config. The ACA path in v3 (`d2bd/src/lib.rs:managed_identity_client_id`, +`d2b-provider-aca/src/lib.rs`) stores the managed identity client ID as an +unwrapped `Option` today (`implemented-and-reachable`). The v3 Provider +config formalizes this as an `OpaqueAzureRef` with the same charset restriction. + +#### v3 current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `d2b-realm-provider/src/credential.rs:ManagedIdentityRef` (implemented-and-reachable); `d2bd/src/lib.rs:managed_identity_client_id` (reachable ACA config field); `d2b-provider-aca/src/lib.rs:managed_identity_client_id` (reachable) | +| Evidence class | Opaque ref data model is reachable; full lease provider is `ADR-only` in v3 | +| Main reuse source | main `a1cc0b2d` `d2b-provider-credential-managed-identity/src/lib.rs`: `ManagedIdentityCredentialClient`, `ManagedIdentityLeaseRequest/Ref/Grant/Inspection/Renewal/Revocation`, `ManagedIdentityCredentialProvider`; `tests.rs`: `FakeClient`, canary enforcement, colocated-consumer and unavailable tests | +| Reuse action | copy and adapt | +| Required delta | v3 contract versions, Provider resource/descriptor, placement restriction (system-domain only), d2b-bus routing, `OpaqueAzureRef` config validation | +| Excluded main assumptions | v2 AgentPlacementBinding, v2 EndpointRole/Realm, v2 ProviderFactory | +| Destination | `packages/d2b-provider-credential-managed-identity/` (reused crate name) | + +## Current-code fit summary + +| Item | Treatment | +| --- | --- | +| Current anchor | `packages/d2b-realm-provider/src/credential.rs`, `provider.rs`; `packages/d2b-core/src/privileges.rs`, `static_invariants.rs`; `packages/d2b-core/src/realm_workloads_launcher.rs:LauncherMetadataInvariants.no_secrets_or_credentials`; `nixos-modules/assertions.nix` | +| Evidence class | `CredentialProvider` trait with status/enrollment is `implemented-and-reachable`; three-plane opaque refs are `implemented-and-reachable`; `ProviderWorkloadIdentity::ManagedIdentity` in `ProviderGuestdBootstrapContract` is `implemented-and-reachable`; `LauncherMetadataInvariants.no_secrets_or_credentials` is `implemented-and-reachable`; v3 `d2b-userd` (`packages/d2b-userd/`) is a guest exec stub only (`test-only-or-preview`; no credential functionality); Credential ResourceType, lease model, operation classes, typed service methods, controller, reconciliation, and async loop are `ADR-only` | +| Behavior retained | Zero secret bytes invariant (structurally enforced); `OpaqueAzureRef` charset/length validation; typed capability denial error shape; bounded/redacted error messages; injected-client pattern keeps secret material in the client process | +| Required delta | Credential ResourceType schema; opaque lease/typed operation service; canonical `use-credential`/`admin-credential` Role admission through exact existing `subresources`; three Credential Provider crates/controllers; d2b-bus routing; async reconciliation integration; Nix resource declaration/assertion; audit/OTEL | +| Reuse path | Copy/adapt three Credential Provider implementations and their test suites from main as named in each dossier; reuse `OpaqueAzureRef` directly from v3 `d2b-realm-provider/src/credential.rs`; adapt `SecretAccess`/privilege audit shape from `d2b-core/src/privileges.rs` | +| Replacement/deletion | Old `CredentialProvider` trait removed only after all three v3 Providers have tested replacement controllers; old three-plane types remain in v3 baseline and are not removed until Credential resource integration is live | +| Feasibility proof | Main proves three Provider implementations; each has fake-client test suites covering acquire/refresh/revoke/inspect, idempotency, locked/unavailable/interaction-required, canary enforcement, generation validation, and lease cardinality limits | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-credential-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-credential-001` | +| Dependency/owner | `ADR046-object-001` (resource envelope); `ADR046-identities-001` (types); W0 shared contract root; `d2b-contracts` | +| Current source | `packages/d2b-realm-provider/src/credential.rs`: `OpaqueAzureRef`, parse/deserialize/charset validation, tests | +| Reuse source | main `a1cc0b2d`: `d2b-provider-credential-secret-service/src/lib.rs` types: `SecretServiceLeaseRequest`, `SecretServiceLeaseRef`, `SecretServiceLeaseGrant`, `SecretServiceLeaseInspection`, `SecretServiceLeaseRenewal`, `SecretServiceLeaseRevocation`, `SecretServiceLeaseState`; parallel entra/managed-identity types; `CredentialLease`, `CredentialLeaseRequest`, `CredentialLeaseState` from `d2b-contracts/src/v2_provider.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/credential.rs` | +| Detailed design | Define `CredentialSpec`, `CredentialStatus`, `CredentialLeaseHandle` (opaque bounded newtype), `CredentialRotationPolicy`, `CredentialRevocationPolicy`, `CredentialScope`, `OperationClass` enum, `CredentialLeaseState`, `PlacementBinding`, `CredentialConditionType`, and all serde/validation/redaction helpers; reuse `OpaqueAzureRef` from v3 baseline directly; enforce zero-secret-bytes charset validation on all string fields at construction; prevent Credential name/ResourceRef/UID or any identity-derived digest from serializing into any status layer or typed error Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Credential controller, Provider dossier schemas, Nix compiler, and resource store all consume one canonical contract | +| Data migration | Full d2b 3.0 reset; no v2 credential import | +| Validation | Schema golden vectors; charset/length tests; serde unknown-field rejection; `OpaqueAzureRef` round-trip and secret-shape rejection parity; `leaseHandle` and `sourceVersion` opaque newtype tests; status/error redaction tests with Credential name/ResourceRef/UID/digest canaries | +| Removal proof | Old `CredentialProvider` trait and `CredentialStatus` enum removed only after all v3 Credential Provider controllers consume this contract | + +### ADR046-credential-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-credential-002` | +| Dependency/owner | `ADR046-credential-001`; `ADR046-api-001` (resource API); `ADR046-bus-001` (d2b-bus); Credential service owner | +| Current source | `packages/d2b-realm-provider/src/provider.rs:CredentialProvider` (status-only trait); `d2b-contracts/proto/v2/provider_credential.proto` (main: Health, Capabilities, Status, AcquireLease, RefreshLease, RevokeLease) | +| Reuse source | main `a1cc0b2d`: `packages/d2b-contracts/proto/v2/provider_credential.proto` method names | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/proto/v3/credential.proto`; `packages/d2b-credential-service/src/{service.rs, client.rs, server.rs}` | +| Detailed design | Define `d2b.credential.v3` protobuf service with exactly five operation methods: `AcquireToken`, `RefreshToken`, `RevokeToken`, `SignChallenge`, and `InspectMetadata`; map those methods one-to-one to the same exact operation class in `spec.allowedOperations` and the existing Role `subresources` field (`acquire-token`, `refresh-token`, `revoke-token`, `sign-challenge`, or `inspect-metadata`) under the canonical `use-credential` resource verb; derive the operation class from the method rather than accepting a caller-selected operation-class field; each request carries `credential_ref`, `operation_id`, `idempotency_key`, `requested_expiry_unix_ms`, `deadline_unix_ms`; map administrative lifecycle to matching `admin-credential` subresources `create`, `update-spec`, and `delete`, supplemental to ordinary CRUD, never to a new Role field or method-name alias; `RevokeToken` and `InspectMetadata` responses carry only non-secret metadata (leaseHandle digest, rotationGeneration, sourceVersion, expiresAtUnixMs, state, outcome code); `AcquireToken`, `RefreshToken`, and `SignChallenge` responses additionally include a `delivery_session_params` field carrying the binding contract fields required to establish the end-to-end credential-delivery ComponentSession (see §Credential-delivery endpoint contract); the token bytes themselves travel in the separate Noise-encrypted delivery session, never in the outer DTO; strict unknown-field rejection; bounded message sizes; all record wrappers for delivery sessions must be zeroizing types | +| Integration | d2b-bus routes `d2b.credential.v3` service to the exact credential provider Process identified by `Credential.spec.providerRef`; before dispatch RBAC requires the exact operation class in both the `use-credential` Role `subresources` and `spec.allowedOperations`; for `AcquireToken`/`RefreshToken`/`SignChallenge`, bus additionally authorizes the credential-delivery endpoint route and forwards opaque Noise-encrypted delivery records without terminating or buffering them; bus never stores or inspects delivery record plaintext | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Protocol golden vectors for each method; Role matrix for the five exact allowed-operation subresources under `use-credential` and exact `create`, `update-spec`, and `delete` subresources under `admin-credential`; prove admin permission is supplemental to ordinary CRUD; deny empty/wildcard/unknown/mismatched subresources, alternate Credential-operation Role fields, and method-name aliases before Provider dispatch; malformed/oversize rejection; `leaseHandle` opacity tests (secret-canary must not appear in outer DTO or delivery routing metadata); locked/unavailable/denied/expired state tests; delivery session binding contract round-trip; zeroizing record type unit tests; delivery channel never materialized in non-delivery method tests | +| Removal proof | Old v2 `CredentialProviderService` proto removed only after all v3 callers migrate | + +### ADR046-credential-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-credential-003` | +| Dependency/owner | `ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`; credential-secret-service owner | +| Current source | `packages/d2b-realm-provider/src/provider.rs:CredentialProvider` (minimal v3 baseline) | +| Reuse source | main `a1cc0b2d`: `packages/d2b-provider-credential-secret-service/src/lib.rs` (full implementation); `src/tests.rs` (full test suite including `FakeOo7Port`, lease lifecycle, locked state, canary enforcement, cardinality limits) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-credential-secret-service/src/{lib.rs, controller.rs, service.rs, main.rs}` (implementation + binary); `packages/d2b-provider-credential-secret-service/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}` (hermetic Cargo integration); `packages/d2b-provider-credential-secret-service/integration/{container-service.sh, host-placement.nix, cleanup-rollback.sh}` (orchestration fixtures); `packages/d2b-provider-credential-secret-service/README.md` (all §Provider README required sections) | +| Detailed design | Adapt `SecretServiceCredentialProvider` and `SecretServiceCredentialProviderFactory` to use v3 `d2b.credential.v3` service interface; replace v2 `CredentialProvider` trait impl with v3 controller/service handler; adapt `Oo7SecretServicePort` trait (retain all methods unchanged); ensure `SecretServiceOwner::Userd` placement restriction rejects system-domain and guest-agent construction; validate `collectionAlias` against provider-internal charset (not `OpaqueAzureRef`; collection aliases may include spaces); integrate with Provider resource descriptor and controller toolkit; test `credential_canary` never appears in any service response Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | Target: user-domain Process (ADR-only `Process` ResourceType) under Host (ADR-only `Host` ResourceType); d2b-bus (ADR-only) routes `d2b.credential.v3` calls to this process; Credential controller reconciles status. Current v3 has no user-credential host process: v3 `d2b-userd` (`packages/d2b-userd/src/lib.rs`) is a guest exec stub only (exits 78 in service mode; `UserAttachRequest`/`UserExecSession` guest wire primitives only; evidence class: `test-only-or-preview`); no credential or keyring functionality. This integration path is fully new (ADR-only) work. | +| Data migration | Full reset; no migration from old `CredentialProvider` trait | +| Validation | **`src/` unit** (`#[cfg(test)]` in `src/`): `Oo7SecretServicePort` trait API surface, `SecretServiceOwner` placement guard, `collectionAlias` charset, `lockPolicy` state transitions. **`tests/` Cargo integration** (`cargo test -p d2b-provider-credential-secret-service`): copied test suite from main with v3 type substitutions; add `lifecycle.rs` (acquire/refresh/revoke/inspect end-to-end with `FakeOo7Port`); `conformance.rs` (all 11 `check_provider_conformance` arms pass); `faults.rs` (locked state → `credential-provider-unavailable`, unavailable, cardinality limit); `canary.rs` (`credential_canary` and `object_path_canary` absent from every response, status field, and delivery record); `delivery.rs` (delivery-session binding contract, zeroizing buffer, replay-safe sequence); `placement.rs` (system-domain and guest-agent construction rejected). **`integration/` fixtures**: `container-service.sh` (container-backed Provider service start/stop/drain); `host-placement.nix` (user-domain Host/Process placement in runNixOSTest); `cleanup-rollback.sh` (Nix-generation removal triggers async Delete and Provider-revoke finalizer). | +| Removal proof | Old `d2b-realm-provider:CredentialProvider` trait removed only after this controller and the other two Credential controllers reach full reconcile parity | + +### ADR046-credential-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-credential-004` | +| Dependency/owner | `ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`; credential-entra owner | +| Current source | `packages/d2b-realm-provider/src/credential.rs:AzureControlPlaneRef`, `OpaqueAzureRef` (v3 baseline, reachable) | +| Reuse source | main `a1cc0b2d`: `packages/d2b-provider-credential-entra/src/lib.rs` (full implementation); `src/tests.rs` (full test suite including `FakeEntraClient`, `credential_canary`/`endpoint_canary`, interaction-required, colocated-consumer, generation-mismatch tests) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-credential-entra/src/{lib.rs, controller.rs, service.rs, main.rs}`; `packages/d2b-provider-credential-entra/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}`; `packages/d2b-provider-credential-entra/integration/{container-service.sh, guest-placement.nix, cleanup-rollback.sh}`; `packages/d2b-provider-credential-entra/README.md` (all §Provider README required sections) | +| Detailed design | Adapt `EntraCredentialProvider` and `EntraCredentialProviderFactory` to v3 service; replace v2 `AgentPlacementBinding` with v3 `PlacementBinding` enum (user-agent, guest-agent only; reject host-system); validate `tenantId` config field using `OpaqueAzureRef::parse` from v3 baseline `d2b-realm-provider/src/credential.rs` (note: current v3 source field is named via `AzureControlPlaneRef`; target field name is `tenantId`); retain `EntraCredentialClient` trait unchanged; map `EntraClientError::InteractionRequired` to `credential-provider-unavailable` (not denied); enforce `EntraCredentialOwner::ExactConsumer` so only the declared `consumerRef` may acquire; enforce canonical Credential verb/subresource admission, provider-visible Endpoint plus exact consumer policy, and audit-only Credential identity as frozen in the dedicated Entra dossier Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | User-domain or system-domain Process under Guest; d2b-bus routing; Credential controller | +| Data migration | Full reset | +| Validation | **`src/` unit**: `EntraCredentialClient` trait API, `OpaqueAzureRef::parse` on `tenantId`, `EntraCredentialOwner::ExactConsumer` guard, `EntraClientState` transitions. **`tests/` Cargo integration**: `lifecycle.rs` (acquire/refresh/revoke/inspect with `FakeEntraClient`); `conformance.rs` (all conformance arms, canonical Endpoint visibility/consumer policy, and exact Role verb/subresource mapping); `faults.rs` (interaction-required → unavailable, generation-mismatch, colocated-consumer and mismatched Role-subresource rejection); `canary.rs` (secret plus Credential name/ResourceRef/UID/digest canaries absent from status/errors/logs and every OTEL Resource/span attribute/metric label; authorized audit retains only `resource_name_digest`); `delivery.rs` (delivery-session binding, zeroizing, replay-safe); `placement.rs` (host-system placement rejected). **`integration/` fixtures**: `container-service.sh`; `guest-placement.nix` (user-domain and system-domain Process on Guest in runNixOSTest); `cleanup-rollback.sh`. | +| Removal proof | Same as ADR046-credential-003 | + +### ADR046-credential-005 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-credential-005` | +| Dependency/owner | `ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`; credential-managed-identity owner | +| Current source | `packages/d2b-realm-provider/src/credential.rs:ManagedIdentityRef` (v3 baseline, reachable); `d2bd/src/lib.rs:managed_identity_client_id` (reachable ACA config); `d2b-provider-aca/src/lib.rs:managed_identity_client_id` (reachable) | +| Reuse source | main `a1cc0b2d`: `packages/d2b-provider-credential-managed-identity/src/lib.rs` (full implementation); `src/tests.rs` (full test suite) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-credential-managed-identity/src/{lib.rs, controller.rs, service.rs, main.rs}`; `packages/d2b-provider-credential-managed-identity/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}`; `packages/d2b-provider-credential-managed-identity/integration/{container-service.sh, host-guest-placement.nix, aca-credential-ref.sh, cleanup-rollback.sh}`; `packages/d2b-provider-credential-managed-identity/README.md` (all §Provider README required sections) | +| Detailed design | Adapt `ManagedIdentityCredentialProvider` to v3 service; enforce `ManagedIdentityCredentialOwner::ExactSdkConsumer`; reject user-agent placement (IMDS is machine-local, not user-session); validate `clientId` config using `OpaqueAzureRef::parse` directly from v3 baseline (note: current v3 source field is named via `ManagedIdentityRef.client_id`; target field name is `clientId`); retain `ManagedIdentityCredentialClient` trait unchanged; map `ManagedIdentityClientState::Unavailable` to `credential-provider-unavailable`; `sign-challenge` operation class returns schema-invalid immediately Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | System-domain Process under Host or Guest; d2b-bus routing; Credential controller; ACA runtime-azure-container-apps Provider may hold a reference to this Credential resource | +| Data migration | Full reset; `d2b-provider-aca` managed_identity_client_id config field migrated to a Credential resource reference in the v3 ACA Provider config | +| Validation | **`src/` unit**: `ManagedIdentityCredentialClient` trait, `OpaqueAzureRef::parse` on `clientId`, `ManagedIdentityCredentialOwner::ExactSdkConsumer` guard, `imdsEndpointAlias` validation, `sign-challenge` schema-invalid fast path. **`tests/` Cargo integration**: `lifecycle.rs` (acquire/refresh/revoke/inspect with `FakeClient`); `conformance.rs`; `faults.rs` (unavailable state, colocated-consumer rejection); `canary.rs` (canary absent from all responses and delivery records); `delivery.rs`; `placement.rs` (user-agent placement rejected). **`integration/` fixtures**: `container-service.sh`; `host-guest-placement.nix` (system-domain Host and Guest placement in runNixOSTest); `aca-credential-ref.sh` (ACA Provider config uses `credentialRef`; raw `managed_identity_client_id` absent); `cleanup-rollback.sh`. | +| Removal proof | `d2b-provider-aca:managed_identity_client_id` raw field removed only after `credential-managed-identity` Provider controller is integrated and the ACA Provider config uses `credentialRef` | + +### ADR046-credential-006 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-credential-006` | +| Dependency/owner | `ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`, `ADR046-reconcile-002`; Credential controller owner | +| Current source | No direct v3 current source; controller is `ADR-only` | +| Reuse source | main `a1cc0b2d`: `packages/d2b-provider-toolkit/src/conformance.rs` (provider conformance pattern); `packages/d2b-provider-toolkit/src/adapter.rs` (controller toolkit pattern) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-credential-/src/controller.rs`; `packages/d2b-contracts/src/v3/credential_controller.rs` | +| Detailed design | Implement Credential controller handler conforming to `ADR-046-resource-reconciliation` async loop; implement `reconcile`, `observe`, `finalize`, `drain`, and `health` handlers; implement rotation state machine (proactive/on-expiry/on-demand policies); implement `provider-revoke` finalizer execution with `revocation.onOwnerDelete` policy; implement provider-generation-change detection and revocation; implement `CredentialReady`, `RotationDue`, `ProviderUnavailable`, `LeaseRevoked` condition logic; use controller Roles with the exact allowed-operation subresource matching each `use-credential` service call, while status/finalizer writes retain their separate structural ownership checks; never add a Credential-specific Role field or coarse alias; implement bounded idempotency key derivation (Credential UID + rotationGeneration + operation class, no secret material); implement `observeInterval=30s` health check calling `InspectMetadata`; bounded retry/backpressure with typed `credential-rotation-failed` outcome; enforce `MAX_LOCAL_LEASES=256` per controller provider instance | +| Integration | Provider controller Process → d2b-bus → `d2b.credential.v3` service in provider process → injected client/port; status updates through resource API; watch subscription on Credential, Provider, Host/Guest dependency types | +| Data migration | None; v3 reset | +| Validation | Controller state-machine golden vectors; rotation-policy matrix (proactive/on-demand/on-expiry × success/locked/unavailable/expired); exact Role-subresource admission matrix; finalizer execution tests; provider-generation-change revocation tests; idempotency key derivation tests; observe-interval drift detection test; canary tests confirm zero secret bytes and no Credential name/ResourceRef/UID/digest in any controller-written status or error | +| Removal proof | Not applicable (new controller) | + +### ADR046-credential-007 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-credential-007` | +| Dependency/owner | `ADR046-credential-001`; `ADR046-identities-002` (Nix resource compiler); `ADR046-api-001` (resource API, for create/update/delete); `ADR046-reconcile-001` (activation-nixos controller); Nix integrator | +| Current source | `nixos-modules/assertions.nix` (secret-shape assertions, `implemented-and-reachable`). Note: `nixos-modules/options-realms.nix` line 318 contains `d2b.realms..relay.credentialRef` — this uses the current v3 **Realm** terminology (current symbol for Zone) and is a relay credential **state-directory path** reference for gateway relay provisioning, not a Zone Credential resource declaration. It is not a source for this work item. No current v3 source exists for Zone Credential resource Nix declarations (ADR-only). | +| Reuse action | adapt | +| Destination | `nixos-modules/options-resources.nix` (generic schema-derived resource options; not type-specific), `nixos-modules/activation-nixos-cleanup.nix` | +| Detailed design | **(1) Schema-derived options and eval-time validation**: implement `d2b.zones..resources. = { type = "..."; spec = { ... }; }` as a generic attrset option; an optional `metadata` sub-attr may contain `ownerRef` and/or presentation `labels`/`annotations`. Nix option types, defaults, and inline docs for `spec` fields are generated from the committed `ResourceTypeSchema` JSON (`docs/reference/schemas/v3/credential.json`) and the signed Provider schema — no bespoke options module is maintained separately. `metadata.name` derives from the attr key; `metadata.zone` from the Zone attr key; `apiVersion` defaults to `resources.d2bus.org/v3`; `status`/`uid`/`generation`/`revision`/timestamps/`managedBy`/`configurationGeneration` are not authored. Core assigns `metadata.managedBy = "configuration"` and `metadata.configurationGeneration = ` to all configuration-managed resources at create/update time; these are never authored in Nix. Eval-time assertions (applied to all entries with `type = "Credential"`): `spec.providerRef` resolves a Provider in the same Zone whose `credentialDomains` includes `spec.scope.domainFilter` and whose `spec.artifactId` (a sibling of `spec.config` on the Provider resource, not inside `spec.config`) resolves an artifact catalog entry of `type = "provider"`; `spec.audience` charset (`^[A-Za-z0-9._:/@-]+$`, max 256); `spec.rotation.proactiveWindowMs < maxLeaseLifetimeMs / 2`; `spec.consumerRef`/`scope.executionRef`/`scope.userRef` resolve declared Zone resources; duplicate `(providerRef, executionRef, userRef, audience)` tuple rejected; `contains_sensitive_shape` on all string fields; Provider-specific placement constraints; `allowedOperations` ⊆ `providerRef.supportedOperations`. **(2) Canonical JSON and bundle emission**: render `spec` attr directly to `spec` object in canonical JSON (no field renames/re-nesting); `metadata` in output contains only derived `name`/`zone` and optionally Nix-authored `ownerRef`/`labels`/`annotations`; `apiVersion` is top-level, not inside `metadata`; `finalizers` is omitted from the Nix-rendered input (core manages finalizers, never accepts them from the bundle); no management labels are emitted by Nix; sort bundle by `(type, name)`; write to `/etc/d2b/zones//resource-bundle.json` with digest. **(2b) Artifact catalog emission**: derivation-valued inputs (`d2b.artifacts.`) are compiled separately into an integrity-pinned artifact catalog (`/etc/d2b/zones//artifact-catalog.json`) with its own digest header; each entry records `id`, `type`, `sha256`, and bounded closure metadata; store paths are private catalog implementation data absent from the resource bundle, status, audit, and logs; `activation-nixos` verifies both digests before any create/update; missing or wrong-type `artifactId` references fail the NixOS build. **(3) Build-time schema validation**: validate rendered JSON against `docs/reference/schemas/v3/credential.json` and Provider-specific schema; enforce `secretRef` fields use `Credential/` refs; enforce no store paths in any resource bundle or status output; drift gate (`make test-drift`) regenerates schemas with `cargo xtask gen-schemas` and asserts `git diff --exit-code`; Nix options module drift checked in the same gate. **(4) Generation transition and cleanup contract**: activation-nixos controller verifies SHA-256 digest of both resource bundle and artifact catalog, creates/updates desired-set resources, activates without blocking on cleanup, issues async Delete for absent configuration-managed resources (those with `metadata.managedBy = "configuration"`), sets Degraded/Cleanup=True on removed resources; retains up to `retainedConfigurationMax` (default 3, range 1..16) prior bundles; oldest prune when count exceeded; no time-based rollback window. **(5) Configuration-managed vs controller/API isolation**: `managedBy` and `ownerRef` are orthogonal; configuration-managed and API-created resources may each carry an optional same-Zone `ownerRef` and participate in owner cascade. Cleanup checks `metadata.managedBy = "configuration"` before issuing Delete; resources with `metadata.managedBy = "controller"` or `metadata.managedBy = "api"` are never deleted by this path; API-created resources persist until explicit Delete and are never generation-swept. | +| Integration | `activation-nixos` Provider creates/updates/deletes Credential resources from emitted envelopes with both the ordinary CRUD permission and the matching `admin-credential/create`, `admin-credential/update-spec`, or `admin-credential/delete` permission; Credential controller `provider-revoke` finalizer handles cleanup Deletes; owner controller reconciles children of deleted configuration-managed Credentials | +| Data migration | None; v3 reset | +| Validation | **(eval/build)**: nix-unit golden JSON envelope for each example (spec shape, no management labels in Nix output, sort, digest); assertion-failure tests for secret-shaped audience, mismatched providerRef/domainFilter, proactiveWindow > half maxLifetime, duplicate binding tuple, unresolved refs; generated activation Role contains `admin-credential` with exact `subresources = [ "create" "update-spec" "delete" ]`, no coarse alias, and no undeclared Role field; permission tests prove each lifecycle action also requires its ordinary CRUD verb; artifact catalog: assertion-failure for missing `artifactId`, wrong-type `artifactId`, duplicate catalog ID; bundle + artifact catalog digest round-trip; artifact catalog store-path absence from resource bundle and status; Provider-specific schema cross-check; `make test-drift` schema drift gate. **(runtime integration in `tests/host-integration/`)**: `credential-cleanup-basic` (removed resource reaches Deleted); `credential-cleanup-nonblocking` (activation Ready before cleanup finalizer finishes); `credential-cleanup-pending-status` (Cleanup=True on removed resource, PendingCleanup=True on Provider); `credential-cleanup-stalled` (Degraded stall detection and recovery); `credential-cleanup-controller-children-preserved` (ownerRef children cleaned by Credential controller); `credential-cleanup-no-dynamic-deletion` (controller-created Credential with `managedBy = "controller"` not deleted); `credential-retained-generation-count` (up to retainedConfigurationMax bundles retained; rollback re-creates from retained bundle; oldest pruned when count exceeded); `credential-bundle-digest-mismatch` (tampered bundle aborts activation). | +| Removal proof | Not applicable (new module) | + +### ADR046-credential-008 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-credential-008` | +| Dependency/owner | `ADR046-credential-001`, `ADR046-credential-006`; audit/OTEL integrator | +| Current source | `packages/d2b-core/src/privileges.rs:SecretAccess` (implemented-and-reachable); `d2b-realm-provider/src/error.rs:ProviderDiagnostic`/`contains_sensitive_shape` (implemented-and-reachable); `packages/d2b-contract-tests/tests/policy_observability.rs` (reachable audit policy tests) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-credential-/src/audit.rs`, `telemetry.rs`; `packages/d2b-contract-tests/tests/credential_audit.rs` | +| Detailed design | Implement audit record emission for all credential service methods and controller events using the field set defined in §Audit, with Credential identity represented only by the authorized bounded `resource_name_digest` after authorization and no identity-bearing record elicited by a denied request; implement OTEL span/metric emission using the closed semantic label set in §OTEL and metrics, with expiry reported as a provider/placement aggregate and no Credential resource name, ResourceRef, UID, digest, derived identity token, Zone/resource-name-derived label, or non-allowlisted OTEL Resource attribute; retain applicable generic collector-allowlisted Resource attributes (`d2b.zone`, `d2b.provider`, `d2b.component`, and service fields); implement `contains_sensitive_shape` checks in all string fields of audit records and metric label values (adapted from `d2b-realm-provider/src/error.rs:contains_sensitive_shape`); add canary-enforcement tests that verify `"secret-canary"`, `"entra-token-canary"`, `"managed-identity-canary"`, Credential name/ref/UID/digest canaries, and Zone name values never appear in any metric label, span attribute, log line, status field, error, or collector diagnostic, and that Credential identity canaries are also absent from OTEL Resource attributes | +| Integration | Credential controller and service handlers emit audit records and telemetry through Zone audit/OTEL paths | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Canary tests across all three Provider crates; authorized audit record field-presence tests require `resource_name_digest` and reject raw Credential name/ResourceRef/UID, while denied-request tests emit no identity-bearing audit; structural metric descriptor tests assert exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `credential_ref`, `credential_uid`, `credential_digest`, `resource_name_digest`, and every resource-name-derived key; Credential name/ref/UID/digest canaries are absent from every status field, error, log/Debug line, collector diagnostic, OTEL Resource attribute, span attribute, and metric label; Zone-name canaries are absent from spans and labels while resource-attribute tests preserve the generic collector allowlist including `d2b.zone`, `d2b.provider`, `d2b.component`, and service fields; complete Credential metric/span frames pass the shared collector ingress validator, while adding `d2b.credential.name` or any Credential identity key/value rejects the whole frame | +| Removal proof | Not applicable | diff --git a/docs/specs/ADR-046-resources-device.md b/docs/specs/ADR-046-resources-device.md new file mode 100644 index 000000000..4c9c4da11 --- /dev/null +++ b/docs/specs/ADR-046-resources-device.md @@ -0,0 +1,2151 @@ +# ADR 0046 Device resource and Provider dossiers + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-resources-device` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-provider-device-*`, Device controller contracts, Nix device emitters | +| Depends on | `ADR-046-resource-object-model`, `ADR-046-primitive-resource-composition`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-provider-model-and-packaging`, `ADR-046-components-processes-and-sandbox` | +| Supersedes | Current ProcessRole device sidecars (Swtpm, SwtpmPreStartFlush, Usbip, SecurityKeyFrontend, Gpu, GpuRenderNode, Video) and their Nix components | + +## Purpose + +Device is the single ResourceType for physical and emulated hardware device +inventory, arbitration, claim, and attachment in a Zone. It is the resource +contract for the four frozen Device Provider families: `device-tpm`, +`device-usbip`, `device-security-key`, and `device-gpu` (combined GPU/video). + +Audio (`device-audio`) is not part of this spec. The `audio-pipewire` Provider +is in the interaction Provider catalog and is independently specified. + +### Physical-device authority (D097) + +Every physical backing a `Device` represents is a D097 authority: the Device (and +its owning Device Provider) declares a signed `AuthorityDescriptor` with +`authorityScope: physical-device`, an **opaque** `authorityKey` class (a digest +of vendor/product/bus selectors — never a raw path/serial/address), +`cardinality: zero-or-one` per physical backing, and the appropriate +`arbitration`: `exclusive` for a full-device/DRM-primary/VFIO GPU, a physical +TPM, a security key's hidraw, or an exclusive USB/USBIP passthrough; `shared` +for a render-node or other genuinely multiplexable engine. Even a `shared` device +has exactly **one** authority owner (the Device Provider's authority service that +opens the backing and issues per-holder grants); consumers never open the +physical node directly. Core's authority index rejects a second authority for the +same `(Zone, physical-device, opaqueKeyDigest)` with the typed `duplicateConflict` +before any open; restart adopts the exact authority by `ownerProof`, and +ambiguity quarantines. Cross-Zone sharing of a device is only via D096 +ResourceExport/ResourceImport (`exportability: explicit-export`), which still +routes through the single owner. `maxConcurrentClaims`/`arbitration` in the base +spec is the requested share mode and cannot bypass the descriptor. + +## Device ResourceType spec + +### Three-layer spec shape (D089) + +D089 freezes Device spec as three layers. Layer 1 is the universal Resource +envelope and metadata. Layer 2 is the Device base spec at top-level `spec.*`, +including `spec.providerRef`; `deviceClass`, `arbitration`, +`maxConcurrentClaims`, and `inventory.selector` remain base fields. Layer 3 is +the optional canonical selected-Provider extension +`spec.provider = { schemaId, schemaVersion, settings }`; it is the only +Provider-specific desired extension and replaces the former top-level +`spec.settings` shape. It omits `providerRef` and +`observedProviderGeneration`: `spec.providerRef` is base, and spec is desired +rather than observed. + +**D091 update policy.** The universal base spec carries `spec.updatePolicy` for +every Device: disruptive changes default to manual, while automatic +non-disruptive upgrades are permitted by policy. A `spec.provider` extension MAY +add provider-specific knobs, but MUST NOT bypass or weaken base +`spec.updatePolicy`. + +Every Device Provider `ResourceApiBinding` MUST implement the exact Device base +spec schema version and fingerprint, accept the canonical minimal valid base +Spec, and pass base lifecycle/status/error/finalizer conformance. A Provider MAY +reject an optional base capability only through its signed standard capability +matrix and a typed provider-neutral `unsupported-capability` error; it MUST NOT +ignore, reinterpret, rename, duplicate, weaken, or require extension data for +base-required behavior. `spec.provider.settings` is strict deny-unknown, +bounded, schema-versioned and digested, validated against `spec.providerRef` at +Nix build and API admission, and fails with `spec-provider-schema-invalid` or +`spec-provider-shadow` when invalid or shadowing/restating/overriding/renaming/ +duplicating a base field. Shared device semantics are promoted to the Device +base spec and never live in `spec.provider`; generic CLI/controllers operate on +base spec plus base status. For the same Provider, the `spec.provider` and +`status.provider` schemas align. + +Provider resource dossiers in this file retain the D075 Provider self-description +shape (`spec.artifactId`, `spec.config`) because a Provider has no non-circular +`spec.providerRef`. + +### Envelope example + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Device +metadata: + name: corp-vm-tpm + zone: dev + uid: + generation: 1 + revision: + ownerRef: Guest/corp-vm + finalizers: [device-tpm.d2bus.org/state-preserved] + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:00:00Z +spec: + providerRef: Provider/device-tpm + deviceClass: emulated + arbitration: exclusive + maxConcurrentClaims: 1 + inventory: + selector: {} # emulated devices carry no physical selector + provider: + schemaId: device-tpm.d2bus.org/Device/spec + schemaVersion: "1.0" + settings: {} # Provider-specific settings +status: + observedGeneration: 1 + phase: Ready + conditions: [] + lastReconciledAt: 2026-07-22T00:00:01Z + device: + present: true + health: healthy + holderRefs: [Guest/corp-vm] + claims: [] + provisionedAt: 2026-07-22T00:00:01Z +``` + +### Device spec fields + +| Field | Type | Required | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | --- | +| `providerRef` | ResourceRef | yes | — | must resolve to an installed Provider | Selects the device Provider | +| `deviceClass` | enum | yes | — | `physical` \| `emulated` | Physical devices exist in sysfs/udev; emulated devices are created by the Provider | +| `arbitration` | enum | yes | — | `exclusive` \| `shared` | Whether the device may be simultaneously claimed by more than one holder | +| `maxConcurrentClaims` | uint | no | 1 | 1–16 | Maximum simultaneous claimants; must equal 1 when `arbitration=exclusive` | +| `inventory` | object | yes | — | see below | Physical or emulated device selector | +| `provider` | object? | no | `null` | canonical `{schemaId,schemaVersion,settings}` | Optional selected-Provider extension envelope (D089). `settings` carries Provider-validated implementation-only device configuration (`.d2bus.org/Device/spec`); strict deny-unknown, bounded. MUST NOT shadow a base field. | + +### Inventory selector + +An inventory selector is a discriminated union keyed on `busClass`. The +`busClass` field is the variant discriminant. Unknown fields for a given +`busClass` variant and unknown `busClass` values are rejected at admission +(strict unknown-field denial). No raw device path appears in the spec. + +```yaml +inventory: + # Emulated device — selector must be absent or {} + selector: {} + + # USB device + selector: + busClass: usb + label: yubikey-work # required; stable human label, max 63 chars + vendorId: "1050" # 4-hex lower-cased; optional + productId: "0407" # 4-hex lower-cased; optional + serial: null # max 128 chars; optional + + # HID/hidraw device + selector: + busClass: hidraw + label: yubikey-primary + vendorId: "1050" + productId: "0407" + serial: null + + # DRM/GPU device + selector: + busClass: drm + label: host-gpu + pciSlot: null # optional PCI slot filter; e.g. "0000:01:00.0"; max 31 chars + + # PCI device (non-GPU; reserved for future physical-passthrough Providers) + selector: + busClass: pci + label: host-pci-dev + slot: null # PCI slot; e.g. "0000:02:00.0"; max 31 chars + + # Physical TPM kernel device (rare; emulated TPM uses {})) + selector: + busClass: tpm + label: host-tpm + index: 0 # /dev/tpm; default 0 +``` + +Closed `busClass` values: `usb`, `hidraw`, `drm`, `pci`, `tpm`. Any other value +is rejected. For `deviceClass=emulated` the `selector` must be `{}` or absent. +For `deviceClass=physical` the `selector.label` field is required on every +variant and serves as the stable operator-defined identifier; the Provider +resolves the physical node using the label plus any provided filter fields. +Vendor and product IDs are stored lower-cased and must be exactly four ASCII hex +digits. A spec containing extra fields beyond a variant's declared field set is +rejected without admission. + +## Claims and attachments in Host/Guest/Process + +Device claims are declared inline on the execution context that holds them. + +### Host/Guest devices list + +`spec.devices` on a Host or Guest resource: + +```yaml +devices: + - deviceRef: Device/corp-vm-tpm + claim: exclusive + passthrough: tpm-socket # Provider-specific passthrough kind +``` + +| Field | Type | Required | Default | Notes | +| --- | --- | --- | --- | --- | +| `deviceRef` | ResourceRef | yes | — | Same-Zone Device resource | +| `claim` | enum | yes | — | `exclusive` \| `read-shared` \| `provider-managed` | +| `passthrough` | string | no | null | Provider-specific passthrough kind (e.g., `tpm-socket`, `usbip-export`, `virtiofs-hidraw`, `gpu-virtio`, `gpu-render-node`) | +| `settings` | object | no | `{}` | Provider-specific per-attachment settings | + +A Device with `arbitration=exclusive` rejects more than one claimed Host/Guest +attachment at a time. The controller writes a conflict condition and sets phase +Degraded/Pending on all but the first successful claimant. + +### Process devices list + +`spec.devices` on a Process or EphemeralProcess resource: + +```yaml +devices: + - deviceRef: Device/corp-vm-gpu + usage: gpu-socket + settings: {} +``` + +Process device entries express a usage dependency. The Process controller +verifies the Device is Ready and claimed by its owning Guest/Host before launch. +A Process that needs a device fd directly (e.g., a GPU worker Process that +receives a socket path) declares its dependency here; the fd handoff is +Provider-specific and is not in the resource spec. + +## Device status + +### Three-layer status shape (D088) + +D088 freezes `Device` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `Device`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +**D091 update currency.** Every Device includes universal `status.update` with +`state` (`Current|UpdateAvailable|UpgradeRequired|Upgrading|Blocked|Unknown`), +`reasons` +(`CoreGenerationChanged|ProviderGenerationChanged|ArtifactChanged|ImageOrSystemGenerationChanged|SpecChanged|DependencyChanged|SecurityPolicyChanged`), +bounded non-secret observed/target generation and digest IDs, `disruption` +(`None|Reload|Restart|Recycle|Replace`), `preserveState`, optional +`operationId`, `lastAssessedAt`, and bounded/truncated `owned:{count,refs}` and +`dependencies:{count,refs}`. Device claim/arbitration/presence base currency is +reported through `status.update`; any Device-specific refinement lives in +`status.resource` and never in `status.provider`. Device controllers set this +via `assess_update` on core/provider/artifact/image-or-system/spec/dependency/ +security-policy triggers and MUST report `UpgradeRequired` for disruptive +changes rather than applying them in place. A Device upgrade such as a GPU +realization change MUST use `UpgradeRequired` or `Blocked` while dependents +exist; the planner drains dependents, recycles the Device realization, and +restarts dependents. TPM/device identity and durable device state use +`preserveState: true` wherever possible. + +The existing `status.device` sub-object is carried within `status.resource` as +`status.resource.device` by the mapping convention. `Device` has multiple +implementations (`device-tpm`, `device-usbip`, `device-gpu`, and +`device-security-key`). Claim/arbitration base fields, presence/provisioning, +health, holder refs, and bounded claim entries are frozen in `status.resource` +and MUST be identical across all implementations. Implementation-specific +observation belongs only in that implementation's `status.provider.details`; +shared fields MUST NOT be duplicated there. + +Status fields beyond the standard common envelope: + +```yaml +status: + phase: Ready | Pending | Degraded | Failed | Unknown + conditions: + - type: DevicePresent + status: "True" + reason: device-probed-present + - type: DeviceClaimed + status: "True" + reason: exclusive-claim-held + device: + present: true | false | null + health: healthy | degraded | failed | unknown + holderRefs: [] # ordered list of claimant ResourceRefs (bounded) + claims: [] # per-claimant claim entry + provisionedAt: null # RFC 3339; set when emulated device is created + lastProbedAt: null # RFC 3339; set on last physical probe + providerDiagnostic: null # bounded redacted one-line string; never paths/secrets +``` + +### Device claim entry + +```yaml +claims: + - holderRef: Guest/corp-vm + claim: exclusive + passthrough: tpm-socket + claimedAt: 2026-07-22T00:00:01Z + health: healthy | degraded | failed | unknown +``` + +### Phase semantics + +| Phase | Meaning | +| --- | --- | +| `Pending` | Device spec committed but not yet provisioned/claimed | +| `Ready` | Device is present/provisioned, claim(s) held, healthy | +| `Degraded` | Usable but one or more conditions impaired (e.g., probe uncertain) | +| `Failed` | Current spec generation cannot complete under retry policy | +| `Unknown` | Controller or Host/Guest cannot currently prove device state | + +`Succeeded` and `Deleted` follow the common phase contract. + +### Condition types + +| Type | Meaning | +| --- | --- | +| `DevicePresent` | Physical device is sysfs/udev-visible; always True for emulated; Unknown after first probe failure; False after three consecutive failures | +| `DeviceProvisioned` | Emulated device (swtpm, virtual HID) has been created | +| `DeviceClaimed` | At least one active claim is held | +| `DeviceHealthy` | Physical or emulated device is responsive | +| `ClaimConflict` | Exclusive device received a second concurrent claim request | +| `StateIntegrity` | Emulated device tamper-marker/state check passed | +| `BrokerAccessible` | Broker can open/pass the physical device fd | + +## Ownership and finalizer contract + +The Device resource's `ownerRef` is typically the Guest or Host that provisioned +it. The owning Provider installs one or more typed finalizers before starting any +external effect. + +### Finalizer IDs + +| Provider | Finalizer ID | +| --- | --- | +| `device-tpm` | `device-tpm.d2bus.org/state-preserved` | +| `device-usbip` | `device-usbip.d2bus.org/attachment-released` | +| `device-security-key` | `device-security-key.d2bus.org/lease-released` | +| `device-gpu` | `device-gpu.d2bus.org/worker-stopped` | + +Deletion sequence: +1. `deletionRequestedAt` is set. +2. The Device Provider finalizer handler terminates owned worker Processes, + releases OS resources (socket, udev rules, swtpm state if policy allows), + and clears the finalizer. +3. If a tamper-sensitive Volume exists (device-tpm only), the finalizer + completes but does NOT delete the persistent Volume; that Volume has its + own lifecycle and owner. +4. Core removes the Device resource after all finalizers clear. + +Persistent TPM state (swtpm NVRAM, EK seed) is never deleted by the Device +finalizer. The Volume resource that owns TPM state has an independent lifecycle +and its own `volume-local.d2bus.org/tpm-state` finalizer. + +## Hotplug + +### Hotplug observation + +Device Providers that manage physical devices declare a bounded observe interval +in their controller descriptor. Core schedules a `scheduled-observe` trigger at +that interval. The controller probes the current device state and writes status. + +Hotplug state changes flow exclusively through Device status and the standard +resource/owner/dependency hint mechanism. The Device controller does not signal +the Host/Guest supervisor directly; it writes updated Device status, and +dependent Host/Guest controllers receive a `dependency-changed` trigger through +the normal resource watch path and react accordingly. There is no direct +supervisor signal bypass. + +### Physical probe failure semantics + +| Consecutive probe failures | Status transition | +| --- | --- | +| 1 (first failure) | phase → `Unknown`; condition `DevicePresent` status=`Unknown`, reason=`device-probe-failed` | +| 2 | condition `DevicePresent` remains `Unknown`; phase remains `Unknown` | +| 3 | phase → `Degraded`; condition `DevicePresent` status=`False`, reason=`device-consecutive-probe-failures-exceeded` | +| Device returns | phase → `Ready`; condition `DevicePresent` status=`True`, reason=`device-probed-present` | + +A single probe failure does not set `DevicePresent=False` or phase `Degraded`. +After three consecutive failures the full consequence chain fires: + +1. Condition `DevicePresent=False` and phase `Degraded`. +2. Claimant Host/Guest/Process controllers receive `dependency-changed` triggers + through the normal resource watch path. +3. If the device is essential (e.g., an exclusive TPM claim), the Guest + controller may set phase `Degraded` or stop the Guest. +4. When the device returns, the Device controller sets `DevicePresent=True` and + phase `Ready`, re-triggering claimants. + +### Hotplug limits + +- Maximum observe interval: 60 s (configurable per Provider, default 30 s). +- A probe failure must transition phase to `Unknown` within one observe period. +- Three consecutive probe failures are required before `DevicePresent=False`. + +## Security model + +### Broker effect limits + +The Device Provider is a Process under a Host. It interacts with the Zone +privileged broker for operations that require root: + +| Provider | Broker operation | Effect | Audit | +| --- | --- | --- | --- | +| `device-tpm` | `PrepareStateDir` (via `PrepareRuntimeDir`/`PrepareSwtpmDir` broker hook) | Provision/harden swtpm state dir, verify tamper marker | Yes | +| `device-tpm` | `SpawnRunner` (swtpm role) | Spawn swtpm Process in user namespace | Yes | +| `device-usbip` | `UsbipBindFirewallRule { action: Ensure \| Remove }` | Ensure or remove the exact per-Network/per-busid nftables rule | Yes | +| `device-usbip` | `SpawnRunner` (usbip role) | Spawn usbipd/bind Process | Yes | +| `device-security-key` | `SecurityKeyOpenDevice` | Open exact FIDO hidraw node; return fd via SCM_RIGHTS; never a path | Yes | +| `device-security-key` | `SecurityKeyApplyUdevRules` | Write udev rules for configured FIDO hidraw nodes | Yes | +| `device-gpu` | `SpawnRunner` (gpu/gpu-render-node/video role) | Spawn crosvm GPU or video-decoder Process | Yes | +| `device-gpu` | `OpenDevice` (kvm/dri/udmabuf/nvidia*) | Open GPU device fds in user namespace pre-spawn | Yes | + +No Device Provider receives a blanket device-path grant, raw socket address, or +ambient host capability. Broker operations are point-specific and audit-logged. + +### Security key specific invariants + +From D046 and the v3 baseline: + +1. The Zone privileged broker is the only entity that opens the physical hidraw + node. It never accepts a caller-supplied path; it derives the physical node + from the trusted bundle device table using only a stable label or session ID. +2. **Current implementation:** the relay is a daemon-internal async accept loop + inside d2bd (`packages/d2bd/src/lib.rs:start_sk_accept_loop`, + `packages/d2bd/src/security_key.rs`): the daemon calls the broker for the + hidraw fd via SCM_RIGHTS, binds a vsock-proxy Unix socket, and spawns an + async accept loop — there is no separate relay process. **v3 target:** this + relay logic is extracted into a dedicated unprivileged relay Process under + the device-security-key Provider. The relay Process receives the hidraw fd + from the broker over SCM_RIGHTS and proxies CTAP HID traffic to the Guest + frontend over AF_VSOCK. It never runs as root and has no further broker access. +3. **Current implementation:** the guest-side UHID virtual HID binary is + `packages/d2b-sk-frontend/src/` (static, implemented-and-reachable), running + inside the Guest VM as a guest systemd service (`d2b-sk-frontend.service`) + declared in `nixos-modules/components/security-key-guest.nix`. It is NOT + a current ProcessRole or Zone Process — the ProcessRole name + `SecurityKeyFrontend` refers to the HOST accept loop, not this guest binary. + **v3 target:** the `d2b-sk-frontend` binary becomes a Zone Process resource + (name `device--sk-frontend`, `executionRef: Guest/`) owned by + the device-security-key Provider controller, giving the controller lifecycle + visibility and replacing the untracked guest systemd unit. +4. At most one Guest may hold the exclusive hidraw lease at a time. A second + claim is rejected; the requesting Guest receives `ClaimConflict`. +5. Security-key proxy and USBIP YubiKey passthrough are mutually exclusive for + the same VM. This invariant is enforced at Nix eval time and at runtime by + the Device controller. + +### TPM state integrity + +The swtpm state directory (`/vms//swtpm`) is identity-bound: + +- Mode 0700, owner `d2b--swtpm`. +- Per-VM sticky 3770 root prevents non-owner rename/replace. +- Root-owned tamper-guard marker at `/var/lib/d2b/swtpm-markers/` records + `st_dev`/`st_ino` + first-provision stamp. +- A missing or mismatched marker fails VM start closed + (`previously-provisioned-swtpm-state-missing`). +- Wiping state is treated as device tampering by IdPs (Entra ID, Intune); + the Device finalizer never deletes swtpm NVRAM. + +These invariants are preserved exactly in the device-tpm Provider. + +### GPU security + +- Full GPU (virtio-gpu, card-node access, VFIO passthrough): always + `arbitration: exclusive`. Only one Guest may hold a full GPU claim at a time. +- Render-node-only mode (`spec.provider.settings.renderNodeOnly=true`) may use + `arbitration: shared` when the Device spec explicitly sets + `arbitration: shared`. A Device spec with `arbitration: shared` and + `spec.provider.settings.renderNodeOnly=false` is rejected at admission. The default + arbitration for GPU devices is `exclusive` regardless of mode; operators must + explicitly opt into shared render-node mode. +- Gpu and GpuRenderNode claim `kvm`, `dri`, `nvidia-ctl`, `nvidia-uvm`, + `nvidia-render`, and `udmabuf` broker device tokens. VFIO/SR-IOV is not + included in the standard GPU device claim. +- The GPU worker Process runs in a user namespace (ADR 0021 broker-pre-NS). + The broker opens GPU device fds before clone and passes them; the Process + has zero ambient host capabilities. +- Render-node-only mode passes only the DRM render node fd without a virtio-gpu + bind-mount. +- Video decode (crosvm video-decoder) runs as the per-Device Process principal, + needs `/dev/dri` and optionally NVIDIA devices. It is a separate Process from + the GPU sidecar but shares the GPU Device claim. + +### USBIP security + +- USBIP per-env/per-busid firewall rules use the ownership-marker pattern: + `comment "d2b managed: usbip:env::bus:"`. +- Broker operation `UsbipBindFirewallRule { action: Ensure | Remove }` is + audit-logged and destructive; acquisition and release share this one closed + operation. +- The bus ID validation is governed by `packages/d2b-contracts/src/usbip.rs`: + max 31 chars (`SYSFS_BUS_ID_MAX`), ASCII digits and separators only, no + shell metacharacters, no leading zeros on segments. +- Explicit-attach (`d2b usb attach --apply`) uses a separate + `UsbipExplicitBind` broker op path distinct from bundle-declared claims. + +## Async reconciliation + +Each Device Provider controller implements the standard async reconcile loop +from `ADR-046-resource-reconciliation`: + +**D090 expedited reconcile.** Authorized Device `Create`, `UpdateSpec`, and +`Delete` calls MAY set `waitForReconcile`. Under one mutation ticket, +`operationId`, and deadline, Core admission and the reserved-revision redb commit +run in parallel with controller preflight/plan, but the controller MUST NOT +perform external effects, finalizer release, or status mutation until Core +supplies `CommittedRevisionProof {resourceUid, generation, revision, +operationId}`; DB failure aborts with no effect. The API returns the committed +object plus one-pass projected layered status, `disposition` +(`Converged|Progressing|Blocked|UpgradeRequired|Failed`), `statusPersistence` +(`pending|committed`), and the last persisted status revision. The durable +commit is never rolled back on reconcile timeout or failure; effect idempotency +keys derive from `(UID,generation,revision,operationId)`, and the expedited pass +uses a bounded priority lane in the same per-resource single-flight. + +1. **spec-generation-changed:** create/update inventory selector, provision + emulated device (swtpm, UHID virtual HID), apply udev rules. +2. **deletion-requested:** stop owned worker Processes, release OS resources, + uninstall udev rules, clear finalizer. Never delete persistent TPM state. +3. **dependency-changed / execution-status-changed:** if the owning Guest + stops, release active claims and set phase Degraded. +4. **scheduled-observe:** probe physical device presence; update status. +5. **owned-resource-changed:** if a worker Process fails, set DeviceHealthy=False + and optionally restart. + +### Fast-path process launch + +Worker Processes (swtpm, crosvm GPU/video, usbipd) follow the standard +Process fast-path (commit-to-handler ≤5 ms p95, launch-attempt ≤20 ms p95). +The Device controller creates the Process resource; the Process controller +manages the launch. + +### Pre-start EphemeralProcess pattern + +`SwtpmPreStartFlush` maps to an EphemeralProcess owned by the device-tpm +controller. The controller creates it before creating the long-lived swtpm +Process, waits for `Succeeded`, then creates the swtpm Process: + +```text +Device/corp-vm-tpm + └─ EphemeralProcess/device--flush (pre-start flush; successfulTtl=1h, D094 canonical) + └─ Process/device--swtpm (long-lived swtpm socket) + └─ Volume/corp-vm-tpm-state (persistent TPM NVRAM; separate owner) +``` + +If the flush EphemeralProcess fails, the Device controller sets phase Failed +and does not launch the swtpm Process. + +## Provider: device-tpm + +### Identity + +```text +Provider/device-tpm +``` + +Crate: `packages/d2b-provider-device-tpm/` +Dossier: `docs/specs/ADR-046-provider-device-tpm.md` (separate, not yet authored) + +### Implements + +Device (emulated, exclusive, TPM class). + +### Root config schema + +| Field | Type | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | +| `logLevel` | uint | 20 | 1–20 | swtpm `--log level` value | +| `startupClear` | bool | true | — | Emit `--flags startup-clear`; requires pre-start flush | +| `stateDirPath` | null | provider-derived | — | Overrides are rejected; path is always derived from Volume/state owner | + +swtpm and swtpm_ioctl binaries are dependencies bundled inside the +`d2b-provider-device-tpm` package closure. Their executable paths are resolved +from the signed component descriptor inside that closure; they are not +configurable fields in the Device spec. + +Config must not carry a raw state directory path; the Provider derives it from +the owned Volume resource. + +### Device spec + +```yaml +spec: + providerRef: Provider/device-tpm + deviceClass: emulated + arbitration: exclusive + maxConcurrentClaims: 1 + inventory: + selector: {} + provider: + schemaId: device-tpm.d2bus.org/Device/spec + schemaVersion: "1.0" + settings: + logLevel: 20 + startupClear: true +``` + +### Worker processes + +| Process name | Role | Executable | Domain | Execution | Notes | +| --- | --- | --- | --- | --- | --- | +| `device--flush` | EphemeralProcess | `swtpm_ioctl -i --unix ` | system | Host | Pre-start flush; once per start cycle | +| `device--swtpm` | Process | `swtpm socket ...` | system | Host | Long-lived; supervised | + +Process resource names are derived deterministically from the owner Device +resource UID (`uid-short` = first 12 hex chars of the Device UID) combined with +a component template from the signed Provider dossier. The name never contains a +VM or Guest human name. The Device UID is stable across restarts. Foundation +canonical process identity format applies. + +### State/Volume + +The swtpm NVRAM Volume is a separate `volume-local` resource with independent +ownership: + +```yaml +type: Volume +metadata: + name: corp-vm-tpm-state +spec: + providerRef: Provider/volume-local + layout: + - path: swtpm + type: directory + ownerRef: User/corp-vm-swtpm-system + mode: "0700" + noFollow: true + sensitivity: private + createPolicy: create-if-never-provisioned + repairPolicy: exact-owner + cleanupPolicy: retain-forever + views: + swtpm-process: + path: swtpm + rights: [read, write, create] +``` + +The Device resource owns the swtpm Process via ownerRef. The Volume is the +TPM persistent state root; the Device does NOT own the Volume to prevent +accidental deletion cascade. + +### Broker operations consumed + +- `PrepareSwtpmDir` (hardening/tamper-marker) — once per start cycle. +- `SpawnRunner` (swtpm role) — for each long-lived swtpm Process. + +### Nix options (v3 successors) + +Current: `d2b.vms..tpm.enable = true` → guest `nixos-modules/components/tpm.nix` + +v3 successor: +```nix +d2b.zones..resources."-tpm" = { + type = "Device"; + metadata.ownerRef = "Guest/"; + spec = { + providerRef = "Provider/device-tpm"; + deviceClass = "emulated"; + arbitration = "exclusive"; + provider = { + schemaId = "device-tpm.d2bus.org/Device/spec"; + schemaVersion = "1.0"; + settings = { + startupClear = true; + }; + }; + }; +}; +``` + +The swtpm and swtpm_ioctl binaries are dependencies inside the +`d2b-provider-device-tpm` package closure (referenced by the Provider resource's +`spec.artifactId`); they require no configuration in the Device spec. + +The guest NixOS module wiring (`--tpm socket=...`, `tpm_crb` kernel module, +`tpm2-tools`, session-flush service) remains in the Guest Provider's +`runtime-cloud-hypervisor` Nix module. + +### Tests + +- `packages/d2b-contract-tests/tests/minijail_swtpm_video.rs`: swtpm profile + shape, user namespace propagation, zero host caps. +- `packages/d2b-contract-tests/tests/policy_swtpm_readiness.rs`: readiness contract. +- `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` unit tests: tamper marker, + fresh/existing dir, symlink/mismatch fail-closed. +- `packages/d2b-host/src/swtpm_argv.rs` unit tests: argv golden vectors. +- New: Device reconcile state-machine test (flush → swtpm → Ready). +- New: Tamper-marker failure closes VM start (does not recreate empty TPM). +- New: Delete finalizer does not remove Volume/state. + +## Provider: device-usbip + +### Identity + +```text +Provider/device-usbip +``` + +Crate: `packages/d2b-provider-device-usbip/` +Dossier: `docs/specs/ADR-046-provider-device-usbip.md` (separate, not yet authored) + +### Implements + +Device (physical, exclusive per Guest attachment). + +### Root config schema + +| Field | Type | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | +| `usbipHostKernelModule` | string | `usbip-host` | fixed | Host kernel module | +| `vhciHcdKernelModule` | string | `vhci_hcd` | fixed | Guest kernel module | +| `backendPort` | uint16 | provider-derived | 1–65535 | Per-env deterministic USBIP backend port | + +The usbip host and guest binaries are dependencies bundled inside the +`d2b-provider-device-usbip` package closure. Their executable paths are resolved +from the signed component descriptor inside that closure; they are not +configurable fields in the Device spec. + +### Device spec + +```yaml +spec: + providerRef: Provider/device-usbip + deviceClass: physical + arbitration: exclusive + maxConcurrentClaims: 1 + inventory: + selector: + busClass: usb + label: yubikey-work + vendorId: "1050" + productId: "0407" + serial: null + provider: + schemaId: device-usbip.d2bus.org/Device/spec + schemaVersion: "1.0" + settings: + env: work +``` + +### Worker processes + +| Process name | Role | Executable | Domain | Execution | Notes | +| --- | --- | --- | --- | --- | --- | +| `device--bind` | EphemeralProcess | `usbip bind --busid ` | system | Host | Bind bus ID at attach | +| `device--unbind` | EphemeralProcess | `usbip unbind --busid ` | system | Host | Release bus ID at detach | +| `device--daemon` | Process | `usbipd --tcp-port ` | system | Host | Long-lived per-Device USBIP backend | + +The per-Device usbipd daemon and proxy Processes are owned by +`Provider/device-usbip`. The Network provider supplies only the +dependency/status/firewall interface: bridge membership, port allocation, and +the `UsbipBindFirewallRule { action: Ensure | Remove }` broker op surface. +Network does not own or supervise +USBIP Processes. + +Process resource names follow the `device--` template +(`uid-short` = first 12 hex chars of the Device UID; component from the signed +Provider dossier). VM or Guest human names never appear in Process resource +names. + +### Bring-up sequence (from usbip_state_machine.rs) + +The canonical per-busid USBIP bring-up order is captured in +`packages/d2bd/src/usbip_state_machine.rs` (implemented-and-reachable): + +``` +modprobe → lock → withhold → firewall → backend → bind → proxy +``` + +This sequence is preserved in the device-usbip Provider reconcile loop. Each +step maps to a broker operation or host-side action: + +| Step | Action | Broker op | +| --- | --- | --- | +| `modprobe` | Load `usbip-host` kernel module | EphemeralProcess (modprobe) | +| `lock` | Acquire per-busid OFD lock | — | +| `withhold` | Prevent OS auto-claim of device | sysfs write via broker | +| `firewall` | Ensure on acquisition; Remove on detach/finalize | `UsbipBindFirewallRule { action: Ensure \| Remove }` | +| `backend` | Start per-env usbipd daemon | `SpawnRunner` (usbip role) | +| `bind` | Bind bus ID to usbip-host | EphemeralProcess (usbip bind) | +| `proxy` | Start TCP proxy on env-host IP | `SpawnRunner` (usbip proxy role) | + +### Broker operations consumed + +- `UsbipBindFirewallRule { action: Ensure | Remove }`: ensure the exact + per-Network/per-busid rule during acquisition and remove it during + detach/finalize; audited/destructive. +- `SpawnRunner` (usbip backend role, `usbip-host` device token): spawn per-env usbipd. +- `SpawnRunner` (usbip proxy role): spawn TCP proxy. + +### Validation + +Bus ID validation follows `packages/d2b-contracts/src/usbip.rs`: +- Max `SYSFS_BUS_ID_MAX` = 31 chars. +- Accepted form: `B`, `B-P`, `B-P.S[.S...]`. +- ASCII digits only. No leading zeros on segments. No metacharacters. + +Vendor/product ID: exactly 4 ASCII hex digits, lower-cased at storage. + +### Guest-side config (Nix) + +Current: `d2b.vms..usbip.yubikey = true` → `nixos-modules/components/usbip.nix` + +v3 guest module wires: +- `vhci_hcd` kernel module. +- `usbip` CLI tools. +- `d2b.guestControl.usbipPath` for guest-side import. + +The guest-side kernel module and tools remain in the Guest's `runtime-cloud-hypervisor` +Nix module under v3. + +### Tests + +- `packages/d2b-contract-tests/tests/usbip_json_contract.rs`: USBIP DTO + serde/schema round-trips, unknown field denial. +- `packages/d2b-contract-tests/tests/usb_network_scoping.rs`: per-env isolation. +- New: Device arbitration conflict test (two Guests claim same bus ID). +- New: Firewall rule ownership-marker preservation. +- New: Bus ID validation corpus (31-char max, metachar rejection, leading-zero segments). +- New: Explicit-attach (EphemeralProcess bind) vs declared (Process bind) path split. + +## Provider: device-security-key + +### Identity + +```text +Provider/device-security-key +``` + +Crate: `packages/d2b-provider-device-security-key/` +Dossier: `docs/specs/ADR-046-provider-device-security-key.md` (separate, not yet authored) + +### Implements + +Device (physical, exclusive per-session lease). + +### Key invariants (from D046) + +- The Provider owns unprivileged Host relay Processes and Guest frontend + Processes plus ceremony/CID/lease. +- The Zone privileged broker only opens/passes the hidraw fd; no path crosses + the public surface. + +### Root config schema + +| Field | Type | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | +| `devices` | list | [] | max 16 | Per-device selector entries | +| `devices[].label` | string | — | `^[a-z][a-z0-9-]{0,62}$` | Stable selector label | +| `devices[].vendorId` | uint16 | — | — | USB vendor ID | +| `devices[].productId` | uint16 | — | — | USB product ID | +| `devices[].serial` | string \| null | null | max 128 chars | Optional serial filter | +| `vsockPort` | uint16 | 14320 | 1–65535 | AF_VSOCK port for host↔guest relay | +| `sessionRingSize` | uint | 32 | 8–256 | Bounded recent-session ring size | +| `leaseTimeoutSecs` | uint | 300 | 30–3600 | Session-level lease timeout | + +The `vsockPort` default 14320 is stable and matches `security-key-guest.nix` +option `d2b.securityKey.vsockPort` default. + +### Device spec + +```yaml +spec: + providerRef: Provider/device-security-key + deviceClass: physical + arbitration: exclusive + maxConcurrentClaims: 1 + inventory: + selector: + busClass: hidraw + label: yubikey-primary + vendorId: "1050" + productId: "0407" + serial: null + provider: + schemaId: device-security-key.d2bus.org/Device/spec + schemaVersion: "1.0" + settings: + vsockPort: 14320 +``` + +### Process model + +The security-key Provider owns two process classes: + +**Host relay Process** (one per Device resource): + +```yaml +type: Process +metadata: + name: device--sk-relay +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + processClass: worker + # Package/template from device-security-key Provider dossier +``` + +The relay process: +- Runs unprivileged; receives the hidraw fd from the broker via SCM_RIGHTS. +- Proxies raw CTAP HID packets to the Guest frontend over AF_VSOCK. +- Has no further broker access. +- Restarts on disconnect; clears its lease on clean exit. + +**Current implementation note:** In the v3 baseline the relay is NOT a separate +spawned process — it is a daemon-internal async accept loop in d2bd +(`packages/d2bd/src/lib.rs:10456` `start_sk_accept_loop` and +`packages/d2bd/src/security_key.rs`). ProcessRole::SecurityKeyFrontend is +handled as a ReadinessOnly node that triggers this daemon coroutine rather than +spawning a runner through the broker. The v3 Provider target extracts this relay +logic into the `d2b-provider-device-security-key` crate running as a proper +unprivileged Process. + +**Guest frontend Process** (per opted-in Guest): + +The guest-side UHID virtual device binary is `packages/d2b-sk-frontend/` +(static binary; implemented-and-reachable). In v3 it becomes a Zone Process +resource owned by the device-security-key Provider controller: + +```yaml +type: Process +metadata: + name: device--sk-frontend +spec: + providerRef: Provider/system-minijail + executionRef: Guest/ + domain: user + processClass: worker + # Package/template from device-security-key Provider dossier +``` + +The `d2b-sk-frontend.service` guest systemd unit is removed from the Nix module +when the Process resource is live; the Process controller manages the lifecycle. +The guest-side Nix module (`nixos-modules/components/security-key-guest.nix`) +continues to wire: +- `uhid` kernel module. +- `plugdev` group + udev rule for `KERNEL=="hidraw*" … KERNELS=="0003:1050:0407.*"`. +- The `d2b-sk-frontend` static binary in the Guest store. + +### Ceremony, CID, and lease + +The security-key lease lifecycle: + +1. Guest firmware initiates a CTAP HID operation; the UHID frontend receives it. +2. The frontend sends an acquire-lease request to the host relay over AF_VSOCK. +3. The relay acquires the per-device OFD lock under `/run/d2b/locks/security-key/`. +4. The relay opens the physical hidraw fd via the broker (`SecurityKeyOpenDevice`). +5. The relay creates a session record (`SecurityKeySession`) with a new opaque + `SecurityKeySessionId` and updates Device status to reflect the active claim. +6. CTAP HID traffic is proxied through the relay until the operation ends or + times out. +7. On completion, the relay releases the OFD lock and updates session status. +8. The relay retains the session in a bounded ring (max `sessionRingSize`). + +The broker's `SecurityKeyOpenDevice` operation: +- Accepts only `device_label` and `session_id` (audit correlation); no path. +- Derives the physical hidraw node from the trusted bundle device table. +- Returns the fd over SCM_RIGHTS; never a path in the response body. +- Audit record: subject, device_label digest, session_id, outcome. + +### Status + +Device status `device.claims` carries at most one entry for the active session: + +```yaml +claims: + - holderRef: Guest/corp-vm + claim: exclusive + passthrough: hidraw-relay + claimedAt: 2026-07-22T00:05:00Z + sessionId: sk-corp-vm-42 # opaque; NOT the physical device descriptor + health: healthy +``` + +The `SecurityKeyStatusResponse` and `SecurityKeySessionsResponse` public wire +DTOs are reachable in `packages/d2b-contracts/src/security_key.rs` and are +preserved in v3 with adapted Zone/ResourceRef identifiers. + +### Mutual exclusion + +Security-key proxy and USBIP passthrough of the same physical USB device are +mutually exclusive. This is enforced: +- At Nix eval time in `assertions.nix`. +- At runtime after trusted physical-USB identity resolution: Core derives one + `physical-usb-backing/v1` opaque digest and requires both Providers to claim + the identical Host-global `(Host, physical-usb-backing, opaqueKeyDigest)` + tuple before any open, withhold, bind, module, relay, or attachment effect. + The losing authority Service transitions to `Failed` with + `BackingAuthorityReady=False` and `physical-usb-backing-conflict`. A + Provider-private authority class or selector digest cannot bypass this + collision. + +### Broker operations consumed + +- `SecurityKeyOpenDevice`: open exact FIDO hidraw node; fd via SCM_RIGHTS. +- `SecurityKeyApplyUdevRules`: write udev rules; called once per activation. + +### Tests + +- `packages/d2b-contract-tests/tests/usb_sk_contract.rs`: DTO serde, + unknown-field denial, broker capability set. +- `packages/d2b-core/src/privileges_w3.rs` unit tests: W3BrokerOperation flags. +- New: Lease acquire/release state machine (Idle → AwaitingLease → Active → Completed). +- New: Second-claim conflict rejection. +- New: Session ring bounded overflow (oldest session evicted). +- New: Broker `SecurityKeyOpenDevice` without inline path. +- New: USBIP mutual-exclusion eval check and runtime rejection. +- New: Guest frontend module udev rule format (`0003:1050:0407.*` pattern). + +## Provider: device-gpu + +### Identity + +```text +Provider/device-gpu +``` + +Crate: `packages/d2b-provider-device-gpu/` +Dossier: `docs/specs/ADR-046-provider-device-gpu.md` (separate, not yet authored) + +This Provider is the combined GPU/video Provider (D046). It manages GPU +graphics acceleration and hardware video decode under one crate and one Device +resource per Guest. + +### Implements + +Device (physical, exclusive per Guest, GPU/video combined). + +### Root config schema + +| Field | Type | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | +| `renderNodeOnly` | bool | false | — | If true, use render-node-only mode (no full virtio-gpu bind-mount) | +| `videoSidecar` | bool | false | — | If true, spawn crosvm video-decoder alongside GPU worker | +| `videoNvidiaDecode` | bool | false | — | If true, expose `/dev/nvidiactl`, `/dev/nvidia0`, `/dev/nvidia-uvm` to video worker | +| `contextTypes` | list | [virgl, virgl2, cross-domain] | closed set | GPU context types: `virgl`, `virgl2`, `cross-domain` | +| `displays` | list | [{hidden: true}] | max 8 | Virtual display config | +| `egl` | bool | true | — | EGL rendering | +| `vulkan` | bool | true | — | Vulkan rendering | +| `crossDomainTrusted` | bool | false | — | Enable trusted cross-domain context (Wayland proxy path) | +| `virglVideo` | bool | false | — | Experimental virglrenderer video forwarding; separate from videoSidecar | + +### Device spec + +```yaml +spec: + providerRef: Provider/device-gpu + deviceClass: physical + arbitration: exclusive + maxConcurrentClaims: 1 + inventory: + selector: + busClass: drm + label: host-gpu + provider: + schemaId: device-gpu.d2bus.org/Device/spec + schemaVersion: "1.0" + settings: + renderNodeOnly: false + videoSidecar: true + videoNvidiaDecode: false + contextTypes: [virgl, virgl2, cross-domain] + displays: [{hidden: true}] + egl: true + vulkan: true + crossDomainTrusted: false +``` + +### Worker processes + +| Process name | Role | Executable | Domain | Execution | Notes | +| --- | --- | --- | --- | --- | --- | +| `device--gpu` | Process | `crosvm device gpu` | system | Host | Full GPU virtio sidecar; exclusive arbitration only | +| `device--render-node` | Process | `crosvm device gpu` (render-node mode) | system | Host | Render-node-only; exclusive or shared per Device spec | +| `device--video` | Process | `crosvm device video-decoder --backend vaapi` | system | Host | Video decode; separate Process, shares Device claim | + +Only one of `device--gpu` and `device--render-node` is +active at a time, selected by `spec.provider.settings.renderNodeOnly`. The +video Process is active only when `spec.provider.settings.videoSidecar=true`. + +Process resource names are derived deterministically from the owner Device UID +(`uid-short` = first 12 hex chars) and a component template from the signed +Provider dossier. Current v3 OS-level process principal names (`d2b-{vm}-gpu`, +`d2b-{vm}-gpu-render-node`) from `bundle_resolver.rs` ProcessRole exec_arg0 are +the process identity at the OS layer; they are distinct from the Process resource +name. VM or Guest human names never appear in Process resource names. + +### Device claims and broker tokens + +The GPU Device broker token set (from `bundle_resolver.rs` ProcessRole::Gpu +exact comment): + +- `kvm`, `dri`, `nvidia-ctl`, `nvidia-uvm`, `nvidia-render`, `udmabuf`. +- `nvidia-ctl`, `nvidia-uvm`, `nvidia-render` are included only when + `videoNvidiaDecode=true` or when the guest is a NVIDIA-graphics VM. + +Device default allowlist in the privileged broker for the GPU worker: +- `/dev/dri/renderD128` always. +- `/dev/nvidiactl`, `/dev/nvidia0`, `/dev/nvidia-uvm` only when `videoNvidiaDecode=true`. + +### Video wire contract pins + +The following constants from `packages/d2b-host/src/video_argv.rs` are frozen +wire contract values and must be preserved by the device-gpu Provider: + +| Constant | Value | Source | +| --- | --- | --- | +| `VIRTIO_ID_MEDIA` | 48 | CH patch `0003-vhost-user-media-device.patch` | +| `VHOST_USER_MEDIA_NUM_QUEUES` | 2 | CH patch `NUM_QUEUES` | +| `VHOST_USER_MEDIA_QUEUE_SIZE` | 256 | CH patch `QUEUE_SIZES` | +| `VHOST_USER_MEDIA_SHM_REGION_BYTES` | 268435456 (256 MiB) | CH patch SHM region | +| `VHOST_USER_MEDIA_VRING_BASE` | 0 | CH patch `activate()` override | +| `VHOST_USER_MEDIA_PROTOCOL_FLAGS` | `BACKEND_REQ\|REPLY_ACK\|SHMEM_MAP_CROSVM` | CH patch `acked_protocol_features` | + +These constants are part of the vhost-user-media wire contract between the +patched Cloud Hypervisor and the crosvm video-decoder sidecar. Changing them +requires an updated CH patch. + +### Broker operations consumed + +- `SpawnRunner` (gpu/gpu-render-node/video role): spawn crosvm worker Process. +- `OpenDevice` (kvm/dri/udmabuf/nvidia*): open GPU device fds before clone. + +### Nix options (v3 successors) + +Current: +- `d2b.vms..graphics.enable = true` → `nixos-modules/components/graphics.nix` (host) +- `d2b.vms..graphics.videoSidecar = true` → `nixos-modules/components/video/guest.nix` +- `d2b.graphics.*` in guest NixOS config + +v3 Device resource: +```nix +d2b.zones..resources."-gpu" = { + type = "Device"; + metadata.ownerRef = "Guest/"; + spec = { + providerRef = "Provider/device-gpu"; + deviceClass = "physical"; + arbitration = "exclusive"; + inventory.selector = { + busClass = "drm"; + label = "host-gpu"; + }; + provider = { + schemaId = "device-gpu.d2bus.org/Device/spec"; + schemaVersion = "1.0"; + settings = { + videoSidecar = true; + contextTypes = ["cross-domain" "virgl" "virgl2"]; + crossDomainTrusted = false; + }; + }; + }; +}; +``` + +Guest-side `virtio_media` module, kernel packages, and `--vhost-user-media` +CH arg remain in the Guest `runtime-cloud-hypervisor` Nix module. + +### Tests + +- `packages/d2b-contract-tests/tests/minijail_gpu.rs`: GPU minijail profile. +- `packages/d2b-contract-tests/tests/minijail_swtpm_video.rs`: video minijail profile. +- `packages/d2b-contract-tests/tests/video_binary_contract.rs`: video binary contract. +- `packages/d2b-host/src/gpu_argv.rs` unit tests: GPU argv golden. +- `packages/d2b-host/src/video_argv.rs` unit tests: video argv golden + wire-contract snapshot. +- New: GPU+video combined Device reconcile state-machine test. +- New: `renderNodeOnly` vs full-GPU Process selection test. +- New: Video sidecar Process dependency on GPU Process readiness. +- New: Wire-contract constant snapshot byte-stability test. + +## RBAC + +Device resources use standard resource verbs. The following roles are required: + +| Role | Verbs | Scope | Subjects | +| --- | --- | --- | --- | +| `device-manager` | get, list, watch, create, update-spec, delete | Zone | Provider controllers | +| `device-status-owner` | update-status | Zone | Owning Device Provider controller only | +| `device-finalizer-owner` | update-finalizers | Zone | Owning Device Provider controller only | +| `device-reader` | get, list, watch | Zone | Guest/Host runtime Providers, CLI | +| `device-claimant` | get, watch | Zone | Guest/Host runtime Providers that hold claims | + +Device attachments in Host/Guest/Process specs are desired state, not imperative +operations. Adding or removing a `devices[]` entry on a Host or Guest resource +is a normal spec update governed by `update-spec` RBAC. The Device Provider +controller detects the change via its reconcile loop, performs arbitration, and +reflects the result in Device status. There is no separate `claim-device` or +`release-device` verb. + +No Role grants wildcard `*` over all Device resources. Provider controllers +have bounded RoleBinding scopes that cover only the Device types and names +matching their provisioned resource set. + +## Broker effect limits + +All Device Provider broker operations are subject to: + +- Audit-logged (no broker op without an audit record). +- Point-specific: no device Provider receives a blanket device-path grant. +- One broker connection per Provider process; FDs returned over SCM_RIGHTS. +- Broker validates all inputs against the trusted bundle before any effect. +- No inline path, raw device node string, or capability byte crosses the + public or broker wire. + +Additional per-operation limits (confirmed conservative defaults; a conformance +test for each limit is required in `packages/d2b-contract-tests/`): + +| Broker operation | Rate limit | FD quota | Notes | +| --- | --- | --- | --- | +| `SecurityKeyOpenDevice` | 1 concurrent per device label | 1 | One active hidraw session per Device at a time | +| `SecurityKeyApplyUdevRules` | Activation-only | — | One batch per Provider activation; not a hot path | +| `UsbipBindFirewallRule { action: Ensure \| Remove }` | One bounded batch per acquisition or release | — | Ownership-marker check prevents duplicate rules; Remove is idempotent | +| `SpawnRunner` (swtpm) | 1 per Device per Guest start cycle | — | Idempotent; broker verifies tamper marker | +| `SpawnRunner` (gpu/video) | 1 per Device (one GPU worker set per Guest) | — | One GPU worker set per Device | +| `OpenDevice` (gpu) | Per-spawn call only | ≤8 per Process launch | Opened before clone; counted per-spawn | + +## Audit and OTEL + +### Audit records + +Each Device broker operation emits a path-free audit record containing: + +| Field | Value | +| --- | --- | +| `subject` | Provider Process identity digest | +| `zone` | Zone name | +| `op` | Broker operation tag (e.g., `SecurityKeyOpenDevice`) | +| `resource_type` | `Device` | +| `resource_name_digest` | Bounded stable hash of device label/name; never raw path | +| `outcome` | `success` \| `failure` \| `denied` | +| `error_class` | closed-set error slug | +| `correlation_id` | operation/trace ID | +| `timestamp` | RFC 3339 UTC | + +The audit record excludes: raw device paths, hidraw node names, sysfs bus IDs, +vendor/product strings, CTAP session contents, TPM NVRAM contents, GPU sockets, +and any credential material. + +### OTEL spans + +Device reconcile telemetry attribute placement — including span vs resource +attribute classification and full label-set boundaries — is specified in +`ADR-046-telemetry-audit-and-support`. `d2b.zone` and `d2b.provider` are +bounded OTEL resource attributes, never metric labels. This spec does not +define competing telemetry decisions. Device Provider implementations must +cross-reference that spec for all OTEL label and span attribute constraints. +No Zone/resource name or UID, device path, busid, serial, vendor/product +string, session content, or process PID may appear in a metric label. + +## Errors + +Stable Device-specific error classes (in addition to common resource errors): + +| Error | Meaning | +| --- | --- | +| `device-not-found` | Physical device absent from sysfs/udev at probe time | +| `device-claim-conflict` | Exclusive device already claimed by another holder | +| `device-claim-max-exceeded` | `maxConcurrentClaims` reached | +| `device-arbitration-violation` | Exclusive Device received a shared claim or vice versa | +| `device-provision-failed` | Emulated device creation failed | +| `device-broker-inaccessible` | Broker cannot open/pass the device fd | +| `device-state-integrity-failure` | Tamper marker mismatch (TPM only) | +| `device-session-timeout` | CTAP session exceeded lease timeout | +| `device-session-cancelled` | CTAP session cancelled by operator | +| `physical-usb-backing-conflict` | A USB or security-key authority already owns the Core-derived Host-global physical USB tuple | +| `device-worker-failed` | Owned worker Process in Failed/Unknown phase | +| `device-wire-contract-mismatch` | Video or GPU wire constant mismatch | + +All error messages are bounded, UTF-8 validated, and must not contain device +paths, sysfs bus IDs, raw hidraw node names, TPM NVRAM content, CTAP session +bytes, GPU socket paths, or credential material. + +## Nix configuration + +### Nix authoring shape + +All resources in a Zone are declared under `d2b.zones..resources`. The +shape mirrors the canonical resource envelope directly: `type` selects the +ResourceType, `metadata` carries author-settable metadata fields (only +`ownerRef` and optional presentation `labels`/`annotations`), and `spec` +carries the exact ResourceType spec fields without renaming or re-nesting. + +```nix +d2b.zones..resources. = { + type = "Device"; # ResourceType discriminant + + # Author-settable metadata only. + # metadata.name, metadata.zone, and apiVersion are derived (see table below). + # metadata.managedBy, metadata.configurationGeneration, uid, generation, + # revision, timestamps, finalizers, and managedBy are Core-managed (omitted). + # status is read-only (omitted). + metadata.ownerRef = "Guest/"; # ResourceRef; required for Device + # metadata.labels. = ""; # optional presentation labels + # metadata.annotations. = ""; # optional annotations + + spec = { + # Required + providerRef = "Provider/device-tpm"; # must resolve to installed Provider + deviceClass = "emulated"; # "emulated" | "physical" + arbitration = "exclusive"; # "exclusive" | "shared" + + # Optional — defaults shown + maxConcurrentClaims = 1; # 1–16; must be 1 when arbitration=exclusive + + inventory.selector = {}; # emulated: {} or absent + # inventory.selector.busClass = "usb" | "hidraw" | "drm" | "pci" | "tpm"; + # inventory.selector.label = ""; # required for physical + # inventory.selector. = ...; # variant-specific fields only + + provider = { + schemaId = ".d2bus.org/Device/spec"; + schemaVersion = "1.0"; + settings = {}; # Provider-specific; see Provider settings schema + }; + }; +}; +``` + +**Derived and read-only fields (not author-specified):** + +| Field | Derived from | +| --- | --- | +| `metadata.name` | Resource attribute key `` | +| `metadata.zone` | Zone attribute key `` | +| `apiVersion` | Constant `"resources.d2bus.org/v3"` | +| `metadata.uid` | Assigned by Core on first creation | +| `metadata.generation` | Incremented by Core on each spec change | +| `metadata.revision` | Opaque; set by Core | +| `metadata.createdAt` / `updatedAt` | Set by Core | +| `metadata.finalizers` | Written by Provider controllers | +| `metadata.deletionRequestedAt` | Set by Core on Delete | +| `metadata.managedBy` | `"configuration"` — set by Core when activating the Nix bundle; closed enum: `configuration \| controller \| api`; Nix input omits it | +| `metadata.configurationGeneration` | NixOS system generation number — set by Core at activation | +| `status` | Entirely read-only; managed by Provider controller | + +**Nix option types, defaults, and documentation** are generated from the +committed ResourceTypeSchema (`docs/reference/schemas/v3/device.schema.json`) and +the per-Provider settings schemas. There is no second bespoke Nix vocabulary: +`spec` field names, types, bounds, and defaults in Nix are identical to those in +the schema. A `spec` field absent from the schema fails eval. A +`spec.provider.settings` field absent from the Provider schema fails eval. + +### Artifact catalog + +Derivation-valued inputs (packages, toolchains, NixOS systems) are configured in +a separate named artifact catalog, never as inline fields inside a ResourceSpec: + +```nix +d2b.artifacts. = { + package = ; # Nix derivation; store path is private catalog data + type = "provider" # closed set: "provider" | "nixos-system" | … + | "nixos-system" + | ...; +}; +``` + +**Key invariants:** + +- ResourceSpec fields that reference catalog entries use a plain bounded ID + (`artifactId` or `systemArtifactId` — **not** `*Ref`, because `Artifact` is + not a ResourceType). For example, a Provider resource's `spec.artifactId` + references its own `d2b-provider-device-*` package; a Guest resource's + `spec.systemArtifactId` references its NixOS system derivation. +- The Nix build hashes the derivation, validates catalog `type`/`id`/duplicate + invariants and trust, and emits a private integrity-pinned artifact catalog + mapping each `id` to `type`, content digest, and closure metadata. +- Store paths are private catalog implementation data. They never appear in + ResourceSpec fields, resource status, audit records, error messages, or + telemetry. +- A missing `artifactId` value fails the NixOS build with `artifact-id-not-found`. +- A catalog entry whose `type` does not match the field's required type fails + with `artifact-type-mismatch`. + +**Device Provider binary resolution** does not use `artifactId` fields in the +Device `spec.provider.settings`. The swtpm, swtpm_ioctl, and usbip binaries are +dependencies bundled inside the `d2b-provider-device-tpm` and +`d2b-provider-device-usbip` package closures respectively. Their exact +executable paths and content digests are embedded in the **signed component +descriptor** shipped inside each Provider's closure; the Provider process +resolves them at startup without any Device-spec field. No Device Provider +`spec.provider.settings` field carries a store path or an `artifactId`. + +The four frozen Device Providers are therefore all closed with respect to +artifact catalog references from the Device resource's spec: the Device spec +carries only configuration values (logLevel, arbitration, selector, etc.). The +Provider resources themselves (defined by the Provider ResourceType, not this +spec) use `spec.artifactId` to reference their packages. + +### Eval-time validation + +The Nix emitter validates the following rules at `nixos-rebuild` / `nix flake +check` time against the committed ResourceTypeSchema: + +| Rule | Eval error | +| --- | --- | +| `metadata.ownerRef` is `/` format | `invalid-owner-ref` | +| `spec.providerRef` is `Provider/` format | `invalid-provider-ref` | +| `spec.providerRef` resolves to an installed Provider in the Zone | `unresolved-provider-ref` | +| `spec.deviceClass` ∈ `{"emulated","physical"}` | `invalid-device-class` | +| `spec.deviceClass=emulated` ⟹ `spec.inventory.selector = {}` or absent | `emulated-with-nonempty-selector` | +| `spec.deviceClass=physical` ⟹ `spec.inventory.selector.label` present | `physical-missing-selector-label` | +| `spec.inventory.selector.busClass` ∈ closed set `{usb,hidraw,drm,pci,tpm}` | `unknown-bus-class` | +| Selector contains only fields declared for its `busClass` variant | `selector-unknown-field` | +| `spec.arbitration=exclusive` ⟹ `spec.maxConcurrentClaims = 1` | `exclusive-max-claims-conflict` | +| `spec.arbitration=shared` ⟹ `spec.deviceClass=physical` | `emulated-shared-arbitration` | +| `spec.arbitration=shared` + GPU Provider ⟹ `spec.provider.settings.renderNodeOnly=true` | `shared-arbitration-requires-render-node-only` | +| `spec.maxConcurrentClaims` ∈ 1–16 | `max-claims-out-of-bounds` | +| No two Device resources in the same Zone share the same `spec.inventory.selector.label` | `duplicate-device-label` | +| USBIP and security-key Provider both referencing same selector label | `usbip-sk-mutual-exclusion` | +| `spec.provider.settings` validates against the Provider's signed JSON Schema | `invalid-provider-settings` | +| No inline secret strings in `spec.provider.settings` (must use `credentialRef`) | `inline-secret-in-settings` | + +### Provider settings schema validation + +Each Device Provider registers a JSON Schema for its `spec.provider.settings` sub-object as +part of its signed Provider descriptor. The Nix emitter imports this schema from +the Provider's store path and validates `spec.provider.settings` against it at eval time. +The schema fingerprint is committed to +`docs/reference/schemas/v3/providers/device-.settings.json`; the drift +gate (`make test-drift` / `cargo xtask gen-schemas`) fails the build on any +mismatch. + +Settings fields that accept sensitive values use a `credentialRef` entry pointing +to a `Credential/` resource in the same Zone. Inline strings in sensitive +settings fields fail eval with `inline-secret-in-settings`. The four frozen +Device Providers have no `artifactId` field in their `spec.provider.settings`; binary +paths are resolved from Provider package closures (see "Artifact catalog"). + +```nix +# Correct — sensitive value via Credential ref +spec.provider.settings.exampleSecret = { credentialRef = "Credential/device-example-key"; }; + +# Rejected at eval time — inline string in a credentialRef-constrained field +spec.provider.settings.exampleSecret = "raw-secret-value"; +``` + +The four frozen Device Providers have no settings fields that accept secrets. +The `credentialRef` constraint is stated here for future Device Providers. + +### Canonical ResourceSpec JSON + +The build emits one JSON object per Device resource, representing the full +resource envelope. All keys are sorted lexicographically at every nesting level. +The emitted JSON contains `apiVersion`, `type`, `metadata`, and +`spec` only. `status` is omitted; Core-managed fields (`uid`, `generation`, +`revision`, `createdAt`, `updatedAt`, `finalizers`, `deletionRequestedAt`) are +not emitted by the Nix build and are filled by the runtime after first apply. + +**device-tpm:** + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Device", + "metadata": { + "name": "corp-vm-tpm", + "ownerRef": "Guest/corp-vm", + "zone": "dev" + }, + "spec": { + "arbitration": "exclusive", + "deviceClass": "emulated", + "inventory": { "selector": {} }, + "maxConcurrentClaims": 1, + "provider": { + "schemaId": "device-tpm.d2bus.org/Device/spec", + "schemaVersion": "1.0", + "settings": { + "logLevel": 20, + "startupClear": true + } + }, + "providerRef": "Provider/device-tpm" + } +} +``` + +**device-usbip:** + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Device", + "metadata": { + "name": "corp-vm-usb", + "ownerRef": "Guest/corp-vm", + "zone": "dev" + }, + "spec": { + "arbitration": "exclusive", + "deviceClass": "physical", + "inventory": { + "selector": { + "busClass": "usb", + "label": "yubikey-work", + "productId": "0407", + "serial": null, + "vendorId": "1050" + } + }, + "maxConcurrentClaims": 1, + "provider": { + "schemaId": "device-usbip.d2bus.org/Device/spec", + "schemaVersion": "1.0", + "settings": { "env": "work" } + }, + "providerRef": "Provider/device-usbip" + } +} +``` + +**device-security-key:** + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Device", + "metadata": { + "name": "corp-vm-security-key", + "ownerRef": "Guest/corp-vm", + "zone": "dev" + }, + "spec": { + "arbitration": "exclusive", + "deviceClass": "physical", + "inventory": { + "selector": { + "busClass": "hidraw", + "label": "yubikey-primary", + "productId": "0407", + "serial": null, + "vendorId": "1050" + } + }, + "maxConcurrentClaims": 1, + "provider": { + "schemaId": "device-security-key.d2bus.org/Device/spec", + "schemaVersion": "1.0", + "settings": { + "leaseTimeoutSecs": 300, + "sessionRingSize": 32, + "vsockPort": 14320 + } + }, + "providerRef": "Provider/device-security-key" + } +} +``` + +**device-gpu (full GPU, exclusive):** + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Device", + "metadata": { + "name": "corp-vm-gpu", + "ownerRef": "Guest/corp-vm", + "zone": "dev" + }, + "spec": { + "arbitration": "exclusive", + "deviceClass": "physical", + "inventory": { + "selector": { + "busClass": "drm", + "label": "host-gpu", + "pciSlot": null + } + }, + "maxConcurrentClaims": 1, + "provider": { + "schemaId": "device-gpu.d2bus.org/Device/spec", + "schemaVersion": "1.0", + "settings": { + "contextTypes": ["cross-domain", "virgl", "virgl2"], + "crossDomainTrusted": false, + "displays": [{ "hidden": true }], + "egl": true, + "renderNodeOnly": false, + "videoNvidiaDecode": false, + "videoSidecar": false, + "virglVideo": false, + "vulkan": true + } + }, + "providerRef": "Provider/device-gpu" + } +} +``` + +**device-gpu (render-node only, shared):** + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Device", + "metadata": { + "name": "dev-vm-render", + "ownerRef": "Guest/dev-vm", + "zone": "dev" + }, + "spec": { + "arbitration": "shared", + "deviceClass": "physical", + "inventory": { + "selector": { + "busClass": "drm", + "label": "host-gpu", + "pciSlot": null + } + }, + "maxConcurrentClaims": 4, + "provider": { + "schemaId": "device-gpu.d2bus.org/Device/spec", + "schemaVersion": "1.0", + "settings": { + "contextTypes": ["virgl2"], + "crossDomainTrusted": false, + "displays": [], + "egl": true, + "renderNodeOnly": true, + "videoNvidiaDecode": false, + "videoSidecar": false, + "virglVideo": false, + "vulkan": false + } + }, + "providerRef": "Provider/device-gpu" + } +} +``` + +Array elements are sorted: primitive arrays lexicographically, object arrays by +first key. `null`-default optional selector fields are included explicitly; +absent `spec.provider.settings` fields receive their schema default. + +### Zone resource bundle/generation + +The Nix build produces a Zone resource generation bundle at +`/etc/d2b/zones//resources.json`: + +```json +{ + "schemaVersion": 1, + "zone": "dev", + "configGeneration": 42, + "generatedAt": "2026-07-22T00:00:00Z", + "contentDigest": "sha256:", + "resources": [ + { "apiVersion": "resources.d2bus.org/v3", "type": "Device", "metadata": { ..., "name": "corp-vm-gpu" }, "spec": { ... } }, + { "apiVersion": "resources.d2bus.org/v3", "type": "Device", "metadata": { ..., "name": "corp-vm-security-key" }, "spec": { ... } }, + { "apiVersion": "resources.d2bus.org/v3", "type": "Device", "metadata": { ..., "name": "corp-vm-tpm" }, "spec": { ... } }, + { "apiVersion": "resources.d2bus.org/v3", "type": "Device", "metadata": { ..., "name": "corp-vm-usb" }, "spec": { ... } } + ] +} +``` + +Bundle properties: + +- `resources` sorted lexicographically by `(type, metadata.name)`. +- `contentDigest` = SHA-256 of the canonical serialization of the `resources` + array alone (sorted keys, no trailing whitespace, no BOM). +- `configGeneration` is the NixOS system generation number. +- Two bundles with identical `contentDigest` produce no resource changes at the + Zone runtime regardless of `configGeneration`. +- The Nix build fails if any `(type, metadata.name)` pair is duplicated, if the + computed digest does not match, if any spec fails ResourceTypeSchema + validation, or if any `spec.provider.settings` field fails Provider schema validation. +- The bundle file is `root:d2bd` `0640`; the Zone runtime reads it at startup and + on generation-change signal. + +### Assertions at eval time + +New assertions added to `nixos-modules/assertions.nix`: + +1. A Guest with `tpm.enable=true` must declare exactly one Device resource with + `spec.providerRef = "Provider/device-tpm"` and `metadata.ownerRef` pointing to + that Guest. +2. Security-key proxy and USBIP passthrough of the same physical device label are + mutually exclusive: no two Device resources in the same Zone may have the same + `spec.inventory.selector.label` if one uses `Provider/device-security-key` and + the other uses `Provider/device-usbip`. +3. `spec.provider.settings.videoSidecar=true` requires a Device resource for the same + Guest with `spec.provider.settings.renderNodeOnly=false` (full GPU). +4. `spec.provider.settings.virglVideo=true` and `spec.provider.settings.videoSidecar=true` are + mutually exclusive on the same Guest. +5. A Device with `spec.arbitration=shared` requires `spec.deviceClass=physical` + and, for `Provider/device-gpu`, `spec.provider.settings.renderNodeOnly=true`. +6. No two Device resources in the same Zone have the same + `spec.inventory.selector.label` value. + +## Zone generation and cleanup + +### Generation model + +A NixOS build produces a Zone resource generation: the complete set of Device +(and other resource-type) specs declared in the Nix config for that Zone. The +generation has a monotonic `configGeneration`, a `contentDigest`, and the full +set of `managedBy=configuration` resource descriptors. Applying a generation with +the same `contentDigest` as the current active generation is a no-op. + +### Configuration-owned vs controller-created resources + +| Metadata field | Value | Set by | Meaning | +| --- | --- | --- | --- | +| `metadata.managedBy` | `"configuration"` | Core at bundle activation | Subject to generation-based lifecycle | +| `metadata.managedBy` | `"controller"` | Controller when creating child resources | Never subject to generation deletion | +| `metadata.managedBy` | `"api"` | Core when resource is created through the API | Persists until explicit Delete; never subject to generation deletion | + +`metadata.managedBy` is a closed enum (`configuration | controller | api`). Nix +input never sets it; Core sets it at activation or creation. Core uses it as the +definitive ownership marker. It: + +- Scopes generation-based deletion strictly to `managedBy=configuration`. +- Never deletes a resource with `managedBy=controller` or `managedBy=api` based + on generation absence. +- Sets `managedBy=configuration` on every resource reconciled from the Nix bundle; + never sets it on resources created by Provider controllers or through the API. + +Provider controllers create child resources (Processes, EphemeralProcesses, +Volumes) with `managedBy=controller`. The Device Provider controller manages them +entirely through its reconcile loop and finalizer sequence; generation cleanup +never touches them. + +### Cleanup contract + +When the Zone runtime activates generation N+1: + +1. **Create/update:** `managedBy=configuration` resources present in N+1 but not + N are Created; those present in both with changed spec receive a + `spec-generation-changed` reconcile trigger. +2. **Mark stale for deletion:** `managedBy=configuration` resources present in N + but absent from N+1 receive `deletionRequestedAt` (normal Delete request). The + Zone transitions to `Degraded/pending-cleanup`. +3. **Non-blocking:** generation N+1 is fully active and operational while stale + resource deletion proceeds asynchronously. New workloads start immediately. +4. **Standard sequence per deleted resource:** Provider finalizer handler runs → + terminates owned Processes → releases OS resources → clears finalizer → Core + removes resource. Duration is bounded per-Provider by the existing finalizer + timeout contract. +5. **Controller-created children:** child Processes/Volumes of a deleted Device + carry `managedBy=controller` and are owned by the Device Provider's finalizer, + not by generation cleanup. Core does not issue Delete to them; the Device + finalizer handler deletes them as part of its normal sequence. +6. **Orphan-sweep prohibition:** Core never sweeps `managedBy=controller` + resources for deletion merely because they are absent from the Nix bundle. +7. **Owner-controller reconcile:** if a `managedBy=configuration` resource's spec + changes (e.g., Device `spec.provider.settings` updated), its child Processes receive + `dependency-changed` triggers and the owner controller reconciles them. + +### Zone cleanup status + +```yaml +# Zone status while cleanup is pending (generation N → N+1) +status: + phase: Degraded + conditions: + - type: GenerationCleanPending + status: "True" + reason: config-resources-pending-deletion + message: "2 Device resource(s) pending deletion from prior generation" + activeGeneration: 43 + priorGeneration: 42 + pendingDeletion: + - type: Device + name: old-corp-vm-tpm + deletionRequestedAt: "2026-07-22T02:00:00Z" + - type: Device + name: old-corp-vm-usb + deletionRequestedAt: "2026-07-22T02:00:00Z" + +# Zone status after cleanup completes +status: + phase: Ready + conditions: + - type: GenerationCleanPending + status: "False" + reason: generation-cleanup-complete + activeGeneration: 43 + priorGeneration: null +``` + +The `Degraded/pending-cleanup` state does not affect scheduling of new workloads +under generation N+1. Resources added in N+1 are reconciled immediately. + +### Prior generation retention + +The Zone runtime retains prior generation bundles for diagnostics and rollback +reference. Retention is count-based: + +- `d2b.zones..retainedGenerations` — number of prior generations + to retain; default `3`; valid range `1..16`. +- When the count of retained prior generations would exceed the configured value, + the oldest fully-cleaned generation is pruned first. +- A generation is eligible for pruning only after all `managedBy=configuration` + resources it declared that are absent from the successor generation have reached + phase `Deleted` (or been removed from the store). +- Operator explicit prune via `d2b zone gc ` — prunes all fully-cleaned + generations beyond the retention count immediately. + +Re-applying a prior bundle requires explicit operator action. + +### Cleanup audit records + +Each `managedBy=configuration` resource deletion triggered by a generation transition emits: + +| Field | Value | +| --- | --- | +| `event` | `config-resource-deletion-requested` | +| `zone` | Zone name | +| `type` | Resource type (e.g., `Device`) | +| `resource_name_digest` | SHA-256 of the resource name; never raw name | +| `prior_generation` | N | +| `active_generation` | N+1 | +| `reason` | `absent-from-new-generation` | +| `timestamp` | RFC 3339 UTC | + +Per-op finalizer/broker audit records continue to be emitted by the Device +Provider controller's existing per-op audit path. + +### Cleanup error classes + +| Error | Meaning | +| --- | --- | +| `cleanup-finalizer-stuck` | Provider finalizer not cleared within bounded timeout; Zone stays `Degraded/pending-cleanup`; operator must inspect | +| `cleanup-child-deletion-failed` | Child Process/Volume deletion failed during Device finalizer; retried with exponential backoff | +| `cleanup-config-ownership-mismatch` | Generation deletion attempted on a resource with `managedBy=controller` or `managedBy=api`; fails closed and audited | +| `cleanup-controller-resource-protected` | Generation cleanup attempted to delete a controller-created resource; rejected, audited, and reported as invariant violation | + +## Tests + +### Layer-1 unit tests (Nix eval) + +- `tests/unit/nix/cases/device-tpm-eval.nix`: Device spec round-trip for TPM; emitted JSON golden vector. +- `tests/unit/nix/cases/device-usbip-eval.nix`: USBIP discriminated-union selector validation; unknown-field rejection. +- `tests/unit/nix/cases/device-security-key-eval.nix`: security-key mutual-exclusion assertion; Credential-ref requirement. +- `tests/unit/nix/cases/device-gpu-eval.nix`: GPU + video settings validation; shared-arbitration render-node-only enforcement. +- `tests/unit/nix/cases/device-schema-validation.nix`: eval-time rule corpus — one test per validation row in the eval-time validation table; each must reject with the documented error slug. +- `tests/unit/nix/cases/device-gen-cleanup-eval.nix`: generation diff — resource removed from Nix config appears in `pendingDeletion`; resource absent from prior generation does not appear; bundle `contentDigest` changes. +- `tests/unit/nix/cases/device-bundle-canonical.nix`: bundle JSON is canonical (sorted keys, sorted resources, stable contentDigest); two identical config subtrees produce identical digests. +- `tests/unit/nix/cases/device-inline-secret-rejected.nix`: inline string in settings field with `credentialRef` constraint fails eval with `inline-secret-in-settings`. +- `tests/unit/nix/cases/device-artifact-catalog.nix`: store path absent from all Device ResourceSpec JSON outputs; `spec.provider.settings` for TPM/USBIP carries no `artifactId` field; private catalog structure (type/digest/closure) is not present in the emitted resource bundle. + +### Layer-1 Rust (contract tests) + +| File | Scope | +| --- | --- | +| `packages/d2b-contract-tests/tests/minijail_swtpm_video.rs` | TPM/video minijail profile (reused) | +| `packages/d2b-contract-tests/tests/minijail_gpu.rs` | GPU minijail profile (reused) | +| `packages/d2b-contract-tests/tests/usbip_json_contract.rs` | USBIP DTO serde (reused) | +| `packages/d2b-contract-tests/tests/usb_sk_contract.rs` | Security-key DTO/broker capability (reused) | +| `packages/d2b-contract-tests/tests/video_binary_contract.rs` | Video wire-contract snapshot (reused) | +| New: `packages/d2b-contract-tests/tests/device_resource_schema.rs` | Device ResourceTypeSchema golden vectors; unknown-field denial; discriminated-union busClass rejection corpus; no `artifactId` or store-path field in any Device Provider settings schema | +| New: `packages/d2b-contract-tests/tests/device_provider_dossiers.rs` | Provider dossier completeness/schema conformance; settings schema fingerprint matches committed file | +| New: `packages/d2b-contract-tests/tests/device_bundle_canonical.rs` | Bundle JSON canonical form: sorted keys, sorted resources, stable contentDigest, duplicate-(type,name) rejection, digest mismatch fails | +| New: `packages/d2b-contract-tests/tests/device_gen_cleanup.rs` | Generation lifecycle: `managedBy=configuration` set on emitted resources; `managedBy=controller` on controller-created resources; `managedBy=api` on API-created resources; stale `managedBy=configuration` resource receives DeleteRequest; `managedBy=controller` and `managedBy=api` resources never receive generation-Delete | + +### Layer-1 Rust (Provider tests — `src/` colocated unit tests) + +Colocated `#[cfg(test)]` modules within each Provider crate's `src/`: + +- `packages/d2b-provider-device-tpm/src/` — swtpm argv golden vectors, state-dir + hardening, tamper-marker detection, flush → start sequencing, finalizer + non-deletion invariant. +- `packages/d2b-provider-device-usbip/src/` — bus ID corpus (31-char max, metachar + rejection, leading-zero segments), firewall rule ownership-marker format, + bind/unbind EphemeralProcess creation. +- `packages/d2b-provider-device-security-key/src/` — lease acquire/release + transitions, session ring eviction at capacity, broker op path-free invariant, + CID translation round-trip. +- `packages/d2b-provider-device-gpu/src/` — GPU/video process role selection, + wire-constant snapshot stability, render-node vs full-GPU path branching. + +### Layer-1 Rust (Provider tests — `tests/` hermetic Cargo integration) + +Each Provider crate's `tests/` directory; run with `cargo test -p d2b-provider-device-`: + +| Crate | Tests | +| --- | --- | +| `d2b-provider-device-tpm/tests/` | `controller_state_machine.rs` — flush→swtpm→Ready cycle with fake broker; `conformance.rs` — spec/status serde vs ResourceTypeSchema; `fault_swtpm_missing.rs` — swtpm absent → phase Degraded | +| `d2b-provider-device-usbip/tests/` | `arbitration_conflict.rs` — second-claim rejects; `conformance.rs` — spec/settings serde; `firewall_marker.rs` — ownership marker preserved in rule; `explicit_attach_split.rs` — EphemeralProcess bind vs declared Process | +| `d2b-provider-device-security-key/tests/` | `lease_state_machine.rs` — full acquire/cancel/expire cycle; `session_ring.rs` — ring wrap and eviction; `mutual_exclusion.rs` — USBIP+SK resolve the same token to an identical Core-derived physical backing tuple and the second claimant fails before effects; `conformance.rs` — spec/status serde; `guest_frontend_process.rs` — frontend Process resource fields | +| `d2b-provider-device-gpu/tests/` | `combined_reconcile.rs` — gpu+video combined state machine; `render_node_enforcement.rs` — shared+renderNodeOnly=false rejected; `wire_constant_snapshot.rs` — byte-stable wire-contract constants; `conformance.rs` — spec/settings serde | + +### Layer-2 integration tests + +- `tests/integration/containers/device-tpm-state.sh`: TPM provision/boot/reboot + cycle; tamper-marker survives restart. +- `tests/integration/containers/device-security-key-lease.sh`: lease acquire, + cancel, and session ring. +- `tests/integration/containers/device-usbip-arbitration.sh`: second-claim + conflict rejection. +- `tests/integration/containers/device-gen-cleanup.sh`: full generation cleanup + cycle — apply generation N with two Devices, apply generation N+1 removing one + Device, verify: (a) Zone transitions to `Degraded/pending-cleanup`, (b) removed + Device enters finalizer sequence, (c) child Processes of removed Device are + deleted by Device finalizer (not by Core), (d) Zone remains operational for the + retained Device during cleanup, (e) Zone transitions to `Ready` after cleanup + completes, (f) `GenerationCleanPending` condition clears. +- `tests/integration/containers/device-controller-resource-protected.sh`: start a + Device, let Provider create a child Process, then switch to a new generation + that does not change the Device; verify the child Process is NOT deleted by + generation cleanup and has `managedBy=controller` (not `managedBy=configuration`). +- `tests/integration/containers/device-gen-cleanup-audit.sh`: each stale-resource + deletion triggered by generation change emits a `config-resource-deletion-requested` + audit record with correct `prior_generation`, `active_generation`, and + `resource_name_digest` (never raw name). + +### Layer-2 integration tests (Provider crate `integration/` fixtures) + +Each Provider crate's `integration/` directory contains heavier container/Host/Guest +scenarios invoked by `make test-integration`. These are distinct from the +top-level `tests/integration/containers/` scripts and are co-located with the +Provider they test: + +| Crate `integration/` path | Scenarios | +| --- | --- | +| `d2b-provider-device-tpm/integration/` | `provision_and_reboot/` — full TPM provision → Guest boot → reboot cycle; `tamper_marker_survives/` — marker present after Provider restart; `finalizer_no_delete/` — Volume not deleted on Device finalizer | +| `d2b-provider-device-usbip/integration/` | `arbitration_conflict/` — second Host claim rejected at runtime; `busid_bind_cycle/` — full modprobe→lock→withhold→firewall→bind→proxy bringup; `network_firewall_coexistence/` — Provider firewall rule does not clobber Network rules | +| `d2b-provider-device-security-key/integration/` | `lease_acquire_cancel/` — full acquire → cancel → re-acquire cycle; `session_ring_capacity/` — ring wraps correctly under real vsock load; `guest_frontend_connect/` — Guest frontend Process connects and authenticates over AF_VSOCK | +| `d2b-provider-device-gpu/integration/` | `gpu_worker_start/` — GPU worker Process obtains broker tokens and becomes Ready; `render_node_shared/` — two Guests share render-node Device simultaneously; `video_dependency/` — video-decoder Process starts only after gpu worker Process is Ready | + +### Feasibility proofs + +- Device reconcile state machine with fake supervisor and broker. +- Security-key relay process with fake hidraw fd and fake vsock Guest. +- swtpm flush → start EphemeralProcess ordering with fake broker. +- GPU worker process broker token set verification. +- Generation cleanup: fake Zone runtime with two Devices; remove one from + config; assert only the `managedBy=configuration` stale resource receives DeleteRequest; + assert controller-created child is not touched; assert Zone stays operational. + +## ProcessRole → future path disposition + +| Current v3 ProcessRole | Future v3 resource | Provider | Notes | +| --- | --- | --- | --- | +| `SwtpmPreStartFlush` | `EphemeralProcess/-tpm-flush` | `device-tpm` | Pre-start ioctl flush; `swtpm_ioctl -i --unix ` | +| `Swtpm` | `Process/-tpm-swtpm` | `device-tpm` | Long-lived swtpm socket process; user NS, zero host caps | +| `Usbip` | `Process/-usbip-daemon` + `EphemeralProcess/-usbip-bind/unbind` | `device-usbip` | Daemon and per-busid bind/unbind | +| `SecurityKeyFrontend` | **Host relay**: `Process/device--sk-relay` (extracted from d2bd daemon-internal accept loop in `packages/d2bd/src/lib.rs:10456` and `packages/d2bd/src/security_key.rs`). **Guest frontend**: `Process/device--sk-frontend` (from `packages/d2b-sk-frontend/`; `executionRef: Guest/`; replaces `d2b-sk-frontend.service` guest systemd unit) | `device-security-key` | KEY FACT: current `ProcessRole::SecurityKeyFrontend` is a d2bd-internal async accept loop (`start_sk_accept_loop`), NOT a spawned process; in v3 the relay is extracted into a separate unprivileged relay Process; the guest frontend binary becomes a Zone Process resource with Guest execution context. | +| `Gpu` | `Process/device--gpu` (full GPU; exclusive) | `device-gpu` | crosvm `device gpu`; Wayland socket, virtio-gpu | +| `GpuRenderNode` | `Process/device--render-node` (render-node mode; exclusive default, shared when explicit) | `device-gpu` | Render-node-only mode; same broker tokens | +| `Video` | `Process/device--video` | `device-gpu` | crosvm `device video-decoder --backend vaapi` | + +No ProcessRole or Nix component is removed until the successor Provider/Process +integration is live and all current tests pass against the new resource model. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | **Process/DAG**: `packages/d2b-core/src/processes.rs` (ProcessRole enum: Swtpm, SwtpmPreStartFlush, Usbip, SecurityKeyFrontend, Gpu, GpuRenderNode, Video; VmProcessDag/VmProcessInvariants structs — old Workload DAG names); `packages/d2b-core/src/bundle_resolver.rs` (process exec names, device token sets, USBIP intents); `packages/d2b-host/src/swtpm_argv.rs`, `gpu_argv.rs`, `video_argv.rs`; **swtpm state**: `packages/d2b-priv-broker/src/ops/swtpm_dir.rs`; **Contracts/broker ops**: `packages/d2b-contracts/src/security_key.rs`, `usbip.rs`, `broker_wire.rs`; `packages/d2b-core/src/privileges_w3.rs` (W3BrokerOperation enum: SecurityKeyOpenDevice, SecurityKeyApplyUdevRules, UsbipBindFirewallRule); **Security-key relay (daemon-internal)**: `packages/d2bd/src/security_key.rs` (CTAPHID relay: CID translation, SO_PEERCRED auth, hidraw async fd, accept loop, lease; lives inside d2bd, NOT a separate spawned process); `packages/d2bd/src/lib.rs:10456` (`start_sk_accept_loop` — ProcessRole::SecurityKeyFrontend is handled as a daemon-internal coroutine: broker fetches hidraw fd, daemon binds vsock-proxy socket, spawns async accept loop); **Guest binary**: `packages/d2b-sk-frontend/src/` (static binary for in-guest UHID virtual HID device; connects over AF_VSOCK to the daemon accept loop; NOT related to the ProcessRole name); **USBIP state machine**: `packages/d2bd/src/usbip_state_machine.rs` (typed per-busid bring-up plan and executor; canonical step order: modprobe→lock→withhold→firewall→backend→bind→proxy); `packages/d2bd/src/usbip_reconcile_state.rs` (restart-safe reconciler state model; internal to daemon, not yet wired to reconciler); `packages/d2bd/src/usbipd_perenv_autostart.rs` (per-env usbipd daemon autostart via broker SpawnRunner, retiring legacy systemd units in `nixos-modules/network.nix`); **Workload/Realm capability surface (old names)**: `packages/d2b-realm-core/src/capability.rs` (old Realm Capability enum: GpuAccel, Usb, Hid, Hotplug — current inter-Realm device capability assertion, target maps to Device ResourceType claims); `packages/d2b-realm-core/src/stream.rs` (StreamKind::DeviceHid → Capability::Hid, StreamKind::DeviceUsb → Capability::Usb); `packages/d2bd/src/realm_access_resolver.rs` (maps old Workload ops: `ops.media.usb_hotplug` → Capability::Usb + Capability::Hotplug, `ops.display.graphics` → Capability::GpuAccel); **Workload manifest (old name)**: `packages/d2b-core/src/manifest_v04.rs` VmEntry fields: `tpm: bool`, `usbip_yubikey: bool`, `security_key: bool`, `graphics: bool`, `gpu_socket: Option` (per-Workload device-enable flags in the v04 manifest; these are the current per-VM device declarations); **Runtime capability surface**: `packages/d2b-core/src/runtime.rs` (RuntimeServiceRole enum maps ProcessRoles to public roles: Tpm←Swtpm/SwtpmPreStartFlush, Display←Gpu/GpuRenderNode, Video←Video, Usb←Usbip+SecurityKeyFrontend; RuntimeMediaCapabilities: `usb_hotplug`; RuntimeDisplayCapabilities: `graphics`/`video`); **Nix options (old Workload namespace)**: `nixos-modules/options-realms-workloads.nix` (`d2b.vms..tpm.enable`, `d2b.vms..graphics.enable` — current Nix Workload device options; v3 target is `d2b.zones..resources. = { type = "Device"; ... }`); `nixos-modules/components/tpm.nix`, `usbip.nix`, `security-key-guest.nix`, `video/guest.nix`, `graphics.nix` | +| Evidence class | ProcessRole enum (ProcessRole, VmProcessDag): **implemented-and-reachable**. swtpm/gpu/video argv generators: **implemented-and-reachable**. swtpm_dir hardening and tamper marker: **implemented-and-reachable**. Security-key broker ops DTOs (`security_key.rs`, `broker_wire.rs` W3BrokerOperation): **implemented-and-reachable** (NOT unwired stubs — the full CTAPHID relay runs in `packages/d2bd/src/security_key.rs` and `packages/d2bd/src/lib.rs:start_sk_accept_loop`). USBIP state machine (`usbip_state_machine.rs`): **implemented-and-reachable**. USBIP reconcile state model (`usbip_reconcile_state.rs`): **implemented-but-unwired** (future restart-safe reconciler, internal state model). USBIP per-env autostart (`usbipd_perenv_autostart.rs`): **implemented-and-reachable**. Realm Capability enum (GpuAccel/Usb/Hid/Hotplug): **implemented-and-reachable** (old Workload/Realm names; in-process capability assertion). StreamKind::DeviceHid/DeviceUsb: **implemented-and-reachable**. realm_access_resolver.rs (Workload ops → Capabilities): **implemented-and-reachable**. manifest_v04.rs VmEntry device fields: **generated-or-eval-contract** (bundle/manifest-driven per-Workload flags). runtime.rs RuntimeServiceRole/RuntimeCapabilities: **implemented-and-reachable** (current public service role and capability surface). d2b-sk-frontend guest binary: **implemented-and-reachable** (guest static binary, not a Zone Process). Nix options-realms-workloads.nix device options: **generated-or-eval-contract**. Device ResourceType schema: **ADR-only**. Provider crates (d2b-provider-device-*): **ADR-only**. | +| Behavior retained | Swtpm user-namespace/zero-host-caps (ADR 0021), tamper-marker/fail-closed, umask=7 socket ACL; GPU broker token set (kvm/dri/udmabuf/nvidia*); video wire-contract constants frozen; USBIP bus ID validation; security-key hidraw-only broker access; eval-time mutual-exclusion assertions | +| Required delta | Device ResourceType schema, four Provider crates, controller reconcile loops, RBAC roles, hot-plug observe interval, Guest frontend Process resolution, consolidated process name templates | +| Reuse path | Extract swtpm_argv.rs, swtpm_dir.rs, gpu_argv.rs, video_argv.rs unmodified into device-tpm/device-gpu crates. Adapt security_key.rs DTOs with Zone ResourceRef identifiers. Adapt usbip.rs with v3 enum changes. Copy ProcessRole disposition table verbatim into Provider dossiers. | +| Replacement/deletion | ProcessRole enum variants are retained until Provider successor crates reach integration parity; Nix components retained until v3 Guest Nix emitters replace them | +| Feasibility proof | State-machine and protocol-conformance tests per Provider; physical-probe absence and hotplug condition transitions | +| Future owner | Work items below and per-Provider dossier specs | + +## Current device capability surface (old Workload/Realm terminology) + +The following baseline code uses old Workload/Realm terminology that maps to the +Device ResourceType and claims model in v3. Current evidence is cited by old +symbol names; v3 target names are in the "maps to" column. + +| Current symbol | File | Class | v3 mapping | +| --- | --- | --- | --- | +| `Capability::GpuAccel` | `packages/d2b-realm-core/src/capability.rs` | implemented-and-reachable | `Device/-gpu` claim, `device-gpu` Provider | +| `Capability::Usb` | `packages/d2b-realm-core/src/capability.rs` | implemented-and-reachable | `Device/-usb` claim, `device-usbip` Provider | +| `Capability::Hid` | `packages/d2b-realm-core/src/capability.rs` | implemented-and-reachable | `Device/-security-key` claim, `device-security-key` Provider | +| `Capability::Hotplug` | `packages/d2b-realm-core/src/capability.rs` | implemented-and-reachable | Device controller `scheduled-observe` trigger / hotplug notification | +| `StreamKind::DeviceHid` | `packages/d2b-realm-core/src/stream.rs` | implemented-and-reachable | `device-security-key` hidraw relay stream | +| `StreamKind::DeviceUsb` | `packages/d2b-realm-core/src/stream.rs` | implemented-and-reachable | `device-usbip` USB export stream | +| `ops.display.graphics` → `Capability::GpuAccel` | `packages/d2bd/src/realm_access_resolver.rs:140` | implemented-and-reachable | GPU Device claim from Workload op flag (old Workload `graphics: bool`) | +| `ops.media.usb_hotplug` → `Capability::Usb + Capability::Hotplug` | `packages/d2bd/src/realm_access_resolver.rs:148` | implemented-and-reachable | USBIP Device claim from Workload op flag (old Workload `usb_hotplug: bool`) | +| `VmEntry::tpm: bool` | `packages/d2b-core/src/manifest_v04.rs` | generated-or-eval-contract | `Device/-tpm` spec; `device-tpm` Provider | +| `VmEntry::usbip_yubikey: bool` | `packages/d2b-core/src/manifest_v04.rs` | generated-or-eval-contract | `Device/-usb` spec; `device-usbip` Provider | +| `VmEntry::security_key: bool` | `packages/d2b-core/src/manifest_v04.rs` | generated-or-eval-contract | `Device/-security-key` spec; `device-security-key` Provider | +| `VmEntry::graphics: bool` | `packages/d2b-core/src/manifest_v04.rs` | generated-or-eval-contract | `Device/-gpu` spec; `device-gpu` Provider | +| `RuntimeServiceRole::Tpm` | `packages/d2b-core/src/runtime.rs` | implemented-and-reachable | Device `device-tpm` public service role | +| `RuntimeServiceRole::Usb` | `packages/d2b-core/src/runtime.rs` | implemented-and-reachable | Device `device-usbip` + `device-security-key` public service role | +| `RuntimeServiceRole::Display` | `packages/d2b-core/src/runtime.rs` | implemented-and-reachable | Device `device-gpu` (GPU) public service role | +| `RuntimeServiceRole::Video` | `packages/d2b-core/src/runtime.rs` | implemented-and-reachable | Device `device-gpu` (video) public service role | +| `RuntimeMediaCapabilities::usb_hotplug` | `packages/d2b-core/src/runtime.rs` | implemented-and-reachable | USBIP Device capability declaration in Workload runtime metadata | +| `RuntimeDisplayCapabilities::graphics` | `packages/d2b-core/src/runtime.rs` | implemented-and-reachable | GPU Device capability declaration in Workload runtime metadata | +| `d2b.vms..tpm.enable` | `nixos-modules/options-realms-workloads.nix` | generated-or-eval-contract | v3: `d2b.zones..resources."-tpm"` | +| `d2b.vms..graphics.enable` | `nixos-modules/options-realms-workloads.nix` | generated-or-eval-contract | v3: `d2b.zones..resources."-gpu"` | + +The `Capability` enum in `packages/d2b-realm-core/src/capability.rs` is not +preserved as an enum in v3. Its values become Device ResourceType claims +(exclusive claim for Usb/Hid/GpuAccel) and Device controller observe-triggers +(Hotplug). The routing through `d2b-realm-router` uses the old Realm names; the +v3 Device controller replaces this routing. + +## Provider crate layout + +Every `packages/d2b-provider--/` crate must contain all +four of the following paths. Missing any path fails the workspace/package policy +check (`make test-policy` / `cargo xtask check-provider-layout`): + +| Path | Required contents | +| --- | --- | +| `src/` | Implementation: lib modules, binaries, controllers, workers, and services. Colocated unit tests as `#[cfg(test)]` modules within the source files they test. | +| `tests/` | Hermetic Cargo integration tests: ResourceType/spec serde round-trips, controller state-machine tests, conformance corpus against the ResourceTypeSchema and Provider settings schema, fault-injection and error-path tests. All tests run in-process with fake/stub dependencies; no container, privileged socket, or root privilege required. | +| `integration/` | Heavier fixtures and scenarios: container-level, Host/Guest-level, cross-process, and provider-system tests. Invoked by the existing test orchestration (`make test-integration` / Layer-2 container runner). Individual scripts or fixtures may be standalone but must be discoverable by the orchestrator without manual wiring. | +| `README.md` | Markdown document that covers all of the following topics: Provider identity (`Provider/`), config schema and Nix authoring shape, ResourceTypes managed, controllers/services/workers/binaries and their roles, placement (Host/Guest/Zone), Provider dependencies and RBAC, security posture and state ownership, telemetry and audit attributes, build/test/integration commands, and future standalone-repo usage notes. | + +The policy check is a `cargo xtask` command invoked by `make test-policy`. It +walks the workspace for crates matching the `d2b-provider-*` naming pattern and +asserts all four paths exist. The check fails closed: any crate matching the +pattern that is missing any required path fails the policy gate with a named +error listing the missing paths. There is no opt-out mechanism. + +## Implementation work items + +### ADR046-device-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0 shared contract root; `d2b-contracts` | +| Current source | `packages/d2b-contracts/src/security_key.rs` (SecurityKeyStatusResponse, SecurityKeySession, SecurityKeyLeaseState, SecurityKeyVmSessionState DTOs; implemented-and-reachable), `usbip.rs`, `broker_wire.rs`; `packages/d2b-core/src/privileges_w3.rs` (W3BrokerOperation: SecurityKeyOpenDevice, SecurityKeyApplyUdevRules, UsbipBindFirewallRule — implemented-and-reachable); `packages/d2b-core/src/manifest_v04.rs` VmEntry device fields (tpm, usbip_yubikey, security_key, graphics — old Workload manifest, generated-or-eval-contract) | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/device.rs` | +| Detailed design | Device ResourceType schema (spec/status/conditions/claims/inventory); closed-set error codes; Device RBAC verbs; broker operation effect-limit constants; shared Device telemetry contract requires fixed semantic metric labels with no Zone/resource-name-derived identity and retains `d2b.zone`/`d2b.provider` only as OTEL resource attributes. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Provider dossiers, resource API/store, CLI status surfaces | +| Data migration | Full reset; no v2 device object import | +| Validation | Schema golden vectors; unknown-field denial; exclusive/shared conflict rejection; arbitration/maxClaims invariant; cross-Provider structural descriptor test asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus Device/Zone-name canary absence while preserving `d2b.zone` resource attributes | +| Removal proof | Old ProcessRole/DTO branches retained until Provider integrations are live | + +### ADR046-device-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-device-001; device-tpm provider owner | +| Current source | `packages/d2b-host/src/swtpm_argv.rs`; `packages/d2b-priv-broker/src/ops/swtpm_dir.rs`; `nixos-modules/components/tpm.nix` | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-device-tpm/src/` (controller, swtpm runner, state-dir logic); `packages/d2b-provider-device-tpm/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-tpm/integration/` (container/Host scenarios); `packages/d2b-provider-device-tpm/README.md` | +| Detailed design | Device spec/status; flush EphemeralProcess → swtpm Process sequencing; state-dir hardening; tamper-marker; finalizer non-deletion of Volume; Nix emitter; all four required crate paths present (see "Provider crate layout") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Zone resource store; Process controller; Volume lifecycle | +| Data migration | State dir and tamper markers preserved across reset | +| Validation | `src/`: swtpm argv golden, state-dir, flush sequencing, finalizer no-delete; `tests/`: `controller_state_machine.rs`, `conformance.rs`, `fault_swtpm_missing.rs`; `integration/`: `provision_and_reboot/`, `tamper_marker_survives/`, `finalizer_no_delete/`; workspace policy check: `make test-policy` passes with all four paths present | +| Removal proof | ProcessRole::Swtpm and SwtpmPreStartFlush removed after parity | + +### ADR046-device-003 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-device-001; device-usbip provider owner | +| Current source | `packages/d2b-contracts/src/usbip.rs` (USBIP DTOs, SYSFS_BUS_ID_MAX, bus-ID validation — implemented-and-reachable); `packages/d2b-core/src/bundle_resolver.rs` USBIP intents; `packages/d2b-core/src/privileges.rs` authz rows; `packages/d2bd/src/usbip_state_machine.rs` (typed per-busid bring-up state machine, step order: modprobe→lock→withhold→firewall→backend→bind→proxy — implemented-and-reachable); `packages/d2bd/src/usbipd_perenv_autostart.rs` (per-env usbipd daemon autostart — implemented-and-reachable); `packages/d2bd/src/usbip_reconcile_state.rs` (restart-safe reconciler state model — implemented-but-unwired); old Workload Nix option: `nixos-modules/options-realms-workloads.nix` `d2b.vms..usbip.*` (generated-or-eval-contract); `nixos-modules/components/usbip.nix` | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-device-usbip/src/` (controller, daemon Process, bind/unbind EphemeralProcess, firewall); `packages/d2b-provider-device-usbip/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-usbip/integration/` (container/Host scenarios); `packages/d2b-provider-device-usbip/README.md` | +| Detailed design | Device spec/status; bus ID validation; firewall rule ownership-marker; bind/unbind EphemeralProcess; per-Device daemon Process (owned by device-usbip; Network supplies dependency/firewall interface); Nix emitter; all four required crate paths present (see "Provider crate layout") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Zone resource store; broker `UsbipBindFirewallRule { action: Ensure \| Remove }`; nftables marker | +| Data migration | None; full reset | +| Validation | `src/`: bus ID corpus, firewall marker format, EphemeralProcess creation; `tests/`: `arbitration_conflict.rs`, `conformance.rs`, `firewall_marker.rs`, `explicit_attach_split.rs`; `integration/`: `arbitration_conflict/`, `busid_bind_cycle/`, `network_firewall_coexistence/`; workspace policy check: `make test-policy` passes with all four paths present | +| Removal proof | ProcessRole::Usbip removed after parity | + +### ADR046-device-004 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-device-001; device-security-key provider owner | +| Current source | `packages/d2b-contracts/src/security_key.rs` (DTOs — implemented-and-reachable); `packages/d2b-core/src/privileges_w3.rs` (W3BrokerOperation — implemented-and-reachable); **KEY: relay is in d2bd** — `packages/d2bd/src/security_key.rs` (CTAPHID relay: CID translation, SO_PEERCRED, hidraw async fd, accept loop — implemented-and-reachable) and `packages/d2bd/src/lib.rs:start_sk_accept_loop` (ProcessRole::SecurityKeyFrontend dispatch — implemented-and-reachable); **guest binary**: `packages/d2b-sk-frontend/src/` (static UHID frontend — implemented-and-reachable); old Workload Nix option: `nixos-modules/options-realms-workloads.nix` `d2b.vms..security_key.*`; `nixos-modules/components/security-key-guest.nix` | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-device-security-key/src/` (controller, relay Process, guest frontend Process, lease/session ring); `packages/d2b-provider-device-security-key/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-security-key/integration/` (container/Host/Guest scenarios); `packages/d2b-provider-device-security-key/README.md` | +| Detailed design | Device spec/status; unprivileged relay Process (`device--sk-relay`); guest frontend Process (`device--sk-frontend`, `executionRef: Guest/`); ceremony/CID/lease/session ring (max 1 session per Device); broker hidraw-only access; mandatory Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` claim shared with every USB Provider before effects; Nix emitter; all four required crate paths present (see "Provider crate layout") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Zone resource store; broker `SecurityKeyOpenDevice`/`SecurityKeyApplyUdevRules`; Guest frontend module | +| Data migration | None; full reset | +| Validation | `src/`: lease transitions, session ring eviction, broker op path-free, CID round-trip; `tests/`: `lease_state_machine.rs`, `session_ring.rs`, `mutual_exclusion.rs` proves security-key and USB implementations resolve one token to a byte-identical physical backing key and the loser receives `physical-usb-backing-conflict` before any effect, `conformance.rs`, `guest_frontend_process.rs`; `integration/`: `lease_acquire_cancel/`, `session_ring_capacity/`, `guest_frontend_connect/`; workspace policy check: `make test-policy` passes with all four paths present | +| Removal proof | ProcessRole::SecurityKeyFrontend removed after parity | + +### ADR046-device-005 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-device-001; device-gpu provider owner | +| Current source | `packages/d2b-host/src/gpu_argv.rs`, `video_argv.rs`; `packages/d2b-core/src/bundle_resolver.rs` Gpu/GpuRenderNode/Video; `nixos-modules/components/graphics.nix`, `video/guest.nix` | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-device-gpu/src/` (controller, GPU/render-node/video worker Processes, broker token set); `packages/d2b-provider-device-gpu/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-gpu/integration/` (container/Host/Guest scenarios); `packages/d2b-provider-device-gpu/README.md` | +| Detailed design | Combined Device spec/status; GPU worker Process (`device--gpu`, exclusive); render-node Process (`device--render-node`, exclusive default, shared when explicit); video-decoder Process (`device--video`); broker token set; wire-contract constants; shared render-node arbitration enforcement; Nix emitter; all four required crate paths present (see "Provider crate layout") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Zone resource store; broker `SpawnRunner`/`OpenDevice`; Display Provider device consumption | +| Data migration | None; full reset | +| Validation | `src/`: process role selection, wire-constant snapshot, render-node vs full-GPU branching; `tests/`: `combined_reconcile.rs`, `render_node_enforcement.rs`, `wire_constant_snapshot.rs`, `conformance.rs`; `integration/`: `gpu_worker_start/`, `render_node_shared/`, `video_dependency/`; workspace policy check: `make test-policy` passes with all four paths present | +| Removal proof | ProcessRole::Gpu, GpuRenderNode, Video removed after parity | + +### ADR046-device-006 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-device-001 through ADR046-device-005; Nix integrator | +| Current source | `nixos-modules/components/tpm.nix`, `usbip.nix`, `security-key-guest.nix`, `video/guest.nix`, `graphics.nix`; `nixos-modules/assertions.nix`; **old Workload Nix namespace**: `nixos-modules/options-realms-workloads.nix` (`d2b.vms..tpm.enable`, `d2b.vms..graphics.enable`, `d2b.vms..usbip.*` — generated-or-eval-contract; v3 replaces `d2b.vms.*` with `d2b.zones.*`) | +| Reuse action | adapt | +| Destination | `nixos-modules/resources-device.nix`; `nixos-modules/bundle-artifacts.nix` (bundle emission for resource store); `nixos-modules/assertions.nix` (six eval-time device assertions) | +| Detailed design | Nix authoring shape `d2b.zones..resources. = { type = "Device"; metadata.ownerRef = ...; spec = { ...exact ResourceSpec fields... }; };` as specified in "Nix configuration" section; `metadata.name`/`metadata.zone`/`apiVersion` derived automatically; `status` and Core management fields (`managedBy`, `configurationGeneration`, uid, generation, revision, timestamps, finalizers) omitted from emitted JSON; `spec` field names/types/defaults identical to ResourceTypeSchema with no renaming; per-Provider `spec.provider.settings` validated against signed Provider schema; no `artifactId` or store-path fields in Device `spec.provider.settings` — binary paths are resolved from Provider package closures; Credential-ref enforcement; artifact catalog emitted as a separate private integrity-pinned map (ID→type/digest/closure) by its own emitter; six eval-time validation assertions; canonical sorted-key full resource-envelope JSON emission (`apiVersion`, `type`, `metadata`, `spec` only); Zone resource bundle with `contentDigest` as specified in "Zone resource bundle/generation" section; Core sets `metadata.managedBy=configuration` and `metadata.configurationGeneration` at activation | +| Integration | Resource store Nix emitter; artifact catalog emitted separately by Provider/system resource emitter (not by this emitter); device resource JSON output; Zone generation object including `priorGeneration`, `pendingDeletion`, `cleanupStatus` fields; cleanup contract logic belongs in the Core configuration handler (`packages/d2b-core-controller/src/configuration.rs`) consuming the generation from this emitter | +| Data migration | Consumers migrate from per-VM options to Zone Device declarations; data migration guide references "Nix configuration" section migration table | +| Validation | nix-unit: `device-tpm-eval.nix`, `device-usbip-eval.nix`, `device-security-key-eval.nix`, `device-gpu-eval.nix`, `device-schema-validation.nix`, `device-gen-cleanup-eval.nix`, `device-bundle-canonical.nix`, `device-inline-secret-rejected.nix`, `device-artifact-catalog.nix`; contract tests: `device_resource_schema.rs`, `device_bundle_canonical.rs`, `device_gen_cleanup.rs` | +| Removal proof | Nix option `d2b.vms..tpm.enable` etc. retained until v3 reset | + +### ADR046-device-007 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-device-006; Zone runtime implementer | +| Current source | None (new work; no equivalent in v3 baseline or main a1cc0b2d) | +| Reuse action | create | +| Destination | `packages/d2b-core-controller/src/configuration.rs`; `packages/d2b-contract-tests/tests/device_gen_cleanup.rs` | +| Detailed design | Implement the cleanup contract described in "Zone generation and cleanup": (1) on new generation activation, diff `resources` against prior generation's `resources` by (type, name) — resources absent from new generation that have `managedBy=configuration` go into `pendingDeletion`; (2) Zone phase transitions to `Degraded/pending-cleanup` until all items in `pendingDeletion` reach terminal Delete; (3) items in `pendingDeletion` with `managedBy=controller` or `managedBy=api` are rejected with `cleanup-config-ownership-mismatch`; (4) `managedBy=controller` and `managedBy=api` resources are never touched by generation cleanup — `cleanup-controller-resource-protected` emitted if attempted; `managedBy=api` resources persist until explicit Delete; (5) prior generations retained by count: default 3, range 1..16, configured via `d2b.zones..retainedGenerations`; (6) each deletion is non-blocking; (7) finalizer-stuck timeout emits `cleanup-finalizer-stuck` and leaves Zone in `Degraded/pending-cleanup`; (8) all deletions emit `config-resource-deletion-requested` audit record with digested resource identity | +| Integration | Consumes Zone resource bundle from ADR046-device-006 emitter; drives Device Provider finalizers via normal resource-Delete protocol; feeds Zone status conditions `GenerationCleanPending` and `GenerationCleanError` | +| Data migration | Full d2b 3.0 reset; no prior generation cleanup state or v2 Device resource import | +| Validation | Fast hermetic (D094): in-process Rust selectors under the owning crate `tests/` — `packages/d2b-core-controller/tests/gen_cleanup.rs`, `packages/d2b-core-controller/tests/gen_cleanup_controller_protected.rs`, `packages/d2b-core-controller/tests/gen_cleanup_audit.rs` — driven by a fake Zone store/clock with deterministic ordering and no wall-clock sleep; slower coverage reuses the existing integration directory scenarios `tests/integration/containers/device-gen-cleanup.sh`, `device-controller-resource-protected.sh`, and `device-gen-cleanup-audit.sh`; feasibility proof: generation cleanup with fake Zone runtime. No new top-level shell gate is added. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-device-008 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-device-002 through ADR046-device-005; workspace/tooling maintainer | +| Current source | `packages/xtask/src/main.rs` (existing `gen-*` and `check-*` commands — implemented-and-reachable); `packages/d2b-contract-tests/tests/workspace_policy.rs` (existing workspace policy tests — implemented-and-reachable) | +| Reuse action | adapt | +| Destination | `packages/xtask/src/main.rs` (`check-provider-layout` subcommand); `packages/d2b-contract-tests/tests/workspace_policy.rs` (provider-layout policy assertions) | +| Detailed design | Add `cargo xtask check-provider-layout`: enumerate workspace members matching `d2b-provider-*`; for each, assert `src/`, `tests/`, `integration/`, and `README.md` all exist relative to the crate root; report all missing paths before failing; no opt-out flag. Add companion test in `workspace_policy.rs` that asserts the same invariant against the static crate list in `Cargo.toml`. Wire `cargo xtask check-provider-layout` into `make test-policy` alongside existing workspace naming and sort checks. The check must also run in CI as part of the policy gate. Primary reuse disposition: `adapt`. Preserved source-plan detail: extend. | +| Integration | `make test-policy`; `make check`; GitHub CI policy job | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Policy gate passes once all four Device Provider crates exist with required paths; gate fails predictably when any path is removed from a Provider crate fixture; test fixture crate with one missing path must produce a named error identifying the exact missing path | +| Removal proof | N/A (new tooling; not removed) | diff --git a/docs/specs/ADR-046-resources-host-guest-process-user.md b/docs/specs/ADR-046-resources-host-guest-process-user.md new file mode 100644 index 000000000..543e82f81 --- /dev/null +++ b/docs/specs/ADR-046-resources-host-guest-process-user.md @@ -0,0 +1,2748 @@ +# ADR 0046 resources: Host, Guest, Process, EphemeralProcess, and User + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-resources-host-guest-process-user` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-contracts`, `d2b-provider-system-core`, `d2b-provider-system-systemd`, `d2b-provider-system-minijail` | +| Depends on | `ADR-046-decision-register`, `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-components-processes-and-sandbox`, `ADR-046-provider-model-and-packaging`, `ADR-046-primitive-resource-composition`, `ADR-046-core-controllers` | +| Supersedes | Current `ProcessRole`/`VmProcessDag` as the public process model; current Realm workload/unsafe-local/execution DTOs; `WorkloadProviderKind`/`IsolationPosture` posture enums | + +## Scope + +This spec exhaustively defines: + +- `Host` — physical/local host execution, policy, and budget parent; +- `Guest` — VM, sandbox, cloud, or remote execution parent; +- shared `ExecutionPolicy` inline schema used by both; +- `Process` — long-lived supervised process; +- `EphemeralProcess` — one-shot asynchronous process with terminal retention; +- shared `ExecutionSpec` inline schema used by both Process types; +- `User` — named host identity, UID/session observation, ACL/process subject. + +Every field, type, default, bound, condition, error, security rule, RBAC verb, +reconcile step, finalizer step, Nix declaration example, and implementation +work item for these five ResourceTypes is defined here. ResourceTypes not in +scope (Volume, Network, Device, Credential, Zone, ZoneLink, Provider, Role, +RoleBinding) are referenced as target types and defined in their owning specs. + +## ResourceSpec shape + +### Three-layer spec shape (D089) + +D089 freezes every ResourceSpec in this file as three layers. Layer 1 is the +universal Resource envelope and metadata. Layer 2 is the ResourceType base spec +at top-level `spec.*`, including `spec.providerRef`; all Host, Guest, Process, +EphemeralProcess, and User fields documented here are base unless explicitly +stated otherwise. Layer 3 is the optional canonical selected-Provider extension +`spec.provider = { schemaId, schemaVersion, settings }`; it is the only +Provider-specific desired extension and replaces the former Host/Guest +`providerSettings` shape. It omits `providerRef` and +`observedProviderGeneration`: `spec.providerRef` is base, and spec is desired +rather than observed. + +**D091 update policy.** The universal base spec carries `spec.updatePolicy` for +every Host, Guest, Process, EphemeralProcess, and User resource: disruptive +changes default to manual, while automatic non-disruptive upgrades are permitted +by policy. A `spec.provider` extension MAY add provider-specific knobs, but MUST +NOT bypass or weaken base `spec.updatePolicy`. + +**D090 expedited reconcile.** Authorized `Create`, `UpdateSpec`, and `Delete` +calls MAY set `waitForReconcile`. Under one mutation ticket, `operationId`, and +deadline, Core admission and the reserved-revision redb commit run in parallel +with the owning controller's preflight/plan, but the controller MUST NOT perform +external effects, finalizer release, or status mutation until Core supplies +`CommittedRevisionProof {resourceUid, generation, revision, operationId}`; DB +failure aborts with no effect. The API returns the committed object plus +one-pass projected layered status, `disposition` +(`Converged|Progressing|Blocked|UpgradeRequired|Failed`), `statusPersistence` +(`pending|committed`), and the last persisted status revision. The durable +commit is never rolled back on reconcile timeout or failure; effect idempotency +keys derive from `(UID,generation,revision,operationId)`, and the expedited pass +uses a bounded priority lane in the same per-resource single-flight. + +Every Provider `ResourceApiBinding` MUST implement the exact base spec schema +version and fingerprint, accept the canonical minimal valid base Spec, and pass +base lifecycle/status/error/finalizer conformance. A Provider MAY reject an +optional base capability only through its signed standard capability matrix and +a typed provider-neutral `unsupported-capability` error; it MUST NOT ignore, +reinterpret, rename, duplicate, weaken, or require extension data for +base-required behavior. `spec.provider.settings` is strict deny-unknown, +bounded, schema-versioned and digested, validated against `spec.providerRef` at +Nix build and API admission, and fails with `spec-provider-schema-invalid` or +`spec-provider-shadow` when invalid or shadowing/restating/overriding/renaming/ +duplicating a base field. Shared semantics are promoted to the ResourceType base +and never live in `spec.provider`; generic CLI/controllers operate on base spec +plus base status. For the same Provider, the `spec.provider` and +`status.provider` schemas align. + +In Host, the no-isolation `isolationPosture` semantic is a promoted Host base +field at top-level `spec.isolationPosture`; it is never carried in the +`spec.provider` extension. + +## Authority and cardinality (D097) + +Host, Guest, Process, EphemeralProcess, and User carry D097 authority semantics +(schema in +[`ADR-046-resource-object-model` §Authority and cardinality](ADR-046-resource-object-model.md)): + +- **Host** is an `exactly-one` host-scoped substrate **allocator/effect + authority** (`authorityScope: host`, `arbitration: exclusive`, reconciled by + `Provider/system-core`); it is the single owner of host allocation/effect for + its node and is `exportability: forbidden`. +- **Guest** is `exactly-one` per Guest; **Process**/**EphemeralProcess** are + `bounded-many` partitioned workloads placed under a Host/Guest authority. +- **User** is a per-user (`authorityScope: user`, `exactly-one` per user) + authority; genuinely per-seat services (a Wayland portal, clipboard, or + notification sink) are `authorityScope: seat|user` authorities owned by the + relevant interaction Provider, not by this base type. +- Per-user/session Provider services (PipeWire mediator, Secret Service/keyring, + systemd user manager, and a **shell supervisor per `ShellSession`** — never a + global one) declare their qualified `AuthorityDescriptor` in the owning + Provider dossier; this base spec only declares the requested share mode and + cannot bypass the descriptor. + +Core's authority index rejects a second authority for the same +`(Zone/scope, authorityClass, opaqueKeyDigest)` with `duplicateConflict` before +any effect; restart adopts by `ownerProof` and ambiguity quarantines. + +### Fixed user-session authority (D097 desktop/session) + +Display portal, the audio mediator, the notification sink, clipboard, Secret +Service, and the shell all depend on a **fixed user-session authority** that +pre-opens the compositor, PipeWire, and session-bus FDs for a login session. +This authority is **named here, not left as ambient prose**: + +| Field | Value | +| --- | --- | +| `authorityScope` | `seat` (bound to a `Host` × `User` × login-session) | +| `authorityKey` | opaque class over `(Host, User, login-session/seat)` — never a raw socket path, XDG_RUNTIME_DIR, DISPLAY, or seat name | +| `cardinality` | `exactly-one` per `(Host, User, login-session/seat)` | +| `arbitration` | `exclusive` (the sole opener of the compositor/PipeWire/session-bus FDs) | +| Owner | a **core/user-agent session authority** (a per-user-session agent Process owned by `Provider/system-systemd` under the user's manager) — **not a new Provider** | +| Adoption | restart adopts by `ownerProof` (the agent Process identity + login-session id); ambiguity quarantines | +| Duplicate | a second session authority for the same `(Host, User, login-session)` is rejected with `duplicateConflict` before any FD open | +| `exportability` | `forbidden` (the session FDs never cross a Zone; desktop Providers receive them only via the D077 EffectPort/LaunchTicket) | + +Every desktop/session Provider service binds to this single authority and +receives its compositor/PipeWire/session-bus FD through the EffectPort/ +LaunchTicket; none opens the session FDs itself. + +**Desktop/session authority classification** (per-Provider detail stays in each +dossier, refined by evidence; owner/cardinality named here): + +| Class | Scope | Cardinality | Owner | Exportability | +| --- | --- | --- | --- | --- | +| Display controller + portal/login | zone | `exactly-one` per Zone | display Provider controller | forbidden | +| Compositor/session FD authority | seat | `exactly-one` per Host×User×session | core/user-agent session authority | forbidden | +| Clipboard host (`clipd-host`) | user | `exactly-one` per `User`; picker global-or-seat arbitration | clipboard Provider | policy-gated (default-denied) | +| Notification sink | user-session | `exactly-one` per User session | notification Provider | policy-gated (default-denied) | +| Audio authority (mediator) | seat/user | `exactly-one` per compositor user; **one `audio.d2bus.org.AudioBinding` per Guest** | audio Provider | explicit-export through the owner `audio.d2bus.org.AudioService` only (D096/D098) | +| systemd user manager | user | `exactly-one` per `User` × (`Host` or `Guest`) | `Provider/system-systemd` | forbidden | +| Entrablau login authority | guest | `exactly-one` per identity Guest/tenant | credential-entra (in the identity Guest) | forbidden (D093) | +| Secret Service / keyring | user-session | `exactly-one` per User session | credential-secret-service | forbidden | +| ShellPool / shell supervisor | shell-session | `exactly-one` per `ShellSession` (never global) | shell Provider | forbidden | +| Host input (`wl_seat`/pointer constraints) | seat | `at-most-one` per seat under the session authority | core/user-agent session authority | forbidden | + +**Host input boundary.** The `wl_seat`/pointer-constraint surface is an explicit +`seat`-scoped authority owned by the fixed user-session authority (`at-most-one` +per seat); until an interaction Provider implements pointer-constraint/relative +input enforcement, that enforcement is a **declared unsupported boundary** +(input is not silently multiplexed — a second seat-input claimant is a +`duplicateConflict`). + +**Admission conflict.** A second same-user Provider service or resource for any +`exactly-one` desktop authority (a second display portal, clipboard host, +notification sink, audio mediator, systemd user manager, Secret Service, or +session FD authority for the same `(Host, User, session)`) is rejected with +`duplicateConflict` naming the incumbent owner digest before any effect; +config activation goes `Degraded`. Multi-user/multi-seat is supported only up to +the **declared per-Host limit** (one authority per distinct `(Host, User, seat)` +tuple); anything beyond the declared limit is rejected. + +**Guest-stop invalidation.** Stopping a Guest invalidates every user-session +authority and lease bound to that Guest across display, audio, notification, +credential, and shell in one dependency-aware cascade (D091): the session FD +authority and each dependent desktop authority are drained/recycled, their +`Endpoint`s revoked, and any `AudioBinding`/projection-Service/lease degraded — no stale +compositor/PipeWire/session-bus FD survives a Guest stop. + +**Cross-Zone exportability (reconciled with D096).** Any prior claim of "no +cross-Zone sharing path" for desktop classes is superseded: D096 +`ResourceExport`/`ResourceImport` are the sole typed bridge. Per class: +**display, Host input, and Secret Service are non-exportable** (`forbidden`) — +they never cross a Zone; **audio is a policy-gated explicit export** (already +supported, D096); **clipboard and notifications are structurally supportable +exports but default-denied** (policy-gated, opt-in only). Credentials/secrets +remain non-exportable by default (D093). This is an explicit per-class decision, +not a blanket omission. + +## Shared field schemas + +### ResourceName constraints + +All resource names in this spec match `^[a-z][a-z0-9-]*$`. Length bounds: + +| Field | Minimum | Maximum | +| --- | --- | --- | +| `metadata.name` | 1 | 63 | +| `spec.displayName` (User) | 1 | 128 | + +### BudgetSpec + +Inline schema embedded in Host, Guest, Process, and EphemeralProcess. Fields +are all optional; omitting a field means no d2b-level enforcement for that +resource. Zone capacity policy may still enforce totals. + +```yaml +budget: + cpu: + request: "500m" # millicpus; "0m" = no reservation; max "1024000m" + limit: "2000m" # millicpus; null = no limit; max "1024000m" + memory: + request: "128Mi" # bytes with SI/IEC suffix; max "4Ti" + limit: "512Mi" # bytes with SI/IEC suffix; null = no limit + pids: + limit: 512 # integer [1, 65535]; null = no limit + fds: + limit: 1024 # integer [1, 1048576]; null = no limit + ioWeight: 100 # cgroup blkio relative weight [1, 10000] + networkEgressBps: null # bytes/s; null = no limit; max 10^12 + threadLimit: null # RLIMIT_NPROC equivalent; null = no limit +``` + +For Host and Guest, each field is the aggregate total across all Processes +placed on that execution target. For Process and EphemeralProcess, each field +applies to that single process's cgroup leaf. Process budget fields must not +exceed the Host or Guest total budget. Core rejects overcommit at spec +admission time using the aggregate reservation already committed in the store. + +### ExecutionPolicy + +Shared inline schema embedded in Host and Guest spec. All fields are optional +except `providerRef`. + +| Field | Type | Default | Bound | Description | +| --- | --- | --- | --- | --- | +| `providerRef` | ResourceRef | required | `Provider/` | The Provider owning this Host or Guest. Host must use `Provider/system-core`. Guest must use one of the four runtime Providers. | +| `defaultDomain` | `system\|user` | `system` | exact enum | Default Process domain when Process.domain is omitted. | +| `allowedDomains` | `[system\|user]` | `[system]` | 1..2 items, unique | Domains permitted for Processes under this Host or Guest. A Process with domain not in this list is rejected at spec admission. | +| `defaultUserRef` | ResourceRef? | `null` | `User/` in same Zone | Used when Process.domain=user and Process.userRef is absent. Required when `allowedDomains` contains `user`. | +| `budget` | BudgetSpec | `{}` | see BudgetSpec | Aggregate budget for all Processes under this execution target. | +| `networkAttachments` | NetworkAttachmentList | `[]` | 0..64 items | Ordered list of Zone Networks that Processes under this Host or Guest may attach to. Each entry: `{networkRef: Network/, default: bool}`. At most one entry may set `default: true`. | +| `deviceAttachments` | DeviceAttachmentList | `[]` | 0..64 items | Device refs available for Process device usage. Each entry: `{deviceRef: Device/, exclusive: bool}`. | +| `volumeAttachmentDefaults` | VolumeAttachmentDefaultList | `[]` | 0..64 items | Default Volume attachment settings propagated to Processes that reference listed volumes. | +| `provider` | object? | `null` | canonical `{schemaId,schemaVersion,settings}` | Optional selected-Provider extension envelope (D089). `settings` carries implementation-only desired settings validated against the selected Provider's exported Host or Guest schema (`.d2bus.org//spec`); strict deny-unknown, bounded. MUST NOT shadow or restate a base field. | + +NetworkAttachmentList entry: + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `networkRef` | ResourceRef | required | `Network/` in same Zone | +| `default` | bool | `false` | If true, this Network is the default for Processes that declare a network usage without an explicit networkRef | + +DeviceAttachmentList entry: + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `deviceRef` | ResourceRef | required | `Device/` in same Zone | +| `exclusive` | bool | `false` | If true, only one Process may hold this device at a time | + +### SandboxSpec + +Inline schema in Process and EphemeralProcess spec. Compiled by the selected +Process Provider to its implementation-specific form. No raw capability +numbers, seccomp BPF programs, minijail argument strings, or systemd unit +property fragments are accepted in spec. The Provider translates semantic +classes to exact implementation. + +| Field | Type | Default | Bound | Description | +| --- | --- | --- | --- | --- | +| `namespaceClasses` | `[NamespaceClass]` | `[]` | 0..8 items, unique | Namespace isolation requests. Empty means inherits all parent namespaces. | +| `capabilityClasses` | `[CapabilityClass]` | `[]` | 0..16 items, unique | Semantic capability grants. Empty means no capabilities beyond user-domain base set. | +| `seccompClass` | `strict\|permissive\|allow-all\|` | `strict` | max 64 chars | Seccomp policy class. `strict` = minimal allow-list for the process class. `permissive` = log-only. `allow-all` = no filter; requires explicit carve-out in the controlling Provider's descriptor. `` = named profile from the Provider's compiled seccomp catalog. | +| `noNewPrivileges` | bool | `true` | — | If true, sets PR_SET_NO_NEW_PRIVS before exec. Must be true when `startRoot` is false. | +| `startRoot` | bool | `false` | — | If true, process starts as the in-namespace root UID before privilege drop. Requires explicit Provider justification in its descriptor. Cannot be true for user-domain Processes. | +| `environmentClass` | `minimal\|safe-inherited\|provider-defined` | `minimal` | — | `minimal` = only the fixed approved environment set. `safe-inherited` = inherits the declared safe subset from the owning Provider's component. `provider-defined` = exact environment from Provider's component template. | +| `readOnlyRoot` | bool | `true` | — | If true, rootfs is mounted read-only. | +| `umask` | string? | `"0022"` | octal 3–4 digits | File-creation mask installed before exec. | +| `oomScoreAdj` | int | `0` | -1000..1000 | OOM score adjustment. | +| `userNamespace` | UserNamespaceSpec? | `null` | — | If set, the process's effect adapter pre-establishes a single-entry user namespace before exec. Required for virtiofsd-class processes per ADR 0021. | + +`NamespaceClass` enumeration: + +| Value | Linux namespace | +| --- | --- | +| `user` | CLONE_NEWUSER | +| `pid` | CLONE_NEWPID | +| `mount` | CLONE_NEWNS | +| `ipc` | CLONE_NEWIPC | +| `uts` | CLONE_NEWUTS | +| `network` | CLONE_NEWNET | +| `cgroup` | CLONE_NEWCGROUP | +| `time` | CLONE_NEWTIME | + +`CapabilityClass` enumeration (bounded; Provider adds no unlisted value without +a descriptor update): + +| Value | Linux capability(ies) | +| --- | --- | +| `network-bind` | CAP_NET_BIND_SERVICE | +| `network-raw` | CAP_NET_RAW | +| `network-admin` | CAP_NET_ADMIN | +| `sys-time` | CAP_SYS_TIME | +| `sys-ptrace` | CAP_SYS_PTRACE | +| `sys-admin` | CAP_SYS_ADMIN (requires explicit carve-out) | +| `dac-override` | CAP_DAC_OVERRIDE | +| `fowner` | CAP_FOWNER | +| `chown` | CAP_CHOWN | +| `setuid` | CAP_SETUID | +| `setgid` | CAP_SETGID | +| `audit-write` | CAP_AUDIT_WRITE | +| `kill` | CAP_KILL | + +`UserNamespaceSpec`: + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `mappingClass` | `MappingClass` | required | Semantic UID/GID mapping class. No numeric host UID/GID appears in the public spec. | + +`MappingClass` enumeration (bounded; frozen): + +| Value | Semantics | +| --- | --- | +| `process-principal-root` | Maps in-namespace UID/GID 0 to the host UID/GID of the Process's resolved principal — a stable `User/` resource identified by the Process's owning template/ownerRef. This is the ADR 0021 virtiofsd-class mapped-root pattern. | + +Core resolves the exact host UID/GID from the named principal and writes +`uid_map`/`gid_map` only into the private LaunchTicket/effect-adapter state at +launch time; the numeric values never appear in the public ResourceSpec, +status, audit, or API surface. The `user` NamespaceClass (CLONE_NEWUSER) is +unaffected by this change. + +### ExecutionSpec + +Shared inline fields embedded in both Process and EphemeralProcess spec. +All fields listed are part of the spec unless noted status-only. + +| Field | Type | Default | Bound | Description | +| --- | --- | --- | --- | --- | +| `providerRef` | ResourceRef | required | `Provider/system-systemd` or `Provider/system-minijail` | Selects the Process Provider implementation. Must be installed and Ready in this Zone. | +| `executionRef` | ResourceRef | required | `Host/` or `Guest/` in same Zone | Target Host or Guest. Process is placed and supervised on this execution target. | +| `domain` | `system\|user` | inherited | inherits from `executionRef` target's `defaultDomain` | Execution domain. Must be in `executionRef.allowedDomains`. Specifying `user` without `userRef` and without a `defaultUserRef` on the target is a spec validation error. | +| `userRef` | ResourceRef? | `null` | `User/` in same Zone | Required when `domain=user` and the target has no `defaultUserRef`, or to override the target default. Must resolve to a Ready User. | +| `processClass` | `controller\|service\|worker` | required | — | Process classification. `controller` = owns ResourceType reconcile loop via d2b-bus. `service` = serves typed ComponentSession methods; no reconcile ownership. `worker` = narrow Process with no controller/bus/dependency/CLI authority. | +| `template` | string | required | `^[a-z][a-z0-9-]*$`; max 63 chars | Plain component/process template ID. Resolved at runtime by the Provider registered as controller for the semantic owner resource identified by `metadata.ownerRef`. When `metadata.ownerRef` is absent, resolved through `spec.providerRef`. The controller maps this ID to an exact executable and content digest. `metadata.ownerRef` may be any resource type (Provider, Volume, Network, Device, or other); there is no restriction to Provider/ only. | +| `configRef` | ResourceRef? | `null` | `Volume/` in same Zone | Sealed config Volume mounted read-only at the Provider-declared config path. | +| `credentialRefs` | `[ResourceRef]` | `[]` | 0..16 items | Credential refs the process may obtain leases from. Each must be `Credential/` in same Zone. | +| `mounts` | `[MountSpec]` | `[]` | 0..64 items | Volume mounts declared by this process. | +| `sandbox` | SandboxSpec | `{}` | see SandboxSpec | Semantic sandbox requirements compiled by the selected Provider. | +| `budget` | BudgetSpec | `{}` | see BudgetSpec | Per-process resource limits. | +| `networkUsage` | NetworkUsageSpec? | `null` | — | Network access specification. | +| `deviceUsage` | `[DeviceUsageSpec]` | `[]` | 0..16 items | Device access specifications. | +| `telemetry` | TelemetrySpec | `{}` | — | Telemetry/observability bindings. | + +`ProcessSpec` carries **no** inline `endpoints` field (D092). A stable endpoint a +Process produces is a separate owned `Endpoint` resource (see § Endpoint +ResourceType) with `producerRef: Process/`; Core creates these from the +owning component descriptor's Endpoint templates, or the owning controller +creates them, and consumers reference `Endpoint/`. Per-connection or +high-churn carriage (named streams, `OwnedTransport` handles, inherited fds) +remains internal and is not an Endpoint. + +`MountSpec`: + +| Field | Type | Default | Bound | Description | +| --- | --- | --- | --- | --- | +| `volumeRef` | ResourceRef | required | `Volume/` in same Zone | Volume to mount. | +| `view` | string | required | max 63 chars | Named view declared by the Volume spec. | +| `mountPath` | string | required | absolute path; max 255 chars | Target path inside the process sandbox. | +| `access` | `read-only\|read-write` | `read-only` | — | Access level for this mount. | +| `required` | bool | `true` | — | If true, process start fails if Volume is not Ready. | + +`NetworkUsageSpec`: + +| Field | Type | Default | Bound | Description | +| --- | --- | --- | --- | --- | +| `networkRef` | ResourceRef? | `null` | `Network/` in same Zone | Named Network. Null uses the target's default network. | +| `ports` | `[PortSpec]` | `[]` | 0..256 items | Ports declared for inbound traffic. | +| `allowEgress` | bool | `false` | — | If true, the process may initiate outbound connections subject to Network policy. | + +`PortSpec`: + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `port` | u16 | required | Port number [1, 65535]. | +| `protocol` | `tcp\|udp\|sctp` | `tcp` | Transport protocol. | +| `purpose` | string | `""` | Stable bounded service label. Max 63 chars. | + +`DeviceUsageSpec`: + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `deviceRef` | ResourceRef | required | `Device/` in same Zone. | +| `access` | `shared\|exclusive` | `shared` | Exclusive requires the device's `exclusive` flag on the owning Host/Guest. | +| `purpose` | string | `""` | Bounded usage purpose. Max 63 chars. | + +`EndpointSpec` (folded): there is no inline `EndpointSpec` on `ProcessSpec`. +Stable endpoints are the `Endpoint` ResourceType below; a former inline endpoint +`{ name, transport, purpose, publicKey }` becomes an `Endpoint` resource with +`producerRef` set to the producing Process. Ports are declared in +`NetworkUsageSpec.ports`; telemetry bindings in `TelemetrySpec`. + +`TelemetrySpec`: + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `metricsEnabled` | bool | `true` | Export process metrics via the Zone observability Provider. | +| `tracingEnabled` | bool | `true` | Export traces. | +| `logLevel` | `off\|error\|warn\|info\|debug` | `info` | Log level hint. | +| `sensitiveLabels` | bool | `false` | If true, this process's telemetry is treated as sensitive and may be redacted in transit. | + +--- + +## Host + +### Purpose + +`Host` represents one physical or local execution, policy, and budget parent +in the Zone. `Provider/system-core` is the sole reconciler. A Zone may declare +several Hosts with different policies, domains, and budgets, all mapping to the +same underlying OS instance. + +The v3 successor to unsafe-local is a user-only Host (D042): +`defaultDomain=user`, `allowedDomains=[user]`, an explicit `defaultUserRef`. +Its explicit no-isolation posture is recorded in Host status and shown in +operator UI. + +### Spec schema + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Host +metadata: + name: host-system # required; ^[a-z][a-z0-9-]*$; max 63 + zone: dev # required; Zone self-name +spec: + providerRef: Provider/system-core # required; fixed value for Host + defaultDomain: system # system|user; default system + allowedDomains: [system, user] # [system|user]; 1..2 items, unique + defaultUserRef: null # User/; required if user in allowedDomains + budget: {} # BudgetSpec; aggregate for all Processes on this Host + networkAttachments: [] # 0..64 NetworkAttachmentList entries + deviceAttachments: [] # 0..64 DeviceAttachmentList entries + volumeAttachmentDefaults: [] # 0..64 VolumeAttachmentDefaultList entries + provider: + schemaId: system-core.d2bus.org/host-spec + schemaVersion: "1.0" + settings: {} # system-core Host extension schema; bounded +``` + +Full field table: + +| Field | Type | Required | Default | Bound | Description | +| --- | --- | --- | --- | --- | --- | +| `providerRef` | ResourceRef | yes | — | `Provider/system-core` exactly | Reconciled by Provider/system-core. Any other providerRef is a spec error. | +| `defaultDomain` | `system\|user` | no | `system` | — | Default domain for Processes targeting this Host. | +| `allowedDomains` | `[system\|user]` | no | `[system]` | 1..2, unique | Domains allowed. Processes with unlisted domain are rejected at admission. | +| `defaultUserRef` | ResourceRef? | conditional | `null` | `User/` | Required when `user` is in `allowedDomains`. | +| `budget` | BudgetSpec | no | `{}` | see BudgetSpec | Aggregate budget for Processes placed on this Host. | +| `networkAttachments` | list | no | `[]` | 0..64 | Network refs available to Processes on this Host. | +| `deviceAttachments` | list | no | `[]` | 0..64 | Device refs available to Processes on this Host. | +| `volumeAttachmentDefaults` | list | no | `[]` | 0..64 | Volume attachment defaults propagated to child Processes. | +| `isolationPosture` | string? | no | `null` | `null\|"none"` | Promoted Host base field (not a provider extension). `"none"` marks a user-only no-isolation Host and requires `defaultDomain=user`, `allowedDomains=["user"]`, and `defaultUserRef` set (and that tuple conversely requires `"none"`; `null` used to evade the no-isolation warning is rejected). Reflected in status. System processes are denied at admission when set to `"none"`. | +| `provider` | object? | no | `null` | canonical `{schemaId,schemaVersion,settings}` | Optional `Provider/system-core` extension envelope (D089), schema `system-core.d2bus.org/Host/spec`; see `spec.provider.settings` below. Strict deny-unknown; MUST NOT shadow a base field. | + +`spec.provider.settings` for `Provider/system-core` (schemaId `system-core.d2bus.org/Host/spec`, schemaVersion `1.0`): + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `kernelVersionMin` | string? | `null` | Minimum required kernel version string, e.g. `"6.1"`. Reconcile fails if running kernel is older. | +| `capabilities` | `[HostCapabilityClass]` | `[]` | Capabilities this Host is expected to expose. Reconcile reports status.capabilities against this claim. | + +The no-isolation `isolationPosture` field is a promoted Host base field at +top-level `spec.isolationPosture` (see the base spec table above); it is never a +`spec.provider.settings` field. + +`HostCapabilityClass` enumeration: + +| Value | Meaning | +| --- | --- | +| `kvm` | KVM hypervisor support | +| `pidfd` | pidfd_open(2) and CLONE_PIDFD kernel support | +| `cgroup-v2` | cgroup v2 delegation support | +| `user-namespace` | unprivileged user namespaces | +| `virtiofs` | virtiofsd/virtio-fs support | +| `audio-pipewire` | host PipeWire session manager running | +| `wayland` | host Wayland compositor socket present | +| `gpu-render` | render node present at /dev/dri/renderD* | +| `gpu-drm` | DRM primary node present | +| `tpm2` | TPM 2.0 device present | +| `usbip` | USBIP kernel module loadable | + +`Provider/system-minijail` has a mandatory platform floor independent of the +optional Host setting: Linux **5.14 or newer**, cgroup v2 delegation, and a +writable `cgroup.kill` file on a delegated test leaf. Linux 5.14 is required +because intentional teardown uses `cgroup.kill` rather than PID/PGID ownership. +When any Process selects system-minijail, Host reconciliation performs this +probe before placement; `kernelVersionMin = null` cannot waive it, while a +higher configured minimum still applies. An older kernel fails with +`kernel-too-old`; missing or unusable `cgroup.kill` fails with +`cgroup-kill-unavailable`. Both keep the Provider/placement not Ready and +launch zero processes. + +### Status schema + +#### Three-layer status shape (D088) + +D088 freezes `Host` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `Host`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +**D091 update currency.** Every Host, Guest, Process, EphemeralProcess, and User +resource includes universal `status.update` with `state` +(`Current|UpdateAvailable|UpgradeRequired|Upgrading|Blocked|Unknown`), `reasons` +(`CoreGenerationChanged|ProviderGenerationChanged|ArtifactChanged|ImageOrSystemGenerationChanged|SpecChanged|DependencyChanged|SecurityPolicyChanged`), +bounded non-secret observed/target generation and digest IDs, `disruption` +(`None|Reload|Restart|Recycle|Replace`), `preserveState`, optional +`operationId`, `lastAssessedAt`, and bounded/truncated `owned:{count,refs}` and +`dependencies:{count,refs}`. ResourceType-specific currency refinements live in +`status.resource` and never in `status.provider`; Core aggregates self, owned, +and dependency currency for list/get. Controllers set `status.update` via +`assess_update` on core/provider/artifact/image-or-system/spec/dependency/ +security-policy triggers and MUST report `UpgradeRequired` for disruptive +changes rather than applying them in place. For Host, Guest, and Process-family +resources, disruptive image/system generation, provider generation, or immutable +spec changes use disruption `Recycle`, `Restart`, or `Replace`; upgrades recycle +realization and owned ephemeral Processes/endpoints while preserving +durable/state Volumes and Guest identity where possible. + +```yaml +status: + observedGeneration: 1 + phase: Ready # Pending|Ready|Succeeded|Degraded|Failed|Deleted|Unknown; long-lived resources do not steadily use Succeeded; Deleted is a terminal event-only phase (row removed after emit) + conditions: [] + lastReconciledAt: "2026-07-22T00:00:01Z" + startedAt: "2026-07-22T00:00:00Z" + completedAt: null + outcome: null + # Host-specific: + capabilities: [] # HostCapabilityClass list observed on this host + kernelRelease: "" # observed uname -r output; bounded; redacted in audit + osName: "" # bounded OS identifier; max 128 chars + userManagerAvailable: false # true if systemd user manager is reachable + activePid: null # not exposed; see security rules + availableProcessSlots: null # null = unknown; integer count + isolationPosture: null # null for normal Hosts; "none" for user-only no-isolation Host + activeProcessCount: 0 # count of non-terminal Processes targeting this Host +``` + +Host-specific status fields: + +| Field | Type | Description | +| --- | --- | --- | +| `capabilities` | `[HostCapabilityClass]` | Capabilities actually observed and verified by system-core. May differ from spec claim. | +| `kernelRelease` | string | Observed kernel release string from uname(2). Bounded to 64 chars. Not included in audit payloads. | +| `osName` | string | Bounded OS identifier string from /etc/os-release NAME. Max 128 chars. | +| `userManagerAvailable` | bool | True when the configured `defaultUserRef`'s systemd user manager is reachable and healthy. | +| `isolationPosture` | string? | Reflects `spec.isolationPosture`. `"none"` when this is a user-only no-isolation Host. Shown in operator UI as explicit no-isolation warning. Null/absent for all other Hosts. | +| `activeProcessCount` | u32 | Count of non-terminal Process/EphemeralProcess resources targeting this Host. Informational only. | + +No PID, pidfd, socket path, internal node name, or raw host diagnostic is +exposed in public status or audit. + +### Conditions + +| Condition type | Ready = True when | Ready = False when | reason codes | +| --- | --- | --- | --- | +| `HostAvailable` | Host OS is reachable and matches `spec.provider.settings` | Host OS unreachable or kernel version requirement unmet | `host-unreachable`, `kernel-too-old`, `cgroup-unavailable` | +| `CapabilitiesVerified` | All `spec.provider.settings.capabilities` are observed | One or more claimed capabilities absent | `capability-absent-` | +| `UserManagerReady` | User domain allowed and user manager reachable | User manager not running or unreachable | `user-manager-unavailable`, `user-manager-unknown` | +| `PolicyValid` | spec fields pass admission invariants | allowedDomains/defaultUserRef inconsistency detected | `spec-invalid-domains`, `spec-missing-default-user-ref` | +| `BudgetAdmitted` | Aggregate budget within Zone capacity | Overcommit detected | `budget-overcommit` | + +Phase is `Ready` only when `HostAvailable` and `CapabilitiesVerified` are +True. `UserManagerReady` may be Degraded without affecting Ready for system- +only Hosts. `Failed` requires persistent unrecoverable error. + +### RBAC + +| Verb | Required rule | Restriction | +| --- | --- | --- | +| `get` | `{resourceTypes:[Host], verbs:[get]}` | Returns complete spec+status | +| `list` | `{resourceTypes:[Host], verbs:[list]}` | Bounded; snapshot revision returned | +| `watch` | `{resourceTypes:[Host], verbs:[watch]}` | Credit-bounded stream | +| `create` | `{resourceTypes:[Host], verbs:[create]}` | Provider/system-core bootstrap only during initial config; thereafter via config publication | +| `update-spec` | `{resourceTypes:[Host], verbs:[update-spec]}` | Config publication controller only | +| `update-status` | `{resourceTypes:[Host], verbs:[update-status]}` | Provider/system-core controller only, with expected revision and observedGeneration | +| `update-metadata` | `{resourceTypes:[Host], verbs:[update-metadata]}` | Bounded labels/annotations; no ownerRef changes | +| `delete` | `{resourceTypes:[Host], verbs:[delete]}` | Blocked while any non-terminal Process or EphemeralProcess targets this Host | + +Structural checks additionally require: +- `providerRef` must resolve to `Provider/system-core` and be Ready; +- `defaultUserRef` must resolve to a `User/` in the same Zone if set; +- `allowedDomains=[user]` and no `defaultUserRef` is rejected at admission; +- `isolationPosture = "none"` with `system` in `allowedDomains` is rejected. +- `allowedDomains = ["user"]` + `defaultDomain = "user"` + `defaultUserRef` set with `isolationPosture = null` is rejected (bidirectional). + +### Reconcile + +Provider/system-core reconcile loop for Host: + +1. Receive trigger: `spec-generation-changed`, `dependency-changed`, `startup-relist`, or `scheduled-observe`. +2. Read fresh spec snapshot. +3. Validate spec invariants (domains, defaultUserRef, budget, capabilities claim). +4. Probe local OS availability: `uname(2)`, cgroup v2 mount, user namespace + check, requested HostCapabilityClass probes. If system-minijail is installed + or selected by a targeting Process, also enforce Linux ≥5.14 and probe a + delegated leaf's writable `cgroup.kill`; failure is a platform-gate error, + not a feature downgrade. +5. If `allowedDomains` contains `user`: contact fixed user supervisor to verify `defaultUserRef`'s user manager availability. +6. Compute aggregate Budget reservation from all non-terminal Processes targeting this Host via `List(executionRef=Host/)`. +7. Check aggregate budget against Zone capacity policy. +8. Write Host status (capabilities, kernelRelease, osName, userManagerAvailable, activeProcessCount, conditions) via UpdateStatus with expected revision. +9. If spec-generation-changed and budget reduced: emit reconcile hints for non-terminal Processes over limit. +10. Return `converged` or `pending` with bounded requeue for next scheduled-observe. + +Reconcile must complete in <=5 s to avoid the controller health timeout. Kernel +probes use bounded timeouts. User manager check uses a bounded IPC timeout. + +### Finalize + +Host uses no finalizer from system-core. Deletion is blocked by structural +check when active Processes exist. Operators must delete all Processes targeting +the Host before deletion succeeds. Core emits `phase=Deleted` event and removes +the Host immediately after the structural check passes. + +--- + +## Guest + +### Purpose + +`Guest` represents one VM, sandbox, cloud, or remote execution parent in the +Zone. One of exactly four runtime Providers reconciles a Guest. The Guest +owns its child Process and EphemeralProcess resources and bootstrap +sub-resources. + +The four accepted runtime Providers (D043): + +| Provider | Execution substrate | +| --- | --- | +| `Provider/runtime-cloud-hypervisor` | Local NixOS VM via Cloud Hypervisor | +| `Provider/runtime-qemu-media` | Local QEMU media/physical-media VM | +| `Provider/runtime-azure-container-apps` | Azure Container Apps sandbox | +| `Provider/runtime-azure-virtual-machine` | Full-host Azure VM | + +### Spec schema + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Guest +metadata: + name: dev-vm + zone: dev +spec: + providerRef: Provider/runtime-cloud-hypervisor # required; one of four runtime Providers + defaultDomain: system + allowedDomains: [system, user] + defaultUserRef: null + systemArtifactId: null # artifact ID for the NixOS system closure; see d2b.artifacts catalog + budget: {} + networkAttachments: [] + deviceAttachments: [] + volumeAttachmentDefaults: [] + provider: + schemaId: runtime-cloud-hypervisor.d2bus.org/guest-spec + schemaVersion: "1.0" + settings: {} # selected runtime Provider's Guest schema extension +``` + +Full field table: + +| Field | Type | Required | Default | Bound | Description | +| --- | --- | --- | --- | --- | --- | +| `providerRef` | ResourceRef | yes | — | One of four runtime Provider refs | Owning runtime Provider. Must be installed and Ready. | +| `defaultDomain` | `system\|user` | no | `system` | — | Default Process domain. | +| `allowedDomains` | `[system\|user]` | no | `[system]` | 1..2, unique | Allowed domains. | +| `defaultUserRef` | ResourceRef? | conditional | `null` | `User/` | Required when `user` is in `allowedDomains`. | +| `budget` | BudgetSpec | no | `{}` | see BudgetSpec | Aggregate budget for all Processes on this Guest. | +| `networkAttachments` | list | no | `[]` | 0..64 | Networks available to Processes on this Guest. | +| `deviceAttachments` | list | no | `[]` | 0..64 | Devices available to Processes on this Guest. | +| `volumeAttachmentDefaults` | list | no | `[]` | 0..64 | Volume attachment defaults. | +| `systemArtifactId` | string? | no | `null` | `^[a-z][a-z0-9-]*$`; max 63 chars | Artifact ID for the NixOS system closure (kernel + initrd + rootfs). Must exist in `d2b.artifacts` with `type="nixos-system"`. Used by local VM Providers (e.g. `runtime-cloud-hypervisor`). `null` for cloud/remote Providers that do not boot a Nix-built system. | +| `provider` | object? | no | `null` | canonical `{schemaId,schemaVersion,settings}` | Optional selected-Provider extension envelope (D089). `settings` carries Provider-specific boot/identity/runtime settings validated against the selected runtime Provider's exported Guest schema (`.d2bus.org/Guest/spec`). Strict deny-unknown; MUST NOT shadow a base field. | + +`spec.provider.settings` is the primary extension point for Provider-specific +behavior. Each runtime Provider exports its Guest schema extension through its +`ResourceApiExport`. Common fields across all four providers (informative; +exact fields owned by each Provider's dossier): + +| Provider (schemaId) | Typical `spec.provider.settings` fields | +| --- | --- | +| `runtime-cloud-hypervisor` (`runtime-cloud-hypervisor.d2bus.org/Guest/spec`) | `vcpus`, `memoryMb`, `cmdline`, `vsockCid`, `machineType`, `consoleType`, `serialPort`, `pvpanic` | +| `runtime-qemu-media` (`runtime-qemu-media.d2bus.org/Guest/spec`) | `mediaSourceRef`, `mediaFormat`, `vcpus`, `memoryMb`, `machineType`, `displayOutput` | +| `runtime-azure-container-apps` (`runtime-azure-container-apps.d2bus.org/Guest/spec`) | `containerGroup`, `environmentId`, `revision`, `minReplicas`, `maxReplicas` | +| `runtime-azure-virtual-machine` (`runtime-azure-virtual-machine.d2bus.org/Guest/spec`) | `vmSize`, `imageRef`, `diskSku`, `adminUser`, `publicKeyRef`, `region` | + +This spec does not define the exact `spec.provider.settings` field list for each +runtime Provider. That is owned by each Provider's dossier spec. API binding +rejects unknown `spec.provider.settings` fields not declared by the +installed Provider's schema. + +### Status schema + +#### Three-layer status shape (D088) + +D088 freezes `Guest` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `Guest`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +`Guest` has multiple runtime implementations (`runtime-cloud-hypervisor`, +`runtime-qemu-media`, `runtime-azure-container-apps`, and +`runtime-azure-virtual-machine`). Runtime readiness, capability, observed +lifecycle phase, bootstrap readiness, identity-digest, and active-process +observations are frozen in `status.resource` and MUST be identical across all +implementations. Implementation-specific observation belongs only in that +implementation's `status.provider.details`; shared fields MUST NOT be duplicated +there. + +```yaml +status: + observedGeneration: 1 + phase: Ready # Pending|Ready|Succeeded|Degraded|Failed|Deleted|Unknown; long-lived resources do not steadily use Succeeded; Deleted is a terminal event-only phase (row removed after emit) + conditions: [] + lastReconciledAt: "2026-07-22T00:00:01Z" + startedAt: "2026-07-22T00:00:00Z" + completedAt: null + outcome: null + # Guest-specific: + providerPhase: "" # bounded provider-specific lifecycle phase; max 64 chars + bootstrapReady: false + guestIdentityDigest: "" # opaque bounded digest of verified guest identity; max 128 chars + activeProcessCount: 0 +``` + +Guest-specific status fields: + +| Field | Type | Description | +| --- | --- | --- | +| `providerPhase` | string | Bounded provider-specific phase label, e.g. `"starting"`, `"running"`, `"stopped"`. Max 64 chars. Stable lower-kebab-case value. | +| `bootstrapReady` | bool | True when the Guest's bootstrap sub-resources (first VMM Process, guest-control session, etc.) have reached Ready/Succeeded phase. | +| `guestIdentityDigest` | string | Opaque bounded hex digest of the verified guest identity material (boot token, generation, provider-specific). Max 128 chars. Not the guest IP/hostname. | +| `activeProcessCount` | u32 | Count of non-terminal Process/EphemeralProcess resources targeting this Guest. | + +No CID, VSOCK address, SSH host, IP address, container name, Azure resource +path, or raw provider diagnostic is public status. Bounded `guestIdentityDigest` +and `providerPhase` are the only provider-observable identity fields. + +### Conditions + +| Condition type | Ready = True when | Ready = False when | reason codes | +| --- | --- | --- | --- | +| `GuestProvisioned` | Provider has provisioned the execution substrate | Substrate not provisioned or provider error | `provider-error`, `quota-exceeded`, `substrate-unavailable` | +| `BootstrapReady` | All required bootstrap sub-resources are Ready | One or more bootstrap resources Pending/Failed | `bootstrap-process-failed`, `bootstrap-process-pending` | +| `GuestReachable` | Guest passes the runtime Provider's authenticated health check | Health check failed or timed out | `health-check-failed`, `health-check-timeout` | +| `CapabilitiesVerified` | All declared device/network attachments reachable | Attachment missing or failed | `device-attachment-failed`, `network-attachment-failed` | +| `PolicyValid` | spec passes admission invariants | allowedDomains/defaultUserRef/`spec.provider.settings` error | `spec-invalid-domains`, `spec-provider-schema-invalid` | +| `BudgetAdmitted` | Aggregate budget within Zone capacity | Overcommit | `budget-overcommit` | + +Phase is `Ready` only when `GuestProvisioned`, `BootstrapReady`, and +`GuestReachable` are all True. + +### RBAC + +| Verb | Required rule | Restriction | +| --- | --- | --- | +| `get` | `{resourceTypes:[Guest], verbs:[get]}` | Returns spec+status | +| `list` | `{resourceTypes:[Guest], verbs:[list]}` | Bounded snapshot | +| `watch` | `{resourceTypes:[Guest], verbs:[watch]}` | Credit-bounded | +| `create` | `{resourceTypes:[Guest], verbs:[create]}` | Config publication controller; runtime Provider bootstrap | +| `update-spec` | `{resourceTypes:[Guest], verbs:[update-spec]}` | Config publication controller | +| `update-status` | `{resourceTypes:[Guest], verbs:[update-status]}` | Owning runtime Provider controller only | +| `delete` | `{resourceTypes:[Guest], verbs:[delete]}` | Blocked while non-terminal Processes target this Guest | + +Structural checks additionally require: +- `providerRef` must resolve to one of the four accepted runtime Providers; +- the runtime Provider must be installed and Ready; +- `defaultUserRef` required when `allowedDomains` contains `user`. + +### Reconcile + +Each runtime Provider controller instance runs the reconcile loop for its +Guests. Concrete reconcile behavior is defined in each Provider's dossier. The +common contract: + +1. Receive trigger. +2. Read fresh Guest spec snapshot plus owned children snapshot. +3. Validate spec invariants and `spec.provider.settings` against the Provider schema. +4. Assert or create the required owned child Process/EphemeralProcess bootstrap + graph (VMM, virtiofsd, guest-control, etc.) using the owner-child mechanism. +5. Wait for bootstrap children to reach Ready. +6. Run the Provider's authenticated health check against the Guest. +7. Write Guest status (providerPhase, bootstrapReady, guestIdentityDigest, + conditions) via UpdateStatus with expected revision. +8. Return `converged`, `pending`, or `failed-retryable` per Provider policy. + +The reconcile loop must not block the controller-wide queue on guest +provisioning. Long-running effects use async tasks; status is written +asynchronously with expected-revision commits. + +### Finalize + +The runtime Provider controller owns one finalizer: `runtime./guest`. + +Finalizer algorithm on `deletion-requested`: + +1. Mark Guest as draining; stop admitting new Processes. +2. Delete all owned child Processes and EphemeralProcesses child-first per the + owner graph, respecting their finalizers. +3. Ask the runtime Provider to deprovision the execution substrate. +4. Verify deprovisioning completion with bounded timeout. +5. Clear the `runtime./guest` finalizer. +6. Return `finalized` after all children are deleted and substrate is gone. + +If deprovisioning is ambiguous (network outage, partial state), the finalizer +returns `blocked` with a typed condition until the operator resolves or a +bounded timeout expires, at which point it returns `failed-terminal` with an +explicit audit record. + +--- + +## Process + +### Purpose + +`Process` is a long-lived supervised process. One installed Process Provider +(`Provider/system-systemd` or `Provider/system-minijail`) manages its full +lifecycle: start, readiness, health, restart, adoption, and stop. Every +Process has exactly one executionRef (Host or Guest) and one process domain +(system or user). + +### Spec schema + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: wayland-proxy + zone: dev + ownerRef: Provider/display-wayland # owning Provider; template is its signed component ID +spec: + # ExecutionSpec common fields: + providerRef: Provider/system-systemd + executionRef: Host/host-system # or Guest/ + domain: system # system|user; default from executionRef + userRef: null # User/; required when domain=user without defaultUserRef + processClass: service # controller|service|worker + template: wayland-proxy-main # plain ID within owning Provider's component descriptor + configRef: null + credentialRefs: [] + mounts: [] + sandbox: {} + budget: {} + networkUsage: null + deviceUsage: [] + telemetry: {} + # Process-specific fields: + desiredLifecycle: running # running|stopped + restartPolicy: + class: on-failure # never|always|on-failure|on-crash + backoffBase: "1s" + backoffMax: "60s" + backoffMultiplier: 2.0 + maxRestarts: null # null = unlimited; integer [1, 65535] + resetAfter: "300s" + readiness: + initialDelay: "0s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + class: ready-condition # ready-condition|provider-defined + healthCheck: + enabled: false + interval: "30s" + timeout: "5s" + failureThreshold: 3 + class: provider-defined + adoptionPolicy: adopt-on-restart # adopt-on-restart|never-adopt + drainTimeout: "30s" +``` + +Process-specific fields: + +| Field | Type | Required | Default | Bound | Description | +| --- | --- | --- | --- | --- | --- | +| `desiredLifecycle` | `running\|stopped` | no | `running` | — | Desired steady-state lifecycle. `stopped` means the controller will not start the process but retains the resource and all status/finalizers. | +| `restartPolicy` | RestartPolicySpec | no | see below | — | Restart/backoff behavior. | +| `readiness` | ReadinessSpec | no | see below | — | Readiness probe settings. | +| `healthCheck` | HealthCheckSpec | no | disabled | — | Health check settings. | +| `adoptionPolicy` | `adopt-on-restart\|never-adopt` | no | `adopt-on-restart` | — | Whether the controller attempts to adopt a running process after controller restart. | +| `drainTimeout` | duration string | no | `"30s"` | `"0s".."3600s"` | Time to wait after exact-main SIGTERM before an unambiguous system-minijail subtree is terminated through its cgroup v2 `cgroup.kill`; no PGID fallback. | + +`RestartPolicySpec`: + +| Field | Type | Default | Bound | Description | +| --- | --- | --- | --- | --- | +| `class` | `never\|always\|on-failure\|on-crash` | `on-failure` | — | `never` = no restart. `always` = restart on any exit. `on-failure` = restart on non-zero exit. `on-crash` = restart only on signal/crash (not clean non-zero). | +| `backoffBase` | duration string | `"1s"` | `"0s".."60s"` | Initial backoff after first restart. | +| `backoffMax` | duration string | `"60s"` | `"1s".."3600s"` | Maximum backoff cap. | +| `backoffMultiplier` | float | `2.0` | `1.0..10.0` | Exponential backoff multiplier. | +| `maxRestarts` | u32? | `null` | `1..65535\|null` | Null means unlimited. When exceeded, phase becomes `Failed`. | +| `resetAfter` | duration string | `"300s"` | `"0s".."86400s"` | If process stays Running for this duration, restart counter resets to 0. | + +`ReadinessSpec`: + +| Field | Type | Default | Bound | Description | +| --- | --- | --- | --- | --- | +| `initialDelay` | duration string | `"0s"` | `"0s".."300s"` | Delay before first readiness probe after start. | +| `timeout` | duration string | `"30s"` | `"1s".."300s"` | Total readiness wait timeout from process start. | +| `failureThreshold` | u32 | `3` | `1..100` | Consecutive failures before marking not-ready. | +| `successThreshold` | u32 | `1` | `1..100` | Consecutive successes to declare ready from not-ready. | +| `class` | `ready-condition\|provider-defined` | `ready-condition` | — | `ready-condition` = process is ready when Ready condition becomes True. `provider-defined` = Provider uses a named readiness mechanism from the process template. | + +`HealthCheckSpec`: + +| Field | Type | Default | Bound | Description | +| --- | --- | --- | --- | --- | +| `enabled` | bool | `false` | — | Whether ongoing health checks run after readiness. | +| `interval` | duration string | `"30s"` | `"1s".."3600s"` | Interval between health checks. | +| `timeout` | duration string | `"5s"` | `"1s".."60s"` | Single health check timeout. | +| `failureThreshold` | u32 | `3` | `1..100` | Consecutive failures before Degraded/Failed. | +| `class` | `provider-defined` | `provider-defined` | — | Health check mechanism defined by the Provider template. | + +### Status schema + +#### Three-layer status shape (D088) + +D088 freezes `Process` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `Process`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +```yaml +status: + observedGeneration: 1 + phase: Ready # Pending|Ready|Succeeded|Degraded|Failed|Deleted|Unknown; long-lived resources do not steadily use Succeeded; Deleted is a terminal event-only phase (row removed after emit) + conditions: [] + lastReconciledAt: "2026-07-22T00:00:01Z" + startedAt: "2026-07-22T00:00:00Z" + completedAt: null + outcome: null + # Process-specific: + providerImplementation: "" # bounded process Provider name; max 64 chars + processIdentityDigest: "" # opaque digest of verified process identity; max 128 chars + waitReapOwner: "" # "d2b" or "systemd"; who owns wait/reap + executionRef: "" # resolved executionRef at last reconcile + domain: "" # system|user at last reconcile + userRef: null # resolved userRef, if any + configRevisionDigest: null # digest of sealed config at last start; max 128 chars + sandboxRevisionDigest: null # digest of compiled sandbox at last start; max 128 chars + lastExitClass: null # clean-exit|crash|signal|timeout|unknown + restartCount: 0 # total restart count since resource creation + lastRestartAt: null # RFC 3339 UTC + adoptionState: null # adopted|fresh|quarantined|adoption-failed +``` + +Process-specific status fields: + +| Field | Type | Description | +| --- | --- | --- | +| `providerImplementation` | string | Stable Provider name that started this process (e.g., `system-systemd`). Max 64 chars. | +| `processIdentityDigest` | string | Opaque hex digest of the verified process identity material at last start (executable hash, template generation, cgroup path digest). Max 128 chars. Not the PID. | +| `waitReapOwner` | string | `"d2b"` for system-minijail means the privileged broker that called `clone3` and is the child parent; `"systemd"` for system-systemd. It never means the non-parent Provider controller. | +| `executionRef` | string | The resolved executionRef at last reconcile start. | +| `domain` | string | The resolved domain (`system` or `user`) at last reconcile. | +| `userRef` | string? | The resolved userRef used at last reconcile, if applicable. | +| `configRevisionDigest` | string? | Bounded hex digest of sealed config at last launch. | +| `sandboxRevisionDigest` | string? | Bounded hex digest of compiled sandbox at last launch. | +| `lastExitClass` | string? | Stable exit classification: `clean-exit`, `crash`, `signal`, `timeout`, `unknown`. | +| `restartCount` | u32 | Total restart count. | +| `lastRestartAt` | string? | RFC 3339 UTC timestamp of last restart. | +| `adoptionState` | string? | `adopted`: previously running process re-adopted after controller restart. `fresh`: new start. `quarantined`: ambiguous prior identity; process not adopted, not killed. `adoption-failed`: adoption attempted but failed; process stopped. | + +No PID, pidfd file descriptor number, cgroup path, socket path, argv, raw +environment, systemd unit name, terminal bytes, or raw Provider diagnostic is +ever public Process status or audit payload. + +### Conditions + +| Condition type | Ready = True when | reason codes | +| --- | --- | --- | +| `Scheduled` | Process is assigned to a running controller instance | `controller-unavailable`, `no-available-controller` | +| `ProviderReady` | Process Provider (system-systemd/system-minijail) is installed and Ready | `provider-unavailable`, `provider-generation-mismatch` | +| `ExecutionReady` | executionRef target (Host or Guest) is in Ready phase | `execution-pending`, `execution-failed`, `execution-unknown` | +| `UserReady` | userRef (if required) resolves to a Ready User | `user-not-found`, `user-not-ready`, `user-manager-unavailable` | +| `DependenciesReady` | All required Volume/Network/Device dependencies are Ready | `volume-not-ready`, `network-not-ready`, `device-not-ready` | +| `Launching` | Process start attempt is underway | `launch-pending` | +| `Ready` | Process is running and has passed readiness check | `process-starting`, `process-crashed`, `process-timed-out`, `readiness-timeout` | +| `Healthy` | Most recent health check passed (if enabled) | `health-check-failed`, `health-check-timeout` | +| `Adopted` | Prior running process successfully adopted | `adoption-ambiguous`, `adoption-identity-mismatch`, `adoption-failed` | + +### Pidfd rules + +These rules are invariant for all Process Providers. Violation is a +`runtime-security-violation` audit event and the process is quarantined. + +1. Every launched process has a local verified pidfd acquired by + its lifecycle owner immediately after exec/launch; the Provider controller + receives only an opaque identity/lease handle. +2. Pidfd is acquired only after ProviderSupervisor verifies the process's + stable identity: executable hash, template generation, cgroup/scope + placement, and provider-specific identity attributes. Any mismatch before + pidfd open quarantines rather than adopts. +3. Pidfd is never serialized to disk, never written to the resource store, + never sent over d2b-bus, and never exposed in public status or API. No + Process Provider controller ever holds or imports a raw pidfd; it holds + only the opaque handle ProviderSupervisor returns. +4. A ProviderSupervisor-held duplicate is closed and reacquired (with full + re-verification) after every ProviderSupervisor restart. A Provider + controller restart transfers no raw fd. For system-minijail, the still-live + broker parent retains its original pidfd and wait/reap record until it reaps + the child. +5. On adoption after restart, ProviderSupervisor locates the candidate + process through the cgroup leaf, verifies all stable identity fields + against the stored processIdentityDigest, and only then obtains a fresh + verified pidfd handle and reports the outcome to the controller. For + system-minijail, the original broker parent supplies a fresh duplicate; for + system-systemd, ProviderSupervisor uses `pidfd_open(2)`. + Ambiguous identity → `adoptionState: quarantined`. +6. For system-minijail: the broker calls + `clone3(CLONE_PIDFD | CLONE_INTO_CGROUP)`, obtains the pidfd atomically, and + remains the child's kernel parent. It alone calls `waitid(P_PIDFD, ...)`, + collects exit status, and reaps. It may provide ProviderSupervisor a + duplicate through a private local attachment for polling and signaling; the + Provider controller never receives the raw fd. +7. For system-systemd: ProviderSupervisor reads the unit's MainPID after + InvocationID + cgroup + start-time verification and calls `pidfd_open(2)` + on that PID. systemd performs wait/reap. +8. A non-parent ProviderSupervisor may poll pidfd readability, but readability + is only a terminal-liveness hint: it is not `waitid`, reap, or exit-status + collection. For system-minijail, only the identity-bound terminal result + relayed after the broker parent's successful wait/reap supplies + `lastExitClass` or `outcome.exitCode`. +9. Any holder of a currently verified pidfd may use `pidfd_send_signal` for the + exact main process; this syscall does not require parenthood. The Provider + controller requests the effect through its opaque handle. The PID is never + written to logs, status, audit, or metrics. +10. ProviderSupervisor maintains no PID/PGID ownership record and never sends + descendant SIGKILL by process group. An unambiguous system-minijail subtree + is terminated only by writing `1` to its anchored cgroup v2 `cgroup.kill` + after the graceful exact-main signal/deadline. + +### system-systemd conformance + +A process launched under `Provider/system-systemd` must satisfy all of: + +- Started as a non-forking transient systemd service or scope. +- Unit type must be `Type=exec` (or equivalent non-daemonizing `Type=simple` + where `Type=exec` is not available). Daemonizing (`Type=forking`) is + forbidden. +- ProviderSupervisor binds InvocationID, cgroup path, MainPID, and + ExecMainStartTimestamp atomically from the unit's active state before + opening pidfd, on behalf of the `system-systemd` Provider controller. The + Provider controller itself never opens a systemd D-Bus connection or a + pidfd directly. +- pidfd is opened from `MainPID` only after all four binding checks pass. +- systemd performs wait/reap. ProviderSupervisor does NOT call `waitpid`. It + monitors process exit via the unit's active state transitions and reports + transitions to the controller. +- No per-Provider static PID1 template unit may be used. All units are + transient; they exist only while the Process resource is non-terminal. +- For user domain: a transient user scope is used via the fixed user supervisor. + The exact authenticated `userRef` UID is verified by the user supervisor + before creating the scope. +- Unit name alone is never treated as identity. Identity requires InvocationID + + cgroup + MainPID + start-time tuple. +- On adoption after restart: ProviderSupervisor rediscovers the live unit by + cgroup path, re-checks InvocationID, cgroup, MainPID, start-time against + stored processIdentityDigest. Mismatch → quarantine. + +### system-minijail conformance + +A process launched under `Provider/system-minijail` must satisfy all of: + +- The `system-minijail` Provider controller never imports or calls the broker + itself. It resolves a `ProcessLaunchEffectPort.spawn` call with the opaque + LaunchTicket; ProviderSupervisor dispatches that call to the privileged + broker via `clone3(CLONE_PIDFD | CLONE_INTO_CGROUP)`, placing the process + directly into its declared cgroup leaf. +- The process is born in its declared cgroup before any instruction executes. +- The broker that called `clone3` remains the process's parent and alone + performs `waitid(P_PIDFD)`, exit-status collection, and reap via its retained + pidfd. ProviderSupervisor may poll a verified duplicate for readability and + use it for exact-main `pidfd_send_signal`, but polling is not wait/reap and + cannot produce exit status. ProviderSupervisor relays only the broker's typed + terminal result and adoption state back to the controller. +- The SandboxSpec's `namespaceClasses`, `capabilityClasses`, `seccompClass`, + and `userNamespace` are compiled by the broker from the trusted bundle + into a minijail/seccomp/namespace plan. The compiled plan digest is stored + in `sandboxRevisionDigest`. +- The broker verifies: executable path/hash, template generation, declared UID/GID, + compiled sandbox plan digest, and cgroup placement before exec. +- No environment variable, mount, or path from the caller resource payload + reaches exec without passing through the trusted bundle compilation step. +- For user namespace processes (virtiofsd class): the broker pre-establishes + the user namespace with `clone3(CLONE_NEWUSER)` + pipe sync + uid_map/gid_map + writes before the process's first instruction. `UserNamespaceSpec.mappingClass: + process-principal-root` resolves, only inside this private effect-adapter + state, to the host UID/GID mapped to in-namespace UID/GID 0. +- On adoption after restart: ProviderSupervisor locates the process by cgroup + leaf and verifies cgroup/PID/start-time/executable identity against + processIdentityDigest, verifies the original broker-parent record, and + obtains a fresh duplicate from that broker. Ambiguity or lost parent + ownership → quarantine, never broad kill. +- Intentional stop/finalize sends SIGTERM to the exact main process, waits the + bounded grace, then has the broker write `1` to the anchored leaf's + `cgroup.kill`; it waits for broker-reaped main status and + `cgroup.events: populated 0` before rmdir. There is no PID/PGID SIGKILL + fallback. +- Linux ≥5.14 plus writable delegated-leaf `cgroup.kill` is a mandatory + placement gate, not an optional capability downgrade. + +### Fast path contract (D030) + +After a Process resource is durably committed with all dependencies Ready: + +- The store post-commit dispatcher emits a matching controller hint immediately + after durable commit returns. +- p95 from durable commit to controller handler start: <=5 ms. +- p95 from durable commit to launch attempt start: <=20 ms. +- The controller launches the process in an independent async task without + blocking the watch loop. +- The watch loop dispatches the next independent Process immediately. +- Status transitions (Queued → Launching → Ready) are async expected-revision + writes; they do not hold the watch loop. +- Only per-resource ordering, explicit dependencies, declared concurrency + limits, or active backpressure may delay dispatch. There is no polling + interval, debounce window, or artificial delay. + +### RBAC + +| Verb | Required rule | Restriction | +| --- | --- | --- | +| `get` | `{resourceTypes:[Process], verbs:[get]}` | — | +| `list` | `{resourceTypes:[Process], verbs:[list]}` | — | +| `watch` | `{resourceTypes:[Process], verbs:[watch]}` | — | +| `create` | `{resourceTypes:[Process], verbs:[create], executionRefs:[Host/host-system]}` | Subject must have permission on the executionRef target | +| `update-spec` | `{resourceTypes:[Process], verbs:[update-spec]}` | Config publication or owning Provider controller | +| `update-status` | `{resourceTypes:[Process], verbs:[update-status]}` | Owning Process Provider controller only | +| `delete` | `{resourceTypes:[Process], verbs:[delete]}` | Blocked while finalizers exist | + +Structural checks also enforce: +- `executionRef` must resolve to a Ready Host or Guest in the same Zone; +- `providerRef` must be `system-systemd` or `system-minijail` and be Ready; +- `domain` must be in `executionRef.allowedDomains`; +- `userRef` required when `domain=user` and no `defaultUserRef` on target; +- when `metadata.ownerRef` is set, the referenced resource must exist and its UID must be bound (no-cycle rule applies); owner phase (Ready/Degraded/Failed/etc.) does not block admission; +- budget fields must not exceed executionRef aggregate budget remaining. + +### Reconcile + +Process Provider reconcile loop: + +1. Receive trigger (spec-generation-changed, execution-status-changed, owned-resource-changed, retry-due, etc.). +2. Validate ExecutionSpec fields: executionRef, domain, userRef, providerRef, template, sandbox, budget, mounts, dependencies. +3. Compile sandbox from SandboxSpec to provider-specific plan; compute sandboxRevisionDigest. +4. Compile all mounts from MountSpec array; verify all volumeRef targets are Ready. +5. If desiredLifecycle=stopped: stop any running process through the same + provider-specific intentional-stop contract used by Finalize (including + system-minijail `cgroup.kill` and broker wait/reap proofs); write + Pending+stopped status; return converged. +6. If no running process: dispatch LaunchTicket to ProviderSupervisor asynchronously; write Launching condition. +7. ProviderSupervisor: verify ticket/resource/controller lease; spawn process via broker or systemd; obtain pidfd; return identity digest. +8. Record processIdentityDigest in status; write Ready condition via UpdateStatus. +9. If health check enabled: schedule periodic health checks. +10. On exit: classify exit (clean/crash/signal/timeout); apply restartPolicy; if restart, re-enter at step 6 with backoff; if maxRestarts exceeded, write Failed phase. +11. On execution-status-changed from Host/Guest: if executionRef becomes non-Ready, write Unknown condition; queue observation. + +### Finalize + +Owning Provider controller registers finalizer `process./cleanup`. + +Finalizer algorithm on `deletion-requested`: + +1. Reverify the exact process/unit and its owned cgroup leaf. Ambiguity takes + the quarantine path without a signal or subtree kill. +2. Signal the exact main process with SIGTERM and wait `drainTimeout`. + A verified pidfd holder may use `pidfd_send_signal`; parenthood is not + required. systemd-owned processes use the verified unit stop path. +3. For system-minijail, after main exit or the grace deadline, have the original + broker parent write `1` to the anchored leaf's cgroup v2 `cgroup.kill`. This + mandatory subtree operation replaces PID/PGID SIGKILL ownership and catches + descendants that called `setsid(2)`. +4. Confirm system-minijail main exit only from the broker parent's relayed + `waitid(P_PIDFD, ...)` result; pidfd poll readability is not proof. Also wait + for `cgroup.events` to report `populated 0`. For system-systemd, require the + verified unit's terminal transition and manager-owned subtree drain. +5. Release cgroup leaf (system-minijail) or unit (system-systemd) only after the + applicable exit/subtree proofs. +6. Release any OFD locks/leases from this process. +7. Clear finalizer and return `finalized`. + +On ambiguous state (pidfd closed, broker-parent ownership lost, cgroup identity +mismatch, `cgroup.kill` unavailable/failing, leaf still populated, or systemd +unit gone without a verified terminal transition), retain the finalizer and +write `Degraded`/`Unknown` with `process-exit-unconfirmed`. No broad kill, +signal, or `cgroup.kill` write targets an ambiguously owned candidate. A +success-shaped `finalized` result without the required proofs is prohibited. + +--- + +## EphemeralProcess + +### Purpose + +`EphemeralProcess` is a one-shot asynchronous process that runs to terminal +state, retains its result for a configurable TTL, and is then cleaned up. +It shares the full `ExecutionSpec` with Process and adds one-shot-specific +fields. It does not reference or create a Process child. + +### Spec schema (delta from ExecutionSpec) + +```yaml +apiVersion: resources.d2bus.org/v3 +type: EphemeralProcess +metadata: + name: swtpm-pre-start-flush-abc123 + zone: dev + ownerRef: Provider/device-tpm # owning Provider; template is its signed component ID +spec: + # All ExecutionSpec fields (same as Process): + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + userRef: null + processClass: worker + template: swtpm-flush # plain ID within owning Provider's component descriptor + configRef: null + credentialRefs: [] + mounts: [] + sandbox: {} + budget: {} + networkUsage: null + deviceUsage: [] + telemetry: {} + # EphemeralProcess-specific fields: + startDeadline: "60s" # max time from spec commit to process start; default 60s + runtimeDeadline: "300s" # max process runtime after start; default 300s + successfulTtl: "1h" # retention after Succeeded; default 1h; D034 + failedTtl: "24h" # retention after Failed; default 24h; D034 + incidentHold: false # if true, cleanup is blocked pending explicit release +``` + +EphemeralProcess-specific fields: + +| Field | Type | Required | Default | Bound | Description | +| --- | --- | --- | --- | --- | --- | +| `startDeadline` | duration string | no | `"60s"` | `"1s".."3600s"` | Maximum time from spec commit to process start. Expiry moves phase to Failed with `reason: start-deadline-exceeded`. | +| `runtimeDeadline` | duration string | no | `"300s"` | `"1s".."86400s"` | Maximum process wall-clock runtime after start. For system-minijail, expiry uses exact-main SIGTERM, bounded grace, then anchored leaf `cgroup.kill`; no PGID fallback. | +| `successfulTtl` | duration string | no | `"1h"` | `"0s".."7d"` | (D034) How long to retain the resource after Succeeded. TTL begins at `status.completedAt`. | +| `failedTtl` | duration string | no | `"24h"` | `"0s".."30d"` | (D034) How long to retain the resource after Failed. TTL begins at `status.completedAt`. | +| `incidentHold` | bool | no | `false` | — | If true, cleanup is blocked regardless of TTL until an authorized caller sets this to false. | + +`processClass` for EphemeralProcess must be `worker`. A `controller` or +`service` EphemeralProcess is rejected at spec admission. + +EphemeralProcess has no `restartPolicy`, `readiness`, `healthCheck`, or +`adoptionPolicy` fields. It runs once. If it fails to start or exits +non-zero, phase becomes `Failed` and the TTL begins at `completedAt`. + +### Status schema + +#### Three-layer status shape (D088) + +D088 freezes `EphemeralProcess` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `EphemeralProcess`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +EphemeralProcess uses all common status fields plus: + +```yaml +status: + observedGeneration: 1 + phase: Succeeded # Pending|Ready|Succeeded|Degraded|Failed|Deleted|Unknown; EphemeralProcess steady lifecycle is Pending→Succeeded|Failed|Unknown; Deleted is a terminal event-only phase (row removed after emit) + conditions: [] + lastReconciledAt: "2026-07-22T00:00:01Z" + startedAt: "2026-07-22T00:00:02Z" + completedAt: "2026-07-22T00:00:05Z" + outcome: + code: process-exited # see outcome codes below + exitCode: 0 + message: "" + retryable: false + occurredAt: "2026-07-22T00:00:05Z" + # EphemeralProcess-specific: + providerImplementation: "" + processIdentityDigest: "" + waitReapOwner: "" + executionRef: "" + domain: "" + userRef: null + sandboxRevisionDigest: null + cleanupEligibleAt: null # set after terminal phase + TTL; RFC 3339 UTC + incidentHeld: false # mirrors spec.incidentHold at last reconcile +``` + +EphemeralProcess-specific status fields: + +| Field | Type | Description | +| --- | --- | --- | +| `cleanupEligibleAt` | string? | RFC 3339 UTC timestamp when the resource becomes eligible for cleanup by the EphemeralProcess cleanup controller. Null until the terminal phase is entered and the TTL computed. Set after finalizers, incident holds, and owner deletion requirements are assessed. | +| `incidentHeld` | bool | True when spec.incidentHold=true is blocking cleanup. | + +EphemeralProcess `outcome.code` stable values: + +| Code | Meaning | +| --- | --- | +| `process-exited` | Process exited; exitCode reflects the actual exit code. | +| `process-crashed` | Process crashed with signal. exitCode may be absent. | +| `start-deadline-exceeded` | Process did not start within startDeadline. | +| `runtime-deadline-exceeded` | Process exceeded runtimeDeadline; was terminated. | +| `launch-failed` | ProviderSupervisor failed to spawn. | +| `execution-unavailable` | executionRef target became non-Ready before start. | +| `cancelled` | Explicitly deleted before terminal phase. | +| `unknown` | Exit not observable; controller restarted during run. | + +EphemeralProcess steady-state lifecycle is strictly `Pending → (Succeeded | Failed | Unknown)`. +`Ready` and `Degraded` are not used in normal EphemeralProcess operation; they are +part of the shared schema but not expected in the EphemeralProcess lifecycle. +`Deleted` is terminal and event-only (row removed on emit). + +### TTL and cleanup (D034) + +EphemeralProcess terminal retention: + +1. When `completedAt` is set (either Succeeded or Failed), the EphemeralProcess + cleanup controller computes `cleanupEligibleAt`: + - Succeeded: `completedAt + successfulTtl` + - Failed: `completedAt + failedTtl` +2. If the EphemeralProcess has `incidentHold=true`, `cleanupEligibleAt` is set + but cleanup is blocked until a caller sets `incidentHold=false`. +3. If the EphemeralProcess has active finalizers, cleanup waits for finalization. +4. If the EphemeralProcess has an `ownerRef` and the owner has `deletionRequestedAt` + set, the owner's finalizer ordering takes precedence. +5. When `cleanupEligibleAt <= now()` and no holds exist, the cleanup controller + calls the normal Delete API on the EphemeralProcess resource with an expected + revision. It does not remove rows directly. +6. Pending and Unknown EphemeralProcesses never age out through TTL. +7. `cleanupEligibleAt` is written via UpdateStatus with expected revision; + a conflict reloads and retries without loss. + +### Conditions + +| Condition type | Meaning | reason codes | +| --- | --- | --- | +| `Scheduled` | Assigned to a controller instance | `controller-unavailable` | +| `Launching` | Start attempt underway | `launch-pending`, `launch-failed` | +| `Running` | Process is actively running | `start-timed-out`, `runtime-deadline-nearing` | +| `CleanupPending` | Terminal; awaiting TTL expiry or incident hold release | `incident-held`, `ttl-pending`, `finalizer-pending` | + +### RBAC + +Same verbs and restrictions as Process, with `EphemeralProcess` as the ResourceType. +`update-spec.incidentHold` additionally requires the `incident-hold-release` sub-verb +on a dedicated Role rule or admin scope. + +### Reconcile + +EphemeralProcess Provider reconcile loop: + +1. Receive trigger. +2. Validate ExecutionSpec fields; check startDeadline not yet exceeded. +3. Compile sandbox and mounts. +4. Dispatch LaunchTicket to ProviderSupervisor; write Launching condition. +5. ProviderSupervisor spawns process; obtains pidfd; returns identity digest. +6. Write Running condition and startedAt. +7. Monitor process exit via broker-relayed wait/reap status (system-minijail) + or the verified systemd unit transition. Pidfd readability alone is not exit + status. +8. On exit: classify exit code/class; write Succeeded/Failed phase, completedAt, outcome. +9. Compute cleanupEligibleAt and write it via UpdateStatus. +10. If runtimeDeadline exceeded before exit: run the Process finalizer's + provider-specific intentional-stop sequence. For system-minijail this is + exact-main SIGTERM, bounded grace, mandatory anchored-leaf `cgroup.kill`, + broker wait/reap, and empty-leaf proof; then write + Failed(runtime-deadline-exceeded). +11. If startDeadline exceeded before launch: write Failed(start-deadline-exceeded). + +### Finalize + +Same provider-specific teardown as Process. For system-minijail that includes +the broker-parent wait/reap result and mandatory cgroup v2 `cgroup.kill` for an +unambiguous intentional stop before empty-leaf proof and rmdir. EphemeralProcess +finalizers are cleared only after process exit and subtree drain are confirmed. +The resource is never force-deleted while the process may still be running or +ownership is ambiguous. + +--- + +## User + +### Purpose + +`User` represents a named host identity in the Zone. `Provider/system-core` +discovers and reconciles Users from the host NSS/passwd/group database. User +spec contains the configured identity name used for NSS lookup. Status +reflects the discovered OS state. Users are referenced by Process `userRef`, +Volume ACL `ownerRef`/`groupRef`, and RoleBinding subjects. + +User is always in the same Zone as the Hosts and Processes that reference it. +A User is not specific to one Host; multiple Hosts may refer to the same User. + +### Spec schema + +```yaml +apiVersion: resources.d2bus.org/v3 +type: User +metadata: + name: alice # required; ^[a-z][a-z0-9-]*$; max 63; Zone-local resource name; used in userRef as User/alice + zone: dev +spec: + osUsername: alice # required; the actual OS username presented to NSS getpwnam; validated by host OS username rules + displayName: "" # optional; bounded human-readable label; max 128 chars + groups: [] # optional; additional group names to verify; 0..64 items +``` + +`metadata.name` is the canonical Zone-local resource identity used in all +`userRef: User/` references throughout the API. It must satisfy the +ResourceName grammar (`^[a-z][a-z0-9-]*$`, max 63 chars). + +`spec.osUsername` is the actual OS username passed to NSS `getpwnam`. It is +validated independently by the host OS username rules: bounded string (1..255 +bytes), no NUL bytes, no control characters (U+0000–U+001F, U+007F), no path +separator characters (`/`, `\`). `spec.osUsername` does not need to satisfy +the ResourceName regex; it may contain underscores or other characters permitted +by the OS but excluded from the ResourceName grammar. The two fields may be +equal (common case) or differ (e.g. `metadata.name: alice-admin`, +`spec.osUsername: alice_admin`). + +Full field table: + +| Field | Type | Required | Default | Bound | Description | +| --- | --- | --- | --- | --- | --- | +| `osUsername` | string | yes | — | 1..255 bytes; no NUL/control/`/` | OS username passed to NSS `getpwnam` for UID/GID/home/shell/group resolution. Validated against host OS username rules, not ResourceName grammar. | +| `displayName` | string | no | `""` | max 128 chars | Human-readable display name shown in operator UI. Not used for NSS lookup. Bounded UTF-8; no control characters. | +| `groups` | `[string]` | no | `[]` | 0..64 items; each max 63 chars | Additional OS group names that the reconcile step verifies membership in. Each item matches `^[a-z_][a-z0-9_-]*$` at validation. Mismatch is surfaced in status conditions. | + +User spec contains no credential material, SSH public key, PAM configuration, +or authentication token of any kind. Credentials are Credential resources. + +### Status schema + +#### Three-layer status shape (D088) + +D088 freezes `User` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `User`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +```yaml +status: + observedGeneration: 1 + phase: Ready # Pending|Ready|Succeeded|Degraded|Failed|Deleted|Unknown; long-lived resources do not steadily use Succeeded; Deleted is a terminal event-only phase (row removed after emit) + conditions: [] + lastReconciledAt: "2026-07-22T00:00:01Z" + startedAt: null + completedAt: null + outcome: null + # User-specific: + uid: null # discovered OS UID; u32; null if unknown + gid: null # primary discovered OS GID; u32; null if unknown + homeExists: false # true if home directory exists and is accessible + shellValid: false # true if login shell is non-empty and exists + sessionManagerAvailable: false # true if systemd user manager is running for this user + groupMembershipVerified: false # true when spec.groups membership verified + observedGroups: [] # observed OS group names; 0..256 items; bounded strings +``` + +User-specific status fields: + +| Field | Type | Description | +| --- | --- | --- | +| `uid` | u32? | Discovered OS UID from NSS getpwnam. Null if NSS lookup fails. | +| `gid` | u32? | Discovered primary GID. Null if NSS lookup fails. | +| `homeExists` | bool | True if the home directory reported by NSS exists and is accessible to d2b's check. | +| `shellValid` | bool | True if the login shell reported by NSS is non-empty and the path exists. | +| `sessionManagerAvailable` | bool | True if the systemd user manager for this UID is running and responding. Relevant to user-domain Processes. | +| `groupMembershipVerified` | bool | True when spec.groups are all present in the observed group membership. | +| `observedGroups` | `[string]` | Observed OS group membership (names only; no GIDs). Max 256 items; each max 63 chars. | + +No UID or GID appears in audit payloads. Status fields containing numeric OS +identifiers are diagnostic only and must not be used as authorization input; +authorization uses the canonical Zone `User/` subject reference. + +### Conditions + +| Condition type | Ready = True when | reason codes | +| --- | --- | --- | +| `UserFound` | NSS getpwnam(spec.osUsername) returns a record | `nss-lookup-failed`, `nss-lookup-timeout`, `user-not-found` | +| `HomeReady` | homeExists is true | `home-directory-missing`, `home-directory-inaccessible` | +| `ShellValid` | shellValid is true | `login-shell-missing`, `login-shell-invalid` | +| `GroupsVerified` | All spec.groups are present in observed membership | `group-membership-missing`, `group-not-found` | +| `SessionManagerReady` | sessionManagerAvailable is true | `session-manager-unavailable`, `session-manager-unknown` | + +Phase is `Ready` when at least `UserFound` and `HomeReady` are True. +`SessionManagerReady` being False produces `Degraded` (not `Failed`) because +user-domain Processes cannot start but the user identity itself is valid. +`GroupsVerified` being False produces `Degraded` when `spec.groups` is +non-empty. + +### RBAC + +| Verb | Required rule | Restriction | +| --- | --- | --- | +| `get` | `{resourceTypes:[User], verbs:[get]}` | Numeric UID/GID in status may require elevated role | +| `list` | `{resourceTypes:[User], verbs:[list]}` | — | +| `watch` | `{resourceTypes:[User], verbs:[watch]}` | — | +| `create` | `{resourceTypes:[User], verbs:[create]}` | Config publication controller or Provider/system-core bootstrap only | +| `update-spec` | `{resourceTypes:[User], verbs:[update-spec]}` | Config publication controller | +| `update-status` | `{resourceTypes:[User], verbs:[update-status]}` | Provider/system-core controller only | +| `delete` | `{resourceTypes:[User], verbs:[delete]}` | Blocked while any Process has `userRef: User/` or any Volume has `ownerRef: User/` | + +### Reconcile + +Provider/system-core User reconcile loop: + +1. Receive trigger: spec-generation-changed, dependency-changed, startup-relist, scheduled-observe. +2. Perform NSS `getpwnam(spec.osUsername)` with bounded timeout (default 5 s). +3. If NSS lookup fails: write UserFound=False, phase=Degraded/Failed (Failed only after consecutive failures exceed threshold); return requeue-at. +4. Record uid, gid, home directory from NSS record. +5. Check home directory existence/accessibility (non-blocking stat). +6. Check login shell existence. +7. Query observed group membership (bounded call to `getgrouplist` or equivalent). +8. Verify spec.groups subset. +9. If systemd user manager check is needed (any Process with domain=user targets this User): query user supervisor for session manager availability. +10. Write User status via UpdateStatus with expected revision. +11. Return `converged` or schedule next `scheduled-observe` at the configured interval. + +User reconcile must not hold OS-user credentials or perform +authentication/login. It is purely observational. + +### Finalize + +User uses no controller-managed finalizer. Deletion is blocked by the +structural check when any Process `userRef` or Volume `ownerRef`/`groupRef` +references this User. The operator must remove or update all such references +before deletion succeeds. + +--- + +## Endpoint + +`Endpoint` is a standard ResourceType (D092): the promoted, provider-neutral +identity for a **stable managed endpoint** (a service/device/control/data/ +transport attachment point) that is referenced across a resource/controller/ +Provider boundary. Per-connection or high-churn carriage (named streams, +`OwnedTransport` handles, inherited fds, `operationId`) is not an Endpoint. + +An `Endpoint` follows the D089 layered spec and D088 layered status. It carries +**no** raw path, address, CID, port, fd, or credential; Core/ProviderSupervisor +resolves the `Endpoint` resource to a private transport/FD only through the +EffectPort/LaunchTicket path. + +### Endpoint base spec (Layer 2) + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `providerRef` | ResourceRef | required | `Provider/` selecting the semantic Endpoint implementation/controller. | +| `producerRef` | ResourceRef | required | The producing resource: `Process/`, `Device/`, `Guest/`, `Host/`, or a qualified `.d2bus.org./`. | +| `endpointClass` | `service\|device\|transport\|control\|data` | required | Closed class of the endpoint. | +| `transport` | `unix\|vsock\|tcp\|fd-attachment\|opaque-carriage` | required | Closed transport class only — never a raw path/address/CID/port/fd. | +| `purpose` | string | required | Bounded stable purpose label; max 63 chars. | +| `serviceFingerprint` | string? | `null` | Bounded service/schema fingerprint or capability class; no raw schema bytes. | +| `locality` | `host-local\|guest-local\|cross-domain\|zone-local` | required | Closed locality/visibility class. | +| `visibility` | `owner\|provider\|zone` | `provider` | Closed visibility scope for consumers. These are the only accepted values; there are no aliases such as `private`, `provider-internal`, `zone-private`, or `authorized-consumers`. | +| `attachmentPolicy` | object | `{ supported: false }` | Bounded attachment support/policy (supported, max attachments); no locator. | +| `consumerPolicy` | object | `{}` | The only finer-grained consumer gate: bounded `allowedSubjects` (exact same-Zone ResourceRefs), `allowedProviderComponents` (exact signed component IDs), and `allowedOperations` (`resolve\|attach\|observe`). Omitted lists add no restriction beyond `visibility`; a present list is an allowlist, and all present dimensions must match. | +| `lifecyclePolicy` | `pinned\|recycle-with-producer\|recreate-on-generation` | `recycle-with-producer` | Closed lifecycle/recycle policy. | +| `provider` | ProviderExtension? | `null` | Optional canonical `spec.provider = { schemaId, schemaVersion, settings }` (D089); implementation-only, no locator. | + +`visibility` is only the coarse candidate scope: `owner` admits the exact +`metadata.ownerRef` subject, `provider` admits authenticated Provider subjects +and signed Provider components in the Zone, and `zone` admits all other +same-Zone subjects. It never grants +resolution by itself. Normal Role/RoleBinding authorization and every +`consumerPolicy` allowlist still apply. `consumerPolicy` is strict and +deny-unknown; providers may not add visibility values or string/array policy +aliases. + +### Endpoint base status (Layer 2) + +On top of the universal base (including `status.update`, D091), the Endpoint +ResourceType base status carries: + +| Field | Values | +| --- | --- | +| `readiness` | `Pending\|Ready\|Degraded\|Unavailable\|Unknown` | +| `observedProducerGeneration` | Numeric generation of the producer resource realized here | +| `observedResourceGeneration` | Numeric generation of this Endpoint resource observed | +| `endpointGeneration` | Numeric; bumped when the producer restarts/recycles so consumers re-resolve | +| `connectionAvailability` | Closed enum `available\|draining\|unavailable` | +| `leaseAvailability` | Closed enum `none\|available\|exhausted` | +| `capability` | Closed capability-class observations | +| `locality` | Observed closed locality class | +| `transport` | Observed closed transport class | +| `conditions` | Bounded standard conditions | + +No raw locator (path/address/CID/port/fd/credential) appears in status. A +Provider-specific implementation extension uses the optional `status.provider` +(D088), never restating a base field. + +### Ownership, resolution, and references + +- `metadata.ownerRef` gives the Endpoint its lifecycle and child-first deletion + (the owning component/Process/Device/Guest/Volume-`Export`). Core creates + static component Endpoints from the signed manifest's Endpoint templates; + dynamic controllers create their owned Endpoints. Static Endpoints may be + Nix/API-authored only where the ResourceType schema permits. +- Consumers reference an endpoint by `Endpoint/` ResourceRef and gain a + dependency edge; a producer restart bumps `endpointGeneration` and fires the + consumer's `dependency-changed` reconcile trigger. +- Resolving an `Endpoint` to a live transport/FD requires authorization; an + unauthorized resolve is denied with a typed error and no locator is returned. +- Core admission first applies the closed `visibility` scope, then normal + Role/RoleBinding authorization, then every present `consumerPolicy` + allowlist. It authenticates the subject and signed Provider component rather + than accepting either identity from the request body. A mismatch at any + layer returns `endpoint-resolve-denied`. +- The virtiofs `Export` resource remains the attachment lifecycle owner and + references its `Endpoint` where the exported endpoint is independently + consumed (`ADR-046-resources-volume`). + +--- + +## Cross-type security invariants + +### Placement validation + +Core enforces at every Process/EphemeralProcess spec create or update-spec: + +1. `executionRef` resolves to an existing Host or Guest in the same Zone. +2. The resolved Host or Guest is not in `Deleted` or `Failed` phase. +3. `domain` is in `executionRef.spec.allowedDomains`. +4. If `domain=user` and no `defaultUserRef` on target: `userRef` is present. +5. `userRef` resolves to a `User/` resource (if present). +6. `providerRef` resolves to an installed and Ready Process Provider. +7. No secret bytes, raw capability lists, cgroup paths, host paths, or minijail + argument strings are in spec payload. Violation → `spec-security-violation` and rejection. +8. Process `budget` fields do not exceed the `executionRef` remaining aggregate budget. +9. When `spec.template` is set: the template ID is resolved at runtime by the Provider registered as controller for the semantic owner resource (`metadata.ownerRef`). If `metadata.ownerRef` is absent, the template is resolved through `spec.providerRef`. The controller verifies the template maps to a declared component in its signed descriptor before launch. `metadata.ownerRef` may be any resource type (Provider, Volume, Network, Device, or other). +10. `spec.template` may be set with any `metadata.ownerRef` type or with no `metadata.ownerRef`. When ownerRef is absent the providerRef itself is the resolver. No type restriction on ownerRef when template is set. +11. All `mounts[].volumeRef` and `credentialRefs[]` resolve in the same Zone. +12. All `deviceUsage[].deviceRef` are present in `executionRef.spec.deviceAttachments`. +13. `networkUsage.networkRef` is present in `executionRef.spec.networkAttachments` if set. + +Violations are returned as `resource-schema-invalid` or `authorization-denied` +with a stable error code and a bounded redacted message. No partial mutation. + +### User-only Host posture (no isolation) + +When `Host.spec.isolationPosture = "none"`: + +- `allowedDomains` must be exactly `[user]`. System processes rejected. +- `defaultDomain` must be `user`. +- `defaultUserRef` must be set. +- The Host status `isolationPosture = "none"` is surfaced in every operator CLI/UI view of this Host and its Processes as an explicit "no isolation boundary" warning. +- A `Process.executionRef` pointing to this Host must carry `domain=user`. +- The operator UI may not suppress or minimize this warning. +- `ProcessEffect` audit records (launch, stop, adopt, quarantine) for every child Process/EphemeralProcess on this Host must carry a stable `no_isolation=true` attribute so the no-isolation posture is recorded in the authoritative audit trail and cannot be absent from them. This attribute belongs on ProcessEffect records only; it must NOT appear on OTEL metric labels, span attributes, log fields, or audit records for other event kinds. + +### Pidfd non-exportability + +The pidfd file descriptor is strictly local lifecycle authority. A Provider +controller holds only an opaque handle. For system-minijail, the broker parent +retains the original pidfd and ProviderSupervisor may hold a verified duplicate: + +- It is never stored in the resource store. +- It is never sent over d2b-bus, ComponentSession, or any ttrpc call. +- It is never written to any log, metric (with a name/PID label), audit record, or status field. +- It is never inferred from status by a caller. +- No API method accepts a pidfd from a caller. +- Every ProviderSupervisor restart reacquires its duplicate through + re-verification; a Provider-controller restart transfers no raw fd and does + not change the broker's parenthood. +- A non-parent holder may poll readability and call `pidfd_send_signal` for the + exact process, but cannot call wait/reap or derive exit status from + readability. +- Only the system-minijail broker that called `clone3` performs + `waitid(P_PIDFD)`, collects exit status, and reaps; it relays a typed terminal + result to ProviderSupervisor/controller. + +### Process isolation rule + +A Process resource spec must not contain: + +- A raw numeric UID, GID, or supplementary group list (user identity comes from `userRef`); +- A raw cgroup path (placement is derived from Resource UID/Provider); +- A raw socket address or file path accessible from the host (these come from Volume/Network resource refs); +- A raw seccomp BPF bytecode program; +- A raw capability bitmask; +- A raw minijail argument string; +- A raw systemd unit property fragment; +- A raw broker operation name or parameters; +- An environment variable containing a credential, token, or secret byte. + +### Status redaction + +The following values MUST NOT appear in any Process, EphemeralProcess, Host, +Guest, or User status field, audit record, telemetry label, or log output: + +- PID values; +- cgroup paths; +- socket file paths; +- argv or environment variable contents; +- credential bytes or token prefixes; +- SSH hostnames or IP addresses; +- terminal byte streams or partial output; +- raw Provider error messages with internal paths. + +Bounded stable machine codes (`reason`, `code`) are always permitted. +Bounded redacted operator messages (max 512 chars per message, UTF-8 clean, +no control characters) are permitted in `status.conditions[].message` and +`status.outcome.message`. + +--- + +## ProcessRole disposition table + +Every current ProcessRole in `packages/d2b-core/src/processes.rs` at baseline +`b5ddbed6` is classified below. No role may be removed before its `Successor` +is integrated and all tests pass. + +| ProcessRole | v3 classification | v3 ResourceType | v3 Provider | Successor condition | +| --- | --- | --- | --- | --- | +| `HostReconcile` | Controller logic, not a Process | — | `system-core` Host reconciler | Delete after Host reconcile handler parity (ADR046-exec-005) | +| `StoreVirtiofsPreflight` | Controller observation, not a Process | — | `volume-virtiofs` or `volume-local` Volume controller | Delete after Volume Provider parity (ADR046-primitives-003) | +| `SwtpmPreStartFlush` | `EphemeralProcess` | EphemeralProcess | `device-tpm` Provider | Delete after device-tpm EphemeralProcess integration | +| `Swtpm` | `Process` | Process | `device-tpm` Provider; owned by Guest | Delete after device-tpm Process integration | +| `Virtiofsd` | `Process` | Process | `volume-virtiofs` Provider; owned by Volume attachment | Delete after volume-virtiofs Process integration | +| `Video` | `Process` | Process | `device-gpu` Provider; owned by Guest | Delete after device-gpu Process integration | +| `Gpu` | `Process` | Process | `device-gpu` Provider; owned by Guest | Delete after device-gpu Process integration | +| `GpuRenderNode` | `Process` | Process | `device-gpu` Provider; owned by Guest | Delete after device-gpu Process integration | +| `Audio` | `Process` | Process | `audio-pipewire` Provider; owned by Guest or Host user | Delete after audio-pipewire Process integration | +| `CloudHypervisorRunner` | `Process` | Process | `runtime-cloud-hypervisor` Provider; owned by Guest | Delete after runtime-cloud-hypervisor Process integration | +| `QemuMediaRunner` | `Process` | Process | `runtime-qemu-media` Provider; owned by Guest | Delete after runtime-qemu-media Process integration | +| `VsockRelay` | `Process` | Process | `transport-vsock` Provider; owned by Guest | Delete after transport-vsock Process integration | +| `OtelHostBridge` | `Process` | Process | `observability-otel` Provider; owned by Host or Guest | Delete after observability-otel Process integration | +| `GuestSshReadiness` | Controller observation, not a Process | — | `runtime-cloud-hypervisor` controller | Delete after GuestControlHealth-equivalent in runtime-ch controller; SSH compat window retained | +| `GuestControlHealth` | Controller observation, not a Process | — | `runtime-cloud-hypervisor` controller | Delete after runtime-ch Guest health check parity | +| `Usbip` | `Process` / `EphemeralProcess` | Process + EphemeralProcess | `device-usbip` Provider; owned by Host or Guest | Delete after device-usbip integration | +| `SecurityKeyFrontend` | `Process` | Process | `device-security-key` Provider; owned by Guest, user domain | Delete after device-security-key Process integration | +| `WaylandProxy` | `Process` | Process | `display-wayland` Provider; owned by Guest or Host | Delete after display-wayland Process integration | + +The ProcessRole/VmProcessDag/processes.json contract is retained until every +role's successor is integrated and passes the corresponding process conformance +test. + +--- + +## Nix configuration, validation, and generation lifecycle + +Bundle format, generation activation algorithm, configuration generation controller, audit events, and prior generation retention mechanics are defined in the Zone configuration lifecycle spec. This section covers only the Nix authoring shape, ResourceType-specific validation rules, canonical ResourceEnvelope JSON, per-type Nix examples, and type-specific deletion cascade behaviour. + +### Option tree + +``` +# Artifact catalog — derivation-valued inputs; separate from and peer to zone resources +d2b.artifacts. = { + package = ; # required; any Nix derivation + type = ""; # required; "provider" | "nixos-system" +}; + +# Zone resources — pure data, no derivation values; artifact IDs are plain bounded strings +d2b.zones..resources. = { + type = ""; # required; one of Host Guest Process EphemeralProcess User + metadata = { # optional; author-settable metadata only + ownerRef = null; # optional; "/" ResourceRef; null = no owner + labels = { }; # optional; string-to-string attrs for selection/grouping + annotations = { }; # optional; arbitrary string-to-string attrs + }; + spec = { /* exact ResourceType spec fields; artifact IDs are plain bounded strings, not ResourceRefs */ }; +}; +``` + +All five ResourceTypes share a single flat `resources` attrset under each Zone. `metadata.name` is derived from the `resources.` attrset key (satisfies `^[a-z][a-z0-9-]*$`, max 63 chars). `metadata.zone` is derived from the enclosing `d2b.zones.` key. `apiVersion` defaults to `"resources.d2bus.org/v3"`. Because `resources` is a flat attrset, no two entries may share the same `` key regardless of `type`. + +**Author-settable metadata fields** (`metadata` submodule; all optional): + +| Nix field | Nix type | Notes | +| --- | --- | --- | +| `metadata.ownerRef` | `types.nullOr types.str` | ResourceRef (`/`) of the owning resource. When the owner is deleted, its controller cascades Delete to resources with `metadata.ownerRef` pointing at it. Serialized to `metadata.ownerRef` in the bundle JSON; NOT a `spec` field. | +| `metadata.labels` | `types.attrsOf types.str` | Key-value string pairs. Keys: `^[a-z][a-z0-9-./]*$`, max 63 chars; values: max 256 chars. | +| `metadata.annotations` | `types.attrsOf types.str` | Arbitrary key-value string pairs. Keys: `^[a-zA-Z0-9-./]*$`, max 253 chars; values: max 4096 chars. | + +**Derived and read-only** — absent from the Nix option; assigned by core or the resource store; not author-configurable: + +| Field | Who sets it | Description | +| --- | --- | --- | +| `metadata.name` | Nix compiler | Derived from `resources.` attrset key | +| `metadata.zone` | Nix compiler | Derived from enclosing `d2b.zones.` key | +| `apiVersion` | Nix compiler | Always `"resources.d2bus.org/v3"` | +| `metadata.managedBy` | Activation controller / core | Valid values: `"configuration"` (set by activation controller for all Nix-declared resources), `"controller"` (set by core for controller-created resources), `"api"` (set by core for API-created resources). Immutable after first Create; NOT author-settable | +| `metadata.configurationGeneration` | Activation controller | NixOS generation number recorded at activation; refreshed on each generation change even for unchanged specs; NOT author-settable | +| `metadata.uid` | Resource store | Stable opaque identity assigned at Create | +| `metadata.generation` | Resource store | Incremented on each spec update | +| `metadata.resourceVersion` | Resource store | Monotonically increasing revision stamp | +| `metadata.createdAt`, `metadata.updatedAt` | Resource store | RFC3339 timestamps | +| `metadata.deletionRequestedAt` | Resource store | RFC3339 timestamp set when a Delete is accepted; absent while the resource is not pending deletion; resource remains in its current phase (not a `Deleting` phase) until finalizers release and core performs atomic row/index removal | +| `metadata.finalizers` | Controllers | Managed by owner/provider controllers; not author-settable | +| `status` | Provider/core | Entirely absent from Nix option; never author-settable | + +### Artifact catalog + +`d2b.artifacts` is a top-level Nix option, peer to `d2b.zones`. It is the only place where derivation-valued inputs (Nix packages, NixOS system closures) appear in the d2b Nix configuration. ResourceSpecs never contain derivation values or store paths; they reference artifacts by plain bounded string IDs. + +| Nix option | Type | Required | Notes | +| --- | --- | --- | --- | +| `d2b.artifacts.` | submodule | — | One entry per artifact. `` satisfies `^[a-z][a-z0-9-]*$`, max 63 chars | +| `.package` | derivation | yes | Any Nix derivation; built and hashed at eval/build time | +| `.type` | enum string | yes | `"provider"` \| `"nixos-system"` | + +**Type semantics:** + +- `"provider"` — a Provider binary/closure; referenced by `Provider.spec.artifactId` (defined in the Provider ResourceType spec, separate document) +- `"nixos-system"` — a complete NixOS system closure (kernel + initrd + rootfs); referenced by `Guest.spec.systemArtifactId` (top-level spec field) for local VM Providers + +**Build-time catalog validation:** + +- Duplicate `` keys: rejected at Nix parse time (attrset key uniqueness); no explicit check required +- Missing catalog entry: `Guest.spec.systemArtifactId` referencing an absent `d2b.artifacts.` → hard eval error (rule 17 below) +- Type mismatch: `Guest.spec.systemArtifactId` referencing a `type = "provider"` entry → hard eval error (rule 17) + +**Private artifact catalog emission:** + +At build time Nix emits a global private integrity-pinned artifact catalog installed at `/etc/d2b/artifact-catalog.json` (owned `root:d2bd`, mode `0640`). The catalog maps each `` to its artifact type, SHA-256 digest, closure size, and store path. Store paths are in the catalog for trusted runtime use (resolving derivation closures) but must never appear in public resource bundle envelopes, resource spec/status fields, or audit records. + +```json +{ + "catalogVersion": 1, + "entries": { + "": { "sha256": "", "size": , "storePath": "/nix/store/...", "type": "" } + } +} +``` + +The resource bundle references artifacts by plain `` string in ResourceEnvelope `spec` fields and includes a catalog integrity digest binding (`catalogSha256`) in the outer bundle wrapper so the activation controller can verify the catalog matches the bundle at activation time. + +**Referencing artifacts in ResourceSpecs:** + +Use the plain bounded `` string as the `Guest.spec.systemArtifactId` value (or `Provider.spec.artifactId` in the Provider spec). The `` is neither a ResourceRef nor a store path: + +```nix +# In artifact catalog (derivation-valued; separate from zone resources) +d2b.artifacts.dev-vm-system = { package = nixosSystemForDevVm.config.system.build.toplevel; type = "nixos-system"; }; +d2b.artifacts.display-wayland-v1 = { package = pkgs.d2b-provider-display-wayland; type = "provider"; }; + +# In zone resources — plain string IDs only; no derivation values in spec +d2b.zones.dev.resources.dev-vm.spec.systemArtifactId = "dev-vm-system"; +# Provider resource (separate spec): spec.artifactId = "display-wayland-v1"; +``` + +### Nix resource option schema + +Each entry in `d2b.zones..resources` is a `types.submodule` with: + +| Nix option | Nix type | Required | Notes | +| --- | --- | --- | --- | +| `type` | `types.enum ["EphemeralProcess" "Guest" "Host" "Process" "User"]` | yes | Selects the ResourceType | +| `metadata` | `types.submodule` | no | Author-settable metadata: `ownerRef`, `labels`, `annotations` only; all optional | +| `spec` | `types.submodule` (type-dependent) | yes | Exact ResourceType spec fields; auto-generated from `packages/d2b-contracts/src/v3/schemas/.json`; field names 1:1 with the spec tables above; no renaming | + +`spec` submodule fields are generated from the committed ResourceTypeSchema JSON. For `spec.provider.settings`, sub-fields are additionally constrained by the Provider's `providerNixSettingsSchema` attribute; the schema version is recorded in the private bundle integrity metadata and verified by the activation controller (fail-closed on mismatch). + +**Key `spec` constraints by type** (full rule list below): + +| `type` | Key `spec` constraints enforced at eval time | +| --- | --- | +| `"Host"` | `spec.providerRef` must be a declared substrate Provider; `spec.allowedDomains` must include `spec.defaultDomain`; `spec.isolationPosture = "none"` implies `spec.allowedDomains == ["user"]`, `spec.defaultDomain == "user"`, and `spec.defaultUserRef` set (and vice-versa: that tuple implies `isolationPosture = "none"`) | +| `"Guest"` | `spec.providerRef` must be a declared runtime Provider; `spec.provider.settings` validated against Provider settings schema; `spec.systemArtifactId`, if set, must exist in `d2b.artifacts` with `type="nixos-system"` (rule 17) | +| `"Process"` | `spec.executionRef` resolves to a declared Host or Guest; `spec.domain ∈ executionRef.spec.allowedDomains`; `spec.template`, when set, is resolved at runtime by the controller of `metadata.ownerRef` (or `spec.providerRef`); no ownerRef type restriction | +| `"EphemeralProcess"` | Same as Process; `spec.startDeadline` and `spec.runtimeDeadline` required; no `spec.restartPolicy` | +| `"User"` | `spec.osUsername` required (1..255 bytes, no NUL/control/path separators); `spec.groups` items match `^[a-z_][a-z0-9_-]*$` | + +### Eval-time validation rules + +The Nix resource compiler enforces all of the following at `nixos-rebuild build` time. Each violation is a hard eval error with a stable rule code: + +1. **ResourceType field**: `type` must be exactly one of the five valid values. +2. **ResourceName grammar**: every `resources.` key matches `^[a-z][a-z0-9-]*$`, max 63 chars. +3. **ResourceRef resolution**: every `spec.*Ref` string field and `metadata.ownerRef` that names a resource in the same Zone resolves to a declared resource in `d2b.zones..resources`. Cross-Zone refs are rejected. `metadata.ownerRef` may name any existing same-Zone resource subject to the no-self (rule 11) and no-cycle rules; no per-type owner restrictions apply. `spec.systemArtifactId` is a plain bounded string ID (not a ResourceRef) and is validated by rule 17 against the artifact catalog, not by this rule. +4. **Provider kind check**: `spec.providerRef` for `type="Host"` must be a substrate Provider; for `type="Guest"` must be a runtime Provider; for `type="Process"` or `type="EphemeralProcess"` must be a Process Provider. +5. **Domain inclusion**: `spec.defaultDomain ∈ spec.allowedDomains`; for Process/EphemeralProcess, `spec.domain ∈ executionRef.spec.allowedDomains`. +6. **User-domain userRef**: when `spec.domain == "user"` and `spec.executionRef` target has no `spec.defaultUserRef`, `spec.userRef` must be set. +7. **Isolation-posture constraints** (bidirectional): `spec.isolationPosture = "none"` implies `spec.allowedDomains == ["user"]`, `spec.defaultDomain == "user"`, and `spec.defaultUserRef` is set. Conversely, `spec.allowedDomains == ["user"]` + `spec.defaultDomain == "user"` + `spec.defaultUserRef` set implies `spec.isolationPosture == "none"`; `null` is rejected to prevent evasion of the no-isolation warning. Any value for `isolationPosture` other than `null` or `"none"` is a hard eval error. +8. **osUsername bounds**: `spec.osUsername` 1..255 bytes, no NUL/control/path-separator characters. +9. **Budget bounds**: `spec.budget..request ≤ spec.budget..limit` for every dimension where both are set. +10. **Mount uniqueness**: no two entries in `spec.mounts` have the same `mountPath`. +11. **No self-referential ownerRef**: `metadata.ownerRef` must not equal `"/"` of the resource itself. +12. **No inline secrets**: `spec.provider.settings` string fields with credential-suffix names (`password`, `token`, `secret`, `key`, `cert`, `credential`) are hard eval errors; credentials must be `Credential/` refs. +13. **Provider settings schema**: each Provider declaring `providerNixSettingsSchema` has `spec.provider.settings` validated against that schema; SHA-256 recorded as `providerSchemaFingerprint`. +14. **Groups grammar**: each item in `spec.groups` matches `^[a-z_][a-z0-9_-]*$`. +15. **Template scoping**: `spec.template`, when set, satisfies `^[a-z][a-z0-9-]*$`, max 63 chars (grammar check). Template resolution is runtime: the registered controller of `metadata.ownerRef` (or `spec.providerRef` when ownerRef is absent) must declare this ID. No ownerRef type restriction at eval time. +16. **Flat namespace uniqueness**: no two entries in `d2b.zones..resources` may share the same `` key regardless of `type`. +17. **System artifact ID resolution**: `spec.systemArtifactId`, if set, must exist as a key in `d2b.artifacts` with `type = "nixos-system"`. Missing key or type mismatch is a hard eval error. + +### Canonical ResourceEnvelope JSON shape + +Every Nix-declared resource compiles to a `ResourceEnvelope` JSON object. `spec` is a direct 1:1 serialization of the Nix `spec` submodule (same field names, same nesting, same values; no renaming). `metadata.ownerRef` appears in the `metadata` object, NOT in `spec`. All JSON object keys are sorted alphabetically at every nesting level; order-significant arrays preserve declaration order; semantically unordered arrays are sorted. + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { + "annotations": {}, + "labels": {}, + "name": "", + "ownerRef": null, + "zone": "" + }, + "spec": { /* all spec fields, keys sorted, defaults included; ownerRef is NOT a spec field */ }, + "type": "" +} +``` + +`metadata.managedBy`, `metadata.configurationGeneration`, `metadata.deletionRequestedAt`, `metadata.uid`, `metadata.generation`, `metadata.resourceVersion`, `metadata.createdAt`, `metadata.updatedAt`, `metadata.finalizers`, and `status` are absent from the bundle envelope; they are set by the activation controller or resource store at runtime. Schema/Provider fingerprints and other bundle integrity metadata reside in the private bundle file alongside the resource envelope array; they are not included in individual ResourceEnvelopes. For the outer bundle wrapper (`bundleSha256`, `manifestVersion`, `resourceCount`, `resourceTypeCounts`) and private integrity fields see the Zone configuration lifecycle spec. + +### Nix declaration examples with canonical JSON + +#### Host — system execution node + +```nix +d2b.zones.dev.resources.host-system = { + type = "Host"; + spec = { + providerRef = "Provider/system-core"; + defaultDomain = "system"; + allowedDomains = [ "system" "user" ]; + defaultUserRef = "User/alice"; + budget = { cpu.request = "2000m"; cpu.limit = "8000m"; memory.request = "1Gi"; memory.limit = "16Gi"; }; + provider = { + schemaId = "system-core.d2bus.org/Host/spec"; + schemaVersion = "1.0"; + settings.capabilities = [ "kvm" "pidfd" "cgroup-v2" "user-namespace" "wayland" "audio-pipewire" ]; + }; + }; +}; +``` + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { + "annotations": {}, "labels": {}, "name": "host-system", + "ownerRef": null, "zone": "dev" + }, + "spec": { + "allowedDomains": ["system", "user"], + "budget": { "cpu": { "limit": "8000m", "request": "2000m" }, "memory": { "limit": "16Gi", "request": "1Gi" } }, + "defaultDomain": "system", "defaultUserRef": "User/alice", + "deviceAttachments": [], "isolationPosture": null, "networkAttachments": [], + "provider": { "schemaId": "system-core.d2bus.org/Host/spec", "schemaVersion": "1.0", "settings": { "capabilities": ["audio-pipewire", "cgroup-v2", "kvm", "pidfd", "user-namespace", "wayland"] } }, + "providerRef": "Provider/system-core" + }, + "type": "Host" +} +``` + +#### Host — user-only (unsafe-local successor) + +```nix +d2b.zones.dev.resources.host-unsafe-local = { + type = "Host"; + spec = { + providerRef = "Provider/system-core"; + defaultDomain = "user"; + allowedDomains = [ "user" ]; + defaultUserRef = "User/alice"; + isolationPosture = "none"; + }; +}; +``` + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { "annotations": {}, "labels": {}, "name": "host-unsafe-local", "ownerRef": null, "zone": "dev" }, + "spec": { + "allowedDomains": ["user"], "budget": null, "defaultDomain": "user", + "defaultUserRef": "User/alice", "deviceAttachments": [], "isolationPosture": "none", + "networkAttachments": [], "provider": null, + "providerRef": "Provider/system-core" + }, + "type": "Host" +} +``` + +#### Guest — cloud-hypervisor VM + +```nix +# Artifact catalog entry (separate from zone resources; owns the derivation) +d2b.artifacts.dev-vm-system = { + package = nixosSystemForDevVm.config.system.build.toplevel; + type = "nixos-system"; +}; + +d2b.zones.dev.resources.dev-vm = { + type = "Guest"; + spec = { + providerRef = "Provider/runtime-cloud-hypervisor"; + defaultDomain = "system"; + allowedDomains = [ "system" "user" ]; + defaultUserRef = "User/alice"; + systemArtifactId = "dev-vm-system"; # top-level spec field; not in spec.provider.settings + budget = { cpu.request = "1000m"; cpu.limit = "4000m"; memory.request = "512Mi"; memory.limit = "4Gi"; }; + networkAttachments = [ { networkRef = "Network/dev-lan"; default = true; } ]; + provider = { + schemaId = "runtime-cloud-hypervisor.d2bus.org/Guest/spec"; + schemaVersion = "1.0"; + settings = { vcpus = 4; memoryMb = 4096; vsockCid = 3; machineType = "q35"; consoleType = "virtio"; }; + }; + }; +}; +``` + +Resource bundle JSON (`systemArtifactId` is a top-level spec field; no store path in envelope): + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { "annotations": {}, "labels": {}, "name": "dev-vm", "ownerRef": null, "zone": "dev" }, + "spec": { + "allowedDomains": ["system", "user"], + "budget": { "cpu": { "limit": "4000m", "request": "1000m" }, "memory": { "limit": "4Gi", "request": "512Mi" } }, + "defaultDomain": "system", "defaultUserRef": "User/alice", "deviceAttachments": [], + "networkAttachments": [{ "default": true, "networkRef": "Network/dev-lan" }], + "provider": { "schemaId": "runtime-cloud-hypervisor.d2bus.org/Guest/spec", "schemaVersion": "1.0", "settings": { "consoleType": "virtio", "machineType": "q35", "memoryMb": 4096, "vcpus": 4, "vsockCid": 3 } }, + "providerRef": "Provider/runtime-cloud-hypervisor", + "systemArtifactId": "dev-vm-system", + "volumeAttachmentDefaults": [] + }, + "type": "Guest" +} +``` + +Private artifact catalog entry (emitted alongside resource bundle; store path private): + +```json +{ "dev-vm-system": { "sha256": "a3c1...", "size": 12456789, "type": "nixos-system" } } +``` + +#### Process — Wayland proxy sidecar + +`metadata.ownerRef` is author-supplied in the `metadata` block; it is NOT a `spec` field. + +```nix +d2b.zones.dev.resources.wayland-proxy = { + type = "Process"; + metadata.ownerRef = "Provider/display-wayland"; + spec = { + providerRef = "Provider/system-systemd"; + executionRef = "Host/host-system"; + domain = "system"; + processClass = "service"; + template = "wayland-proxy-main"; + sandbox = { namespaceClasses = [ "mount" "ipc" "uts" "network" ]; capabilityClasses = []; seccompClass = "strict"; readOnlyRoot = true; noNewPrivileges = true; }; + budget = { cpu.limit = "500m"; memory.limit = "128Mi"; }; + restartPolicy = { class = "on-failure"; backoffBase = "1s"; backoffMax = "60s"; }; + readiness = { timeout = "30s"; class = "ready-condition"; }; + }; +}; +``` + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { "annotations": {}, "labels": {}, "name": "wayland-proxy", "ownerRef": "Provider/display-wayland", "zone": "dev" }, + "spec": { + "budget": { "cpu": { "limit": "500m", "request": null }, "memory": { "limit": "128Mi", "request": null } }, + "domain": "system", "executionRef": "Host/host-system", "mounts": [], + "processClass": "service", + "providerRef": "Provider/system-systemd", + "readiness": { "class": "ready-condition", "timeout": "30s" }, + "restartPolicy": { "backoffBase": "1s", "backoffMax": "60s", "class": "on-failure" }, + "sandbox": { "capabilityClasses": [], "namespaceClasses": ["ipc", "mount", "network", "uts"], "noNewPrivileges": true, "readOnlyRoot": true, "seccompClass": "strict" }, + "telemetry": null, "template": "wayland-proxy-main", "userRef": null + }, + "type": "Process" +} +``` + +#### EphemeralProcess — swtpm state flush + +```nix +d2b.zones.dev.resources."swtpm-flush-dev-vm" = { + type = "EphemeralProcess"; + metadata.ownerRef = "Provider/device-tpm"; + spec = { + providerRef = "Provider/system-minijail"; + executionRef = "Host/host-system"; + domain = "system"; + processClass = "worker"; + template = "swtpm-flush"; + mounts = [ { volumeRef = "Volume/dev-vm-tpm-state"; view = "flush"; mountPath = "/state"; access = "read-write"; } ]; + startDeadline = "30s"; + runtimeDeadline = "60s"; + successfulTtl = "1h"; + failedTtl = "24h"; + }; +}; +``` + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { "annotations": {}, "labels": {}, "name": "swtpm-flush-dev-vm", "ownerRef": "Provider/device-tpm", "zone": "dev" }, + "spec": { + "domain": "system", "executionRef": "Host/host-system", "failedTtl": "24h", + "mounts": [{ "access": "read-write", "mountPath": "/state", "view": "flush", "volumeRef": "Volume/dev-vm-tpm-state" }], + "processClass": "worker", + "providerRef": "Provider/system-minijail", + "runtimeDeadline": "60s", "sandbox": null, "startDeadline": "30s", + "successfulTtl": "1h", "template": "swtpm-flush", "userRef": null + }, + "type": "EphemeralProcess" +} +``` + +#### User + +```nix +d2b.zones.dev.resources.alice = { + type = "User"; + spec = { osUsername = "alice"; displayName = "Alice"; groups = [ "wheel" "audio" "video" "d2b" ]; }; +}; + +# metadata.name (alice-admin) and spec.osUsername (alice_admin) are independent +d2b.zones.dev.resources.alice-admin = { + type = "User"; + spec = { osUsername = "alice_admin"; displayName = "Alice (admin)"; groups = [ "wheel" "d2b" ]; }; +}; +``` + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { "annotations": {}, "labels": {}, "name": "alice", "ownerRef": null, "zone": "dev" }, + "spec": { "displayName": "Alice", "groups": ["audio", "d2b", "video", "wheel"], "osUsername": "alice" }, + "type": "User" +} +``` + +### Configuration management authority + +The activation controller sets `metadata.managedBy = "configuration"` on every resource it creates from a bundle envelope, and sets `metadata.configurationGeneration` to the current NixOS generation number. `metadata.managedBy` is immutable after first Create and is NOT author-settable. `metadata.configurationGeneration` is core-updatable: the activation controller refreshes it on every generation activation, including for unchanged specs. The generation cleanup controller identifies the config-declared population by querying `metadata.managedBy = "configuration"`; it does not use any author-supplied label or `configOwned` field. Controller-created resources (`metadata.managedBy = "controller"`) and API-created resources (`metadata.managedBy = "api"`) are never touched by generation cleanup. See the Zone configuration lifecycle spec for the full activation algorithm and cleanup controller design. + +### Removed-resource deletion cascade + +When a resource is absent from the new bundle and has `metadata.managedBy = "configuration"`, the activation controller issues an async Delete. The resource's `metadata.deletionRequestedAt` is set and a `CleanupPending=True` condition is added with `reason=config-generation-cleanup`. The resource remains in its current phase (Pending or Degraded) while finalizers drain. When all finalizers release, the resource store commits a single atomic transaction: a `Deleted` revision event is written and the resource row and all indexes are removed. Following the committed transaction, the audit subsystem appends the deletion audit record using a dedup/exactly-once recovery key so retried recoveries never produce a duplicate audit entry. There is no `phase=Deleting`; the valid terminal deletion phase is `Deleted`. Owner controllers must release their finalizers. Cascade Delete responsibility by ResourceType: + +- **Guest deleted**: the Guest controller issues Delete on all resources with `metadata.ownerRef = "Guest/"` (Processes and EphemeralProcesses). Processes drain gracefully; EphemeralProcesses are allowed to complete or killed per provider policy. All child finalizers released before the Guest finalizer. +- **Host deleted**: config-declared Processes with `spec.executionRef = "Host/"` have concurrent Delete operations in-flight from the same generation diff. Controller-created Processes with that `executionRef` are issued Delete by their owner controllers when the Host's `metadata.deletionRequestedAt` is set. +- **User deleted**: Delete is blocked if any Process has `spec.userRef = "User/"`; this is a structural conflict requiring the operator to update or delete those Processes first. No implicit cascade. +- **Process or EphemeralProcess deleted**: the Process Provider drains and stops the process per drainTimeout policy, then releases the pidfd and cgroup leaf. + +The generation cleanup controller issues Delete only on top-level config-declared resources and does not descend into controller-created children. + +### Per-resource cleanup conditions and error codes + +Resources deleted by generation cleanup carry: + +| Condition type | True when | reason codes | +| --- | --- | --- | +| `CleanupPending` | Delete has been issued; `metadata.deletionRequestedAt` is set; finalizers draining | `config-generation-cleanup` | +| `CleanupBlocked` | Deletion is blocked by a finalizer or owned children beyond threshold | `finalizer-blocked`, `owner-child-blocked` | + +Stable error codes: + +| Code | Description | +| --- | --- | +| `config-bundle-integrity-failed` | `bundleSha256` mismatch; entire bundle activation aborted | +| `config-catalog-mismatch` | `catalogSha256` in bundle wrapper does not match installed `/etc/d2b/artifact-catalog.json`; entire bundle activation aborted | +| `config-schema-mismatch` | One or more resource type schemas in the bundle do not match the currently committed ResourceTypeSchema; entire bundle activation aborted | +| `provider-schema-mismatch` | One or more Provider schema versions in the bundle do not match the installed Provider; entire bundle activation aborted | +| `config-collision` | New config-declared Create collides with an existing controller-created or api-created resource of the same name; per-item error; other intents continue | +| `config-activation-failed` | Activation returned a terminal error not covered by the above codes | +| `cleanup-finalizer-blocked` | Resource deletion blocked by unreleased finalizer beyond threshold | +| `cleanup-owner-blocked` | Resource deletion blocked by owned children beyond threshold | +| `cleanup-timeout` | Cleanup did not complete within the configured deadline | + +### Prior generation retention + +The zone configuration controller retains the N most recently activated, cleanup-complete bundle records. Default N=3; operator-configurable in range 1..16. No time-based TTL. A bundle record is eligible for release when its cleanup is complete and the retained count exceeds N (oldest-beyond-N released first). Rollback procedure and bundle GC-root management are defined in the Zone configuration lifecycle spec. + +### Tests for Nix configuration and ResourceType-specific lifecycle + +**Eval/build tests** (run by `make test-flake` / `nix-unit`): + +1. `type = "Host"` with all `spec` fields renders exact golden JSON vector; `metadata.ownerRef = null`; `metadata.annotations = {}`; `metadata.labels = {}`; `spec` keys sorted; no `configOwned`, no `managedBy`, no `configurationGeneration`, no `schemaFingerprint`, no `providerSchemaFingerprint` in ResourceEnvelope. +2. `type = "Host"`, `spec.isolationPosture = "none"`, `spec.allowedDomains = ["system"]` → eval error; rule 7 code. +3. `type = "Host"`, `spec.isolationPosture = "none"`, `spec.defaultUserRef = null` → eval error; rule 7 code. +4. `type = "Guest"` with `spec.providerRef = "Provider/system-core"` (substrate, not runtime) → eval error; rule 4 code. +5. `type = "Process"`, `spec.domain = "system"`, `spec.executionRef` resolves to Host with `spec.allowedDomains = ["user"]` → eval error; rule 5 code. +6. `type = "Process"`, `spec.domain = "user"`, no `spec.userRef`, no `executionRef.spec.defaultUserRef` → eval error; rule 6 code. +7. `type = "User"`, `spec.osUsername = "alice/admin"` (path separator) → eval error; rule 8 code. +8. `type = "User"`, `spec.osUsername = "alice_admin"`, resource key `alice-admin`: valid; JSON `metadata.name = "alice-admin"`, `spec.osUsername = "alice_admin"`. +9. `type = "User"`, `spec.groups = ["WHEEL"]` → eval error; rule 14 code. +10. `type = "Process"`, `metadata.ownerRef = "Guest/dev-vm"`: JSON has `metadata.ownerRef = "Guest/dev-vm"` and `spec` contains no `ownerRef` field. +11. `metadata.ownerRef` referencing a non-existent resource in the same Zone → eval error; rule 3 code. +12. `metadata.ownerRef = "Bogus/nonexistent"` (unknown ResourceType) → eval error; rule 3 code. Also: `metadata.ownerRef = "Process/self"` on the same resource → eval error; rule 11 (no-self-ref) code. Also: `metadata.ownerRef` referencing a declared resource in a different Zone → eval error; rule 3 cross-Zone rejection. Also: `metadata.ownerRef = "Host/unresolved"` where `Host/unresolved` is not declared in `d2b.zones..resources` → eval error; rule 3 unresolved ref code. +13. `d2b.artifacts.dev-vm-system = { package = ; type = "nixos-system"; }`, Guest resource `spec.systemArtifactId = "dev-vm-system"` (top-level spec field) → valid; artifact catalog emits `"dev-vm-system": { "sha256": "...", "size": ..., "type": "nixos-system" }`; ResourceEnvelope contains `"systemArtifactId": "dev-vm-system"` in `spec` (not in `spec.provider.settings`) and no store path anywhere in the envelope; rule 17 passes. +14. Guest resource `spec.systemArtifactId = "missing-system"` with no `d2b.artifacts.missing-system` entry → hard eval error; rule 17 code. +15. Guest resource `spec.systemArtifactId = "display-wayland-v1"` where `d2b.artifacts."display-wayland-v1".type = "provider"` → hard eval error; rule 17 code (type mismatch). +16. Duplicate `d2b.artifacts.` key in the same `d2b.artifacts` attrset → Nix parse-time attrset key collision error before any eval rule runs. +17. `type = "Host"`, `spec.isolationPosture = "none"`, `spec.defaultDomain = "user"`, `spec.allowedDomains = ["user"]`, `spec.defaultUserRef = "User/alice"` → valid; rendered JSON has top-level `"isolationPosture": "none"` in `spec` (not under `provider`); positive test for the user-only no-isolation Host configuration. +18. `type = "Host"`, `spec.isolationPosture = null`, `spec.defaultDomain = "user"`, `spec.allowedDomains = ["user"]`, `spec.defaultUserRef = "User/alice"` → eval error; rule 7 code (bidirectional: null cannot be used to evade the no-isolation posture declaration). + +**Runtime/integration tests** (run by `make test-integration`): + +19. New generation adds a Host → Create issued; stored resource has `metadata.managedBy = "configuration"` and `metadata.configurationGeneration` matches current NixOS generation; Host reaches `phase=Ready`; no `configOwned` field on stored resource; ResourceEnvelope contains no `schemaFingerprint`, `providerSchemaFingerprint`, or store path fields. +20. New generation removes a User not referenced by any Process → async Delete issued; `metadata.deletionRequestedAt` set; `CleanupPending=True` condition added; no `phase=Deleting`; finalizers release; deletion transaction atomically commits `Deleted` revision event + row/index removal; audit append follows committed revision with dedup/exactly-once recovery (no duplicate emit on recovery retry); Zone cleanup state empties. +21. New generation removes a Guest owning Processes → Guest Delete issued; Guest controller cascades Delete to all resources with `metadata.ownerRef = "Guest/"`; Processes stop; each Process deletion atomically commits `Deleted` revision event + row/index removal; audit append follows committed revision with dedup/exactly-once recovery; Guest finalizer released; Guest deletion atomically commits `Deleted` revision event + row/index removal; audit append follows; `Deleted` Watch events observed for all. +22. New generation removes a User referenced by an active Process → Delete blocked; `cleanup-owner-blocked` error; after operator updates Process, Delete completes. +23. Controller-created EphemeralProcess (`metadata.managedBy = "controller"`) absent from new bundle → NOT deleted by generation cleanup; still present after activation. +24. system-minijail spawn → the broker that called `clone3` alone performs + `waitid(P_PIDFD)` and reaps exactly once; ProviderSupervisor poll + readability cannot supply `lastExitClass`/`outcome.exitCode`; the controller + receives the broker-relayed typed status; a verified duplicate holder can + still `pidfd_send_signal` the exact main process. +25. system-minijail intentional stop with a descendant that calls `setsid(2)` + and an unrelated recycled-PGID decoy → exact-main SIGTERM/grace is followed + by writing `1` to the anchored leaf's `cgroup.kill`; the owned leaf reaches + `populated 0`, the decoy is untouched, and rmdir/finalizer clearing occurs + only after broker-reaped main status. Ambiguous adoption performs no signal + or `cgroup.kill`. +26. Linux <5.14 or a delegated cgroup v2 leaf without writable `cgroup.kill` → + Host/system-minijail placement remains not Ready with + `kernel-too-old`/`cgroup-kill-unavailable`, and the broker receives zero + spawn requests. + +--- + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor (Host) | `packages/d2b-core/src/host_check.rs`: `HostCheckReport`, `HostCheckSummary`, `HostCheckFinding`, `HostCheckSeverity`; `packages/d2bd/src/kernel_module_check.rs`, `pidfs_probe.rs`; `nixos-modules/options-host.nix`; `packages/d2b-core/src/provider_capabilities.rs`; `packages/d2b-realm-core/src/ids.rs`: `HostResourceId`, `NodeId`; `packages/d2b-realm-core/src/node.rs`: `NodeKind` (`FullHost`/`Gateway`/`ProviderManaged`), `NodeSummary` (id/realm/kind/capabilities) — `NodeKind::FullHost` is the closest current analog for a v3 Host execution node | +| Host evidence class | `HostCheckReport`/`HostCheckSummary` implemented-and-reachable; `NodeSummary`/`NodeKind::FullHost` implemented-and-reachable (current node inventory concept); Host ResourceType is ADR-only | +| Current anchor (Guest) | `packages/d2b-realm-core/src/workload.rs`: `WorkloadProviderKind` (`LocalVm` → runtime-cloud-hypervisor Provider, `QemuMedia` → runtime-qemu-media Provider, `ProviderManaged` → ACA/relay Providers; `UnsafeLocal` → user-only **Host**, not Guest — this variant is cited here only for enum completeness; its evidence and target mapping are owned by the unsafe-local anchor row), `IsolationPosture` (`VirtualMachine` → Guest, `ProviderManaged` → Guest; `UnsafeLocal` → Host `isolationPosture="none"`, not Guest), `WorkloadExecutionPosture`, `WorkloadSummary`, `WorkloadState` (`Stopped`/`Starting`/`Running`/`Degraded`/`Failed`), `WorkloadSelector`; `packages/d2b-realm-core/src/ids.rs`: `RealmId`, `WorkloadId`, `NodeId`, `ProviderId`, `ExecutionId`, `GatewayId`; `packages/d2b-realm-core/src/realm.rs`: `RealmPath`, `RealmControllerPlacement`, `EntrypointMode`; `packages/d2b-realm-core/src/target.rs`: `RealmTarget`, `TargetName`, `RealmTargetParser` (current analog for `/` ResourceRef); `packages/d2b-core/src/workload_identity.rs`: `WorkloadIdentity`, `WorkloadTarget`, `WorkloadBackend` (`LocalVm`/`LocalQemuMedia`/`ProviderManaged` → Guest; `UnsafeLocal` → Host — not a Guest binding, not a v3 Provider; see unsafe-local anchor), `WorkloadRuntimeIntent`; `packages/d2b-core/src/realm_controller_config.rs`: `RealmControllerConfig`, `RealmControllerRuntimeProviderType`, `RealmControllerLocalWorkload`; `nixos-modules/options-realms-workloads.nix` (`d2b.realms..workloads..kind`: local-vm/qemu-media → Guest; unsafe-local → Host, see unsafe-local anchor); `nixos-modules/options-realms.nix` (`d2b.realms`, `providerKind` regex `^[a-z][a-z0-9-]*$`) | +| Guest evidence class | `WorkloadSummary`/`WorkloadState`/`WorkloadProviderKind` implemented-and-reachable for local-vm/qemu-media paths; ACA (`d2b-provider-aca/`) and relay (`d2b-provider-relay/`) Providers have live runtime paths via `AcaWorkloadProvider`/`WorkloadProvider` trait; host Provider adapters in `d2b-host-providers/src/lib.rs` (`HostCheckSubstrateProvider`, `LocalMicroVmProvider`) are thin adapters, not fully wired to the daemon (see `packages/d2bd/src/realm_stubs.rs` `dead_code` note); Guest ResourceType is ADR-only; v3 runtime Provider resources are ADR-only | +| Current anchor (Guest runtime Providers) | `packages/d2b-realm-provider/src/provider.rs`: `HostSubstrateProvider`, `RuntimeProvider`, `WorkloadProvider`, `DurableExecutionProvider`, `GuestControlEndpointProvider`, `PersistentShellProvider`, `DisplayProvider`, `RelayProvider`, `NodeProvider` (traits); `packages/d2b-realm-provider/src/capabilities.rs`: `RuntimeCapabilitySet`, `WorkloadCapabilitySet`, `DisplayCapabilitySet`, `NodeCapabilitySet`, `HostSubstrateKind`; `packages/d2b-host-providers/src/lib.rs`: `HostCheckSubstrateProvider` (NIXOS_HOST_SUBSTRATE_PROVIDER_ID, GENERIC_LINUX_HOST_SUBSTRATE_PROVIDER_ID), `LocalMicroVmProvider` (CLOUD_HYPERVISOR_RUNTIME_PROVIDER_ID), LOCAL_QEMU_MEDIA_RUNTIME_PROVIDER_ID, LOCAL_CROSS_DOMAIN_WAYLAND_PROVIDER_ID; `packages/d2b-provider-aca/src/lib.rs`: `AcaWorkloadProvider` (ACA sandbox path, live); `packages/d2b-provider-relay/src/lib.rs` (relay transport, live) | +| Guest runtime Provider evidence class | ACA (`AcaWorkloadProvider`) and relay Provider are implemented-and-reachable with live data-plane REST and relay paths; `LocalMicroVmProvider`/`HostCheckSubstrateProvider` are implemented-but-unwired (`realm_stubs.rs` stubs are `dead_code`; gateway-mode wiring is incomplete); all v3 Provider resources are ADR-only | +| Current anchor (Process/EphemeralProcess) | `packages/d2b-core/src/processes.rs`: `ProcessRole` (18 variants), `VmProcessDag`, `ProcessNode`, `RoleProfile`, `NamespaceSet`, `MountPolicy`, `CgroupPlacement`, `ReadinessPredicate`; `packages/d2bd/src/supervisor/`: `DagExecutor`, `NodeOutcome`, `NodeHistory`, `NodeBudget`, `SplitReadinessMode`; `packages/d2bd/src/supervisor/pidfd_table.rs`: `PidfdTable`, `PidfdEntry`, `PidfdRegistration`, `WaitTermination`, `BrokerReapLog`; `packages/d2b-priv-broker/` SpawnRunner; `packages/d2b-guestd/src/exec.rs`, `exec_linux.rs`, `exec_pty.rs`, `detached.rs`; `packages/d2b-realm-core/src/execution.rs`: `ExecState`, `ExecAttachMode`, `ExecStartRequest`, `ExecAttachRequest` (current exec lifecycle analog for EphemeralProcess); `packages/d2b-realm-core/src/workload.rs`: `WorkloadProviderKind::UnsafeLocal` → user-only Host `isolationPosture="none"`, `IsolationPosture::UnsafeLocal` → Host `isolationPosture="none"` (these current enum variants are evidence for the no-isolation posture concept; they are retained in current evidence citations but not carried forward as target naming); `packages/d2b-core/src/workload_identity.rs`: `WorkloadBackend::UnsafeLocal` → Host `isolationPosture="none"` (same evidence classification) | +| Process evidence class | `ProcessRole`/`VmProcessDag`/`ProcessNode`/`RoleProfile`/`PidfdTable` implemented-and-reachable; `ExecState`/`ExecStartRequest` implemented-and-reachable for guest exec; Process/EphemeralProcess ResourceTypes are ADR-only | +| Current anchor (unsafe-local) | `packages/d2b-unsafe-local-helper/src/`: `lib.rs`, `protocol.rs` (`HelperClient`), `runtime.rs` (`ScopeRuntime`, `SupervisorSpec`), `systemd.rs`; `packages/d2bd/src/unsafe_local_helper.rs`; `packages/d2b-realm-core/src/workload.rs`: `WorkloadProviderKind::UnsafeLocal`, `IsolationPosture::UnsafeLocal`; `packages/d2b-core/src/workload_identity.rs`: `WorkloadBackend::UnsafeLocal`; `nixos-modules/options-realms-workloads.nix` (`d2b.realms..workloads..kind = "unsafe-local"`) | +| unsafe-local evidence class | implemented-and-reachable (`HelperClient`/`ScopeRuntime`/`WorkloadProviderKind::UnsafeLocal` are all live); v3 user-only Host (`isolationPosture="none"`) is ADR-only | +| Current anchor (guestd) | `packages/d2b-guestd/src/`: `auth.rs`, `exec.rs` (`ExecPolicy`, `ExecState`, `ExecError`, `ExitOutcome`, `SpawnedProcess`, `RingChunk`), `exec_linux.rs`, `exec_pty.rs`, `detached.rs` (`ManagedUnit`, `UnitError`, `UnitIdentity`), `detached_registry.rs`, `service.rs`, `shell.rs` (`ShellRuntime`, `ShellRuntimeConfig`), `login_session.rs`; `packages/d2b-realm-core/src/execution.rs`: `ExecState`, `ExecAttachMode`, `ExecStartRequest` (guestd uses these DTOs in the vsock/ttrpc protocol) | +| guestd evidence class | implemented-and-reachable for guest exec; v3 EphemeralProcess/Process/ComponentSession is ADR-only | +| Current anchor (userd) | `packages/d2b-userd/src/lib.rs`: `UserdConfig`, `UserSessionIdentity`, `UserAttachRequest`, `UserdError`, `UserdTransport`, `UserExecSession` trait | +| userd evidence class | implemented-but-partially-wired; v3 User/Process domain is ADR-only | +| Current anchor (User) | `packages/d2b-realm-core/src/ids.rs`: `PrincipalId`, `RealmId`, `NodeId`, `WorkloadId` (identity foundation for Zone principal model); `packages/d2b-realm-core/src/access.rs`: `HostLocalPeerCredentialSemantics`, `HostLocalPeerCredentialSource`, `RealmAccessClientContract` (current host-local UID/credential resolution); `packages/d2bd/src/admission.rs` (`SO_PEERCRED` peer-uid); `nixos-modules/` host user configuration | +| User evidence class | `PrincipalId`/`HostLocalPeerCredentialSemantics` implemented-and-reachable for `SO_PEERCRED` authorization; OS UID/group NSS lookup reachable in broker/daemon; User ResourceType is ADR-only | +| Behavior retained | Fine-grained namespace/capability/seccomp/mount sandbox policy; pidfd identity/adoption; direct cgroup placement; systemd transient units; user-manager scope; typed readiness predicates; fail-closed identity checks; bounded audit/status messages; no-new-privileges enforcement; virtiofsd user namespace (ADR 0021) | +| Required delta | Host/Guest ResourceTypes; common Process/EphemeralProcess contract; ExecutionPolicy; User ResourceType; system-core reconcilers; Process Provider controllers; owner-child graph for Guests; TTL cleanup controller | +| Reuse path | Extract from exact sources named in work items below | +| Replacement/deletion | No ProcessRole/processes.json/workload DTO/unsafe-local-helper/guestd/userd path removed until successor ResourceType is integrated, all tests pass, and the exact disposition row is satisfied | +| Feasibility proof | system-core Host + User reconcile; system-systemd/system-minijail Process lifecycle; Guest with owned VMM Process; EphemeralProcess TTL; <=5 ms/<=20 ms fast path benchmarks | + +--- + +## Implementation work items + +### Provider crate standard layout + +Every `packages/d2b-provider--/` crate introduced by any work item in this spec or any downstream spec must satisfy the following directory and file layout. Absence of any required entry fails the workspace package-policy check (`make test-policy` / `cargo xtask delivery wave`). + +| Path | Required | Contents | +| --- | --- | --- | +| `src/` | yes | Implementation source, binary entry points (`main.rs` / `bin/`), and colocated unit tests (`#[cfg(test)]` modules). All provider controller, reconcile, service, and worker logic lives here. | +| `tests/` | yes | Hermetic Cargo integration tests with no external services or live sockets: ResourceType lifecycle (create/update/delete/watch), controller reconcile/finalize state machines, conformance gate (all shared process conformance cases must pass), and fault injection (crash/restart/timeout/overload). Tests in this directory are run by bare `cargo test -p `. | +| `integration/` | yes | Heavier fixtures and scenarios that require containers, live Hosts or Guests, cross-process IPC, or real Provider subsystems. These are invoked by the existing test orchestration (`make test-integration`, `make test-host-integration`) and are not run by bare `cargo test`. Each scenario must include a `README.md` describing its prerequisites and invocation. | +| `README.md` | yes | Provider identity and descriptor (ID, version, supported ResourceTypes); `spec.provider.settings` config schema with all fields, types, defaults, and validation rules; ResourceTypes reconciled by this Provider and their lifecycle guarantees; controllers, services, workers, and binaries with their roles and cgroup/principal placement; Host/Guest placement rules and restrictions; dependencies, RBAC verbs, and Credential requirements; security posture, state ownership model, and canonical telemetry labels; `cargo build`, `cargo test`, and integration test invocation commands; future standalone-repo usage notes. | + +A work item whose `Destination` row introduces a new `d2b-provider-*` crate must list all four required paths in that row and must include README content acceptance criteria in its `Validation` row. Work items that extend an existing crate (adding new source files to `src/` or new tests to `tests/`) inherit the layout from the introducing work item and need not repeat it. + +### ADR046-exec-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-001` | +| Dependency/owner | W0 shared contract root; `d2b-contracts` | +| Current source | `packages/d2b-core/src/processes.rs`: `ProcessRole` (18 variants), `ProcessNode`, `RoleProfile`, `NamespaceSet`, `MountPolicy`, `CgroupPlacement`, `ReadinessPredicate`; `packages/d2b-core/src/minijail_profile.rs`: `MinijailProfile`, `UserNamespaceProfile`, `NamespaceSet`, `MountPolicy`, `BindMount`, `CgroupPlacement`; `packages/d2b-core/src/storage.rs`: `StoragePathSpec`, `AclGrant`, `CleanupPolicy`, `RepairPolicy`; `packages/d2b-realm-core/src/ids.rs`: `RealmId`, `WorkloadId` (→ GuestRef), `NodeId` (→ HostRef), `ProviderId` (→ Provider ResourceRef), `ExecutionId` (→ EphemeralProcess exec identity), `PrincipalId` (→ User ResourceRef), `AllocatorLeaseId`, `ControllerGenerationId`; `packages/d2b-realm-core/src/workload.rs`: `WorkloadProviderKind` (`LocalVm`→runtime-cloud-hypervisor Provider, `QemuMedia`→runtime-qemu-media Provider, `ProviderManaged`→ACA/relay Providers, `UnsafeLocal`→user-only Host `isolationPosture="none"`), `IsolationPosture` (`VirtualMachine`→Guest, `ProviderManaged`→Guest, `UnsafeLocal`→Host `isolationPosture="none"`), `WorkloadExecutionPosture`, `WorkloadSummary`, `WorkloadState`; `packages/d2b-realm-core/src/target.rs`: `RealmTarget`, `TargetName`, `RealmTargetParser` (current analog for `/` ResourceRef parsing); `packages/d2b-realm-core/src/realm.rs`: `RealmPath`, `RealmControllerPlacement`, `EntrypointMode` (current Zone hierarchy analog); `packages/d2b-core/src/workload_identity.rs`: `WorkloadIdentity`, `WorkloadTarget` (= `RealmTarget`), `WorkloadBackend`, `WorkloadRuntimeIntent` (identity/backend separation reuse model for Host/Guest ResourceType split) | +| Reuse source | `packages/d2b-contracts/src/v3/` as destination; no equivalent main source for Host/Guest/Process ResourceType contracts | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/host.rs`, `packages/d2b-contracts/src/v3/guest.rs`, `packages/d2b-contracts/src/v3/execution_policy.rs`, `packages/d2b-contracts/src/v3/process.rs`, `packages/d2b-contracts/src/v3/ephemeral_process.rs`, `packages/d2b-contracts/src/v3/user.rs`, `packages/d2b-contracts/src/v3/endpoint.rs` | +| Detailed design | Implement strict typed Rust structs for HostSpec, GuestSpec, ExecutionPolicy, ExecutionSpec, SandboxSpec, BudgetSpec, MountSpec, NetworkUsageSpec, DeviceUsageSpec, EndpointSpec, EndpointConsumerPolicy, TelemetrySpec, ProcessSpec, EphemeralProcessSpec, UserSpec; EndpointSpec accepts exactly `owner\|provider\|zone`, and EndpointConsumerPolicy owns the only finer gates (`allowedSubjects`, `allowedProviderComponents`, `allowedOperations`) with no schema aliases; strict serde deny_unknown_fields; bounds/redaction on all string fields; stable error types; `UserSpec.osUsername` validated as OS username (1..255 bytes, no NUL/control/path-separator), not ResourceName grammar Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Provider dossiers, controller descriptors, Zone resource API, Nix resource compiler | +| Data migration | Full reset; no v2 resource import | +| Validation | Golden JSON vectors for each ResourceType; Endpoint vectors accept only `owner\|provider\|zone`, reject every legacy/private visibility alias, reject scalar/array `consumerPolicy` aliases, and cover each canonical consumer allowlist; a docs drift test parses every `type: Endpoint` YAML/Nix example and fails unless visibility is canonical and finer gates occur only under `consumerPolicy`; serde unknown-field rejection; bounds enforcement; `UserSpec.osUsername` OS-username validation (underscore allowed, NUL rejected) | +| Removal proof | Old DTO types removed only after owning Resource/Provider integrations are live | + +### ADR046-exec-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-002` | +| Dependency/owner | ADR046-exec-001; `d2b-contracts` | +| Current source | `packages/d2b-core/src/processes.rs`: `ReadinessPredicate`, `VmProcessInvariants`; `packages/d2b-core/src/processes.rs`: `SpawnRunnerPlanOp`; `packages/d2b-priv-broker/src/ops/` SpawnRunner | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/process_provider.rs`: LaunchTicket, ProcessIdentityDigest, AdoptionCandidate, PidfdEvidence, WaitReapOwner, BrokerTerminalResult, ProcessOutcome, ExitClass | +| Detailed design | LaunchTicket (Process/EphemeralProcess ref/UID/revision/generation, owner Provider/component/template, executionRef/domain/userRef, providerRef, compiled sandbox/budget/mount/device/network/endpoint digest, inherited FD table, operation/deadline/cancellation, expected identity/readiness); ProcessIdentityDigest (opaque bounded hex string); AdoptionCandidate (cgroup leaf path relative to controller root, start-time token, executable hash); BrokerTerminalResult binds process identity/operation to the clone3 parent's exact-once wait/reap status and cannot be constructed from pidfd readability; all types zeroizing where credential-adjacent Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | ProviderSupervisor adapter; system-systemd and system-minijail Process Providers | +| Data migration | Full reset | +| Validation | Golden LaunchTicket and BrokerTerminalResult vectors; field redaction test; digest-binding test; duplicate/mismatched/non-parent terminal relay rejection | +| Removal proof | None — net-new types; no prior owner to remove | + +### ADR046-exec-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-003` | +| Dependency/owner | ADR046-exec-001; system-core Provider owner | +| Current source | `packages/d2b-core/src/host_check.rs`: `HostCheckReport`, `HostCheckSummary`, `HostCheckFinding`, `HostCheckSeverity`; `packages/d2bd/src/pidfs_probe.rs`; `packages/d2bd/src/kernel_module_check.rs`; `packages/d2b-core/src/provider_capabilities.rs`; `packages/d2b-realm-core/src/ids.rs`: `HostResourceId` (current host-identity handle), `NodeId` (execution node identity); `packages/d2b-realm-core/src/node.rs`: `NodeKind::FullHost`, `NodeSummary` (host node's capability advertisement — direct reuse model for Host status `capabilities[]`) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-core/src/host.rs`: HostReconciler; status/conditions/capability probe implementation; `packages/d2b-provider-system-core/tests/`: hermetic reconcile/conformance/fault tests; `packages/d2b-provider-system-core/integration/`: Host probe and lifecycle integration scenarios; `packages/d2b-provider-system-core/README.md`: Provider identity, `spec.provider.settings` schema, ResourceTypes, placement, RBAC, security posture, telemetry labels, build/test commands (provider crate standard layout — see §Provider crate standard layout) | +| Detailed design | Async Host reconcile loop per this spec's Reconcile section; HostCapabilityClass probe set (kvm/pidfd/cgroup-v2/user-namespace/wayland/audio-pipewire/gpu-render/tpm2/usbip); bounded OS probes with timeout; mandatory system-minijail placement gate for Linux ≥5.14 plus writable delegated-leaf `cgroup.kill` independent of optional `kernelVersionMin`; `isolationPosture` validation and status; aggregate budget reservation tracking via List; status write with expected revision Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Provider/system-core fixed bootstrap process; ResourceClient Get/List/UpdateStatus | +| Data migration | New Host resources from Nix; no v2 state import | +| Validation | Multiple Hosts per Zone; system-only and user-only Hosts; capability probe mocks; Linux <5.14 and missing/unwritable `cgroup.kill` reject system-minijail before spawn; Linux ≥5.14 positive probe; `isolationPosture="none"` rejection of system processes; budget overcommit rejection; `tests/` all pass under `cargo test`; `integration/` scenario passes in container fixture; `README.md` present and covers all required sections (provider crate standard layout acceptance) | +| Removal proof | Current host capability checks in `d2bd` removed after Host reconcile parity | + +### ADR046-exec-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-004` | +| Dependency/owner | ADR046-exec-001; system-core Provider owner | +| Current source | `packages/d2b-userd/src/lib.rs`: `UserdConfig`, `UserSessionIdentity`; `packages/d2bd/src/admission.rs` (`SO_PEERCRED` UID/GID lookup); `packages/d2b-realm-core/src/ids.rs`: `PrincipalId` (current host-local principal identity); `packages/d2b-realm-core/src/access.rs`: `HostLocalPeerCredentialSemantics`, `HostLocalPeerCredentialSource`, `RealmAccessClientContract` (current host-local credential/uid resolution model; direct reuse precedent for v3 User → Process userRef resolution) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-core/src/user.rs`: UserReconciler; NSS lookup implementation | +| Detailed design | Async User reconcile loop per this spec's Reconcile section; NSS `getpwnam(spec.osUsername)` with bounded timeout (default 5 s); `spec.osUsername` validated on admission (1..255 bytes, no NUL/control/path-separator); `metadata.name` used only as Zone-local resource identity and `User/` ref; uid/gid/home/shell/group discovery written to status; session manager availability check via fixed user supervisor; status write; phase Degraded on SessionManagerReady=False Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Provider/system-core fixed bootstrap process; ResourceClient Get/List/UpdateStatus; other controllers resolve User status via ResourceClient | +| Data migration | New User resources from Nix; no v2 state import | +| Validation | User found/not-found; group membership; sessionManagerAvailable; multiple Users; deletion blocked by Process userRef; `spec.osUsername` with underscore succeeds NSS lookup where ResourceName grammar would reject it; `metadata.name` and `spec.osUsername` differ (e.g. `alice-admin` / `alice_admin`); `spec.osUsername` containing NUL/control/path-separator rejected at admission | +| Removal proof | Current local uid/group lookup in `d2bd/src/admission.rs` removed after User resource parity | + +### ADR046-exec-005 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-005` | +| Dependency/owner | ADR046-exec-001 + ADR046-exec-004; system-core Provider owner | +| Current source | `packages/d2bd/src/lib.rs` (daemon startup); `packages/d2b-core/src/host_check.rs`; `nixos-modules/host.nix` (host activation); `nixos-modules/options-host.nix` (host options) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-core/src/host.rs` (continued); bootstrap startup sequence | +| Detailed design | ProviderSystem-core fixed bootstrap: start with embedded Zone runtime and compiled bootstrap authorization; run Host reconcile before any Process Provider launches; create initial User resources from Nix; publish initial Role/RoleBinding from Nix config; hand off to stored RBAC | +| Integration | Zone runtime startup sequence; system-minijail bootstrap process launch | +| Data migration | Full v3 reset | +| Validation | Bootstrap without prior Host; User-before-Process ordering; compilation bootstrap authorization closed-set tests | +| Removal proof | Current d2bd initialization sequence removed after bootstrap parity | + +### ADR046-exec-006 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-006` | +| Dependency/owner | ADR046-exec-001 + ADR046-exec-002; system-systemd Process Provider owner | +| Current source | `packages/d2b-unsafe-local-helper/src/systemd.rs`; `packages/d2b-guestd/src/exec.rs`: `SystemdRunUnitManager`, `ManagedUnit`, `ExecPolicy`; `packages/d2b-guestd/src/exec_linux.rs`; `packages/d2bd/src/supervisor/` (transient unit management) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-systemd/src/`: launch.rs, adoption.rs, pidfd.rs, wait.rs, user_supervisor.rs; `packages/d2b-provider-system-systemd/tests/`: hermetic lifecycle/conformance/fault tests; `packages/d2b-provider-system-systemd/integration/`: transient-unit and user-supervisor integration scenarios; `packages/d2b-provider-system-systemd/README.md`: Provider identity, `spec.provider.settings` schema, ResourceTypes, placement, RBAC, security posture, telemetry labels, build/test commands (provider crate standard layout — see §Provider crate standard layout) | +| Detailed design | system-systemd Process/EphemeralProcess provider conformance per this spec's system-systemd conformance section; transient system unit (Type=exec); InvocationID+cgroup+MainPID+start-time binding before pidfd_open; systemd-owned wait/reap; user domain via fixed user supervisor; adoption re-verification; sandboxSpec compilation to systemd hardening directives; runtimeDeadline enforcement; drainTimeout SIGTERM/SIGKILL sequence Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Zone-installed Provider/system-systemd; ProviderSupervisor LaunchTicket interface; ResourceClient UpdateStatus | +| Data migration | Current ProcessRole/systemd unit roles converted by ProcessRole disposition table after parity | +| Validation | Shared process conformance test matrix (lifecycle/readiness/crash/drain/adoption/user-domain/sandboxSpec/pidfd); system-specific InvocationID binding; no-static-unit test; `tests/` all pass under `cargo test`; `integration/` scenario passes in container fixture; `README.md` present and covers all required sections (provider crate standard layout acceptance) | +| Removal proof | ProcessRole roles using systemd (Audio, WaylandProxy, VsockRelay, etc.) removed per disposition table after system-systemd parity | + +### ADR046-exec-007 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-007` | +| Dependency/owner | ADR046-exec-001 + ADR046-exec-002; system-minijail Process Provider owner | +| Current source | `packages/d2b-core/src/processes.rs`: `ProcessNode`, `RoleProfile`, `NamespaceSet`, `MountPolicy`, `CgroupPlacement`; `packages/d2b-core/src/minijail_profile.rs`: full; `packages/d2b-priv-broker/src/ops/spawn_runner.rs` (if present at baseline); `packages/d2bd/src/supervisor/` pidfd/wait; `packages/d2b-core/src/process_builder.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-minijail/src/`: sandbox_compiler.rs, launch.rs, adoption.rs, pidfd.rs, wait.rs, user_ns.rs; `packages/d2b-provider-system-minijail/tests/`: hermetic sandbox-compilation/lifecycle/conformance/fault tests; `packages/d2b-provider-system-minijail/integration/`: clone3/user-namespace and broker-spawn integration scenarios; `packages/d2b-provider-system-minijail/README.md`: Provider identity, `spec.provider.settings` schema, ResourceTypes, placement, RBAC, security posture (capabilities, namespaces, seccomp), telemetry labels, build/test commands (provider crate standard layout — see §Provider crate standard layout) | +| Detailed design | system-minijail Process/EphemeralProcess provider conformance per this spec's system-minijail conformance section; SandboxSpec-to-minijail plan compilation; Linux ≥5.14/cgroup.kill placement gate; broker clone3(CLONE_PIDFD|CLONE_INTO_CGROUP) parent retains sole waitid(P_PIDFD)/reap/exit-status ownership and relays a typed terminal result; ProviderSupervisor polls a verified duplicate and retains exact-main pidfd_send_signal semantics but never waits/reaps; adoption verifies original broker parent; runtimeDeadline/drainTimeout use graceful main signal then anchored leaf cgroup.kill, broker wait/reap, and empty-leaf proof; no PID/PGID fallback; EphemeralProcess one-shot launch Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Zone-installed Provider/system-minijail fixed bootstrap process; ProviderSupervisor LaunchTicket; privileged broker effect adapter | +| Data migration | Current RoleProfile/NamespaceSet/MountPolicy/CgroupPlacement adapted to SandboxSpec/BudgetSpec | +| Validation | Shared process conformance test matrix; minijail-specific sandbox compilation tests; user namespace tests; clone3 parent-only wait/reap and broker-relay tests; poll-readability-not-status test; pidfd_send_signal duplicate-holder test; setsid descendant/recycled-PGID cgroup.kill teardown test; Linux 5.14/cgroup.kill platform-gate tests; adoption quarantine asserts no signal/cgroup.kill; `tests/` all pass under `cargo test`; `integration/` scenario passes; `README.md` present and covers all required sections (provider crate standard layout acceptance) | +| Removal proof | ProcessRole roles using minijail (Virtiofsd, Swtpm, SecurityKeyFrontend, etc.) removed per disposition table after system-minijail parity | + +### ADR046-exec-008 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-008` | +| Dependency/owner | ADR046-exec-006 + ADR046-exec-007; conformance test owner | +| Current source | `packages/d2b-core/src/processes.rs` test coverage; `packages/d2bd/src/supervisor/` tests; minijail/seccomp test vectors | +| Reuse action | adapt | +| Destination | `packages/d2b-process-conformance/src/`: shared conformance test matrix run against both system-systemd and system-minijail providers | +| Detailed design | Full process lifecycle tests: start/ready/crash/restart/maxRestarts/drain/stop/delete; EphemeralProcess start/succeed/fail/ttl/cleanup; adoption after controller restart (fresh/quarantine); sandboxSpec compilation contract; pidfd rules (never-serialized/never-exported/re-verified-after-supervisor-restart; clone3 broker parent alone waits/reaps; non-parent readability is not status; verified duplicate holder may pidfd_send_signal exact main); system-minijail intentional teardown uses anchored cgroup.kill against setsid descendant and recycled-PGID fixtures, with no kill on ambiguity; Linux ≥5.14/cgroup.kill platform gate; user domain (system-systemd only); desiredLifecycle=stopped; fast path latency gate (<=5ms/<=20ms p95); 1/10/100 concurrent Process start | +| Integration | system-systemd and system-minijail providers must both pass all shared tests | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Hard pass/fail per-test; latency gates enforced; no exception for partial conformance | +| Removal proof | None — permanent conformance tests; no prior owner to remove | + +### ADR046-exec-009 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-009` | +| Dependency/owner | ADR046-exec-001; unsafe-local migration owner | +| Current source | `packages/d2b-unsafe-local-helper/src/lib.rs`: `UserdConfig`, protocol traits; `packages/d2b-unsafe-local-helper/src/runtime.rs` (`ScopeRuntime`, `SupervisorSpec`); `packages/d2b-unsafe-local-helper/src/systemd.rs`; `packages/d2bd/src/unsafe_local_helper.rs`; `packages/d2b-realm-core/src/workload.rs`: `WorkloadProviderKind::UnsafeLocal`, `IsolationPosture::UnsafeLocal` (current evidence that the no-isolation posture exists and is classified separately from VM isolation — the exact semantics this spec's `Host.spec.isolationPosture="none"` preserves); `packages/d2b-core/src/workload_identity.rs`: `WorkloadBackend::UnsafeLocal`; `nixos-modules/options-realms-workloads.nix` (`d2b.realms..workloads..kind = "unsafe-local"`) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-core/src/host.rs` (user-only no-isolation Host); `nixos-modules/options-zones.nix` (Nix unsafe-local Host declaration) | +| Detailed design | v3 unsafe-local migration: `kind = "unsafe-local"` in the current Nix Realm workload model becomes a Host resource with `providerRef: Provider/system-core`, `spec.isolationPosture: "none"`, `defaultDomain: user`, `allowedDomains: [user]`, `defaultUserRef: User/`. This is a Host ResourceType, not a Guest and not a v3 Provider. Child Process and EphemeralProcess resources on this Host use the normal Process Providers (Provider/system-systemd for user-domain transient user scope; Provider/system-minijail is also valid for callers that explicitly request namespace isolation within the user session). No special unsafe-local-specific Provider is introduced. The explicit no-isolation posture and its warnings are preserved: Host status reflects `isolationPosture="none"` and this is surfaced in every operator CLI/UI view as an explicit "no isolation boundary" warning; `ProcessEffect` audit records (launch, stop, adopt, quarantine) for child Processes and EphemeralProcesses carry the stable `no_isolation=true` attribute; operator CLI/UI always shows the warning and may not suppress it. The `no_isolation=true` attribute belongs on ProcessEffect records only — it must NOT appear on OTEL metric labels, span attributes, log fields, or audit records for other event kinds. The legacy helper protocol (`d2b-unsafe-local-helper`) is not exposed as a v3 ComponentSession service. | +| Integration | Host resource reconcile; User resource; system-systemd user-domain Process launch | +| Data migration | Full reset; no unsafe-local session state migration | +| Validation | User-only no-isolation Host rejected for system processes; `isolationPosture="none"` Host rejected for `allowedDomains` containing `system`; `allowedDomains=["user"]`+`defaultDomain=user`+`defaultUserRef` set with `isolationPosture=null` rejected at eval time (bidirectional evasion test); posture warning visible in CLI/UI status; `no_isolation=true` attribute present on ProcessEffect launch/stop/adopt/quarantine audit records for child Processes/EphemeralProcesses; `no_isolation=true` absent from OTEL span attributes, metric labels, log fields, and non-ProcessEffect audit records; user-domain Process under user-only Host starts correctly with normal Process Provider | +| Removal proof | `d2b-unsafe-local-helper` helper binary and protocol removed after user-only Host + shell-terminal Provider parity; `options-realms-workloads.nix` unsafe-local kind removed in Nix reset | + +### ADR046-exec-010 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-010` | +| Dependency/owner | ADR046-exec-001 + ADR046-exec-007; guestd migration owner | +| Current source | `packages/d2b-guestd/src/exec.rs`: `ExecPolicy`, `ExecError`, `ExecState`, `ExitOutcome`, `ValidatedCommand`, `SpawnedProcess`, `RingChunk`, `ExecSnapshot`, `ExecCreateInput`; `packages/d2b-guestd/src/exec_linux.rs`; `packages/d2b-guestd/src/exec_pty.rs`; `packages/d2b-guestd/src/detached.rs`: `ManagedUnit`, `UnitError`, `UnitIdentity`; `packages/d2b-guestd/src/detached_registry.rs`; `packages/d2b-guestd/src/service.rs`; `packages/d2b-guestd/src/auth.rs`; `packages/d2b-guestd/src/login_session.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-systemd/src/guest_exec.rs` (guest-domain EphemeralProcess launch via systemd-run inside guest); `packages/d2b-session/` (ComponentSession replacing ad-hoc guest ttrpc); runtime Provider guest bootstrap Process | +| Detailed design | Guest-side execution transitions to EphemeralProcess/Process resources under the owning Guest; guestd service becomes a fixed bootstrap Process owned by the runtime Provider controller; exec operations become EphemeralProcess creates via the Zone ResourceClient; detached sessions become EphemeralProcess with failedTtl=24h; shell sessions become shell-terminal Provider Processes; guestd auth.rs becomes ComponentSession via d2b-session (copy/adapt from ADR046-session-001 per ComponentSession spec) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | runtime-cloud-hypervisor Guest controller creates guest-bootstrap Process; Zone ResourceClient in guest-bootstrap creates EphemeralProcess/Process on behalf of guest workloads | +| Data migration | Full reset; no guestd session state migration | +| Validation | Guest EphemeralProcess exec lifecycle; detached exec TTL; guestd auth replaced by ComponentSession; no SSH fallback | +| Removal proof | `d2b-guestd` binary removed after all guest-side behaviors have Process/EphemeralProcess/ComponentSession successors and tests pass | + +### ADR046-exec-011 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-011` | +| Dependency/owner | ADR046-exec-004 + ADR046-exec-010; userd migration owner | +| Current source | `packages/d2b-userd/src/lib.rs`: `UserdConfig`, `UserSessionIdentity`, `UserAttachRequest`, `UserOutputCursor`, `UserExecSession` trait, `UserdError`, `UserdTransport`, `UnixSocketOnly`, `validate_attach_request`; `packages/d2b-userd/src/main.rs` | +| Reuse action | adapt | +| Destination | guest-domain process attachment becomes a ComponentSession named stream to the EphemeralProcess running in the guest; `UserExecSession` trait reimplemented as a typed ResourceClient+ComponentSession attachment | +| Detailed design | `UserdConfig.socket_name` replaced by Zone-local EphemeralProcess ResourceRef and ComponentSession attach verb; `UserSessionIdentity.uid`/`gid` become Process `userRef` resolved against User resource status; `UserAttachRequest.exec_id`/`tty`/`initial_size` become ComponentSession method parameters on the EphemeralProcess attachment service; `UserdError` becomes stable v3 error codes; `validate_attach_request` tty/size validation retained in the ComponentSession session schema Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (logic retained; protocol replaced). | +| Integration | CLI/client uses ResourceClient to Get/Watch EphemeralProcess; attaches via d2b-bus ComponentSession attach verb | +| Data migration | Full reset; no session state import | +| Validation | Attach/detach; TTY size; stream close; error path parity with UserdError variants | +| Removal proof | `d2b-userd` binary removed after ComponentSession attach parity on EphemeralProcess; all d2b exec CLI paths must use new attach mechanism | + +### ADR046-exec-012 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-012` | +| Dependency/owner | ADR046-exec-001; Nix resource compiler owner | +| Current source | `nixos-modules/options-realms-workloads.nix`: `d2b.realms..workloads..kind` = `local-vm`/`qemu-media` → `d2b.zones..resources.` (flat, `type="Guest"`); `unsafe-local` → `d2b.zones..resources.` (flat, `type="Host"`, user-only; see unsafe-local anchor); `nixos-modules/options-realms.nix`: top-level `d2b.realms` option shape including `providerKind` regex `^[a-z][a-z0-9-]*$` and `realmPath` regex `^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)*$` (Zone hierarchy path encoding); `nixos-modules/processes-json.nix`; `nixos-modules/options-host.nix`; `nixos-modules/host.nix`; `packages/d2b-realm-core/src/realm.rs`: `RealmPath`, `RealmControllerPlacement` (current Zone declaration structure analog) | +| Reuse action | adapt | +| Destination | `nixos-modules/options-zones.nix`: `d2b.zones..resources` option as `types.attrsOf (types.submodule resourceModule)` where each resource module has `type` (required enum), optional `metadata` submodule (`ownerRef`, `labels`, `annotations`), and `spec` (type-dependent, auto-generated submodule); `nixos-modules/zone-bundle.nix`: zone resource bundle emitter (see ADR046-exec-014); `nixos-modules/resource-schemas/`: generated per-type Nix option modules imported by `options-zones.nix` | +| Detailed design | `d2b.zones..resources` is a flat attrset; each entry has `type`, optional `metadata` (`ownerRef`, `labels`, `annotations`), and `spec`. The attrset key is the resource name. `spec` submodule fields and their Nix types/defaults/docs are auto-generated from the committed `packages/d2b-contracts/src/v3/schemas/.json` via `xtask gen-resource-nix-options`; no second vocabulary and no renaming of `spec.*` fields. `spec.provider.settings` sub-fields are constrained to the specific Provider's `providerNixSettingsSchema` attribute if present. All 17 eval-time validation rules from the "Eval-time validation rules" section are enforced by `lib.assertMsg` on the flat resource attrset. The `spec` object in the emitted JSON is the direct 1:1 serialization of the `spec` submodule. `metadata.ownerRef`, `metadata.labels`, `metadata.annotations` are serialized into the `metadata` object of the ResourceEnvelope JSON. `metadata.managedBy` and `metadata.configurationGeneration` are NOT in the bundle; they are set by the activation controller at runtime. `metadata.name` is the attrset key; `metadata.zone` is the enclosing zone key. Eval errors carry stable rule codes (1–17). Status is never present in the Nix option. `Guest.spec.systemArtifactId` (top-level spec field, not in `spec.provider.settings`) is validated against `d2b.artifacts` by rule 17; the `spec` submodule never contains derivation values or store paths. | +| Integration | Zone Nix configuration → eval-time validation → Nix-to-ResourceEnvelope compilers → zone bundle emitter (ADR046-exec-014) → configuration publication controller (ADR046-exec-015) | +| Data migration | Full reset; Realm/Workload options removed in the Nix reset wave | +| Validation | nix-unit eval/build tests 1–18 from the "Tests" section; eval validation rule tests 1–17 with expected error messages and stable codes; `spec` fields in emitted JSON match `spec` submodule values exactly (1:1 invariant test); `type` field in JSON matches `type` option value; `metadata.name` = attrset key; `metadata.zone` = enclosing zone key; `Guest.spec.systemArtifactId` plain string in resource bundle JSON at top-level spec (no store path, not in `spec.provider.settings`); missing/wrong-type artifact ID raises rule 17 eval error | +| Removal proof | Realm/Workload Nix options removed only after Zone resource Nix option parity and successful eval tests | + +### ADR046-exec-013 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-013` | +| Dependency/owner | ADR046-exec-008; EphemeralProcess cleanup controller owner | +| Current source | No direct current equivalent; current processes.json has no TTL/cleanup concept | +| Reuse action | create | +| Destination | `packages/d2b-core-controller/src/cleanup.rs`: EphemeralProcess TTL cleanup controller handler | +| Detailed design | Cleanup controller handler as specified in core-controllers spec; watches EphemeralProcess resources for terminal phase; computes cleanupEligibleAt from successfulTtl/failedTtl + completedAt; handles incidentHold; respects finalizers; issues normal Delete via ResourceClient; does not remove rows directly; bounded requeue-at for TTL expiry | +| Integration | core-controller process; ResourceClient Watch(EphemeralProcess); ResourceClient Delete | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Succeeded TTL default 1h; Failed TTL default 24h; incidentHold blocking; finalizer blocking; Delete with expected revision; cleanup controller restart recovery | +| Removal proof | None — net-new controller; no prior owner to remove | + +### ADR046-exec-014 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-014` | +| Dependency/owner | ADR046-exec-001 + ADR046-exec-012; Nix resource compiler owner | +| Current source | `nixos-modules/processes-json.nix` (current Nix-to-JSON serialization pattern); `nixos-modules/manifest.nix` (current bundle manifest emitter with `bundleVersion`, `manifestVersion`, integrity-tracking pattern); `packages/xtask/src/main.rs` (`gen-schemas`) (current schema-fingerprint generation method); `packages/d2b-core/src/bundle.rs`: `Bundle`, `BundleVersion`, `BundleManifest`, `BundleArtifact` (current bundle integrity model) | +| Reuse action | adapt | +| Destination | `nixos-modules/zone-bundle.nix`: Zone resource bundle emitter; `nixos-modules/resource-schemas/`: generated per-type Nix option submodules; `packages/d2b-contracts/src/v3/resource_bundle.rs`: `ResourceBundle`, `ResourceEnvelope`, `BundleManifest`, `BundleIntegrityPin` Rust types; `packages/xtask/src/gen_resource_schemas.rs`: `xtask gen-resource-schemas` (generates schema JSON and Nix option modules) | +| Detailed design | `zone-bundle.nix` iterates the flat `d2b.zones..resources` attrset (all types share the same attrset; each entry has `type`, optional `metadata`, and `spec`). For each entry it serializes the `spec` submodule to canonical JSON 1:1 (field names unchanged; keys sorted at every level; order-significant arrays preserve declaration order; semantically unordered arrays sorted). `metadata.ownerRef`, `metadata.labels`, `metadata.annotations` are serialized from the author-supplied `metadata` submodule into the envelope's `metadata` object. `metadata.managedBy` and `metadata.configurationGeneration` are NOT in the bundle envelope; they are set by the activation controller at runtime. `metadata.name` is the attrset key; `metadata.zone` is the zone key. `xtask gen-resource-schemas` generates both the schema JSON files AND the per-type Nix option submodule files under `nixos-modules/resource-schemas/` from the Rust DTO definitions; both must be regenerated when any ResourceType spec or Rust struct changes (same drift-gate pattern as current `make test-drift` / `xtask gen-schemas`). The bundle sorts all envelopes by `type` then `metadata.name` alphabetically, computes `bundleSha256` from the canonical JSON of the `resources` array, and emits the bundle manifest as a NixOS store artifact. No secret values, credentials, or OS paths not already declared in `spec` or `metadata` fields may appear in any envelope JSON. The bundle file includes private integrity metadata (resource type schema fingerprints, per-Provider schema fingerprints) alongside the `resources` array; these are bundle-level fields, never per-envelope fields. Alongside the resource bundle, the emitter installs the global private artifact catalog at `/etc/d2b/artifact-catalog.json` (root:d2bd 0640) mapping each `d2b.artifacts.` to `{ "sha256", "size", "storePath", "type" }`; this catalog is never included in public resource bundle envelopes. The bundle outer wrapper includes a `catalogSha256` binding the catalog to the bundle for activation-time integrity verification. | +| Integration | Zone Nix configuration → ADR046-exec-012 compilers → ADR046-exec-014 bundle emitter → `/etc/d2b/zones//resource-bundle.json` symlink → ADR046-exec-015 activation controller | +| Data migration | Full d2b 3.0 reset; new artifact replaces current `processes.json` and `manifest.json` pattern for Zone resources without importing v2 state | +| Validation | nix-unit test: bundle sort order (Host < Guest < Process; within type, names alphabetical); nix-unit test: `bundleSha256` recomputed from resources array matches recorded value; nix-unit test: schema fingerprints appear in private bundle file fields, not in any individual ResourceEnvelope `metadata` or `spec` object; nix-unit test: `catalogSha256` changes when any artifact derivation changes; nix-unit test: `schemaFingerprint` changes when ResourceTypeSchema JSON changes; nix-unit test: `providerSchemaFingerprint` changes when Provider settings schema changes and is null when Provider declares no schema; nix-unit test: no inline secret value passes through to bundle JSON; nix-unit test: identical configuration produces byte-identical bundle JSON across two builds; nix-unit test: artifact catalog JSON contains `storePath` field for each entry; nix-unit test: no envelope in `resources` array contains `storePath`, `nixSystem`, `schemaFingerprint`, or `providerSchemaFingerprint` | +| Removal proof | `nixos-modules/processes-json.nix` and current `manifest.nix` bundle artifact for Host/Guest/Process retained in parallel until Zone resource bundle replaces all roles; then removed | + +### ADR046-exec-015 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-015` | +| Dependency/owner | ADR046-exec-014 + ADR046-exec-001 + ADR046-exec-022; configuration generation controller owner | +| Current source | No direct current equivalent; current realm/workload configuration is applied at NixOS activation time directly, not through a Resource API generation controller | +| Reuse action | create | +| Destination | `packages/d2b-core-controller/src/configuration.rs`: `ZoneConfigController`, `GenerationState`, `PendingCleanup`, `BundleActivation`, `ActivationResult`, `ActivationError`, `GenerationDiff`, `DiffEntry`, `DiffKind`, `CleanupRecord`, `CleanupPhase`, `CleanupOutcome`, `RetentionPolicy`, `RetentionState`; `packages/d2b-core-controller/src/audit.rs`: audit event emission per the "Audit events" section | +| Detailed design | The Zone configuration controller (`packages/d2b-core-controller/src/configuration.rs`) runs as a fixed process in the `d2b-core-controller` crate. It watches `/etc/d2b/zones//resource-bundle.json` (inotify or polling). On change: (1) read and verify `bundleSha256` integrity; fail closed on mismatch, emit `d2b.zone.config.activate.error` with `config-bundle-integrity-failed`, make no changes. (2) Compare candidate `bundleSha256` against the currently active bundle record; if identical, this is a no-op re-activation (return immediately). (3) Verify resource type schema fingerprints (from bundle private fields) against committed schemas at `packages/d2b-contracts/src/v3/schemas/.json`; any mismatch fails the entire bundle closed with `config-schema-mismatch`, emits error, makes no changes. (4) Verify Provider schema fingerprints against installed Provider schemas; any mismatch fails the entire bundle closed with `provider-schema-mismatch`, emits error, makes no changes. (5) Verify `catalogSha256` binding against `/etc/d2b/artifact-catalog.json`; mismatch fails bundle closed. (6) Fetch current `metadata.managedBy="configuration"` resources via ResourceClient List (one call per type). (7) Compute `GenerationDiff` (new/changed/unchanged/removed) by type+name key. (8) Submit Create, UpdateSpec, and Delete intents concurrently with bounded async concurrency (default max 32 in-flight); activation returns after all intents are durably queued by the resource store, without waiting for reconcile loops to complete. For unchanged specs: issue UpdateConfigGeneration to refresh `metadata.configurationGeneration` to the new generation number; no controller reconcile triggered. For new resources: core sets `metadata.managedBy="configuration"` and `metadata.configurationGeneration=` on Create; if a same-name resource already exists with `metadata.managedBy="controller"` or `metadata.managedBy="api"`, record a per-item `config-collision` error for that resource without seizing it, emit error, continue other intents. For changed specs: submit UpdateSpec with `expectedRevision`; retry on optimistic lock conflict. For removed resources: submit Delete; set `metadata.deletionRequestedAt`. (9) Set Zone `phase=Pending` while create/update intents are outstanding. (10) Return after durable queue commit; do not block on reconcile completion. (11) The cleanup controller watches ResourceClient Watch streams for `Deleted` revision events (not polling GET) for each pending-cleanup resource by type+name+expectedRevision. When all finalizers release, the resource store commits the `Deleted` revision event atomically with row and index removal in a single transaction; following this commit, the audit subsystem appends the deletion audit record using a dedup/exactly-once recovery key (audit is NOT part of the atomic store transaction). Zone transitions to `phase=Degraded` immediately whenever any pending-cleanup item is outstanding. When all pending-cleanup items receive `Deleted` Watch events, `pendingCleanup` empties and Zone transitions to `phase=Ready`. Cleanup-stuck threshold: 10 min default; configurable; stuck resources remain Degraded without blocking later activations. Prior generation retention: controller retains the N most recently activated, cleanup-complete bundle records (default N=3; range 1..16; no time-based TTL). | +| Integration | Fixed process in `packages/d2b-core-controller`; ResourceClient (Create/UpdateSpec/UpdateConfigGeneration/UpdateStatus/Delete/List/Watch) per ADR046-exec-022; Zone resource status UpdateStatus (configGeneration, pendingCleanup, lastActivatedAt, lastActivationError); Zone `phase` transitions (Pending while outstanding intents; Degraded immediately when cleanup remains; Ready when create/update complete and cleanup empty); audit segment per ADR046-exec-014 audit events table | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Runtime/integration tests 19–23 from the "Tests for Nix configuration and ResourceType-specific lifecycle" section in this spec; additionally: `GenerationDiff` hermetic unit tests (new/changed/unchanged/removed classification); `bundleSha256` integrity failure aborts and emits correct audit event; `catalogSha256` mismatch aborts bundle; UpdateSpec optimistic lock conflict retried correctly; Watch `Deleted` revision events consumed (not polling GET) to track cleanup completion; Zone `phase=Pending` while intents outstanding; Zone `phase=Degraded` immediately when any cleanup outstanding (no grace window); Zone `phase=Ready` when complete; activation returns after durable queue commit, not after reconcile; same-name `managedBy=controller` OR `managedBy=api` collision emits per-item `config-collision` error without seizing resource, other intents continue; unchanged spec refreshes `configurationGeneration` without triggering controller reconcile; final deletion: atomic tx commits `Deleted` revision event + row/index removal only; audit append follows committed revision via dedup/exactly-once recovery (NOT part of atomic tx); recovery retry produces no duplicate audit record; prior bundle record released after cleanup-complete and retention count exceeded; activation with zero diff and identical bundleSha256 is a no-op | +| Removal proof | None — net-new controller; no prior owner to remove | + +### ADR046-user-session-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-user-session-001` | +| Dependency/owner | ADR046-zone-control-019 (authority index); `Provider/system-systemd` (user manager) + core/user-agent owner | +| Current source | None — the fixed user-session authority is today ambient prose across the display/audio/clipboard/notification/secret-service dossiers; no named owner exists | +| Reuse source | `Provider/system-systemd` user-manager scope; D077 EffectPort/LaunchTicket FD handoff | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/user_session_authority.rs` (or a core/user-agent per-session agent Process under `Provider/system-systemd`); `AuthorityDescriptor` on the session authority | +| Detailed design | Name and implement the **fixed user-session authority** (D097 desktop/session): `authorityScope: seat` bound to `(Host, User, login-session/seat)`, opaque `authorityKey` (never a raw socket path/XDG_RUNTIME_DIR/DISPLAY/seat name), `cardinality: exactly-one` per `(Host, User, login-session)`, `arbitration: exclusive`, owner = a core/user-agent per-user-session agent Process (NOT a new Provider), adoption by `ownerProof` (agent Process identity + login-session id), `exportability: forbidden`. It is the sole opener of the compositor/PipeWire/session-bus FDs and hands them to desktop Providers only via the EffectPort/LaunchTicket. Core's authority index rejects a duplicate session authority (or a duplicate same-user display portal, clipboard host, notification sink, audio mediator, systemd user manager, Secret Service, or seat-input claimant) with `duplicateConflict` before any FD open; multi-user/seat is admitted only up to the declared per-Host limit. Guest-stop invalidates every session authority/lease bound to that Guest across display/audio/notification/credential/shell in one dependency-aware cascade (D091), with no stale FD surviving. Host input (`wl_seat`/pointer constraints) is an `at-most-one`-per-seat authority under this session authority; pointer-constraint enforcement is a declared boundary until an interaction Provider implements it. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (name and implement the shared user-session authority). | +| Integration | Core authority index (ADR046-zone-control-019); `Provider/system-systemd` user manager; display/audio/clipboard/notification/secret-service/shell Provider services bind to this single authority for their FDs; D091 Guest-stop cascade | +| Data migration | None — full d2b 3.0 reset | +| Validation | Single session authority per `(Host, User, session)`; duplicate same-user session authority / desktop service rejected with `duplicateConflict`; multi-seat declared-limit enforcement; Guest-stop invalidates all bound desktop/audio/notification/credential/shell authorities and leases (no stale compositor/PipeWire/session-bus FD); seat-input second claimant rejected; adoption by `ownerProof` and quarantine on ambiguity; hermetic with fakes | +| Removal proof | Not applicable (net-new named authority; replaces ambient prose) | + +--- + +## Bus and ComponentSession reuse work items + +The following work items record exact reuse from main commit `a1cc0b2d` into the v3 +implementation. The pre-ADR45 v3 baseline has no equivalent for any of these; all +are ADR-only until the corresponding destination crate is created. Main commit +code is the sole reuse source. No item may claim the behavior is already +implemented on the pre-ADR45 v3 baseline. + +Each item records: exact main commit file and symbol set selected for reuse; +excluded ADR 0045 assumptions that must not be copied; v3 destination crate and +integration point. + +### ADR046-exec-016 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-016` | +| Dependency/owner | ADR046-exec-001; `d2b-session` v3 crate owner | +| Current source | No pre-ADR45 v3 baseline equivalent; ADR-only | +| Main reuse source | main `a1cc0b2d` `packages/d2b-session/src/`: `driver.rs` (`ComponentSessionDriver` trait, `SessionDriverHandle`); `engine.rs` (`SessionEngine`, `SessionEvent`); `handshake.rs` (`NoiseHandshake`, `EstablishedHandshake`, `HandshakeRole`, `HandshakeCredentials`, `NegotiatedOffer`, all encode/decode/negotiate functions, generation-discovery functions, `GENERATION_DISCOVERY_REQUEST_LEN`, `GENERATION_DISCOVERY_RESPONSE_LEN`); `lifecycle.rs` (`SessionLifecycle`, `SessionPhase`, `KeepaliveAction`); `record.rs` (`ProtectedRecord`, `RecordProtector`); `fragmentation.rs` (`Fragment`, `Fragmenter`, `Reassembler`); `scheduler.rs` (`FairScheduler`, `OutboundFrame`, `QueueClass`); `streams.rs` (`NamedStreamMux`, `StreamEvent`, `StreamId`, `StreamPhase`); `cancellation.rs` (`Cancellation`, `RequestRegistry`); `deadline.rs` (`DeadlineBudget`); `bootstrap.rs` (`BootstrapPsk`, `AdmittedBootstrapPsk`, `BootstrapAdmission`, `Secret32`); `attachment.rs` (`AttachmentPayload` trait, `AttachmentValidationError`, `OwnedAttachment`); `transport.rs` (`OwnedTransport` trait, `TransportDescriptor`, `TransportError`, `TransportPacket`); `metrics.rs` (`MetricEvent`, `MetricsSink`, `NoopMetrics`); `server.rs` (`serve_ttrpc_services`, `SessionServerError`); `error.rs` (`Result`, `SessionError`); `tests/component_session.rs` (all 10+ tests: fixed negotiation, Noise profiles, record protection, fragmentation, deadline, cancellation, lifecycle/keepalive/reconnect, named stream credit/fairness, bootstrap, transport portability); `tests/noise_vectors.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-bus-session/src/`: all above modules verbatim; `packages/d2b-bus-session/tests/`: all above tests verbatim | +| Detailed design | The entire `d2b-session` portable ComponentSession runtime is transport-agnostic and contains no ADR 0045 realm-specific types. All Noise handshake parameters, record framing, fragmentation, named stream mux, fair scheduler, cancellation, deadlines, bootstrap PSK, and attachment bindings are directly reusable. `ComponentSessionDriver` trait and `serve_ttrpc_services` are the primary integration surface for every v3 bus service. The `sessions/lib.rs` re-export boundary (`pub use d2b_contracts::v2_component_session as contract`) must be updated to point at the v3 wire contract module. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy verbatim; rename crate from `d2b-session` to `d2b-bus-session` or retain name. | +| Integration | All v3 bus service implementations (`d2b-zone-service`, `d2b-provider-agent`, `d2b-bus-client`) depend on this crate; EphemeralProcess attach service; Process Provider launch ticket channel | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Copy tests verbatim; all tests must pass on v3 baseline without modification; re-run `tests/noise_vectors.rs` golden vectors; add one v3-specific test: endpoint policy identity uses v3 `ZoneId`/zone-name binding rather than ADR45 `RealmId` | +| Removal proof | None — net-new; no prior owner to remove | +| Excluded ADR45 assumptions | `d2b_contracts::v2_component_session as contract` import alias points at v2 wire types; v3 must point at v3 bus wire types. No other realm/workload assumption exists in this crate. | + +### ADR046-exec-017 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-017` | +| Dependency/owner | ADR046-exec-016; `d2b-bus-session-unix` crate owner | +| Current source | `packages/d2b-session-unix/src/` at pre-ADR45 baseline `b5ddbed6` (partially equivalent: `d2b-session-unix` existed with `d2b-session-unix/src/adapter.rs` `UnixSeqpacketTransport`, `d2b-session-unix/src/descriptor.rs`, `d2b-session-unix/src/pidfd.rs`); main commit extends with credit pools, full pidfd identity verifier, and `host-socket` feature gate | +| Main reuse source | main `a1cc0b2d` `packages/d2b-session-unix/src/`: `adapter.rs` (`UnixSeqpacketTransport`, `UnixStreamTransport`, `UnixAttachmentPayload`, `OwnedUnixAttachment`, `PeerIdentityPolicy`, `DescriptorPolicyResolver`, `PathnamePeerVerifier`); `credit.rs` (`CreditPool`, `CreditScope`, `CreditScopeSet`, `CreditBundle`, `CreditError`, `ProcessCreditLimit`); `descriptor.rs` (`ReceivedPacket`, `VerifiedPacket`, `AcceptedAttachment`, `DescriptorPolicy`, `FirstPacketCredentials`, `ObjectIdentity`, `PeerCredentials`); `pidfd.rs` (`PidfdEvidence`, `PidfdIdentityVerifier`, `PidfdInfoSource`, `ProcPidfdIdentityVerifier`, `ProcSelfFdInfoSource`, `DigestEvidenceCallback`, `parse_pidfd_fdinfo`); `socket.rs`, `systemd.rs`, `vsock.rs`; `error.rs` (`UnixSessionError`); `tests/unix_session.rs` (20+ tests: seqpacket/stream transport, attachment transfer, SO_PEERCRED credential probing, pidfd identity verification, credit pool, full end-to-end session engine) | +| Reuse action | adapt | +| Destination | `packages/d2b-bus-session-unix/src/`: all above modules verbatim; `packages/d2b-bus-session-unix/tests/`: all above tests verbatim | +| Detailed design | Provides the Linux-specific `OwnedTransport` implementation for Unix seqpacket and stream sockets. `CreditPool`/`ProcessCreditLimit` enforces per-scope FD attachment budget (ADR45 constants: `MAX_PROCESS_ATTACHMENT_CREDITS = 2048`, `MAX_HOST_ATTACHMENT_CREDITS = 8192`, `RESERVED_CONTROL_FDS = 64`). `PidfdIdentityVerifier` provides the `/proc//fdinfo/` parse path that Process controllers use to verify process identity before `pidfd_open(2)` — this is a direct dependency of system-systemd and system-minijail Process Providers (ADR046-exec-006, ADR046-exec-007). Primary reuse disposition: `adapt`. Preserved source-plan detail: copy verbatim; rename crate from `d2b-session-unix` to `d2b-bus-session-unix` or retain name. | +| Integration | v3 Zone runtime public socket listener; system-minijail/system-systemd pidfd identity verification; EphemeralProcess attach named stream | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Copy all 20+ tests verbatim; all must pass; add v3-specific test: `PathnamePeerVerifier` verifies against `d2b-zonert` daemon uid; credit pool per-Zone-runtime limits match v3 constants | +| Removal proof | If the crate is renamed, the superseded `packages/d2b-session-unix/` owner is removed or reduced to a compatibility wrapper after `packages/d2b-bus-session-unix/` passes copied and v3-specific tests; if the name is retained, no prior owner is removed. | +| Excluded ADR45 assumptions | `d2b-daemon-access` uses `d2b_contracts::v2_identity::RealmId/RealmPath/WorkloadName` for its route table — not present in session-unix itself; no exclusions needed in this crate | + +### ADR046-exec-018 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-018` | +| Dependency/owner | ADR046-exec-016; `d2b-bus-wire` contract owner | +| Current source | No pre-ADR45 v3 baseline equivalent for v3 bus wire types; ADR-only | +| Main reuse source | main `a1cc0b2d` `packages/d2b-contracts/src/v2_component_session.rs`: all protocol constants (`PREFACE_MAGIC`, `COMPONENT_SESSION_MAJOR=2`, `COMPONENT_SESSION_MINOR=0`, `MAX_HANDSHAKE_OFFER_BYTES`, `HANDSHAKE_OFFER_CANONICAL_LEN`, `MAX_PROTECTED_CIPHERTEXT_BYTES`, `NOISE_TAG_BYTES`, `RECORD_LENGTH_BYTES`, `MAX_LOGICAL_MESSAGE_BYTES=1MiB`, `MAX_ACTIVE_NAMED_STREAMS=128`, `MAX_PACKET_ATTACHMENTS=32`, `MAX_REQUEST_ATTACHMENTS=64`, `MAX_OPERATION_ATTACHMENTS=128`, `MAX_SESSION_ATTACHMENTS=256`, `MAX_PROCESS_ATTACHMENT_CREDITS=2048`, `MAX_HOST_ATTACHMENT_CREDITS=8192`, `RESERVED_CONTROL_FDS=64`, `MAX_NAMED_STREAM_QUEUE_BYTES=256KiB`, `MAX_AGGREGATE_NAMED_STREAM_QUEUE_BYTES=4MiB`, `MAX_TTRPC_CONTROL_QUEUE_BYTES=2MiB`, `MAX_SESSION_CONTROL_QUEUE_BYTES=64KiB`, `MAX_CLOCK_SKEW_MS=30000`, `MAX_REQUEST_LIFETIME_MS=900000`, `LOCAL_HANDSHAKE_DEADLINE_MS=5000`, `REMOTE_HANDSHAKE_DEADLINE_MS=15000`, `LOCAL_RECONNECT_DEADLINE_MS=5000`, `REMOTE_RECONNECT_DEADLINE_MS=30000`, `MAX_RECONNECT_ATTEMPTS=10`, `MAX_RECONNECT_WINDOW_MS=300000`, `MAX_KEEPALIVE_INTERVAL_MS=60000`, `MAX_KEEPALIVE_TIMEOUT_MS=30000`, `RECORD_HEADER_LEN=24`, `FRAGMENT_HEADER_LEN=24`, `GUEST_SESSION_CREDENTIAL_MAGIC`, `GUEST_SESSION_CREDENTIAL_SCHEMA_VERSION`); `AttachmentDescriptor`; `LimitProfile`; `EndpointRole`; `PurposeClass`; `Locality`; `ServicePackage`; `RequestId`; `SessionErrorCode`; `BoundedVec`; `EndpointPolicy`; `EndpointPolicyIdentity`; `CancelRequest`/`CancelAck`/`CancelResult` | +| Reuse action | adapt | +| Destination | `packages/d2b-bus-wire/src/session.rs`: v3 bus protocol constants and wire types; all numeric constants copied verbatim; `PREFACE_MAGIC` retained; `EndpointPolicy` and `EndpointPolicyIdentity` adapted to use v3 `ZoneId`/`ProviderId` instead of ADR45 `RealmId` in the policy identity fingerprint | +| Detailed design | All numeric constants (frame sizes, credit limits, deadline values, reconnect limits) are directly reusable without change — they are derived from protocol analysis, not from realm semantics. `LimitProfile::local_default()` is the source for `serve_ttrpc_services` capacity; retain the value. `EndpointPolicy` carries the Noise static key and schema fingerprint; the fingerprint computation does not embed realm names and is reusable. The `EndpointPolicyIdentity` type carries the zone runtime's static public key — update from ADR45 `RealmId` to v3 `ZoneId` string encoding. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | `d2b-bus-session` imports constants from here; all v3 bus service and client crates import protocol constants from `d2b-bus-wire` | +| Data migration | Full d2b 3.0 reset; no existing constants module state to migrate | +| Validation | Compile-time assertions on all copied numeric constants matching source values; `EndpointPolicyIdentity` golden-vector test with v3 zone name encoding; `LimitProfile::local_default()` round-trip test | +| Removal proof | None — net-new; no prior owner to remove | +| Excluded ADR45 assumptions | `v2_identity.rs` `RealmId`/`RealmPath`/`WorkloadName` in the `EndpointPolicyIdentity` fingerprint must be replaced with v3 zone name string. `ServicePackage` enum variants referencing ADR45 realm/guest service names must be reviewed against v3 service inventory. `GUEST_SESSION_CREDENTIAL_*` constants are for guest-control bootstrap; adapt for v3 guest bootstrap credential if the format changes. | + +### ADR046-exec-019 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-019` | +| Dependency/owner | ADR046-exec-016 + ADR046-exec-018; `d2b-provider-runtime` crate owner | +| Current source | `packages/d2b-realm-provider/src/provider.rs` at baseline `b5ddbed6` (`HostSubstrateProvider`, `RuntimeProvider`, `WorkloadProvider` traits — baseline, unwired); ADR-only for the runtime registry and RPC proxy | +| Main reuse source | main `a1cc0b2d` `packages/d2b-provider/src/`: `registry.rs` (`ProviderRegistry`, `ProviderRegistryBuilder`, `ProviderRegistryManager`, `RegistryLimits`, `AdmittedProvider`, `InFlightPermit`, `AdmissionOptions`); `rpc.rs` (`SessionIdentity`, `ProviderClock`, `SystemProviderClock`, `RpcOperation`, `RpcPayload`, `RpcCall`, `RpcResponse`, `AuthenticatedProviderRpc` trait, `RpcProviderProxy`); `instance.rs` (`ProviderInstance`, `ProviderFactory`, `provider_capabilities_are_dispatchable`, `provider_inspection_method`, `provider_method_is_dispatchable`); `context.rs` (`OwnedOperationContext`, `CancellationToken`); `error.rs` (`FactoryError`, `ProviderRuntimeError`, `RegistryBuildError`, `RegistryShutdownReport`); `lib.rs` re-exports; re-exports from `d2b_contracts::v2_provider` (all Provider trait objects: `RuntimeProvider`, `StorageProvider`, `NetworkProvider`, `DeviceProvider`, `CredentialProvider`, `AudioProvider`, `DisplayProvider`, `InfrastructureProvider`, `ObservabilityProvider`, `SubstrateProvider`, `TransportProvider`) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-runtime/src/`: `registry.rs`, `rpc.rs`, `instance.rs`, `context.rs`, `error.rs`; provider trait objects moved to `d2b-bus-wire` or `d2b-provider-contracts` | +| Detailed design | `ProviderRegistry` manages in-flight permits, draining, and provider lifecycle. `RegistryLimits { total_in_flight, per_provider_in_flight }` is directly reusable. `RpcProviderProxy` wraps a `ComponentSessionDriver` and dispatches typed `RpcCall` to an `AuthenticatedProviderRpc` implementation — the proxy pattern is fully reusable for v3 Provider resource controllers. `SessionIdentity { peer_role, service, provider_id, provider_type, provider_generation }` maps directly to a v3 Provider session credential. `InFlightPermit` RAII guard is directly reusable. The provider trait object set (`RuntimeProvider`, `StorageProvider`, etc.) adapts to v3 Provider resource typed methods; the trait hierarchy is preserved but `ProviderMethod` enum variant names may be renamed to drop ADR45 workload terminology. `ProviderRegistry::MAX_PROVIDER_REGISTRY_ENTRIES` bound from v2_provider is retained. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | Zone runtime provider-agent ComponentSession; system-core Provider controller; every v3 Provider resource controller | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Copy `d2b-provider` tests; `RegistryLimits::validate` enforces non-zero and per≤total; `InFlightPermit` RAII release; drain/retire state machine; `RpcProviderProxy` round-trip over `FakeProvider` (from toolkit ADR046-exec-020) | +| Removal proof | Supersedes the baseline `packages/d2b-realm-provider/src/provider.rs` runtime/provider trait owner after `packages/d2b-provider-runtime/` registry/RPC tests pass and no v3 registry path imports ADR45 workload terminology. | +| Excluded ADR45 assumptions | `ProviderRegistrySnapshot`/`ProviderRegistryUpdate` and the `d2b_contracts::v2_provider::RegistryLifecycle`/`RegistryDrainPolicy` protocol belongs to the ADR45 provider-agent registration handshake; v3 replaces with Provider resource lifecycle. `ProviderRegistryAxis` — not needed in v3 flat provider registry. `ProviderId`/`ProviderType` from `v2_identity` must be rebased on v3 `Provider/` ResourceRef. | + +### ADR046-exec-020 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-020` | +| Dependency/owner | ADR046-exec-019; `d2b-provider-toolkit` owner | +| Current source | `packages/d2b-realm-provider/src/conformance.rs` at baseline `b5ddbed6` (`check_provider_conformance`); ADR-only for toolkit server and fixture | +| Main reuse source | main `a1cc0b2d` `packages/d2b-provider-toolkit/src/`: `adapter.rs` (`ProviderAgentAdapter` — wraps `ComponentSessionDriver` + `ProviderRegistry` into a session-driven dispatch loop); `server.rs` (`GeneratedProviderServiceServer` — wires generated ttrpc service stubs to `ProviderRegistry` dispatch); `conformance.rs` (`ConformanceError`, `check_descriptor_conformance`, `check_provider_conformance` — five conformance checks: Descriptor, CapabilityPublication, FixtureMismatch, Provider failure, ObservabilityQueryResult); `fixture.rs` (`DeterministicClock`, `FakeProvider`, `Fixture`, `sample_lease_request` — hermetic test fixture with all Provider trait implementations returning deterministic results); `redaction.rs` (`Redacted`, `Secret` — zero-copy redaction wrappers for audit/log outputs); `registration.rs` (`register_exact_instances`, `ToolkitError` — exact-instance registry validation); `values.rs` (`ProviderValues`); `lib.rs` re-exports | +| Main reuse source (provider-agent) | main `a1cc0b2d` `packages/d2b-gateway-runtime/src/provider_agent.rs`: `ProviderAgentProcess` (entry point for a provider-agent process: accepts a pre-registered `ProviderRegistry` + established `ComponentSessionDriver`, runs `MAX_DISPATCH_IN_FLIGHT=64` concurrent dispatch, bounded audit ring `DEFAULT_AUDIT_CAPACITY=1024`, shutdown within `SHUTDOWN_TIMEOUT=5s`); `run_registered`, `run`; `ProviderAgentError`, `ProviderAgentAuditEvent`, `ProviderAgentAuditOutcome` | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-toolkit/src/`: retain all modules verbatim; adapt `ProviderAgentAdapter` to use v3 `ProviderRegistry` (ADR046-exec-019) and v3 bus wire types; adapt `GeneratedProviderServiceServer` to use v3 generated service stubs (ADR046-exec-021); `packages/d2b-provider-agent/src/`: adapted from `gateway-runtime/src/provider_agent.rs` | +| Detailed design | `ProviderAgentAdapter` is the core: it drives a `ComponentSessionDriver` receive loop, dispatches decoded ttrpc frames to `ProviderRegistry`, and forwards responses. `GeneratedProviderServiceServer` closes the loop by registering all generated service stubs with `serve_ttrpc_services`. `FakeProvider` implements every v2 Provider trait with deterministic outputs — adapt each trait method to v3 Provider resource semantics while retaining the fixture pattern. `Redacted` / `Secret` zero-copy wrappers are used in every audit log path; copy verbatim. Provider conformance check pattern is retained: descriptor validation, capability publication, fixture round-trip, observability query result. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | Every v3 Provider resource controller uses `ProviderAgentAdapter` + `ProviderAgentProcess`; conformance tests gate Provider dossier acceptance; `FakeProvider` is used in all Provider controller hermetic tests | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Copy conformance tests verbatim; `check_descriptor_conformance` passes on a `FakeProvider` descriptor; `check_provider_conformance` covers all five `ConformanceError` variants; `ProviderAgentProcess` shutdown within deadline test; `MAX_DISPATCH_IN_FLIGHT` semaphore back-pressure test | +| Removal proof | Supersedes baseline `packages/d2b-realm-provider/src/conformance.rs` ownership only after `packages/d2b-provider-toolkit/` conformance coverage passes; ADR45 provider-agent registration behavior is not retained in the v3 provider-agent path. | +| Excluded ADR45 assumptions | `ProviderAgentAdapter` and `ProviderAgentProcess` use `d2b_contracts::v2_identity::ProviderId`/`ProviderType` and ADR45 `ProviderRegistrySnapshot` for registration — v3 replaces with Provider resource `spec.providerRef` and static registration. `GeneratedProviderServiceServer` registers ADR45 generated stubs; v3 must regenerate stubs from v3 service protobuf definitions (ADR046-exec-021). Audit ring in `provider_agent.rs` uses bounded `VecDeque` — retain size constants, adapt audit event type. | + +### ADR046-exec-021 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-021` | +| Dependency/owner | ADR046-exec-016 + ADR046-exec-018; v3 Zone service contract owner | +| Current source | No pre-ADR45 v3 baseline equivalent; ADR-only | +| Main reuse source | main `a1cc0b2d` `packages/d2b-contracts/src/generated_v2_services/`: all ttrpc stub files (`daemon.rs`, `daemon_ttrpc.rs`, `guest.rs`, `guest_ttrpc.rs`, `realm.rs`, `realm_ttrpc.rs`, `activation.rs`, `broker.rs`, `clipboard.rs`, `provider_storage.rs`, `provider_substrate.rs`, `provider_transport.rs`, `runtime_systemd_user.rs`, `security_key.rs`, `shell.rs`, `terminal.rs`, `tty.rs`, `user.rs`, `wayland.rs` and their `_ttrpc.rs` counterparts); `v2_services.rs` (`StrictWireMessage` trait, `MethodSpec`, `ServiceSpec`, `ServiceInventoryDocument`, `ServiceDocument`, `service_inventory_document`, `service_schema_fingerprint`, `public_daemon_schema_fingerprint`, `direct_guest_schema_fingerprint`, `ServerStreamLease`, `TerminalStreamValidator`, `TerminalFrameDirection`, `server_stream_name`/`parse_server_stream_name`, `admit_metadata`, `validate_terminal_open_response_for_request`, `validate_spawn_response_for_request`, `decode_spawn_response_for_request`, `validate_provider_response_for_method`); `v2_guest_services.rs`, `v2_guest_configured_launches.rs` (for guest exec message types); `d2bd/src/control_services/daemon.rs` (`DaemonServiceV2`, `DaemonOperationHandler` trait, `DaemonCallContext`, `DaemonMethod` enum, `DaemonPeerRole`, `DaemonAdapter`, `daemon_endpoint_policy`, `daemon_channel_binding`, `DaemonSeqpacketTransport`); `d2bd/src/control_services/provider.rs`, `guest.rs`, `realm.rs`, `allocator.rs`, `broker.rs` — service handler skeletons | +| Reuse action | adapt | +| Destination | `packages/d2b-bus-contracts/src/generated_v3_services/`: v3 generated ttrpc stubs for Zone service methods (Resource CRUD, Watch, ComponentSession service verbs); `packages/d2b-zone-service/src/`: Zone runtime service handler adapted from `DaemonServiceV2` pattern; `packages/d2b-zone-service/src/admission.rs`, `handler.rs`, `routing.rs` | +| Detailed design | `StrictWireMessage` trait (decode_strict, encode_strict) is directly reusable — it enforces deny-unknown-fields decode and schema-pinned fingerprint validation. `ServiceInventoryDocument` / `service_schema_fingerprint` pattern provides the service schema publication mechanism that v3 Provider resources use to advertise their ComponentSession service interface. `DaemonServiceV2` / `DaemonOperationHandler` pattern becomes the v3 Zone service handler base: `DaemonCallContext` → v3 `ZoneCallContext` with `ZoneId`, principal `User/`, operation deadline; `DaemonMethod` enum → v3 `ZoneMethod` (ResourceGet, ResourceList, ResourceWatch, ResourceCreate, ResourceUpdateSpec, ResourceUpdateStatus, ResourceDelete, BusAttach); `daemon_endpoint_policy` → v3 zone endpoint policy with v3 `ZoneId`-bound static key and schema fingerprint. `server_stream_name`/`parse_server_stream_name` for Watch stream naming is reusable verbatim. `TerminalStreamValidator` / `ServerStreamLease` for terminal byte stream safety is reusable for EphemeralProcess attach. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt; v3 generates new protobuf definitions and new ttrpc stubs; reuse message shaping, method dispatch patterns, and service inventory pattern. | +| Integration | Zone runtime ttrpc service over public socket ComponentSession; Provider resource controller attaches via `RpcProviderProxy`; ResourceClient Watch uses server stream naming convention | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `StrictWireMessage` decode rejects unknown fields on all v3 message types; `service_schema_fingerprint` is stable across builds; `ZoneCallContext` deadline enforcement test; `DaemonSeqpacketTransport` → `ZoneSeqpacketTransport` end-to-end roundtrip; generated v3 stub compile check | +| Removal proof | None — net-new; no prior owner to remove | +| Excluded ADR45 assumptions | ADR45 realm/workload/guest service operations (`realm_ttrpc.rs`, `guest_ttrpc.rs`, `v2_guest_services.rs`) map to ADR45 `WorkloadId`/`RealmId` — do NOT import these stub types directly into v3; regenerate from v3 proto definitions. ADR45 `DaemonMethod` variants that reference `ExecOp`/`ExecOpResponse`, workload-target routing, and realm management are replaced by v3 Resource verbs. `daemon_channel_binding(uid, gid)` channel binding token uses UID/GID directly — v3 uses `User/` ResourceRef binding; adapt. | + +### ADR046-exec-022 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-022` | +| Dependency/owner | ADR046-exec-016 + ADR046-exec-021; `d2b-bus-client` crate owner | +| Current source | No pre-ADR45 v3 baseline equivalent; ADR-only | +| Main reuse source | main `a1cc0b2d` `packages/d2b-client/src/`: `client.rs` (`Client` with `RetryPolicy`, `CallOptions`, `CancellationToken`, `MetadataInput`, `ConnectedClient`, `Response`, `WallClock`, `SystemClock`); `session.rs` (`ConnectedSession`, `SessionCall`, `SessionReply`, `NamedStream`, `SharedDriver`, `ComponentSessionConnector` trait, `SessionFailure`); `service.rs` (`ServiceHandle`, `MethodHandle`, `GeneratedClient`, `ServiceKind`); `daemon_service.rs` (`DaemonClient`, `DaemonLifecycleRequest`, `DaemonMethod`, `DaemonTerminal`, `daemon_call_options`); `guest_service.rs` (`GuestClient`, `GuestOperation`, `GuestCancelCall`, `GuestInspectCall`, `GuestRetainedLogCall`); `host_socket.rs` (`HostSocketConnector`, `local_daemon_endpoint_identity`); `target.rs` (`TargetResolver` trait, `RouteTable`, `ResolvedTarget`, `RouteRecord`, `TargetInput`, `TransportKind`, `TransportSelection`, `ServiceOwner`); `error.rs` (`ClientError`, `RemoteErrorKind`, `RetryClass`); `tests/client.rs` | +| Main reuse source (daemon-access) | main `a1cc0b2d` `packages/d2b-daemon-access/src/component_session.rs`: `LocalDaemonSession` (one authenticated local daemon session wrapping `DaemonClient`; connection via `connect_component_session` using `HostSocketConnector` + `local_daemon_endpoint_identity` + peer uid verification against `d2bd` uid); `connect_seqpacket` (blocking connect helper) | +| Reuse action | adapt | +| Destination | `packages/d2b-bus-client/src/`: all above modules; `DaemonClient` → `ZoneClient` (v3 Resource CRUD/Watch verbs); `GuestClient` → `ProcessAttachClient`; `HostSocketConnector` → `ZoneSocketConnector`; `LocalDaemonSession` → `LocalZoneSession` | +| Detailed design | `Client` is the core typed async client with bounded retry, wall-clock injection, and cancellation. `ConnectedSession` wraps a `ComponentSessionDriver` and provides `call()`, `open_stream()`, and `close()`. `ComponentSessionConnector` trait decouples connection establishment from the client — v3 `ZoneSocketConnector` implements this for the local Zone runtime public socket. `TargetResolver`/`RouteTable` provides request routing to local vs remote Zone runtimes. `HostSocketConnector::local_daemon_endpoint_identity` provides the peer identity pinning that prevents MITM on the local socket — this is a security-critical invariant; copy verbatim, rename from `d2bd` to `d2b-zonert` uid. `DaemonClient` method table adapts to v3 Resource verbs: `ResourceGet`, `ResourceList`, `ResourceWatch` (streaming), `ResourceCreate`, `ResourceUpdateSpec`, `ResourceUpdateStatus`, `ResourceDelete`. `ServiceHandle`/`MethodHandle`/`GeneratedClient` provide the typed client stub generation pattern. `RetryPolicy`/`RetryClass`/`RemoteErrorKind` error classification is directly reusable. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | CLI (`d2b` binary), external Zone API callers, Process/EphemeralProcess controller ResourceClient, all consumer-facing API paths | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Copy `tests/client.rs` verbatim; add v3-specific tests: `ResourceWatch` streaming teardown; `ZoneSocketConnector` peer-uid mismatch rejection; retry policy respects `RetryClass::Transient`/`Permanent`; `TargetInput`→`ResolvedTarget` for local-only v3 Zone; `local_daemon_endpoint_identity` returns correct v3 zone-rt uid | +| Removal proof | None — net-new; no prior owner to remove | +| Excluded ADR45 assumptions | `DaemonClient` methods reference `DaemonMethod` variants tied to ADR45 workload ops (`ExecOp`, `AllocatorOp`, workload lifecycle) — do NOT import ADR45 daemon service stubs; generate v3-specific client stubs from v3 service definitions. `GuestClient` / `GuestOperation` reference `WorkloadId`/`WorkloadName` scoping — replaced by `EphemeralProcess/` ResourceRef in v3. `d2b-daemon-access` connects via hardcoded `d2bd` user lookup — adapt to Zone runtime `d2b-zonert` user. `RouteTable` dedup key uses `(realm, workload)` tuples — v3 uses `(zone, resource-type, resource-name)`. | + +### ADR046-exec-023 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-exec-023` | +| Dependency/owner | ADR046-exec-021 + ADR046-exec-019; Zone bus routing owner | +| Current source | `packages/d2b-realm-router/src/` at baseline `b5ddbed6` (`OperationRouter`, `RouteDecision`, `OperationRoutePlan`, `ReconcilableLease` — unwired in daemon); ADR-only for Zone service routing with idempotency | +| Main reuse source | main `a1cc0b2d` `packages/d2b-realm-router/src/`: `lib.rs` (`OperationRouter`, dedup semantics: `(realm, principal, node, operation kind, idempotency key)` tuple namespace; tombstone after retention window; replay = same-key/same-request carries original `operation_id`; conflict = same-key/different-request; `IdempotencyKeyExpired` fail-closed after window); `service_v2.rs` (`RealmServiceServer`, `RealmServiceProcess`, `RealmSessionAuthority`, `RealmServiceLimits` (`DEFAULT_MAX_REALM_BINDINGS=256`, `DEFAULT_MAX_SHORTCUTS=256`, `DEFAULT_MAX_MUTATION_RECORDS=1024`, `DEFAULT_AUDIT_CAPACITY=1024`, `MAX_CONFIGURED_BOUND=4096`, `MAX_DISPATCH_IN_FLIGHT=64`), `RealmServiceError`, `RealmMethod`, `RealmAuditEvent`, `RealmAuditOutcome`, `CredentialCustody`, `REALM_SERVICE_NAME="d2b.realm.v2.RealmService"`); `session_lifecycle.rs`; `target_resolver.rs` (`TargetResolver` trait, `RealmEntrypointTable`, `RemoteFullHostAdapter`, `RemoteNodeRegistry`, `RemotePeerClient`); `remote_node.rs`; `execution.rs` (`DurableExecTable`, `DEFAULT_MAX_EXECUTIONS`); `d2bd/src/realm_stubs.rs` (dead-code wiring skeletons for `SharedRouter`, `PeerOperationRouter`, `ProviderExecutor`, `LocalExecutor`, `PeerDaemon`) | +| Reuse action | adapt | +| Destination | `packages/d2b-zone-router/src/`: `router.rs` (v3 `ZoneOperationRouter` — idempotency semantics copied verbatim; dedup key namespace adapted from `(realm, principal, node, kind, key)` to `(zone, resource-type, resource-name, verb, idempotency-key)`); `service.rs` (v3 `ZoneServiceLimits`, `ZoneServiceServer`, `ZoneAuditEvent`); `resolver.rs` (v3 `ZoneTargetResolver`, `ZoneEntrypointTable`) | +| Detailed design | The idempotency/dedup semantics from `OperationRouter` are security-critical and must be copied exactly: (1) dedup key is the full 5-tuple namespace — reusing a key under a different principal is a conflict, not a replay; (2) expired keys leave tombstones for a no-reuse horizon; (3) same-key/same-request returns the original `operation_id` and recorded result; (4) same-key/different-request returns conflict error fail-closed. These semantics apply to all v3 Resource mutation verbs (Create, UpdateSpec, UpdateStatus, Delete) that carry an idempotency key. `RealmServiceLimits` numeric bounds are copied verbatim: `MAX_DISPATCH_IN_FLIGHT=64` gates concurrent resource mutations per Zone session. `RealmSessionAuthority` principal-binding model (session principal MUST match request principal field, derived in trusted code from authenticated session) maps directly to v3 Zone RBAC: `ZoneCallContext` carries the authenticated `User/` principal from `SO_PEERCRED`; no caller-supplied principal field is accepted. `DurableExecTable`/`DEFAULT_MAX_EXECUTIONS` from `execution.rs` provides the EphemeralProcess in-flight table bound for the Zone router. | +| Integration | Zone runtime ttrpc service (ADR046-exec-021); Zone ResourceClient (ADR046-exec-022); every Resource mutation verb | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Idempotency replay returns original result; conflict returns error; expired tombstone fails closed; `MAX_DISPATCH_IN_FLIGHT` semaphore back-pressure; principal-binding enforcement (mismatched principal returns auth-denied); `DurableExecTable` capacity limit; v3 5-tuple dedup key golden vector test | +| Removal proof | Supersedes the baseline `packages/d2b-realm-router/src/` routing owner after `packages/d2b-zone-router/` passes idempotency, dispatch-limit, and principal-binding tests; ADR45 realm/workload route tables are not imported into v3. | +| Excluded ADR45 assumptions | `OperationRequest` envelope format uses ADR45 `RealmId`/`PrincipalId`/`NodeId`/`OperationKind` from `d2b-realm-core` — v3 `ZoneOperationRequest` uses v3 `ResourceRef`, `User/` principal, Resource verb enum; do NOT import `d2b-realm-core` types. `d2b-realm-router` `REALM_SERVICE_NAME = "d2b.realm.v2.RealmService"` — v3 uses `"d2b.zone.v3.ZoneService"`. `TargetResolver` resolves `RealmTarget` to node/provider — v3 `ZoneTargetResolver` resolves `ResourceRef` to local vs remote Zone runtime. `RemoteFullHostAdapter`/`RemotePeerClient` gateway routing is ADR45 constellation; v3 remote routing shape is out of scope for this spec. | diff --git a/docs/specs/ADR-046-resources-network.md b/docs/specs/ADR-046-resources-network.md new file mode 100644 index 000000000..548f5bc59 --- /dev/null +++ b/docs/specs/ADR-046-resources-network.md @@ -0,0 +1,2595 @@ +# ADR 0046 resources: Network + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-resources-network` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 2 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-provider-network-local`, `d2b-contracts` network types, Nix network emitter | +| Depends on | `ADR-046-resource-object-model`, `ADR-046-primitive-resource-composition`, `ADR-046-resource-reconciliation`, `ADR-046-provider-model-and-packaging`, `ADR-046-terminology-and-identities` | +| Supersedes | `d2b.envs.` and `d2b.realms..network` Nix surfaces (v3 reset) | + +## Purpose + +This spec defines the `Network` ResourceType, its provider `Provider/network-local`, +and the complete contract between the network fabric, its Host/Guest attachments, +and the controllers that reconcile it. + +A Network is an independently shared, lifecycle-managed local fabric. It owns: + +- two host kernel bridges (LAN and uplink) and their tap dispatch; +- one auto-declared owned net-VM Guest that runs NAT/DHCP/DNS/firewall; +- DHCP address reservations and DNS forwarding for all attached Guests; +- stateful nftables firewall and NAT rules on the host and inside the net VM; +- per-attachment isolation policy (east-west, hostBlocklist, egress CIDRs); +- optional external network attachment (macvtap) with port-forward and egress. + +`Provider/network-local` is the only initial Network Provider. Azure Container +Apps and Azure Virtual Machine networking remain inside their respective Guest +Providers (`Provider/runtime-azure-container-apps` and +`Provider/runtime-azure-virtual-machine`) until those networking surfaces +require independent sharing across Guests. See [Azure/ACA scope boundary](#azureaca-scope-boundary). + +## ResourceType threshold + +Network satisfies all threshold criteria from +`ADR-046-primitive-resource-composition`: + +- **independent identity**: a fabric exists before any workload Guest runs; +- **independent lifecycle**: bridges and the net VM outlive any single Guest; +- **independent controller/status**: the controller watches its own resource + class through the reconciliation loop; +- **sharing**: several Guest resources attach to one Network concurrently; +- **Provider substitution**: `Provider/network-local` may be replaced by a + future provider (e.g. WireGuard mesh, VLAN fabric) without changing the + ResourceType. + +## NetworkSpec + +### Three-layer spec shape (D089) + +D089 freezes Network spec as three layers. Layer 1 is the universal Resource +envelope and metadata. Layer 2 is the Network base spec at top-level `spec.*`, +including `spec.providerRef`; the CIDR, bridge, isolation, routing, DHCP/DNS, +attachment, mDNS, and net-VM fields documented here are base fields. Layer 3 is +the optional canonical selected-Provider extension +`spec.provider = { schemaId, schemaVersion, settings }`; it is the only +Provider-specific desired extension. It omits `providerRef` and +`observedProviderGeneration`: `spec.providerRef` is base, and spec is desired +rather than observed. + +**D091 update policy.** The universal base spec carries `spec.updatePolicy` for +every Network: disruptive changes default to manual, while automatic +non-disruptive upgrades are permitted by policy. A `spec.provider` extension MAY +add provider-specific knobs, but MUST NOT bypass or weaken base +`spec.updatePolicy`. + +**D090 expedited reconcile.** Authorized Network `Create`, `UpdateSpec`, and +`Delete` calls MAY set `waitForReconcile`. Under one mutation ticket, +`operationId`, and deadline, Core admission and the reserved-revision redb commit +run in parallel with controller preflight/plan, but the controller MUST NOT +perform external effects, finalizer release, or status mutation until Core +supplies `CommittedRevisionProof {resourceUid, generation, revision, +operationId}`; DB failure aborts with no effect. The API returns the committed +object plus one-pass projected layered status, `disposition` +(`Converged|Progressing|Blocked|UpgradeRequired|Failed`), `statusPersistence` +(`pending|committed`), and the last persisted status revision. The durable +commit is never rolled back on reconcile timeout or failure; effect idempotency +keys derive from `(UID,generation,revision,operationId)`, and the expedited pass +uses a bounded priority lane in the same per-resource single-flight. + +Every Network Provider `ResourceApiBinding` MUST implement the exact Network +base spec schema version and fingerprint, accept the canonical minimal valid +base Spec, and pass base lifecycle/status/error/finalizer conformance. A +Provider MAY reject an optional base capability only through its signed standard +capability matrix and a typed provider-neutral `unsupported-capability` error; +it MUST NOT ignore, reinterpret, rename, duplicate, weaken, or require extension +data for base-required behavior. `spec.provider.settings` is strict +deny-unknown, bounded, schema-versioned and digested, validated against +`spec.providerRef` at Nix build and API admission, and fails with +`spec-provider-schema-invalid` or `spec-provider-shadow` when invalid or +shadowing/restating/overriding/renaming/duplicating a base field. Shared Network +semantics are promoted to the Network base spec and never live in +`spec.provider`; generic CLI/controllers operate on base spec plus base status. +For the same Provider, the `spec.provider` and `status.provider` schemas align. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Network +metadata: + name: work-net + zone: dev + uid: + generation: 1 + revision: + ownerRef: null + finalizers: [] + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:00:00Z +spec: + providerRef: Provider/network-local + + # --- CIDR allocation --- + lanCidr: "10.20.0.0/24" + uplinkCidr: "192.0.2.0/30" + + # --- Layer-2/3 settings --- + mtu: null # null = 1500; applies to bridges, taps, and net-VM NICs + mssClamp: false # TCP MSS clamping in net-VM forward chain (tunneled uplinks) + + # --- Isolation policy --- + isolation: + allowEastWest: false + # allowEastWest = true is the explicit per-Network opt-in for east-west. + # No Zone-level gate is required. + + # --- Egress policy --- + routing: + hostBlocklist: + - "10.0.0.0/8" + - "172.16.0.0/12" + - "192.168.0.0/16" + - "169.254.0.0/16" + # The controller merges host network inventory from the Host resource and + # all peer-Network lanCidrs/uplinkCidrs into this list before emitting + # firewall rules. Caller-supplied entries are additive; duplicates deduplicated. + + # --- DHCP/DNS --- + dhcp: + domain: null # optional dnsmasq domain name for the LAN + ignoreClientNames: true # prevents workloads spoofing hostnames + # dhcp-authoritative is always on. DHCP pool: lanCidr.251–254 (unreserved). + # Static reservations derive from spec.attachments. + + dns: + forwarders: + - "1.1.1.1" + - "8.8.8.8" + cacheSize: 1000 + + # --- External network attachment (optional) --- + externalAttachment: null # or ExternalAttachmentSpec; see below + + # --- mDNS (optional) --- + mdns: + enable: false + # enable: true causes the Network controller to create owned Process + # resources for the mDNS reflector and, when dnsmasqLocal is true, the + # local DNS bridge. Both run inside the net-VM Guest via executionRef. + # Foundation requires every ordinary process to be a Process resource; + # mDNS is not an inline untracked systemd service. + reflector: true # create avahi reflector Process inside net VM + dnsmasqLocal: false # create local DNS bridge Process inside net VM + dnsmasqLocalPort: 53530 # listen port for dnsmasqLocal Process + publishWorkstation: false # avahi advertises workstation presence + + # --- Net-VM Guest name override --- + netVmNameOverride: null + # Null → "net-". Must match ResourceName regex. + # When set, the controller creates Guest/ instead. + + # --- Net-VM system artifact (REQUIRED) --- + netVmSystemArtifactId: "" + # Required. Must reference a declared d2b.artifacts entry with type = "nixos-system". + # Resolved and verified at Nix build time (Stage 2). Absent or wrong type is a + # hard build error. The resolved artifact ID is stored verbatim in + # Guest.spec.systemArtifactId. There is no implicit default; every Network must + # name its net-VM nixos-system artifact explicitly. + + # --- Host/Guest attachment table --- + attachments: [] + # List of AttachmentSpec; see below. +status: {} +``` + +### AttachmentSpec (inline in NetworkSpec) + +Each entry reserves one address and MAC on the LAN bridge for a Guest or Host: + +```yaml +executionRef: Guest/corp-vm +index: 10 +# index: 2..250; 1 is reserved for the net VM's LAN interface. +# Uniqueness within the Network is enforced at validateSpec time. +mac: null +# null = derive deterministically from (networkName, index) using mkMac. +# A fixed value must be a valid unicast MAC in colon notation. +# Indices are stable across reconcile cycles; changing an index +# changes both the DHCP reservation and the tap IfName. +``` + +Addresses: + +| Role | Formula | +| --- | --- | +| Host uplink IP | `uplinkCidr.1` | +| Net VM uplink IP | `uplinkCidr.2` | +| Net VM LAN IP | `lanCidr.1` | +| Workload IP for attachment at index N | `lanCidr.N` | +| DHCP dynamic pool | `lanCidr.251`–`lanCidr.254` | + +The `subnetIp` formula extracts the first three octets of the CIDR base and +appends the host ordinal, preserving the v3-baseline `subnetIp` helper in +`nixos-modules/lib.nix` line 399. + +### ExternalAttachmentSpec (inline in NetworkSpec) + +```yaml +externalAttachment: + mode: macvtap # only mode in the initial contract + parentInterface: eno1 # host physical interface; IfName-validated + macvtapMode: bridge # bridge | private | vepa | passthru + sharingPolicy: exclusive # exclusive | multiplexed; multiplexed MUST be + # explicitly authored and is valid only with bridge + mac: null # null = derive from (networkName, "home", 3) + + ipv4: + method: dhcp # dhcp | static + address: null # CIDR notation; required when method=static + gateway: null # required for static default route + dns: [] # static resolvers; used only when method=static + + egress: + enable: false + allowedCidrs: [] # CIDRs reachable through external0; MUST NOT + # overlap peer Network cidrs or the Host resource's + # network inventory + masquerade: true # MASQUERADE outbound on external0 + + portForwards: [] # list of PortForwardSpec; see below +``` + +`parentInterface` is a requested host inventory selector, not the authority +identity. Core resolves it against trusted Host network inventory and derives a +non-reversible `external-physical-nic/v1` identity. The resulting authority +index key is Host-global: +`(Host, external-physical-nic, opaqueKeyDigest)`. The digest is never supplied +by a caller or exposed in spec, status, audit, or telemetry. + +`passthru`, `private`, and `vepa` always require exclusive arbitration. +`bridge` is also exclusive by default. It becomes multiplexed only when every +claimant explicitly authors `sharingPolicy: multiplexed` and the signed +Provider quota admits another holder. An absent policy defaults exclusive; a +mixed exclusive/multiplexed set, a non-bridge multiplexed claim, or quota excess +fails closed with +`external-physical-nic-conflict` before macvtap creation or VMM spawn. Because +the index is Host-global, this rule applies across Zones on the same Host. + +### PortForwardSpec (inline in ExternalAttachmentSpec) + +```yaml +- protocol: tcp # tcp | udp + listenPort: 2222 # port on net VM external0 + targetRef: null # AttachmentSpec.executionRef resolved to + # its index→IP; mutually exclusive with targetIp + targetIp: null # explicit LAN IP; mutually exclusive with targetRef + targetPort: 22 + sourceCidrs: [] # optional ingress source filter +``` + +Both `targetRef` and `targetIp` null is rejected at validateSpec time. +Both non-null is also rejected. + +## NetworkStatus + +### Three-layer status shape (D088) + +D088 freezes `Network` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `Network`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +**D091 update currency.** Every Network includes universal `status.update` with +`state` (`Current|UpdateAvailable|UpgradeRequired|Upgrading|Blocked|Unknown`), +`reasons` +(`CoreGenerationChanged|ProviderGenerationChanged|ArtifactChanged|ImageOrSystemGenerationChanged|SpecChanged|DependencyChanged|SecurityPolicyChanged`), +bounded non-secret observed/target generation and digest IDs, `disruption` +(`None|Reload|Restart|Recycle|Replace`), `preserveState`, optional +`operationId`, `lastAssessedAt`, and bounded/truncated `owned:{count,refs}` and +`dependencies:{count,refs}`. Network-specific fabric currency refinements live +in `status.resource` and never in `status.provider`; controllers set +`status.update` via `assess_update` on core/provider/artifact/spec/dependency/ +security-policy triggers and MUST report `UpgradeRequired` for disruptive +changes rather than applying them in place. Disruptive fabric recycle affects +dependent attachments through the dependency-aware planner, which drains, +recycles, and restarts dependents instead of mutating them in place. + +The existing `status.network` sub-object is carried within `status.resource` as +`status.resource.network` by the mapping convention. `Network` currently uses +`network-local`; the provider-neutral Network fields are frozen in +`status.resource` for `network-local` and any future implementation. Stable +resource references, bounded readiness, external authority state, and +attachment phase are the shared cross-resource dependency surface. Runtime +addresses, interface names, MACs, authority keys, attachment handles, and the +implementation-specific firewall digest are not common fields. The +network-local digest belongs only in its `status.provider.details`; shared +fields MUST NOT be duplicated there. + +```yaml +status: + observedGeneration: 1 + phase: Ready # Pending | Ready | Succeeded | Degraded | Failed | Deleted | Unknown + conditions: + - type: FabricReady + status: "True" + reason: bridges-present + observedGeneration: 1 + lastTransitionAt: 2026-07-22T00:00:01Z + - type: NetVmReady + status: "True" + reason: guest-ready + observedGeneration: 1 + lastTransitionAt: 2026-07-22T00:00:02Z + - type: DhcpReady + status: "True" + reason: dnsmasq-bound + observedGeneration: 1 + lastTransitionAt: 2026-07-22T00:00:02Z + - type: FirewallReady + status: "True" + reason: nft-applied + observedGeneration: 1 + lastTransitionAt: 2026-07-22T00:00:01Z + - type: CidrConflict + status: "False" + reason: no-conflict + observedGeneration: 1 + lastTransitionAt: 2026-07-22T00:00:00Z + - type: ExternalNicAuthorityReady + status: "True" + reason: external-physical-nic-claimed + observedGeneration: 1 + lastTransitionAt: 2026-07-22T00:00:01Z + lastReconciledAt: 2026-07-22T00:00:02Z + outcome: null + + # --- Network-specific typed fields --- + network: + netVmRef: Guest/net-work-net # owned Guest ref + lanBridge: + phase: Ready + uplinkBridge: + phase: Ready + externalAttachment: null # or ExternalAttachmentStatus + attachments: [] # list of AttachmentStatus; see below + provider: + providerRef: Provider/network-local + schemaId: network-local.d2bus.org/Network/status + schemaVersion: 1.0.0 + observedProviderGeneration: 1 + details: + firewallDigest: "" # Network-owned rules only +``` + +### AttachmentStatus (inline in NetworkStatus) + +```yaml +- executionRef: Guest/corp-vm + phase: Ready # Pending | Ready | Degraded | Unknown +``` + +### ExternalAttachmentStatus (inline in NetworkStatus) + +```yaml +externalAttachment: + phase: Ready + authority: + available: true + holderCount: 1 + queueDepth: 0 + arbitration: exclusive + updateCurrency: Current +``` + +### Condition types + +| Type | Meaning | +| --- | --- | +| `FabricReady` | Host bridges and tap dispatch rules are present and correct | +| `NetVmReady` | Owned net-VM Guest is Ready per its own status | +| `DhcpReady` | dnsmasq is bound on the LAN bridge IP and DHCP reservations match spec | +| `FirewallReady` | nftables `inet d2b` table is applied and digest matches | +| `CidrConflict` | No CIDR overlap detected with peer Networks or the Host resource's network inventory | +| `ExternalNicAuthorityReady` | Core admitted or adopted the Host-global physical-NIC authority claim; absent external attachments report `not-required` | +| `ExternalAttachmentReady` | External macvtap interface is up and net VM external0 is configured | +| `ReconcileError` | Latest reconcile attempt produced a retryable or terminal error | +| `ConfigVolumeReady` | Owned config Volume is Ready and contains current configuration; guest-agent has applied it | +| `NetworkDraining` | Network deletion requested; owned resources and attachments are being removed | + +Phase rules: + +- `Ready`: `FabricReady`, `NetVmReady`, `DhcpReady`, `FirewallReady`, and + `ConfigVolumeReady` are all True; no `CidrConflict`; when an external + attachment is configured, `ExternalNicAuthorityReady` and + `ExternalAttachmentReady` are also True. +- `Degraded`: fabric is present but at least one condition is impaired; workloads + may be running with reduced capability. +- `Failed`: CIDR conflict, `external-physical-nic-conflict`, or another terminal + reconcile error; fabric cannot converge without a spec or sharing-policy + change. +- `Pending`: initial creation before first reconcile completes. +- `Unknown`: controller/host disconnect; last known state reported. +- `Succeeded`: reserved schema value; Network never steadily occupies this phase + (network operation is continuous, not a one-shot task). Present in the common + phase enum for schema compatibility. +- `Deleted`: schema value present in the common phase enum; appears only in the + final single store transaction that atomically removes the resource row and index. + No persisted resource row ever carries `phase = Deleted`; controllers wait for + the Deleted watch event or resource absence, not for a phase transition to `Deleted`. + +## Provider/network-local + +### Package and crate boundary + +`Provider/network-local` maps to one independently buildable crate +`packages/d2b-provider-network-local/`. It contains: + +- one controller binary `d2b-provider-network-local-ctrl`; +- one signed net-VM NixOS guest-config template library; +- declared exported `Network` ResourceType schema and controller descriptor; +- no dependency on `d2bd`, broker internals, or another Provider's + implementation. + +#### Required crate layout + +Every `packages/d2b-provider--/` crate must have all four of the +following paths present. **Absence of any path is a workspace/package policy +failure** (enforced by `xtask workspace-policy` and `make test-policy`): + +| Path | Role | +| --- | --- | +| `src/` | Implementation binaries and libraries; colocated `#[cfg(test)]` unit tests within each source file | +| `tests/` | Hermetic Cargo integration tests: ResourceType schema round-trips, controller state-machine tests (deterministic clock), conformance suite, fault-injection tests; no containers or external processes | +| `integration/` | Heavier fixture scenarios: container-based, Host/Guest lifecycle, cross-process, and provider-system tests; invoked by the existing test orchestration (`make test-integration` / `make test-host-integration`) not directly by `cargo test` | +| `README.md` | Documents: Provider identity and `providerRef` value; config schema and all `spec` fields per ResourceType; controllers, services, workers, and binaries produced; Host/Guest placement and executionRef requirements; dependencies and RBAC grants; security invariants, state ownership, and telemetry surface; exact `cargo build`, `cargo test`, `cargo test --test '*'`, and `integration/` invocation commands; future standalone-repo migration path | + +`src/`, `tests/`, and `integration/` must each contain at least one tracked +file; an empty directory does not satisfy the policy. The `README.md` must +cover all seven documented topics (identity through standalone-repo path). + +The crate depends on: + +- `d2b-contracts` for Network, IfName, and network-related DTOs; +- `d2b-controller-toolkit` for the async reconcile loop and ResourceClient; +- `d2b-host` for IfName derivation, nftables, bridge-port, and route-preflight + modules (extracted from the current `d2b-host` crate; see work items); +- `d2b-provider-toolkit` for Provider registration and conformance. + +The controller process runs as a Host Process resource under a system domain: + +```yaml +type: Process +metadata: + name: network-local-ctrl + zone: dev + ownerRef: Provider/network-local +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + processClass: controller + template: controller-main + sandbox: { ... } # broker-compiled minimal profile; no net caps + budget: { ... } +``` + +The controller has no ambient host capabilities; all host-kernel bridge/tap/ +nftables/sysctl effects are mediated through the broker via typed operations. + +### IfName derivation + +Interface names are derived deterministically from `(networkName, role, optional guestName)` using FNV-1a 64-bit, base32-encoded (Crockford alphabet, no I/L/O/U), truncated to 8 characters, prefixed by: + +| Role | Prefix | Example | +| --- | --- | --- | +| LAN bridge | `d2b-b` | `d2b-ba7mk3vp` | +| Uplink bridge | `d2b-b` | `d2b-b2qnxy4c` | +| Net-VM LAN tap | `d2b-t` | `d2b-t5f8rw9d` | +| Net-VM uplink tap | `d2b-t` | `d2b-tp3cjm6e` | +| Workload Guest tap | `d2b-t` | `d2b-tv8lzq1k` | +| External macvtap | `d2b-t` | `d2b-te4gnb0s` | + +All derived names satisfy the IFNAMSIZ-1 (15-byte) Linux constraint by +construction. The derivation algorithm is identical to +`packages/d2b-host/src/ifname.rs:derive_ifname`, which replaces the legacy +`br--lan` / `br--up` naming from `nixos-modules/network.nix`. + +Collision detection (`detect_collisions`) is re-run at every reconcile cycle. +A collision is a terminal condition; the controller reports `ReconcileError` +with `reason: ifname-collision` and halts reconciliation of the affected +Network. Operators must adjust the `networkName` to resolve collisions. + +The `IfNameMapping` table from `d2b-core/src/host.rs` (line 242) remains a +Core-private resolver input. It is not copied into Network status. Broker +adapters resolve user-visible resource identity to a kernel interface only +after authorization. + +### Net-VM Guest lifecycle + +The Network controller creates and owns one Guest resource for the net VM: + +```yaml +type: Guest +metadata: + name: net-work-net # or netVmNameOverride value + zone: dev + ownerRef: Network/work-net +spec: + providerRef: Provider/runtime-cloud-hypervisor + defaultDomain: system + allowedDomains: [system] + budget: { ... } + # Core resolves the Network owner relationship and supplies tap/macvtap FDs + # directly to the selected runtime's ProviderSupervisor through an authorized + # LaunchTicket. Runtime controllers, including runtime-qemu-media, receive + # only opaque Network/Endpoint refs. No IfName, address, MAC, + # parentInterface, fd, broker op, or authority key is copied here. + systemArtifactId: + # Artifact ID referencing the net-VM nixos-system artifact (type=nixos-system) + # in the artifact catalog. Set from the REQUIRED Network.spec.netVmSystemArtifactId; + # absent or wrong-type artifact fails closed at Nix build time. +``` + +The controller reads `Network.spec.netVmSystemArtifactId` (a REQUIRED field validated +at build time) and stores it verbatim in `Guest.spec.systemArtifactId`. The nixos-system +artifact contains only the **generic net-VM OS** — the guest-agent binary and runtime, +kernel, base NixOS services, and systemd-networkd NIC bootstrap with the +`lib.mkForce` override — but NOT per-Network desired data (DHCP reservations, +nftables rules, attachment table, routing policy). Per-Network configuration is +delivered at runtime through a controller-created config Volume; the guest-agent +reads the Volume view and applies dnsmasq/nftables/routing policy inside the net VM. +See [Config Volume and guest-agent delivery](#config-volume-and-guest-agent-delivery). + +Mutations to `Network.spec` that change only DHCP/DNS, firewall, or attachment +configuration update the config Volume and trigger a guest-agent reload; a Guest +switch or restart is NOT required for config-only changes. Mutations that change NIC +topology (attachment index changes, external attachment add/remove) additionally +require a Guest spec update; `Provider/runtime-cloud-hypervisor` reconciles the +Guest lifecycle accordingly. + +The net VM's NixOS config preserves the `lib.mkForce` override on the +`10-eth-dhcp` catch-all network definition. See [Security invariants](#security-invariants). + +### Config Volume and guest-agent delivery + +The Network controller creates and owns two additional child resources per Network +to deliver per-Network configuration to the net VM at runtime without requiring a +Guest switch on every spec change. + +#### Config Volume + +```yaml +type: Volume +metadata: + name: net--config + zone: + ownerRef: Network/ +spec: + providerRef: Provider/volume-local + kind: ephemeral # tmpfs-backed; boot-scoped; no persistent backing + source: + executionRef: Host/ # backing tmpfs allocated on this Host + settings: + kind: tmpfs # memory-backed; no hostPath; charged to Host memory budget + quota: + maxBytes: 4194304 # 4 MiB; tmpfs size= option; kernel-enforced + maxInodes: 128 # bounded nonzero; tmpfs nr_inodes= option + enforcement: hard # required for tmpfs (kernel enforces unconditionally) + layout: + - path: "" # Volume root directory + type: directory + ownerRef: User/net-local-controller + groupRef: User/net-local-controller + mode: "0750" + accessAcl: [] + defaultAcl: [] + noFollow: true + createPolicy: create-if-absent + repairPolicy: exact-owner + cleanupPolicy: owner-controlled + - path: "dnsmasq.conf" # DHCP reservations, forwarders, domain config + type: file + ownerRef: User/net-local-controller + groupRef: User/net-local-controller + mode: "0640" # owner rw; group r (guest view is read-only) + accessAcl: [] + defaultAcl: [] + noFollow: true + createPolicy: create-if-absent + repairPolicy: exact-owner + cleanupPolicy: owner-controlled + - path: "nftables.rules" # inet filter/nat chains rendered from spec + type: file + ownerRef: User/net-local-controller + groupRef: User/net-local-controller + mode: "0640" + accessAcl: [] + defaultAcl: [] + noFollow: true + createPolicy: create-if-absent + repairPolicy: exact-owner + cleanupPolicy: owner-controlled + - path: "routing.conf" # external attachment static routes + type: file + ownerRef: User/net-local-controller + groupRef: User/net-local-controller + mode: "0640" + accessAcl: [] + defaultAcl: [] + noFollow: true + createPolicy: create-if-absent + repairPolicy: exact-owner + cleanupPolicy: owner-controlled + - path: "attachments.json" # attachment table: MAC→IP, index→IfName + type: file + ownerRef: User/net-local-controller + groupRef: User/net-local-controller + mode: "0640" + accessAcl: [] + defaultAcl: [] + noFollow: true + createPolicy: create-if-absent + repairPolicy: exact-owner + cleanupPolicy: owner-controlled + views: + guest-readonly: + path: "" # root of the Volume subtree + rights: [read, traverse] # read file contents; traverse to enter directories + attachments: [] + # attachments is empty on initial create; the Guest attachment is added after + # the Guest reaches Ready (two-phase sequence — see Delivery lifecycle). +``` + +The Volume content is bounded: it carries only structured network configuration +rendered from `Network.spec`. No workload VM names, hostnames, or other per-workload +identifiers appear in paths or entries. `views.guest-readonly` grants read and +traverse rights exclusively to the declared Guest virtiofs attachment; the +`read, traverse` set is the minimum required for the agent to read config files. +`User/net-local-controller` is a proper Resource with full lifecycle. +`Provider/network-local`'s Nix package/module provisions the reserved +`net-local-controller` OS account with a private fixed UID/GID in Host +prerequisites and in the generic net-VM nixos-system artifact (same account and +UID/GID inside the Guest), ensuring virtiofs ACLs on config Volume layout entries +are enforced consistently on both sides. The network-local controller creates and +owns the `User/net-local-controller` Resource with `spec.osUsername: +net-local-controller` (`ownerRef: Provider/network-local`, `managedBy: controller`); +`Provider/system-core` verifies the account via NSS lookup and reconciles the User +Resource to Ready — it does not provision the OS account. Numeric UID/GID never +enter any ResourceSpec field, authz check, or audit record; `User.status` MAY carry +diagnostic `uid`/`gid` values discovered by NSS lookup, but those are informational +only and are never authorization inputs. The network-local +controller waits for `User/net-local-controller` to reach `Ready` before creating +any config Volume; this is a reconcile precondition, not a bootstrap side effect. +The tmpfs `quota.maxBytes = 4 MiB` is charged to the Host's memory budget at Volume +creation time. + +The Volume is provisioned in two phases: + +**Phase 1 — backing ready**: the controller creates the Volume with `source`, +`layout`, and `views` but an empty `attachments` list. The backing tmpfs on the +Host becomes Ready without any Guest attachment. The controller writes the initial +config content through the Volume write service before any Guest exists. + +**Phase 2 — Guest attachment**: after the Guest reaches Ready, the controller +updates the Volume to add the attachment entry: +```yaml +attachments: + - executionRef: Guest/ + transport: virtiofs + view: guest-readonly + access: read-only + mountPath: "/run/d2b/net-config" + settings: + posixAcl: false + xattr: false + cache: auto # auto | always | never + inodeFileHandles: never # never | prefer | mandatory + threadPoolSize: null # null → vcpu count of the target Guest + socketGroup: null # null → broker-default (runner gid) +``` +Only once the attachment reaches Ready may the guest-agent Process be created; +the agent's Volume mount depends on an active attachment. + +#### Guest-agent Process + +```yaml +type: Process +metadata: + name: net--agent + zone: + ownerRef: Network/ +spec: + providerRef: Provider/system-minijail + executionRef: Guest/ + domain: system + processClass: worker # narrow worker; no reconcile loop or d2b-bus authority + template: net-vm-agent + sandbox: + namespaceClasses: [] # empty: inherit all parent (Guest) namespaces; + # the Process runs inside the net-VM Guest and + # therefore inherits the Guest's network namespace, + # not the host's; no new CLONE_NEWNET is created + capabilityClasses: [network-admin, network-bind, network-raw] + # network-admin → CAP_NET_ADMIN: required for nft ruleset load and ip route + # network-bind → CAP_NET_BIND_SERVICE: required for dnsmasq binding to port 53 (DNS) + # and port 67 (DHCP server) + # network-raw → CAP_NET_RAW: required for dnsmasq's DHCP raw socket operations + # All three are granted within the inherited Guest network namespace only; + # none confers any capability on the host network namespace. + mounts: + - volumeRef: Volume/net--config + view: guest-readonly + mountPath: "/run/d2b/net-config" + access: read-only + required: true + budget: { } +``` + +The guest-agent binary is included in the nixos-system artifact (generic). On +startup it reads the config Volume view mounted at `/run/d2b/net-config`, enters its +reconciliation loop, and applies the current config: loading the nftables ruleset +(`nft -f /run/d2b/net-config/nftables.rules`), starting and configuring dnsmasq +from `/run/d2b/net-config/dnsmasq.conf`, and applying routing policy from +`/run/d2b/net-config/routing.conf`. `capabilityClasses: [network-admin, network-bind, network-raw]` +grants `CAP_NET_ADMIN` (for `nft` and `ip route`), `CAP_NET_BIND_SERVICE` (for +dnsmasq binding to port 53/DNS and port 67/DHCP), and `CAP_NET_RAW` (for dnsmasq's +DHCP raw socket). Because `namespaceClasses` is empty the Process inherits the +Guest VM's network namespace; all three capabilities are effective only within that +isolated Guest namespace and have no effect on the host network stack (INV-NET-009). On each Volume update notification (via the Volume service's +watch interface), the agent re-reads changed entries and applies the diff atomically +(SIGHUP dnsmasq for config changes; atomic `nft replace` for firewall changes). +The agent reports readiness predicates (`dnsmasq-bound`, `firewall-applied`) that +the Network controller uses to set `DhcpReady` and `FirewallReady` conditions. + +#### Delivery lifecycle + +| Event | Controller action | +| --- | --- | +| Network created | Create Volume (backing + layout + views; empty attachments); backing Ready → write config content → create Guest → Guest Ready → update Volume with Guest attachment → attachment Ready → create guest-agent Process | +| Network spec changes (DHCP/DNS/firewall/attachment config only) | Update Volume content; agent receives watch notification and applies diff; no Guest switch required | +| Network spec changes (NIC topology: attachment index add/remove, external attachment) | Update Volume content AND update Guest spec; Guest reconcile handles NIC changes | +| Guest-agent Process fails | Controller observes Process status; re-creates Process on terminal failure; sets `NetVmReady=False/agent-restart` | +| Network deleted | Delete guest-agent Process → remove Guest attachment from Volume → delete Guest → wait Deleted → delete Volume → wait Deleted → broker ops (nftables/bridges) → clear finalizer | + +### Bridge and tap lifecycle (host-side) + +**Bridge creation and deletion are dynamic broker operations.** `Provider/network-local` +creates and deletes host kernel bridge devices at reconcile time via new closed +broker operations (`CreateBridge` / `DeleteBridge`). A NixOS generation switch +is NOT required to create or remove a Network; the controller provisions all +fabric state at runtime. + +Nix still provisions bootstrap/static prerequisites and policy artifacts that do +not require knowing runtime bridge IfNames: + +- `networking.networkmanager.unmanaged` pattern block for the `d2b-*` prefix + (emitted via `00-d2b-unmanaged.conf`; covers all dynamically-created d2b + bridges and taps regardless of specific IfNames); +- schema validation and controller binary deployment; +- static host security policy artifacts. + +The broker's `CreateBridge` operation: + +- creates the kernel bridge device with the derived IfName; +- sets MTU from `spec.mtu` (or 1500); +- disables STP and multicast snooping unconditionally; +- applies IPv6 suppression sysctls + (`net.ipv6.conf..disable_ipv6 = 1`, + `net.ipv6.conf..accept_ra = 0`, + `net.ipv6.conf..autoconf = 0`) + atomically before returning, closing any race between interface + creation and the controller's subsequent `ApplySysctl` defense-in-depth step. + +The broker's `DeleteBridge` operation removes only the kernel bridge device, +after every persistent tap has been confirmed removed through +`DeletePersistentTap`. It never cascades deletion to an attached tap: a +remaining d2b-owned tap is retryable after tap cleanup, and a foreign +port/marker fails closed. It is idempotent when the bridge is already absent. + +The network-local controller performs the following runtime effects through the +broker's typed effect interface: + +| Broker op | Current source | Purpose | +| --- | --- | --- | +| `CreateBridge` | **New broker op** (v3; no v3-baseline equivalent) | Create host kernel bridge with derived IfName, MTU, STP/multicast-snooping disabled, IPv6 suppression sysctls applied atomically | +| `DeleteBridge` | **New broker op** (v3; no v3-baseline equivalent) | Remove an empty host kernel bridge device; never cascade tap deletion; idempotent on absence | +| `ApplyNftables` | `d2b_contracts::broker_wire::ApplyNftablesRequest`; `d2b-host/src/nftables.rs` | Install/replace the host-side `inet d2b` table | +| `ApplyNmUnmanaged` | `d2b_contracts::broker_wire::ApplyNmUnmanagedRequest` | Write `00-d2b-unmanaged.conf` bridge/tap pattern block | +| `ApplyRoute` | `d2b_contracts::broker_wire::ApplyRouteRequest`; `d2b-host/src/routes.rs` | Static host route to LAN CIDR via uplink bridge | +| `ApplySysctl` | `d2b_contracts::broker_wire::ApplySysctlRequest`; `d2b-host/src/netlink.rs` | Per-bridge IPv6 suppression defense-in-depth (re-applied after networkd restart or sysctl drift) | +| `CreatePersistentTap` | Existing closed broker op | Create/adopt the persistent tap for an opaque attachment realization | +| `DeletePersistentTap` | **New closed broker op** paired with `CreatePersistentTap` | Delete exactly one opaque, generation-fenced, d2b-owned persistent tap; validated absence is success | +| `SetBridgePortFlags` | `d2b_contracts::broker_wire::SetBridgePortFlagsRequest`; `d2b-host/src/bridge_port.rs` | Isolated/neigh-suppress per-tap after tap creation | +| `UpdateHostsFile` | `d2b_contracts::broker_wire::UpdateHostsFileRequest` | VM→IP entries in the `d2b-managed` /etc/hosts block | +| `SeedDnsmasqLease` | `d2b_contracts::broker_wire::SeedDnsmasqLeaseRequest` | Pre-seed DHCP reservations for known attachment MACs | + +IPv6 is suppressed per-bridge at `CreateBridge` time (atomically by the broker) +AND via `ApplySysctl` at each reconcile cycle (defense-in-depth, handling +`systemctl restart systemd-networkd` and any other sysctl drift). No +boot-time Nix sysctl entry is required for specific bridge IfNames because +bridges are created dynamically and do not exist at host activation. + +**Persistent-tap creation** uses one operation chain. The network-local +controller declares an opaque attachment realization through +`NetworkEffectPort`. Its Core-owned adapter maps that semantic effect to +`CreatePersistentTap`, applies `SetBridgePortFlags`, and transfers the +already-authorized connected `OwnedFd` directly to ProviderSupervisor for the +selected Guest runtime's Process LaunchTicket. For QEMU, the qemu +Provider/controller receives only opaque Network/Endpoint refs; it receives no +broker operation or fd. The fd is never represented in a ResourceSpec/status, +serialized on d2b-bus, or delivered through a Provider controller +ComponentSession. + +`CreateTapFd` is a distinct baseline operation-scoped, non-persistent +SCM_RIGHTS path. This ADR does not use it for Network attachment realizations; +the retained Network lifecycle and generation-fenced deletion require the +canonical `CreatePersistentTap`/`DeletePersistentTap` pair. + +The Network adapter owns the connected `OwnedFd` with `FD_CLOEXEC` set until +ProviderSupervisor accepts it. ProviderSupervisor keeps its parent copy +CLOEXEC, makes only the declared child fd slot inheritable immediately before +exec, and closes its copy after successful spawn. The Guest VMM owns the child +copy until exit. Cancellation, LaunchTicket rejection, or spawn failure closes +every copy before the adapter invokes generation-fenced +`DeletePersistentTap`; the opaque realization remains retained until deletion +is confirmed. On attachment removal or Network finalization, network-local +likewise waits for the Guest/VMM fd owner to close before invoking +`DeletePersistentTap`. + +`DeletePersistentTapRequest` contains only an opaque attachment ID, +`expectedNetworkGeneration`, and `expectedAttachmentGeneration`. It accepts no +IfName, path, or caller-authored marker. The broker resolves trusted private +realization state, validates both generations and the d2b ownership marker, +then deletes only that tap. Already-absent is idempotent success only when the +trusted record and marker state show no foreign replacement; a stale +generation or foreign marker fails closed without deletion. The controller +retains the opaque attachment handle and retries retryable failures before +advancing or clearing a finalizer. + +### DHCP and DNS lifecycle (inside net VM) + +dnsmasq runs inside the net VM supervised by the guest-agent Process +(`Process/net--agent`). The Network controller writes `dnsmasq.conf` +to the config Volume; the guest-agent reads it from the mounted read-only Volume +view and manages dnsmasq as a supervised child process. The nixos-system artifact +does not encode per-Network dnsmasq configuration; all DHCP/DNS desired state flows +through the config Volume. + +Key dnsmasq invariants preserved from `nixos-modules/net.nix` lines 302–441, +now encoded in the controller-rendered `dnsmasq.conf` Volume entry: + +- `bind-interfaces = true` binds only to `eth1` (LAN interface); +- `dhcp-ignore-names = true` prevents hostname spoofing; +- static DHCP host reservations are derived from `spec.attachments[]`; +- DHCP dynamic pool covers `lanCidr.251`–`lanCidr.254`; +- DNS forwarders are set from `spec.dns.forwarders`; +- the guest-agent runs dnsmasq under the `dnsmasq` system user with hardened + confinement (preserving the hardening from net.nix lines 363–441). + +The `DhcpReady` condition is set by the network-local controller observing the +guest-agent Process status. The guest-agent reports the `dnsmasq-bound` readiness +predicate when dnsmasq has bound its socket. The controller does NOT manage dnsmasq +directly; the guest-agent owns its lifecycle inside the net VM. + +### Firewall and NAT lifecycle + +**Host side** (`inet d2b` table, `d2b-host/src/nftables.rs`): + +The network-local controller applies nftables rules through the broker +`ApplyNftables` operation. The `inet d2b` table: + +- blocks all traffic on LAN bridges (host has no IP there); +- installs per-rule `comment "d2b managed: "` markers + (ownership ID is the Network resource UID); +- coexists with other firewall managers per the `FirewallCoexistencePolicy` + (Coexist/Refuse/RequireUnmanaged matrix preserved from + `packages/d2b-host/src/nftables.rs`). + +Network-local emits no TCP/3240 or other USBIP allow rule. Its +`status.provider.details.firewallDigest` is the SHA-256 of the canonical +projection containing only rules owned by that Network UID. USBIP-owned chains, +rules, and ownership markers are excluded, so USBIP attach/detach cannot create +false Network drift. Network-local compares only this ownership-scoped digest +on each observe cycle. + +**Net VM side** (nftables rules delivered through the config Volume): + +The Network controller writes the net VM's nftables ruleset to the `nftables.rules` +entry in the config Volume; the guest-agent reads this entry from the mounted +read-only Volume view and applies it atomically using `nft -f`. The nixos-system +artifact does not encode per-Network nftables rules; all firewall desired state flows +through the config Volume. The ruleset preserves all semantics from +`nixos-modules/net.nix` lines 168–296: + +- `ip6 filter` table with policy drop on all chains (d2b is IPv4-only); +- `inet filter input` chain with stateful connection tracking; +- LAN DHCP/DNS accept (UDP/53, UDP/67, TCP/53 on `eth1`); +- ICMP echo rate-limited to 10/s burst 20; +- `inet filter forward` chain with per-env east-west, hostBlocklist drop, + internet egress allow, external-network egress CIDRs; +- MSS clamp rule when `spec.mssClamp = true`; +- `inet nat postrouting` MASQUERADE on `eth0` (internet uplink); +- optional `MASQUERADE` on `external0` when egress.masquerade is true; +- optional DNAT prerouting rules from `spec.externalAttachment.portForwards`. + +IPv6 is dropped on all chains in the net VM as well as suppressed at the +interface level via `disable_ipv6 = 1` sysctl. + +### External network attachment lifecycle + +When `spec.externalAttachment` is non-null, the network-local controller: + +1. Core resolves the declared external-attachment intent from the owning + Network, admits its Host-global physical-NIC authority, and records only an + opaque claim/owner proof. The Network controller does not copy + `parentInterface` or an authority key into the net-VM Guest spec and does not + call a macvtap creation broker op. No `CreateMacvtap` broker op exists in the + v3 baseline. +2. Core supplies the admitted attachment intent to + `Provider/runtime-cloud-hypervisor` through the private owner/dependency + resolver and an authorized LaunchTicket. The runtime calls `SpawnRunner` on + the broker. The broker creates the macvtap FD internally (`live_create_macvtap_fd` in + `packages/d2b-priv-broker/src/runtime.rs` line 5097) as part of VMM spawn + dispatch, resolving the macvtap intent from `processes.json` fields + (`ProcessMacvtapInterface` in `packages/d2b-core/src/processes.rs`). +3. The net VM configures `external0` using systemd-networkd (DHCP or static, + preserving the logic from `nixos-modules/net.nix` lines 106–146). +4. Port-forward DNAT rules are written to the `nftables.rules` config Volume + entry by the Network controller and applied by the guest-agent inside the net VM. +5. Egress CIDRs are included in the forward chain and postrouting chain. + +Before step 1, Core resolves `parentInterface` against trusted Host inventory +and admits the Host-global `external-physical-nic/v1` authority claim. No +macvtap or VMM effect is permitted until `ExternalNicAuthorityReady=True`. +`passthru`, `private`, and `vepa` claims are exclusive. `bridge` is exclusive +unless every concurrent claimant explicitly requests compatible multiplexing. +A same- or cross-Zone conflict sets +`ExternalNicAuthorityReady=False/external-physical-nic-conflict` and performs no +host effect. + +`parentInterface`, `macvtapMode`, or sharing-policy changes require a disruptive +drain/recycle. Core drains the net VM and dependent attachments, releases the +old authority only after the old macvtap FD is closed, admits the replacement +claim, and then starts the replacement. Delete follows the same order and +releases the claim last. Restart adopts only an exact resource/process +`ownerProof`; ambiguity quarantines rather than creating a second attachment. +The `ExternalAttachmentReady` condition reflects the macvtap interface state +observed through the net VM's Guest readiness predicates, while +`ExternalNicAuthorityReady` reports authority admission/adoption. + +When `spec.mdns.enable = true`, the Network controller creates owned `Process` +resources for the mDNS reflector (avahi) and, when `dnsmasqLocal = true`, +the local DNS bridge, each with `executionRef: Guest/`. Foundation +requires every ordinary process to be a Process or EphemeralProcess resource; +mDNS is not an inline untracked service. The Process spec parameters derive +entirely from the `spec.mdns.*` fields; no manual lifecycle management +is required. + +### USBIP proxy boundary + +The USBIP backend and proxy processes are **not** owned by the Network +controller. They are owned by `Provider/device-usbip`. Its controller may watch +only the referenced Network identity, phase, and generation. Core privately +resolves that Network UID to the relay attachment; no bridge IfName, host +uplink address, route table, or firewall body crosses the Provider boundary. + +The device-usbip controller consumes `Network/work-net` through a +`networkRef` dependency, owns exactly one multiplexed relay `Endpoint` authority +per Network, declares its own backend/relay/Binding-proxy `Process` resources, +and invokes its typed `UsbipEffectPort`. The Core adapter alone resolves the +opaque per-Network/per-busid intent and dispatches the closed +`UsbipBindFirewallRule` broker operation with the closed action enum +`Ensure|Remove`. That path owns all USBIP TCP/3240 +exposure, ownership markers, drift observation, and status. A Binding proxy +receives an authorized connected relay stream through Endpoint resolution and +its LaunchTicket; network-local therefore emits no generic TCP/3240 allow rule +on the host or in the net VM. + +`Network.spec` has no `usbipCarveOut` or device-usbip extension field and must +not be mutated by device-usbip. Network-local's provider firewall digest and +the Network `FirewallReady` condition exclude USBIP-owned rules; USBIP drift is +reported only by the owning Service's strict `status.provider`. + +## Host/Guest attachment mechanics + +### How Guests attach to a Network + +A Guest (workload VM) requests attachment to a Network by being listed in +`Network.spec.attachments`: + +```yaml +# In Network/work-net spec: +attachments: + - executionRef: Guest/corp-vm + index: 10 + - executionRef: Guest/personal-vm + index: 11 +``` + +The attachment is owned by the Network, not the Guest. When a listed Guest +exists and Core validates it as launch-eligible, the network-local controller +declares the opaque attachment effect. The Core adapter performs +`CreatePersistentTap → SetBridgePortFlags` and routes the connected CLOEXEC +`OwnedFd` directly into ProviderSupervisor's LaunchTicket attachment; the +runtime controller never receives the fd or broker operation. The Guest's own +spec references the network for firewall/routing/sandbox purposes: + +```yaml +# In Guest/corp-vm spec (inline ExecutionPolicy field): +networks: + - networkRef: Network/work-net + # The controller reconciles this ref and validates it matches an + # attachment entry in the Network spec. +``` + +If a Guest is listed in `spec.attachments` but its `Guest` resource does not +exist, the attachment remains `Pending`; the Network is not blocked from +becoming `Ready` on its other conditions. Removal of an attachment entry waits for the Guest/VMM FD owner to close, then +triggers `DeletePersistentTap` with the retained opaque attachment ID and the +expected Network/attachment generations. The request has no IfName/path input; +validated absence succeeds, while a stale generation or foreign ownership +marker fails closed without deleting anything. + +### Process network attachment + +A Process resource running inside a Guest that is attached to a Network +inherits its network connectivity through the Guest's tap interface. The +Process spec declares network usage settings: + +```yaml +# In Process spec: +network: + networkRef: Network/work-net + ingressPorts: [] + egressPolicy: inherit # inherit | blocked | explicit +``` + +The Process Provider (system-minijail or system-systemd) enforces the declared +egress policy through the sandbox profile. No Process has direct access to the +host bridge or tap; all network reachability flows through the net VM. + +### Host attachment + +A Host may reference a Network to declare site policy. The network-local +controller validates that the selected Host resource grants the controller +permission to install host bridges, nftables rules, and sysctl entries. +The exact attachment field in the Host resource spec is defined by the Host +ResourceType spec; this Network spec does not invent or depend on a specific +`Host.spec.networks` field name. + +When checking for host LAN CIDR collisions, the controller reads the Host +resource's observed network inventory (a runtime fact from the Host resource's +status or runtime-observable fields, as defined by the Host ResourceType). +At Nix build time the eval may validate against declared host configuration +input where available. + +## CIDR allocation and validation + +### Constraints (all enforced by the network-local controller) + +| Field | Constraint | +| --- | --- | +| `lanCidr` | Must be exactly `/24`; base address must end in `.0` | +| `uplinkCidr` | Must be exactly `/30` | +| `lanCidr` ↔ `uplinkCidr` | Must not overlap within the same Network | +| Any Network `lanCidr` ↔ any other Network's `lanCidr` or `uplinkCidr` | Must not overlap within the Zone | +| Any Network CIDR ↔ Host resource network inventory | Must not overlap | +| `externalAttachment.egress.allowedCidrs` ↔ any Network CIDR in Zone | Must not overlap | +| Attachment `index` | 2–250 inclusive; unique within the Network | + +CIDR overlap uses the same two-prefix IPv4 arithmetic as `cidrOverlaps` in +`nixos-modules/lib.nix` lines 429–462: two CIDRs overlap if and only if their +shorter prefix matches when applied to both network addresses. Containment +counts as overlap. + +Validation runs at: + +1. `validateSpec` call in the reconcile async interface — before any host-side + effect. A failing validateSpec returns a `ValidationResult` with + stable code `network-cidr-conflict` and sets the `CidrConflict` condition. +2. Each reconcile cycle — the controller re-checks against current peer Network + specs because concurrent creates can pass individual validation and still + conflict. + +The same CIDR rules apply to `externalAttachment.egress.allowedCidrs` entries. +A port-forward `sourceCidrs` entry must not coincide with a peer Network CIDR +(prevents accidental cross-env routing). + +### Env name / interface name constraints + +The controller enforces at validateSpec time: + +- `networkName` regex: `^[a-z][a-z0-9-]*$` (standard ResourceName); +- Effective LAN bridge name ≤ 15 bytes after IfName derivation (guaranteed by + construction; verified via `detect_collisions`); +- `netVmNameOverride`, if set, must match `^[a-z][a-z0-9-]*$` and must not be + `launcher` or start with `sys-`. + +## Isolation, hierarchy, and budgets + +### East-west isolation + +`isolation.allowEastWest` is the explicit per-Network opt-in for intra-network +east-west traffic. No Zone-level policy gate is required. + +When `isolation.allowEastWest = false` (default), workload taps on the LAN +bridge are set to `Isolated = true` and the net-VM forward chain has no +`eth1→eth1 new accept` rule, preventing direct L2 communication between +workloads in the same Network. + +When `isolation.allowEastWest = true`, the controller: +1. Sets workload tap isolation bits to `Isolated = false` via broker + `SetBridgePortFlags`. +2. Includes the east-west accept rule in the net VM's forward chain. + +Bridge isolation is enforced at two layers: +- **Host kernel**: tap entries in the LAN bridge carry `Isolated = true` by + default (all workload taps), preventing direct L2 frames between workloads. + Only the net-VM tap is non-isolated (it can reach all workload taps). +- **Net-VM nftables**: the forward chain has no `eth1→eth1 new accept` rule + unless `allowEastWest = true`. + +Changing `allowEastWest` from true to false requires a full reconcile to +re-apply bridge isolation flags and regenerate the net-VM nixos-system artifact. + +### hostBlocklist invariant + +The effective `hostBlocklist` is: + +```text +spec.routing.hostBlocklist + ∪ { Host resource's observed network inventory (runtime fact; schema-neutral) } + ∪ { lanCidr, uplinkCidr of every other active Network in the Zone } +``` + +The controller computes this union at each reconcile cycle by querying the +Host resource's observed network inventory (a runtime-observable fact from the +Host resource, as defined by the Host ResourceType; at Nix build time the eval +may validate against declared host configuration input where available). Each +entry generates a `drop` rule in the net VM's forward chain (before the broad +`lan→internet accept`). This prevents workloads from routing to: +- host LAN ranges (from the Host resource's observed network inventory); +- other d2b networks in the Zone; +- link-local and other RFC-reserved ranges in the default list. + +The hostBlocklist cannot be entirely emptied; it is only additive relative +to the default RFC1918+link-local set. + +### Network budget + +The Network resource does not have its own compute/memory budget (it is not +itself a Process). Resource budgets for the net-VM Guest and the controller +Process are declared on those resources. The Network spec only declares +interface-level policy (MTU, isolation, egress) that informs the net VM's +resource envelope. + +### Multiple Networks in one Zone + +A Zone may have several Networks, each with distinct, non-overlapping CIDRs +and independent net-VM Guests. There is no parent Network resource; Networks +are peers. The Zone self resource is the shared policy anchor for Zone-wide +policy; Network-specific isolation is per-Network only. + +## Async reconcile/observe/adopt/delete + +The network-local controller implements the full reconciliation contract from +`ADR-046-resource-reconciliation`. + +### Reconcile + +1. Call `validateSpec`: verify CIDRs, attachment indices, interface name + constraints, CIDR overlaps against peer Networks, and external-NIC mode/ + sharing constraints. Core resolves `parentInterface` from trusted Host + inventory and preflights the Host-global authority index. Return a conflict + condition if validation or authority admission fails; do not proceed to host + effects. +2. Call `plan`: compute desired bridge, tap, nftables, sysctl, and Guest states. + Diff against current status; produce a `ReconcilePlan`. +3. Call `reconcile`: + a. Create LAN and uplink bridge devices via broker `CreateBridge` if not + present. `CreateBridge` specifies the derived IfName, MTU, STP disabled, + multicast-snooping disabled, and applies IPv6 suppression sysctls + atomically. Returns success immediately if the bridge already exists + with matching parameters. A `CreateBridge` failure sets + `FabricReady=False` with reason `bridge-create-error` and aborts. + b. Ensure IPv6 suppression sysctls are applied to all bridges via broker + `ApplySysctl` (defense-in-depth after possible networkd restart or drift). + c. Apply host nftables `inet d2b` table via broker `ApplyNftables`. + d. Ensure NetworkManager unmanaged config covers all bridge/tap patterns. + e. Create the owned `Volume/net--config` resource with + `providerRef: Provider/volume-local`, `kind: ephemeral`, `source.executionRef: + Host/`, `source.settings.kind: tmpfs`, `quota.maxBytes: 4194304`, + `quota.maxInodes: 128`, `quota.enforcement: hard`, the `layout` entries (root + directory + four files each with `type: file`, `ownerRef: User/net-local-controller`, + `mode: "0640"`, and conservative policies), and `views.guest-readonly.rights: + [read, traverse]`. Set `attachments: []` (no Guest attachment yet). The tmpfs + quota is charged to the Host memory budget at Volume creation time. + `User/net-local-controller` must be a Ready User resource in the same Zone. + It is owned by `Provider/network-local` controller (`spec.osUsername: + net-local-controller`, `ownerRef: Provider/network-local`, `managedBy: + controller`); `Provider/system-core` verifies the account via NSS lookup and + reconciles it to Ready — it does not provision the OS account. The + `net-local-controller` OS account is provisioned by `Provider/network-local`'s + Nix package/module in Host prerequisites and the net-VM artifact. No numeric + UID/GID never enter ResourceSpec fields, authz checks, or audit records; + `User.status` MAY carry diagnostic `uid`/`gid` discovered by NSS lookup, but + those are informational only and are never authorization inputs. + See work item ADR046-network-001. If Volume creation returns a terminal error, set + `ConfigVolumeReady=False/config-volume-error` and abort. + f. Write the bounded canonical Network config content to the Volume through the + Volume write service (dnsmasq.conf, nftables.rules, routing.conf, + attachments.json; no workload names or raw host paths). Wait for the Volume + backing to reach `Ready`. If `Degraded` or `Failed`, set + `ConfigVolumeReady=False/volume-backing-error` and requeue. + g. Set `Guest.spec.systemArtifactId` from the REQUIRED + `Network.spec.netVmSystemArtifactId` (already validated at build time; + controller fails closed if absent at runtime). Create or update the owned + `Guest/` resource with network interface parameters. The Volume + attachment is NOT added to the Guest spec yet. + h. Wait for the Guest to reach `Ready`. Then update the Volume to add the Guest + attachment: + `attachments: [{executionRef: Guest/, transport: virtiofs, + view: guest-readonly, access: read-only, mountPath: "/run/d2b/net-config", + settings: {posixAcl: false, xattr: false, cache: auto, + inodeFileHandles: never, threadPoolSize: null, socketGroup: null}}]`. + Wait for the Volume attachment to reach `Ready`. If `Degraded`, set + `ConfigVolumeReady=False/attachment-not-ready` and requeue. + i. Create or update the owned guest-agent `Process/net--agent` with + `executionRef: Guest/`, `processClass: worker`, `sandbox.namespaceClasses: []` + (inherits Guest network namespace), `sandbox.capabilityClasses: [network-admin, network-bind, network-raw]` + (`CAP_NET_ADMIN`/`CAP_NET_BIND_SERVICE`/`CAP_NET_RAW` effective in Guest network namespace only; no host capability; INV-NET-009), + and mount `[{volumeRef: Volume/net--config, view: guest-readonly, + mountPath: "/run/d2b/net-config", access: read-only, required: true}]`. + Create or update opaque tap realization records for each attachment entry + that has a Core-authorized, launch-eligible Guest. Declare + `CreatePersistentTap` through + `NetworkEffectPort`; after the Core adapter creates/adopts the tap, apply + `SetBridgePortFlags` before it routes the connected CLOEXEC `OwnedFd` + directly to ProviderSupervisor's LaunchTicket attachment. No Provider + controller or d2b-bus payload receives the fd or broker operation. If + `spec.mdns.enable = true`, create or update the owned mDNS reflector + `Process` resource (and local DNS bridge `Process` if `dnsmasqLocal = true`) + with `executionRef: Guest/`. + j. Confirm bridge port flags (Isolated, neigh-suppress) for every tap before + allowing its LaunchTicket handoff. On ticket rejection, cancellation, or + spawn failure, close every `OwnedFd` copy and invoke + `DeletePersistentTap`; retain the opaque realization until confirmed. + k. For each attachment removed from the current spec, wait for its Guest/VMM + FD ownership to close, then invoke `DeletePersistentTap` through the + EffectPort with the retained opaque attachment ID and current expected + Network/attachment generations. Retain the handle across retryable + failures; refresh/requeue on generation mismatch; fail closed on an + ownership-marker conflict. + l. Commit a `ResourceMutationBatch` with the Volume, Guest, and Process updates + and status. +4. Report conditions and phase. +5. On any child (Volume, Guest, or Process) mutation, receive + `owned-resource-changed` hint and re-evaluate `ConfigVolumeReady`, + `NetVmReady`, `DhcpReady`, and `FirewallReady` from the Volume/Guest/Process + status. + +Long-running operations (nftables apply, sysctl, route) are dispatched in +background tasks through bounded blocking adapters. The reconcile handler does +not hold a redb transaction across these calls. + +### Observe + +The controller declares an observe interval for external drift detection: +`observeInterval: 60s`. On each observe cycle: + +- Re-read the applied `inet d2b` table digest via broker and compare against + `status.provider.details.firewallDigest`, considering only Network-UID-owned + rules. If drift, set `FirewallReady=False` and queue a reconcile. Ignore + every device-usbip ownership marker; that Provider owns USBIP drift/status. +- Check bridge IPv6 sysctl values against expected. If drift, queue reconcile. +- Check bridge isolation flags via broker readback. If drift, queue reconcile. +- Re-check CIDR overlaps against current peer Network specs. +- Re-check the external-NIC authority owner proof and compatible holder policy. + If missing, ambiguous, or conflicting, set + `ExternalNicAuthorityReady=False` and do not recreate the macvtap. + +Observation results are committed as status-only updates without incrementing +generation. + +### Adopt + +On controller restart (continuation event), the controller: + +1. Lists all Network resources in the Zone. +2. For each Network, reads current host bridge state through broker ops + (bridge present/not, isolation flags, nftables digest). +3. For an external attachment, asks Core to adopt the exact Host-global + physical-NIC authority by resource/process owner proof. Ambiguity + quarantines the attachment. +4. If bridges are present and the Network-owned nftables digest matches, marks the + attachment as adopted without re-applying. The net-VM Guest lifecycle + is separately adopted by `Provider/runtime-cloud-hypervisor`. +4. If bridges are absent (not yet created, or lost after host restart), + the normal reconcile loop creates them via `CreateBridge` on the next + reconcile cycle. No special adoption path is needed. + +Adoption does not modify any running state; it only updates the controller's +internal observed state. No bridge or tap is deleted during adoption. + +### Delete + +The finalizer `network.d2bus.org/fabric-cleanup` is owned by the network-local +controller. On deletion (strictly child-first order): + +1. Controller receives `deletion-requested` trigger. +2. Sets `NetworkDraining` condition; updates attached workload Guest resources to + request their own deletion (through their owner chain, not directly). +3. Waits for all attachment-phase statuses to become non-Ready (workload Guests + are stopped by their own controllers). +4. Calls `DeletePersistentTap` for every retained attachment realization using + its opaque attachment ID and expected Network/attachment generations. Each + tap is removed only after its Guest/VMM FD owner has closed. Validated absence + is success; transient failures retain the handle and retry; stale generations + refresh and requeue; a foreign ownership marker blocks cleanup. +5. Requests deletion of the owned guest-agent `Process/net--agent` + and any owned mDNS `Process` resources. Waits for their Deleted watch events + (each Deleted step is a single store transaction: the REVISION event with + `phase = Deleted` and row/index removal happen atomically; there is no persistent + phase=Deleted row for the controller to observe). +6. Updates the Volume to remove the Guest attachment entry (sets `attachments: []`); + waits for the attachment removal to be confirmed. This unbinds the read-only view + from the net-VM before the Guest is stopped. +7. Deletes the owned net-VM `Guest/` resource; waits for the Deleted + watch event. The net VM's macvtap FD (external attachment, if any) is released + as part of the VMM teardown inside `Provider/runtime-cloud-hypervisor` — the + broker destroys the macvtap interface when the SpawnRunner child exits. +8. Deletes the owned `Volume/net--config` resource; waits for the + Deleted watch event. At this point the Guest attachment has already been removed + (step 6) and the Volume backing is released cleanly. +9. Removes `inet d2b` rules scoped to this Network's ownership-id via broker + `ApplyNftables` (empty rule set for this UID). +10. Clears NetworkManager unmanaged config for the removed patterns via broker + `ApplyNmUnmanaged`. +11. Clears /etc/hosts entries for this Network's VMs via broker `UpdateHostsFile`. +12. Deletes the LAN bridge and uplink bridge via broker `DeleteBridge` for each. + `DeleteBridge` is idempotent and succeeds if the bridge is already absent. +13. Clears the finalizer. + +Deletion is strictly child-first. If any owned child (Process, Guest, or Volume) +cannot be deleted (finalizer blocked by a dependency), the Network deletion is +blocked and the `NetworkDraining` condition describes the blocker. + +## RBAC + +### Roles + +```yaml +type: Role +metadata: + name: network-operator + zone: dev +spec: + rules: + - resourceTypes: [Network] + verbs: [get, list, watch, create, update-spec, delete] + zones: [dev] + - resourceTypes: [Zone] + verbs: [get] # read Zone for topology inspection + zones: [dev] +``` + +```yaml +type: Role +metadata: + name: network-reader + zone: dev +spec: + rules: + - resourceTypes: [Network] + verbs: [get, list, watch] + zones: [dev] +``` + +```yaml +type: Role +metadata: + name: network-local-controller + zone: dev +spec: + rules: + - resourceTypes: [Network] + verbs: [get, list, watch, update-status, update-finalizers] + zones: [dev] + - resourceTypes: [Guest] + verbs: [get, list, watch, create, update-spec, delete] + zones: [dev] + # Scoped to Guests whose ownerRef resolves to a Network resource. + - resourceTypes: [Volume] + verbs: [get, list, watch, create, update-spec, delete] + zones: [dev] + # Scoped to Volumes whose ownerRef resolves to a Network resource. + - resourceTypes: [Process] + verbs: [get, list, watch, create, update-spec, delete] + zones: [dev] + # Scoped to Processes whose ownerRef resolves to a Network resource + # (guest-agent, mDNS reflector, local DNS bridge). + - resourceTypes: [Host] + verbs: [get] # read Host network inventory for hostBlocklist computation + zones: [dev] +``` + +The `network-local-controller` role is bound to the authenticated process +subject `Provider/network-local` through: + +```yaml +type: RoleBinding +metadata: + name: network-local-ctrl-binding + zone: dev +spec: + roleRef: Role/network-local-controller + subjects: + - Provider/network-local + externalPrincipalSelector: null + scopeNarrowing: null +``` + +Guest controllers may need `get` on `Network` to resolve a `networkRef` in +Guest/Process specs. The relevant Guest Provider role includes: + +```yaml + - resourceTypes: [Network] + verbs: [get] + zones: [dev] +``` + +No Process or Guest has `update-spec` on Network; attachment changes always +require an explicit operator `UpdateSpec` on the Network resource. + +### Status ownership + +The `network-local` controller is the sole `update-status` owner for +`Network` resources. Status updates carry `observedGeneration` and expected +revision. The Guest-owned child resources are status-owned by their respective +Provider controllers. + +## Audit, OTEL, and redaction + +### Audit records + +The resource API emits one audit record per mutation with the standard fields +from `ADR-046-resource-api-and-authorization`. Network-specific additions: + +| Field | Included | Rationale | +| --- | --- | --- | +| ResourceType and resource name | Yes | operational identity | +| verb / subresource | Yes | standard | +| `network.lanCidr` | Yes | address allocation decision | +| `network.uplinkCidr` | Yes | address allocation decision | +| `network.isolation.allowEastWest` | Yes | security-relevant policy change | +| `network.attachments[].executionRef` | Yes | Guest identity is operational | +| Workload hostname, IP, MAC | **No** | redacted from API-level audit | +| nftables rule text | **No** | redacted from API-level audit | +| DHCP lease data | **No** | never written to audit | +| dnsmasq config contents | **No** | not audit material | +| `externalAttachment.portForwards[].targetIp` | **No** | workload-internal | +| Operator-declared `externalAttachment.parentInterface` | Yes, on spec mutation only | requested Host inventory selector; never the derived authority identity | +| Runtime IfName values | **No** | Core-private bridge/tap/macvtap identity | +| firewallDigest | Yes | drift evidence | +| External-NIC authority key/owner proof | **No** | Core-private authority material | +| Bridge/tap drift reason | Yes (stable code, no paths) | diagnostic | + +Broker operations (`ApplyNftables`, `ApplyNmUnmanaged`, `ApplyRoute`, +`ApplySysctl`, `CreatePersistentTap`, `DeletePersistentTap`, +`SetBridgePortFlags`, `UpdateHostsFile`, `SeedDnsmasqLease`, etc.) emit their +own audit records with path-free outcome codes. `DeletePersistentTap` audit is +post-effect and contains only the exact op name, an opaque attachment digest, +the expected Network/attachment generations, outcome, error class, and +correlation ID. It contains no attachment-handle bytes, IfName, path, or +ownership-marker body. + +### OTEL spans and metrics + +Network reconcile cycles emit one root span per Network resource per reconcile +attempt: + +``` +d2b.network.reconcile + network.generation: + reconcile.trigger: + reconcile.attempt: + outcome: converged | pending | degraded | failed-retryable | failed-terminal +``` + +Child spans for broker operations: + +``` +d2b.network.bridge.create +d2b.network.bridge.delete +d2b.network.nftables.apply +d2b.network.volume.sync +d2b.network.guest.sync +d2b.network.agent.sync +d2b.network.mdns.sync +``` + +Metric labels use closed semantic cardinality and contain no Zone or Network +identity. No workload IP, MAC, hostname, nftables rule text, or DHCP lease data +appears in any span attribute, metric label, or log field. Zone identity +remains in the `d2b.zone` OTEL resource attribute. Network identity is likewise +available only as a bounded OTEL resource attribute and permitted audit field, +never as a span attribute or metric label. + +Metrics: + +| Metric | Labels | +| --- | --- | +| `d2b_network_reconcile_total` | `outcome` | +| `d2b_network_phase` | `phase` | +| `d2b_network_attachment_count` | (none) | +| `d2b_nftables_apply_total` | `outcome` | +| `d2b_nftables_drift_total` | (none) | +| `d2b_bridge_create_total` | `outcome` | +| `d2b_bridge_delete_total` | `outcome` | +| `d2b_network_volume_sync_total` | `outcome` | +| `d2b_network_agent_restart_total` | `outcome` | + +## Security invariants + +The following invariants are normative. Any change to the network-local +Provider or net-VM template that would violate them is a panel-blocking finding. + +### INV-NET-001: lib.mkForce on 10-eth-dhcp + +**Invariant**: the net VM's generated NixOS config MUST contain a +`lib.mkForce` override that replaces the `10-eth-dhcp` catch-all networkd +network definition with a non-matching bogus MAC address +(`00:00:00:00:00:00`). + +**Rationale**: `nixos-modules/base.nix` declares `10-eth-dhcp` with +`matchConfig.Type = "ether"`, which DHCP-configures every Ethernet NIC. +The net VM has two NICs (uplink, LAN) that require static addressing. Without +the override, systemd-networkd would DHCP both NICs, breaking static addressing. + +**Implementation**: the net-VM nixos-system artifact contains this +override unconditionally. It is not parameterized; it cannot be disabled by +any `netVmSystemArtifactId` substitution or operator field. + +**Test**: `tests/unit/nix/cases/net-vm-network.nix` must assert that every +generated net-VM config contains +`config.systemd.network.networks."10-eth-dhcp".matchConfig.MACAddress == "00:00:00:00:00:00"`. + +### INV-NET-002: IPv6 suppression at bridge creation and reconcile time + +**Invariant**: every host bridge created by the network-local controller MUST +have `net.ipv6.conf..disable_ipv6 = 1`, +`net.ipv6.conf..accept_ra = 0`, and +`net.ipv6.conf..autoconf = 0` applied in two independent layers: + +1. **At bridge creation**: the broker `CreateBridge` operation applies these + sysctls atomically before returning, closing the race window between + interface creation and the controller's next step. +2. **At each reconcile cycle**: the controller re-applies via broker + `ApplySysctl` as a defense-in-depth step, handling `systemctl restart + systemd-networkd`, manual sysctl resets, or any other drift path. + +No boot-time Nix `boot.kernel.sysctl` entry per bridge IfName is required +because bridges are created dynamically and do not exist at host activation. + +**Rationale**: dynamically-created kernel interfaces inherit `/proc/sys/net/ +ipv6/conf/default/*` defaults, which may have IPv6 active. The broker's +`CreateBridge` baseline closes the race; the `ApplySysctl` reconcile step +closes any drift window introduced by subsequent host operations. + +**Test**: `packages/d2b-priv-broker/tests/create_bridge_applies_ipv6_sysctl.rs` +asserts the broker applies all three sysctls before `CreateBridge` returns. +`packages/d2b-provider-network-local/tests/reconcile_applies_sysctl_defense_in_depth.rs` +asserts the controller re-applies via `ApplySysctl` independently. + +### INV-NET-003: Bridge port isolation default + +**Invariant**: workload Guest taps on the LAN bridge MUST have +`Isolated = true` (kernel bridge isolation flag) by default. The net VM's +LAN tap MUST have `Isolated = false`. Only when +`Network.spec.isolation.allowEastWest = true` may workload taps be set to +`Isolated = false`. + +**Rationale**: L2 isolation prevents direct workload-to-workload frames even if +the net VM's forwarding rules allow it. The per-Network opt-in is the sole +requirement; no Zone-level policy gate is needed. + +**Test**: `packages/d2b-host/src/bridge_port.rs` bridge-port conformance tests; +`tests/host-integration/bridge-isolation.nix` host integration test. + +### INV-NET-004: hostBlocklist cannot be emptied + +**Invariant**: the effective hostBlocklist must always contain at least the +default RFC1918+link-local set: +`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`. + +**Rationale**: these prevent workloads from routing to host infrastructure +networks. The operator may add entries via `spec.routing.hostBlocklist`; the defaults cannot be removed. + +**Implementation**: the controller enforces the default set during +`validateSpec`; any spec that specifies a `hostBlocklist` missing any of the +four defaults is rejected with `network-spec-invalid`. + +### INV-NET-005: IPv4-only by construction + +**Invariant**: d2b networking is IPv4-only. IPv6 is explicitly dropped on all +chains in the net VM nftables ruleset and suppressed on all bridges. The +Network spec must not expose IPv6 CIDR, address, or policy fields. + +**Rationale**: no current v3 baseline feature requires IPv6. IPv6 dual-stack +is a separate future feature requiring its own reviewed ADR. + +**Note**: when IPv6 dual-stack is added, INV-NET-002 and INV-NET-005 must both +be updated in a joint change. + +### INV-NET-006: No workload identifiers in host nftables rules + +**Invariant**: the `inet d2b` table installed on the host MUST NOT contain +workload VM names, user identifiers, or DHCP-assigned hostnames. Rules +reference only bridge IfNames, CIDR prefixes, IP addresses, and ports. + +**Rationale**: the ownership-marker convention requires that foreign-rule +preservation be enforced fail-closed. Workload identity in rules would make +rules non-deterministic across configuration changes. + +**Implementation**: nftables rule generation (`d2b-host/src/nftables.rs`) +uses only derived IfNames and configured CIDRs. + +### INV-NET-007: ExternalAttachment CIDR isolation + +**Invariant**: `externalAttachment.egress.allowedCidrs` entries MUST NOT +overlap with any Zone-local Network CIDR (lan or uplink) or the Host +resource's network inventory. A DNAT port-forward source CIDR must not +coincide with a LAN CIDR. + +**Rationale**: an external CIDR overlapping a local CIDR would create an +ambiguous routing table; traffic to the CIDR would split between the external +and local paths unpredictably. + +**Test**: `tests/unit/nix/cases/net-vm-network.nix` external network +section; CI assertions eval test. + +### INV-NET-008: nixos-system artifact is generic; per-Network config via Volume only + +**Invariant**: the nixos-system artifact referenced by `Network.spec.netVmSystemArtifactId` +MUST NOT contain per-Network desired state — DHCP reservations, nftables rules scoped +to Network spec fields, attachment tables, routing policy, or any data that varies +across Networks. The artifact contains only the generic net-VM OS: guest-agent binary +and runtime, kernel, base NixOS services, and systemd-networkd NIC bootstrap. +All per-Network configuration is delivered at runtime through the controller-owned +config Volume (`Volume/net--config`). + +**Rationale**: encoding per-Network data in the nixos-system artifact would require +a new artifact build (and Guest switch) for every Network spec change, defeating the +live-update advantage of the config Volume delivery path. It would conflate a shared, +reusable generic system image with per-Network runtime state, making each Network's +nixos-system artifact unique and uncacheable. + +**Implementation**: the nixos-system artifact build is a fixed-output derivation keyed +by generic OS inputs only, not by any Network spec data. The controller validates at +runtime that `netVmSystemArtifactId` resolves to a `type=nixos-system` artifact. + +**Test**: `packages/d2b-provider-network-local/tests/net_vm_artifact_is_generic.rs` +asserts that two Networks with different CIDRs, attachment tables, and firewall policies +but the same `netVmSystemArtifactId` produce the same `Guest.spec.systemArtifactId` +and different config Volume content; the nixos-system artifact hash is unchanged by +config-only Network spec mutations. + +### INV-NET-009: guest-agent capabilities are confined to the Guest network namespace + +**Invariant**: the capabilities granted by `sandbox.capabilityClasses: +[network-admin, network-bind, network-raw]` to the guest-agent Process are effective +only within the net-VM Guest's network namespace. Because `sandbox.namespaceClasses` +is empty the Process inherits the Guest's existing network namespace (`CLONE_NEWNET` +is NOT set); the Process Provider (system-minijail) must NOT add `CLONE_NEWNET` or +any other namespace class that would create a new, potentially host-adjacent network +context. `CAP_NET_ADMIN`, `CAP_NET_BIND_SERVICE`, and `CAP_NET_RAW` MUST NOT appear +in the effective capability set of any process that shares the host network namespace. + +**Rationale**: the net-VM Guest runs in its own isolated network namespace by +construction. A guest-agent Process that inherits this namespace has no path to +the host network stack, so the three capabilities pose no host-escalation risk. +A process with these capabilities in the host network namespace could manipulate +host routing, firewall rules, or bind privileged ports on host interfaces. + +**Test**: `packages/d2b-provider-network-local/tests/host_capability_leakage.rs` +— negative leakage test: assert that after the guest-agent Process is running, no +process in the host network namespace (namespace identified by `/proc/1/ns/net`) +carries `CAP_NET_ADMIN`, `CAP_NET_BIND_SERVICE`, or `CAP_NET_RAW` in its effective +set as a result of the guest-agent launch. Verified by reading `/proc//status` +`CapEff` for all processes sharing the host netns and asserting none of the three +bits are set on any process not already carrying them before the agent started. +Host-integration counterpart: `tests/host-integration/guest-agent-cap-confinement.nix` +— runNixOSTest asserts zero capability leakage to the host network namespace after +guest-agent start. + +## Azure/ACA scope boundary + +Azure Container Apps (ACA) and Azure Virtual Machine (AVM) network integration +remains inside `Provider/runtime-azure-container-apps` and +`Provider/runtime-azure-virtual-machine` as declared by decision D045. + +These Guest Providers manage: + +- ACA virtual network injection and subnet delegation; +- AVM private virtual network attachment; +- Azure network security groups and load balancer rules; +- Azure DNS and private DNS zone integration. + +The `Network` ResourceType and `Provider/network-local` are for local host +fabrics only. A future ADR may introduce `Provider/network-azure-vnet` or +similar when Azure-hosted Guests in a Zone require shared fabric management +that crosses Guest Provider boundaries. + +Until then: processes that run inside an ACA or AVM Guest reference Azure +network policy through Guest-Provider-specific spec fields, not through a +`networkRef` to a local Network resource. + +## Nix configuration contract + +> **Generic contract**: the zone-wide resource authoring shape, bundle assembly +> pipeline, and generation lifecycle are specified in the forthcoming +> `ADR-046-nix-configuration-contract.md`. This section covers only the +> Network-specific Nix option surface, validation checks, examples, and +> Network finalizer behavior during generation-driven removal. + +This section is normative for `Network` resources. It specifies the Nix option +surface, canonical JSON rendering, eval/build validation, Provider schema +verification, Zone resource bundle output, and example configurations. + +### Option surface + +Network resources are declared as: + +``` +d2b.zones..resources. = { type = "Network"; spec = { }; }; +``` + +The **attribute key is the resource name**; `metadata.name` is derived from it. +`metadata.zone` is derived from the Zone attribute key. `apiVersion` defaults to +`"resources.d2bus.org/v3"`. The `type` field is **required** (not inferred). + +`status` is absent from the Nix form; it is read-only. All core management +metadata — `uid`, `generation`, `revision`, `managedBy`, +`configurationGeneration`, and all timestamps — are filled by core and must +never appear in the Nix authoring form. + +An optional `metadata` attrset may appear containing only `ownerRef` for +explicit ownership declaration; all other metadata keys in the Nix form are +rejected at eval time: + +```nix +work-net = { + type = "Network"; + metadata.ownerRef = "Provider/network-local"; # optional, presentation only + spec = { ... }; +}; +``` + +The `spec` sub-object fields are **identical** to the canonical ResourceSpec +JSON fields — same names, same nesting, same semantics. There is no separate Nix +vocabulary: no aliases, no re-nesting, no Nix-specific field names. The Nix +option types, defaults, and constraints for `spec.*` fields are generated from +the same `Network.schema.json` (ResourceTypeSchema) used for JSON validation; +they are not hand-written. Provider-specific extension fields in `spec` are +generated from `Provider/network-local`'s signed schema in the artifact catalog. + +Resource names must be **unique across all resource types** within a Zone; the +`resources` attrset is keyed by name only, so a `Network` and a `Guest` with the +same name cannot coexist. + +```nix +# In any NixOS module imported by the host configuration +{ config, lib, ... }: +{ + d2b.zones.dev.resources = { + + work-net = { + type = "Network"; # required; determines which spec schema applies + spec = { + # spec fields are the exact NetworkSpec JSON fields — no renaming + providerRef = "Provider/network-local"; # required + lanCidr = "10.20.0.0/24"; # required; exactly /24; base ends .0 + uplinkCidr = "192.0.2.0/30"; # required; exactly /30 + + mtu = null; # null → 1500 (schema default) + mssClamp = false; + + isolation.allowEastWest = false; + # allowEastWest = true is the sole opt-in; no Zone-level gate. + + routing.hostBlocklist = [ + "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/16" "169.254.0.0/16" + ]; + + dhcp.domain = null; + dhcp.ignoreClientNames = true; + + dns.forwarders = [ "1.1.1.1" "8.8.8.8" ]; + dns.cacheSize = 1000; + + externalAttachment = null; + + mdns.enable = false; + mdns.reflector = true; + mdns.dnsmasqLocal = false; + mdns.dnsmasqLocalPort = 53530; + mdns.publishWorkstation = false; + + netVmNameOverride = null; # null → "net-work-net" + + # REQUIRED: must reference a d2b.artifacts entry with type = "nixos-system". + # Absent or wrong-type fails the build. + netVmSystemArtifactId = "nixos-system/net-vm-base-abc123"; + + # attachments list mirrors AttachmentSpec JSON exactly + attachments = [ + { executionRef = "Guest/corp-vm"; index = 10; } + # mac = null; (schema default → deterministic derivation) + ]; + }; + }; + + }; +} +``` + +#### Required spec fields + +These constraints are enforced by the generated Nix option types, not +hand-written custom checks. The source of truth is `Network.schema.json`. + +| `spec` field | Constraint | +| --- | --- | +| `providerRef` | Required; must be a registered Provider in the artifact catalog | +| `lanCidr` | Required; IPv4 CIDR; exactly `/24`; base address ends `.0` | +| `uplinkCidr` | Required; IPv4 CIDR; exactly `/30` | +| `netVmSystemArtifactId` | Required; must reference a declared `d2b.artifacts` entry with `type = "nixos-system"`; absent or wrong type is a hard build error | + +The `attachments` list may be empty on declaration but must be non-null. + +#### Credential ref convention + +Any spec field declared `secret: true` in the Provider schema MUST reference a +declared Credential resource rather than carrying an inline value: + +```nix +someProviderField = { credentialRef = "Credential/work-vpn-psk"; }; +``` + +The Nix resource compiler rejects an inline string value for a secret field with +a Nix eval error. `Credential/` must appear in +`d2b.zones..resources` or an imported Credentials module before the +reference is accepted. + +### Canonical ResourceSpec JSON shape + +The Nix resource compiler renders each declared Network to canonical JSON. This +is the **Nix-emitted input form**: `apiVersion`, `type`, `metadata.name`, +`metadata.zone`, optional `metadata.ownerRef`, and `spec`. Core adds `uid`, +`generation`, `revision`, timestamps, `managedBy`, and `configurationGeneration` +at activation; they are absent from the Nix-emitted form. The resource store +and controller validate incoming specs against the Network ResourceTypeSchema +derived from this shape. + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Network", + "metadata": { + "name": "work-net", + "zone": "dev" + }, + "spec": { + "providerRef": "Provider/network-local", + "lanCidr": "10.20.0.0/24", + "uplinkCidr": "192.0.2.0/30", + "mtu": null, + "mssClamp": false, + "isolation": { + "allowEastWest": false + }, + "routing": { + "hostBlocklist": [ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "169.254.0.0/16" + ] + }, + "dhcp": { + "domain": null, + "ignoreClientNames": true + }, + "dns": { + "forwarders": ["1.1.1.1", "8.8.8.8"], + "cacheSize": 1000 + }, + "externalAttachment": null, + "mdns": { + "enable": false, + "reflector": true, + "dnsmasqLocal": false, + "dnsmasqLocalPort": 53530, + "publishWorkstation": false + }, + "netVmNameOverride": null, + "netVmSystemArtifactId": "nixos-system/net-vm-base-abc123", + "attachments": [ + { + "executionRef": "Guest/corp-vm", + "index": 10, + "mac": null + } + ] + } +} +``` + +Object keys within each `spec` sub-object are sorted lexicographically. Array +elements preserve declaration order. Null-valued optional `spec` fields are +included explicitly (not omitted) to enable stable content hashing. + +### Eval/build validation pipeline + +Validation is staged: early structural checks run at `nix eval` time; +Provider-schema validation and bundle assembly run at `nix build` time. + +#### Stage 1 — eval-time (nix eval / nix flake check) + +Eval-time checks are enforced by the **generated Nix option types** for +`d2b.zones..resources.`. The types and their constraints are derived +from `Network.schema.json` and the Provider schema; they are not hand-written in +the module system. Cross-resource checks (CIDR overlap, unique names) are +enforced by the `d2b.zones.` module aggregation logic. + +| Check | Error class | +| --- | --- | +| Attr key (resource name) matches `^[a-z][a-z0-9-]*$` | eval error | +| `type` field is present and names a declared ResourceType | eval error | +| Resource name is unique across all types in the Zone | eval error | +| `providerRef`, `lanCidr`, `uplinkCidr`, `netVmSystemArtifactId` present | eval error (generated required-field check from schema) | +| `lanCidr` is exactly `/24` with a `.0` base address | eval error (generated from schema `format: "cidr-v4-slash24"`) | +| `uplinkCidr` is exactly `/30` | eval error (generated from schema `format: "cidr-v4-slash30"`) | +| `lanCidr` and `uplinkCidr` do not overlap each other | eval error | +| All Network `lanCidr` and `uplinkCidr` values in the Zone are pairwise non-overlapping and do not overlap the declared host network inventory (when available as Nix configuration input); uses `lib.d2b.cidrOverlaps` (exact algorithm of `nixos-modules/lib.nix` lines 429–462) | eval error | +| Attachment `index` values are in `[2, 250]` | eval error | +| Attachment `index` values are unique within each Network | eval error | +| `attachments[].executionRef` matches `^(Guest\|Host)/[a-z][a-z0-9-]*$` | eval error | +| `netVmNameOverride`, if non-null, matches `^[a-z][a-z0-9-]*$` and is not `"launcher"` and does not start with `"sys-"` | eval error | +| `externalAttachment.portForwards[].targetRef` and `targetIp` are mutually exclusive; both null rejected | eval error | +| `externalAttachment.egress.allowedCidrs` do not overlap any Zone Network CIDR | eval error | +| Any `{ credentialRef = "..." }` value references a declared `Credential/` resource | eval error | +| No inline value for a field declared `secret: true` in the Provider schema — enforced by the generated option's `type = lib.d2b.secretOrCredentialRef` type | eval error | + +#### Stage 2 — build-time (nix build / nixos-rebuild) + +1. **Provider schema resolution**: the resource compiler resolves `Provider/network-local` + through its `Provider.spec.artifactId` entry in the artifact catalog (type=`provider`). + The `Network.schema.json` is embedded in that `type=provider` artifact. Its SHA-256 + digest is verified against the artifact catalog's recorded digest for that artifact. + A missing artifact or digest mismatch is a hard build error. +2. **`netVmSystemArtifactId` resolution**: REQUIRED; the value must reference a + declared `d2b.artifacts` entry with `type = "nixos-system"`. Absent field or + wrong type is a hard build error; there is no implicit default. The build + records the resolved artifact ID in the bundle for runtime use. +3. **`attachments[].mac` format**: if non-null, must be a unicast MAC in colon + notation (`^([0-9a-f]{2}:){5}[0-9a-f]{2}$`, LSB of first octet = 0). +4. **Cross-Zone `executionRef`**: any `executionRef` pointing outside the + current Zone is a build error in v3 initial implementation. +5. **`portForwards[].sourceCidrs` overlap**: each entry must not coincide with + any Zone Network LAN CIDR; checked against the full resolved Zone resource set. + +#### Stage 3 — build output: Zone resource bundle + +The build derivation produces a Zone resource bundle at +`/nix/store/-d2b-zone--bundle/bundle.json`. The bundle is a +**fixed-output derivation**: identical Nix configuration always produces an +identical store path. Any change to any resource spec produces a different +store path and a different `contentHash`. + +```json +{ + "schemaVersion": 3, + "bundleVersion": 1, + "zone": "dev", + "contentHash": "sha256:e3b0c44298fc1c149afbf4c8996fb924...", + "generatedAt": "1970-01-01T00:00:00Z", + "resources": [ + { + "apiVersion": "resources.d2bus.org/v3", + "type": "Network", + "metadata": { + "name": "work-net", + "zone": "dev" + }, + "spec": { "..." : "..." } + } + ], + "providerSchemaDigests": { + "Provider/network-local": "sha256:" + } +} +``` + +Bundle properties: + +| Property | Value | +| --- | --- | +| **Canonical sort** | Resources sorted lexicographically by `(type, name)`. Sort is stable and deterministic. | +| **Content hash** | SHA-256 of the UTF-8 serialization of the sorted `resources` array (excluding `contentHash` and `generatedAt`). | +| **`generatedAt`** | Set to the Unix epoch in the Nix-built artifact (reproducible builds). The Zone runtime records actual activation time separately in its own generation record. | +| **`managedBy` / `configurationGeneration` in bundle** | Absent from the Nix-emitted bundle. Core sets `managedBy = "configuration"` and assigns `configurationGeneration` on every resource in the bundle when it applies it at activation. | +| **`providerSchemaDigests`** | Every `providerRef` used in the bundle has its Provider schema digest recorded from the `type=provider` artifact resolved via `Provider.spec.artifactId`. The runtime verifies these digests when applying the bundle. | +| **Activation path** | The NixOS activation script writes the bundle store path to `/etc/d2b/zones//bundle-store-path`. The core controller reads this path on startup or after activation. Prior bundle copies are retained at `/var/lib/d2b/zones//configuration/prior/.json`, never under `/etc`. | + +### Examples + +#### Minimal single-env network declaration + +```nix +# In any NixOS module — host configuration or a dedicated network module +{ + d2b.zones.dev.resources = { + work-net = { + type = "Network"; + spec = { + providerRef = "Provider/network-local"; + lanCidr = "10.20.0.0/24"; + uplinkCidr = "192.0.2.0/30"; + netVmSystemArtifactId = "nixos-system/net-vm-base-abc123"; + attachments = [ + { executionRef = "Guest/corp-vm"; index = 10; } + ]; + }; + }; + }; +} +``` + +#### Two Networks with east-west opt-in + +```nix +{ + d2b.zones.dev.resources = { + work-net = { + type = "Network"; + spec = { + providerRef = "Provider/network-local"; + lanCidr = "10.20.0.0/24"; + uplinkCidr = "192.0.2.0/30"; + netVmSystemArtifactId = "nixos-system/net-vm-base-abc123"; + isolation.allowEastWest = true; + mssClamp = true; + attachments = [ + { executionRef = "Guest/corp-vm"; index = 10; } + { executionRef = "Guest/dev-vm"; index = 11; } + ]; + }; + }; + personal-net = { + type = "Network"; + spec = { + providerRef = "Provider/network-local"; + lanCidr = "10.30.0.0/24"; + uplinkCidr = "198.51.100.0/30"; + netVmSystemArtifactId = "nixos-system/net-vm-base-abc123"; + attachments = [ + { executionRef = "Guest/personal-vm"; index = 10; } + ]; + }; + }; + }; +} +``` + +#### External network attachment with port-forward + +```nix +{ + d2b.zones.dev.resources.work-net.spec.externalAttachment = { + mode = "macvtap"; + parentInterface = "eno1"; + macvtapMode = "bridge"; + sharingPolicy = "exclusive"; + ipv4.method = "dhcp"; + egress = { + enable = true; + allowedCidrs = [ "192.168.1.0/24" ]; + }; + portForwards = [ + { + protocol = "tcp"; + listenPort = 2222; + targetRef = "Guest/corp-vm"; # → attachment index 10 → IP 10.20.0.10 + targetPort = 22; + } + ]; + }; +} +``` + +#### mDNS opt-in (creates owned Process resources) + +```nix +{ + d2b.zones.dev.resources.work-net.spec.mdns = { + enable = true; + reflector = true; # creates Process/net-work-net-mdns-reflector + dnsmasqLocal = true; # creates Process/net-work-net-mdns-dns-bridge + }; +} +``` + +When `enable = true`, the Network controller creates the listed `Process` +resources on the next reconcile cycle. They are owned by `Network/work-net` and +appear in `Network/work-net` status as child Process refs. Removing `enable` +from the Nix config or setting it to `false` does NOT directly delete the +Process resources; instead it updates the Network spec, and the controller's +reconcile path deletes the owned Processes through their finalizers. + +## Generation lifecycle and cleanup contract + +### Overview + +Each NixOS activation produces or reads a Zone resource bundle with a unique +`contentHash`. Core maintains a monotonically increasing +`configurationGeneration` counter and increments it whenever a bundle with a new +`contentHash` is applied. Core diffs the incoming resource set against the +previous generation's resource set and classifies each resource: + +| Classification | Action | +| --- | --- | +| Present in new generation only (added) | Schedule async `Create` | +| Present in both; spec changed | Schedule async `UpdateSpec` | +| Present in both; spec unchanged | No action | +| Present in prior generation only (removed) | Schedule async `Delete` via normal finalizer path | + +New generation activation is **non-blocking**. Core marks the generation as +`active` as soon as the bundle validates. It does not wait for removed-resource +cleanup or newly-added resource reconciliation to complete. + +### Configuration-managed, controller-owned, and api-created resources + +Core classifies every resource by its `metadata.managedBy` field: + +| Class | `metadata.managedBy` | Lifecycle authority | +| --- | --- | --- | +| Configuration-managed | `"configuration"` | Declared in a Nix bundle; absent from new bundle → async Delete | +| Controller-owned | `"controller"` | Created by a controller as an owned child; exact controller identity/UID tracked in separate internal metadata; deleted only through parent finalizer chain | +| API-created | `"api"` | Created via resource API without Nix origin; persists until explicit delete; no bundle-driven lifecycle | + +**Core NEVER schedules bundle-driven Delete for resources where +`managedBy ≠ "configuration"`.** It only removes configuration-managed +resources absent from the new bundle. +Controller-owned children (the net-VM Guest, config Volume, +guest-agent Process, mDNS Process resources, etc.) are deleted exclusively +through their owner controller's finalizer path when their parent is deleted +or their parent spec no longer requires them. + +**Name-collision per-item handling**: when a new bundle contains a resource whose +name already exists in the Zone store with `managedBy = "controller"` or +`managedBy = "api"`, core does NOT apply that item. The conflicting item is recorded +with `phase = Degraded, reason: name-conflict` identifying the existing resource's +`managedBy` value and UID. A `ResourceConflictSkipped` audit record is emitted for +that item. Non-conflicting items in the bundle proceed normally (Provider-state +contract: partial activation is preferable to whole-bundle rejection). Configuration +activation never seizes, overwrites, or replaces an existing controller-created or +API-created resource. The operator must delete the conflicting resource explicitly +via the resource API, after which the next bundle application will apply the item. + +These invariants are enforced in core's generation-transition logic +and are tested by `INV-NET-LIFECYCLE-001` and `INV-NET-LIFECYCLE-002` in the +cleanup test suite (ADR046-network-008). + +### Cleanup sequence for a removed Network resource + +When `Network/work-net` is present in generation N but absent from generation N+1: + +1. Core sets `metadata.deletionRequestedAt = ` on + `Network/work-net`. The finalizer `network.d2bus.org/fabric-cleanup` is already + set from initial create. +2. The `NetworkDraining` condition is set by the network-local controller with + `reason: configuration-generation-removed`. +3. The controller runs its normal Delete path (see [Delete](#delete)): requests + deletion of owned children in child-first order (workload Guest/VMM FD close + → generation-fenced `DeletePersistentTap` confirmation → guest-agent Process + and mDNS Processes → net-VM Guest → config Volume), waits for each required + confirmation/Deleted watch event, then calls remaining broker ops and clears + the finalizer. +4. Core observes the finalizer cleared; the final store transaction atomically + removes `Network/work-net` from the resource store and index and emits the + `Deleted` REVISION event. A dedup-guarded `CleanupComplete` audit record follows + the committed transaction. Controllers waiting for child deletion receive the + watch event; there is no persistent phase=Deleted row in the store. + +This entire sequence is fully asynchronous. Generation N+1 resources proceed +with their own `Create`/`UpdateSpec` independently of step 3. + +### Zone and Network status during cleanup + +The Zone self resource carries a `PendingCleanup` condition while any +prior-generation configuration-managed resource has not yet reached `Deleted`: + +```yaml +# Zone/dev status excerpt +conditions: + - type: PendingCleanup + status: "True" + reason: prior-generation-resources-pending + message: "1 resource(s) from configurationGeneration 6 pending deletion" + observedGeneration: 7 + lastTransitionAt: 2026-07-22T00:10:00Z +``` + +The Zone's aggregate `phase` is `Degraded` (not `Failed`) while `PendingCleanup` +is True and no other fatal condition exists. + +The Network resource undergoing deletion reports: + +```yaml +# Network/old-net status excerpt +phase: Degraded +deletionRequestedAt: "2026-07-22T00:10:00Z" +conditions: + - type: NetworkDraining + status: "True" + reason: configuration-generation-removed + message: "absent from Zone configurationGeneration 7; deletion in progress" + observedGeneration: 3 + lastTransitionAt: 2026-07-22T00:10:00Z + - type: ReconcileError + status: "False" + reason: none +``` + +The Network does NOT enter a `Terminating` phase. It remains `Degraded` with +`NetworkDraining = True` and `deletionRequestedAt` set throughout the drain +sequence. Final deletion is a single store transaction that atomically removes +the row and index and emits the `Deleted` REVISION event (with `phase = Deleted` +in the event payload); there is no `Terminating` phase between `Degraded` and +removal, and no persisted resource row ever carries `phase = Deleted`. The `Deleted` +phase value is a valid schema value that appears only in the final REVISION event; +controllers wait for the Deleted watch event or resource absence, not for a phase +transition. The dedup-guarded audit append follows the committed transaction. + +### Owner controller behavior on parent spec change + +When an existing `Network/` spec changes between generations (an +`UpdateSpec` rather than a `Delete`), the network-local controller reconciles +the new spec through its normal reconcile path. Spec-driven child changes: + +| Spec change | Controller action | +| --- | --- | +| New `attachments[]` entry | Updates config Volume (attachments.json, dnsmasq.conf); creates persistent tap; stores opaque attachment realization; sets bridge port flags | +| Removed `attachments[]` entry | Updates config Volume; waits for Guest/VMM FD closure; calls generation-fenced `DeletePersistentTap`; retains handle until confirmed | +| `spec.dhcp.*` / `spec.dns.*` change | Updates config Volume (dnsmasq.conf); guest-agent applies SIGHUP reload; no Guest restart required | +| `spec.routing.hostBlocklist` change | Updates config Volume (nftables.rules); guest-agent applies atomic `nft replace`; no Guest restart required | +| `spec.isolation.allowEastWest` change | Updates config Volume (nftables.rules); guest-agent applies atomic `nft replace`; also updates bridge port flags via broker | +| `spec.externalAttachment.*` port-forward / egress change | Updates config Volume (nftables.rules); no Guest restart required | +| `spec.externalAttachment` add/remove | Updates config Volume AND updates Guest spec (NIC topology change); Guest switch/restart may occur | +| `spec.mdns.enable` false → true | Creates owned mDNS `Process` resources | +| `spec.mdns.enable` true → false | Deletes owned mDNS `Process` resources through their finalizers | +| Any `spec.lanCidr` / `spec.uplinkCidr` change | Updates config Volume; re-reconciles full bridge/nftables/net-VM chain | + +Core delivers only the parent `UpdateSpec`; it does NOT directly mutate +controller-owned children. Owner controllers reconcile their children as a +consequence of parent spec updates. + +### Prior generation retention + +Core retains prior generation bundle copies at +`/var/lib/d2b/zones//configuration/prior/.json`. + +- **Retention count**: configurable via `d2b.zones..retainedGenerations` + (a Nix/compiler-level Zone setting outside `Zone.spec`; default `3`, range `1..16`). + No TTL. Oldest eligible generation is pruned when the count would be exceeded. +- **Eligibility for pruning**: a generation is eligible when all + configuration-managed resources from it have either reached `Deleted` or are + present unchanged in a newer generation with identical spec. A generation that + is still cleaning up is never pruned regardless of count. +- **Rollback lock**: an operator may lock a specific `contentHash` via the Zone + API to exclude it from count-based pruning. Rollback locks require an explicit + `ReleaseRollbackLock` call before that slot is reclaimed. Rollback + (re-applying a prior bundle) is not automatic; the operator re-declares the + prior configuration in Nix and triggers a new activation. + +Retained bundle files are copied into the mutable `/var/lib` state directory; +they are not Nix store paths and incur modest disk cost proportional to bundle size. + +### Error handling during cleanup + +If the network-local controller's Delete path encounters a retryable error +(e.g., broker `DeletePersistentTap` or `DeleteBridge` returns a temporary +failure): + +- The controller sets `ReconcileError` with a stable coded reason on + `Network/`. +- Retries proceed with exponential backoff bounded by the + `ADR-046-resource-reconciliation` retry policy. +- A persistent-tap transient failure retains the opaque handle and retries with + the same fence. A generation mismatch first refreshes Network/attachment + realization and then requeues; it never blindly retries a stale delete. +- The Zone `PendingCleanup` condition persists; the Zone phase remains + `Degraded`. + +If cleanup is permanently blocked (terminal error): + +- The controller sets `ReconcileError` with `terminal: true`. +- The Zone `PendingCleanup` condition carries a `blockingResource` detail. +- Operator options: manually remove the blocking child via the resource API; + or re-declare the Network in Nix (reverting the removal) to stop the cleanup + and allow re-reconciliation. + +A `DeletePersistentTap` foreign-marker/ownership conflict is terminal and +fail-closed. The controller neither deletes the interface nor clears the +finalizer; status and audit expose only the stable +`attachment-ownership-conflict` code. + +### Audit records for generation transitions + +Core emits one audit record per generation-driven lifecycle event. All records +use stable coded fields; no spec contents, CIDRs, attachment refs, or child +resource names appear in these records. + +| Event | Fields emitted | +| --- | --- | +| `BundleActivated` | `zone`, `contentHash`, `configurationGeneration`, `resourceCount`, `providerSchemaDigests` map (digests from `type=provider` artifacts via `Provider.spec.artifactId`) | +| `ResourceDeletionScheduled` | `zone`, `resourceType`, `resourceName`, `removedConfigurationGeneration`, `activeConfigurationGeneration` | +| `ResourceCreationScheduled` | `zone`, `resourceType`, `resourceName`, `activeConfigurationGeneration` | +| `ResourceUpdateScheduled` | `zone`, `resourceType`, `resourceName`, `activeConfigurationGeneration`, `specChanged: true/false` | +| `CleanupComplete` | `zone`, `resourceType`, `resourceName`, `removedConfigurationGeneration`, `outcome: deleted\|superseded` | +| `CleanupBlocked` | `zone`, `resourceType`, `resourceName`, `removedConfigurationGeneration`, `reason` (stable code) | + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `nixos-modules/network.nix` (bridge/NAT/sysctl, 500+ lines), `nixos-modules/net.nix` (net-VM NixOS config, 450 lines), `nixos-modules/options-envs.nix` (`d2b.envs..*`), `nixos-modules/options-realms-network.nix` (`d2b.realms..network.*` mode/cidrs), `nixos-modules/options-vms.nix` (`d2b.vms..env` line 944, `d2b.vms..index` line 962, `d2b.vms..staticIp` deprecated line 974), `nixos-modules/options-site.nix` (`d2b.site.allowUnsafeEastWest` line 48, `d2b.hostLanCidrs` line 382), `nixos-modules/options-realms-workloads.nix` (`d2b.realms..workloads..networkIndex` line 326), `nixos-modules/host-json.nix` (emits `host.json` `environments[].nftables`, `environments[].ifNameMappings`, `environments[].usbipBusidLocks`), `nixos-modules/processes-json.nix` (emits `processes.json` `ProcessNetworkInterface`/`ProcessMacvtapInterface` per-VM runner), `nixos-modules/lib.nix` (`subnetIp` line 399, `subnetMask` line 408, `mkMac` ~line 60, `cidrOverlaps` lines 429–462), `nixos-modules/index.nix` (netMeta section), `packages/d2b-core/src/host.rs` (`NetEnv` lines 290–328, `VmRuntimeRow` lines 155–167 with `tap`/`bridge`/`net_vm`/`env` fields, `ExternalNetworkPolicy` lines 332–413, `NftablesModel` lines 520–549, `BridgePortFlags`, `TapRole`, `Ipv6SysctlEntry`, `IfNameMapping` lines 242–256), `packages/d2b-core/src/processes.rs` (`ProcessNetworkInterface` lines 98–113, `ProcessMacvtapInterface`), `packages/d2b-contracts/src/broker_wire.rs` (`ApplyNftables`, `ApplyNmUnmanaged`, `ApplyRoute`, `ApplySysctl`, `CreatePersistentTap`, `CreateTapFd`, `SetBridgePortFlags`, `UpdateHostsFile`, `SeedDnsmasqLease`), `packages/d2b-host/src/ifname.rs` (FNV-1a derivation), `packages/d2b-host/src/nftables.rs` (`NftBatch`, `hash_inet_d2b_table`, coexistence policy), `packages/d2b-host/src/bridge_port.rs`, `packages/d2b-host/src/routes.rs`, `packages/d2b-host/src/netlink.rs`, `packages/d2b-host-providers/src/lib.rs` (unwired ADR 0032 runtime/display/substrate provider adapters; no network Provider trait) | +| Evidence class | All current network Nix modules, host.rs/processes.rs DTOs, and broker ops are `implemented-and-reachable`. `d2b.realms..network` with `mode="declared"` is `implemented-and-reachable` as the v2-native transitional surface (superseded by Network ResourceType at v3 reset). `packages/d2b-host-providers/src/lib.rs` provider trait surface is `implemented-but-unwired` (ADR 0032 realm trait adapters). The v3 Network ResourceType, `Provider/network-local`, and controller are `ADR-only`. `ProcessNetworkInterface`/`ProcessMacvtapInterface` are `implemented-and-reachable` in the current daemon but map to Guest spec network fields under `Provider/runtime-cloud-hypervisor`, not to NetworkSpec. | +| Behavior retained | `lib.mkForce` 10-eth-dhcp neutralization; bridge isolation (`Isolated=true` default); IPv6 suppression at boot and runtime; `cidrOverlaps` arithmetic; `hostBlocklist` default set; IfName FNV-1a derivation and collision detection; per-Network east-west opt-in (`isolation.allowEastWest`); dnsmasq DHCP static reservations with `dhcp-ignore-names`; `bind-interfaces`; hardened systemd confinement; nftables `inet d2b` table with ownership markers; firewall coexistence policy matrix; net-VM nftables drop IPv6 on all chains; MSS clamp; macvtap external attachment (via SpawnRunner/runtime-ch); DHCP/static IPv4 on `external0`; egress CIDRs and MASQUERADE; port-forward DNAT; `ConfigureWithoutCarrier` on uplink bridge (emitter-owned) | +| Required delta | Network ResourceType schema; spec/status API; `network-local` Provider and controller crate; async reconcile loop; owned net-VM Guest lifecycle; owned config Volume lifecycle (`Volume/net--config`) and guest-agent Process lifecycle (`Process/net--agent`) for runtime config delivery; owned mDNS Process lifecycle (D-NETWORK-001); CIDR overlap validation at reconcile time; RBAC for network resources (Network, Guest, Volume, Process, Host); OTEL spans/metrics; Nix resource emitter (`resources-network.nix`, bootstrap/static prerequisites only — no `systemd.network.netdevs`); removal of `d2b.envs.*` and `d2b.realms..network` surfaces; new canonical `DeletePersistentTap` paired with `CreatePersistentTap`, plus new broker ops `CreateBridge` and `DeleteBridge`, in `broker_wire.rs` and `runtime.rs` (D-NETWORK-003) | +| Reuse path | Extract `subnetIp`/`mkMac`/`cidrOverlaps` from `lib.nix`; copy IfName/derive/detect_collisions from `ifname.rs`; adapt `NetEnv`/`ExternalNetworkPolicy`/`NftablesModel`/`BridgePortFlags`/`TapRole`/`Ipv6SysctlEntry`/`IfNameMapping` from `host.rs`; extract nftables/bridge-port/routes/netlink modules from `d2b-host`; adapt `net.nix` and `network.nix` into sealed v3 template and controller. `VmRuntimeRow.tap`/`bridge`/`net_vm`/`env` fields (host.rs lines 155–167) become Network attachment status fields. `ProcessNetworkInterface`/`ProcessMacvtapInterface` (processes.rs) migrate to Guest spec under Provider/runtime-cloud-hypervisor (not NetworkSpec). | +| Replacement/deletion | `nixos-modules/network.nix`, `nixos-modules/net.nix`, `nixos-modules/options-envs.nix`, `nixos-modules/options-realms-network.nix`, `nixos-modules/index.nix` envMeta section removed only after `nixos-modules/resources-network.nix` and Provider/network-local controller pass parity tests; `d2b.envs.*` options removed only after the v3 cutover and consumer migration | +| Feasibility proof | All network invariants have passing golden/integration tests at v3 baseline; IfName derivation has property tests; bridge-port isolation has integration tests; nftables apply has coexistence matrix unit tests; no new proof required before spec acceptance | +| Future owner | `ADR046-network-*` work items below | + +## Decisions + +All decisions for this spec are resolved. No action is required from the +integrator before spec acceptance. + +### D-NETWORK-001: mDNS reflector process identity — RESOLVED + +**Resolution**: The mDNS reflector (avahi) and the local dnsmasq DNS bridge, +when enabled, are normal `Process` resources owned by the Network controller, +running inside the net-VM Guest via `executionRef: Guest/`. They +are not inline untracked NixOS services. Foundation requires every ordinary +process to be a `Process` or `EphemeralProcess` resource; this decision +applies that rule without exception. + +**Rationale**: an inline systemd service would be invisible to the resource +control plane, could not be independently observed or restarted through the +reconcile loop, and would violate the foundation invariant that every managed +process has a typed resource identity. The Process resource overhead is minimal +and enables independent health monitoring via `NetVmReady` sub-conditions. + +**Effect on spec**: `spec.mdns.enable = true` causes the Network controller to +create and own a `Process` resource per enabled mDNS component. All +`spec.mdns.*` fields remain as configuration inputs passed through to those +Process resources. No inline NixOS service for mDNS appears in the net-VM +template. + +**Unblocks**: ADR046-network-003 (net-VM template omits mDNS inline services), +ADR046-network-005 (controller creates mDNS Process resources in reconcile step g). + +### D-NETWORK-002: USBIP proxy process ownership — RESOLVED + +**Resolution**: The USBIP backend and proxy Processes are owned by +`Provider/device-usbip`, not the Network controller. The device controller +watches only Network identity/readiness/generation. Its typed EffectPort +privately resolves the Network UID and dispatches the closed +`UsbipBindFirewallRule` broker operation with closed action enum +`Ensure|Remove` for exact per-Network/per-busid TCP/3240 exposure. +`Remove` is generation-bound, ownership-scoped, foreign-marker fail-closed, and +idempotent after validated absence. Device-usbip owns one multiplexed relay `Endpoint` authority +per Network and owns its firewall drift/status. Network-local emits no USBIP +rule on either host or net VM, and its digest excludes device-usbip ownership +markers. `Network.spec` has no `usbipCarveOut` or device-usbip extension field; +the device-usbip controller does not mutate Network desired spec. Firewall +status/token and relay authority are released only after the broker confirms +the `Remove` effect. + +**Rationale**: USBIP is a device access mechanism, not a network routing +mechanism. Placing its process lifecycle inside the Network controller would +couple infrastructure and device concerns, preventing independent delivery and +testing of the device-usbip Provider. + +**Unblocks**: ADR046-network-007 (USBIP integration boundary work item). + +### D-NETWORK-003: Runtime bridge creation — RESOLVED + +**Resolution**: `Provider/network-local` dynamically creates and deletes host +kernel bridge devices at reconcile time via two new closed broker operations: +`CreateBridge` and `DeleteBridge`. A NixOS generation switch is NOT required +to create or remove a Network. Nix still provisions bootstrap/static +prerequisites and policy artifacts that do not depend on runtime bridge IfNames +(NetworkManager unmanaged prefix pattern, schema artifacts, controller binary). + +**Rationale**: requiring a NixOS generation switch to create or delete a +Network resource would make the Network ResourceType a second-class citizen +in the control plane. Dynamic provisioning is required for the network-local +controller to satisfy the standard async reconcile contract (create on +`Pending → Ready`, delete on `DeletionRequested`). The alternative (static +activation path) would mean a `DeleteSpec` call only removes the resource +object while leaving kernel state alive until the next switch — violating the +finalizer contract. + +**New broker ops required**: `CreateBridge` and `DeleteBridge` must be authored +and added to `packages/d2b-contracts/src/broker_wire.rs` and implemented in +`packages/d2b-priv-broker/src/runtime.rs` as `RealBrokerRequest` handlers. Both +ops require security review and broker policy coverage. + +**Unblocks**: ADR046-network-004 (Nix emitter no longer generates +`systemd.network.netdevs`), ADR046-network-005 (controller creates/deletes +bridges at reconcile time). + + +### ADR046-network-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-network-001` | +| Dependency/owner | W0 shared contract root; `d2b-contracts` | +| Current source | `packages/d2b-core/src/host.rs` lines 290–520 (`NetEnv`, `IfName`, `ExternalNetworkPolicy`, `NftablesModel`, `BridgePortFlags`, `TapRole`, `Ipv6SysctlEntry`, `IfNameMapping` lines 242–256; **also** `VmRuntimeRow` lines 155–167 with `tap`/`bridge`/`net_vm`/`env` fields — attachment status precursors); `packages/d2b-core/src/processes.rs` lines 98–141 (`ProcessNetworkInterface`, `ProcessNetworkInterfaceType`, `ProcessMacvtapInterface` — current VMM runner network interface DTOs; these are per-Guest VMM fields, not Network-level fields, and migrate to Guest spec under `Provider/runtime-cloud-hypervisor`); `packages/d2b-contracts/src/broker_wire.rs` (authoritative broker op list; network-relevant: `ApplyNftables`, `ApplyNmUnmanaged`, `ApplyRoute`, `ApplySysctl`, `SetBridgePortFlags`, `UpdateHostsFile`, `SeedDnsmasqLease`, `CreatePersistentTap`, `CreateTapFd`); `nixos-modules/lib.nix` lines 396–460 (`subnetIp`, `subnetMask`, `mkMac`, `cidrOverlaps`) | +| Reuse source | None from main; all from v3 baseline | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/network.rs`: NetworkSpec, NetworkStatus, AttachmentSpec, AttachmentStatus, ExternalAttachmentSpec, ExternalAttachmentStatus, PortForwardSpec, NetworkConditionType, opaque AttachmentHandle, and AttachmentGenerationFence; `packages/d2b-contracts/src/v3/ifname.rs`: IfName newtype, derivation, collision detection (extracted from `d2b-host/src/ifname.rs`). Also defines `User/net-local-controller` as a proper Resource with explicit lifecycle: `Provider/network-local`'s Nix package/module provisions the reserved `net-local-controller` OS account with a private fixed UID/GID in Host prerequisites and in the generic net-VM nixos-system artifact (same account, same UID/GID inside the Guest); the network-local controller creates and owns the User Resource (`spec.osUsername: net-local-controller`, `ownerRef: Provider/network-local`, `managedBy: controller`); `Provider/system-core` verifies the account via NSS lookup and reconciles the User Resource to Ready — it does not provision the OS account. No numeric UID/GID enters any ResourceSpec field, authz check, or audit record; `User.status` MAY carry diagnostic `uid`/`gid` values discovered by NSS lookup, but those are informational only and are never authorization inputs. The network-local controller waits for `User/net-local-controller` to reach `Ready` before creating any config Volume (reconcile precondition, not a bootstrap side effect). | +| Detailed design | Strict ResourceEnvelope with Network-specific spec/status. IfName newtype: IFNAMSIZ-1 validated, FNV-1a 64-bit derivation, base32 Crockford, 8-char suffix, bridge/tap role prefixes, detect_collisions over IfNameMapping slice. cidrOverlaps: pure Rust IPv4 arithmetic, same algorithm as lib.nix. NetworkSpec validators: /24 lanCidr with .0 base, /30 uplinkCidr, unique attachment indices 2–250, default hostBlocklist enforcement. The opaque attachment realization binds Network UID/generation and attachment UID/generation so deletion can supply a non-printable ID plus explicit expected generation fence without an IfName/path. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Provider dossiers, Nix resource compiler, resource store/API bind these canonical types | +| Data migration | Full d2b 3.0 reset; no env→Network import | +| Validation | Golden JSON/CBOR vectors; CIDR overlap property tests; IfName collision and derivation determinism tests; default hostBlocklist enforcement; attachment index uniqueness; `User/net-local-controller` User resource lifecycle/readiness test: controller creates User Resource with `spec.osUsername = "net-local-controller"` (`ownerRef: Provider/network-local`); controller waits for User resource to reach `Ready` before proceeding; controller aborts with `ConfigVolumeReady=False/user-not-ready` if User resource is not Ready; verifies no numeric UID/GID appears in the Resource spec, authz check, or audit record; verifies that any diagnostic `uid`/`gid` in `User.status` is never used as an authorization input | +| Removal proof | Old `d2b_core::host::NetEnv` and related types removed only after v3 resource API consumers use `d2b_contracts::v3::network` types | + +### ADR046-network-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-network-002` | +| Dependency/owner | ADR046-network-001; d2b-host network modules owner | +| Current source | `packages/d2b-host/src/ifname.rs` (FNV-1a derivation, detect_collisions, DEFAULT_PREFIX, BRIDGE_TAG, TAP_TAG); `packages/d2b-host/src/bridge_port.rs` (BridgePortReadback, east-west policy, TapRole defaults); `packages/d2b-host/src/nftables.rs` (NftBatch, hash_inet_d2b_table, coexistence policy); `packages/d2b-host/src/routes.rs` (route/dnsmasq-bound/IPv6 preflight); `packages/d2b-host/src/netlink.rs` (IPv6 sysctl sequence) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-network-local/src/ifname.rs`, `bridge_port.rs`, `nftables.rs`, `routes.rs`, `netlink.rs` | +| Detailed design | Move IfName derivation to `d2b-contracts` (ADR046-network-001); keep bridge_port, nftables, routes, netlink in Provider crate. nftables: retain the Network-owned `inet d2b` chain layout, ownership markers, and coexistence matrix; emit no USBIP/TCP-3240 rule and compute drift over only the Network UID ownership projection. routes: adapt dnsmasq-bound check to use Network status instead of `HostJson.environments`. netlink: keep IPv6-off sequence; add defense-in-depth re-application path. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract (ifname, bridge_port, nftables) into shared network-local Provider library; adapt (routes, netlink) into controller observe loop. | +| Integration | Controller observe loop uses nftables digest drift, bridge_port readback, and IPv6 sysctl to drive `FirewallReady`, `FabricReady` conditions | +| Data migration | None (behavior preserved; host bridge names change from `br--*` to `d2b-b` after cutover) | +| Validation | Existing `bridge_port::tests::readback_matches_defaults`, `ops::tap::tests::set_bridge_port_flags_readback_drift_fails_closed`, `netlink::tests::ipv6_off_sequence_runs_in_order`, nftables coexistence matrix tests; all pinned in `tests/golden/pinned/host-prepare-network.txt` and `tests/golden/pinned/net-canaries.txt` | +| Removal proof | `packages/d2b-host/src/{ifname,bridge_port,nftables,routes,netlink}.rs` removed only after Provider conformance tests pass | + +### ADR046-network-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-network-003` | +| Dependency/owner | ADR046-network-001, ADR046-network-002; Provider/runtime-cloud-hypervisor dossier owner | +| Current source | `nixos-modules/net.nix` (full file, 450 lines); `nixos-modules/net-mdns.nix`; `nixos-modules/lib.nix` subnetIp/mkMac/cidrOverlaps | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-network-local/` — artifact catalog integration for net-VM nixos-system artifact resolution; `packages/d2b-provider-network-local/nix/` — default net-VM NixOS module (parameterized successor to net.nix), built and registered as a nixos-system artifact in `d2b.artifacts` | +| Detailed design | `Network.spec.netVmSystemArtifactId` is REQUIRED. It must reference a declared `d2b.artifacts` entry with `type = "nixos-system"`; verified at Nix build time (Stage 2 check, hard build error if absent or wrong type). No implicit default exists; Provider artifacts cannot silently provide a separately typed system artifact. The controller sets `Guest.spec.systemArtifactId` to the artifact ID value at reconcile time (the value is already validated by the build; the controller fails closed if absent at runtime). The net-VM nixos-system artifact is **generic** (INV-NET-008): it contains the guest-agent binary and runtime, kernel, base NixOS services, systemd-networkd NIC bootstrap, and the `net-local-controller` **OS account** provisioned by `Provider/network-local`'s Nix module (same private fixed UID/GID as on the Host, so that virtiofs view ACLs on config Volume layout entries are enforced consistently inside the Guest; `Provider/system-core` performs NSS lookup reconciliation, not OS account provisioning; no numeric UID/GID appears in any ResourceSpec field, authz check, or audit record; `User.status` MAY carry diagnostic `uid`/`gid` from NSS lookup but those are informational only and never authorization inputs). It does NOT encode per-Network desired data; per-Network config (dnsmasq, nftables, routing, attachments) is delivered via the controller-owned config Volume and applied by the guest-agent Process. The artifact preserves compile-time-fixed content: `lib.mkForce` on 10-eth-dhcp (INV-NET-001); two systemd-networkd interface units matched by MAC; IPv6 suppression sysctls on NIC interfaces; ip6 filter table drop-all policy. **mDNS reflector and local dnsmasq DNS bridge are separate owned Process resources** (D-NETWORK-001); they are not inline services in the artifact. | +| Integration | Network controller resolves artifact ID → sets `Guest.spec.systemArtifactId`. Controller separately creates `Volume/net--config` with per-Network config and `Process/net--agent` (guest-agent). `Provider/runtime-cloud-hypervisor` reads `systemArtifactId` to produce the net-VM bundle and mounts the Volume view into the Guest. | +| Data migration | Destructive v3 reset; existing net VMs are re-created under new IfNames | +| Validation | nix-unit: `tests/unit/nix/cases/net-vm-network.nix` (adapted to v3 resource API); INV-NET-001 assertion in new nix-unit case; no mDNS inline service appears in the generated artifact; no per-Network dnsmasq or nftables data in artifact (INV-NET-008); integration test: mDNS Process resources are created when `spec.mdns.enable = true`; Stage 2 build test: absent `netVmSystemArtifactId` fails with required-field build error; wrong artifact type fails with `artifact-type-mismatch` error; `packages/d2b-provider-network-local/tests/net_vm_artifact_is_generic.rs` — two Networks with different CIDRs produce same `systemArtifactId` and different config Volume content | +| Removal proof | `nixos-modules/net.nix` and `nixos-modules/net-mdns.nix` removed only after net-VM artifact parity tests pass | + +### ADR046-network-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-network-004` | +| Dependency/owner | ADR046-network-001, ADR046-network-002, ADR046-network-003; Nix integrator | +| Current source | `nixos-modules/network.nix` (full file, 500+ lines; bridge/netdev/sysctl/NM-unmanaged/route sections); `nixos-modules/host-json.nix` (emits `host.json` `environments[].nftables`, `environments[].ifNameMappings`, `environments[].usbipBusidLocks` — superseded by Network status API); `nixos-modules/processes-json.nix` (emits `processes.json` `ProcessNetworkInterface`/`ProcessMacvtapInterface` per runner — superseded by Guest spec network fields); `nixos-modules/index.nix` envMeta/netMeta sections; `nixos-modules/options-envs.nix`; `nixos-modules/options-realms-network.nix`; `nixos-modules/options-vms.nix` (`d2b.vms..env` line 944, `d2b.vms..index` line 962 — current attachment semantics source; maps to `Network.spec.attachments[].executionRef` + `index`); `nixos-modules/options-site.nix` (`d2b.site.allowUnsafeEastWest` line 48 — maps to per-Network `Network.spec.isolation.allowEastWest`; Zone.spec is empty in v3; `d2b.hostLanCidrs` line 382 — maps to Host resource network inventory at runtime; not a Zone.spec field) | +| Reuse action | adapt | +| Destination | `nixos-modules/resources-network.nix`: Nix resource object emitter for Network ResourceType; `nixos-modules/index.nix`: network resource compilation section | +| Detailed design | The emitter replaces `d2b.envs.` with `d2b.zones..resources. = { type = "Network"; spec = { ... }; }` (attr key = resource name; `type` explicit field; `spec` fields identical to the canonical ResourceSpec JSON — no bespoke Nix vocabulary). It validates CIDR shape, attachment index uniqueness, external attachment constraints, and CIDR overlap (reusing `cidrOverlaps` from `lib.nix`). **Bridges are NOT emitted as `systemd.network.netdevs` entries** (D-NETWORK-003 resolved; bridges are created dynamically by the broker `CreateBridge` op at reconcile time). The Nix emitter provisions only bootstrap/static prerequisites that do not require runtime bridge IfNames: `networking.networkmanager.unmanaged` pattern for the `d2b-*` prefix (covers all dynamically-created bridges and taps regardless of specific IfNames; emitted to `00-d2b-unmanaged.conf`); schema validation and controller binary deployment artifacts. Current `d2b.vms..env` + `d2b.vms..index` attachment semantics (`options-vms.nix` lines 944/962) become `Network.spec.attachments[].executionRef` + `index`. Current `d2b.site.allowUnsafeEastWest` (`options-site.nix` line 48) moves to the per-Network `isolation.allowEastWest` field; Zone.spec is empty in v3. Current `d2b.hostLanCidrs` (`options-site.nix` line 382) becomes the Host resource's network inventory, queried at runtime; at Nix build time the eval may validate CIDRs against declared host configuration input. The emitter does not emit `boot.kernel.sysctl` entries per bridge IfName (bridges do not exist at activation time; IPv6 suppression is applied by `CreateBridge` and `ApplySysctl` per INV-NET-002). **Nix option types** for `spec.*` fields are generated from `Network.schema.json`; they are not hand-written. **Bundle generation**: the emitter collects all declared `Network` resource objects, sorts them lexicographically by `(type, name)`, serializes each as canonical JSON **omitting `managedBy` and `configurationGeneration`** (core sets these at activation), and assembles the Zone resource bundle at `$out/bundle.json` (see [Nix configuration contract — Stage 3](#stage-3--build-output-zone-resource-bundle)). The emitter records a `providerSchemaDigest` entry for `Provider/network-local` in the bundle resolved from the artifact catalog. The bundle's `contentHash` is a SHA-256 of the sorted canonical resource array; the derivation is fixed-output so that identical configuration always produces the same store path. The `managedBy` field is NOT set by the emitter; core sets `managedBy = "configuration"` and assigns the `configurationGeneration` counter when applying the bundle. The core controller retains prior bundle copies under `/var/lib/d2b/zones//configuration/prior/` per [Generation lifecycle and cleanup contract](#generation-lifecycle-and-cleanup-contract). | +| Integration | Nix resource objects serialize exactly the Rust NetworkSpec contract (ADR046-network-001). The provider install declares the schema digest. Zone runtime generation-transition logic (ADR046-network-008) reads the bundle at activation. | +| Data migration | Full v3 reset; `d2b.envs.*` declarations must be rewritten as Network resources | +| Validation | nix-unit CIDR overlap, assertion eval, and bridge-sysctl cases; `make test-flake` with updated examples; `make test-drift` for schema/emitter parity; `packages/d2b-contracts/tests/generation_bundle.rs` for bundle format and `contentHash` stability; nix-unit `tests/unit/nix/cases/generation-cleanup-absent-network.nix` for removed-resource scheduling (added by ADR046-network-008) | +| Removal proof | `nixos-modules/network.nix`, `nixos-modules/options-envs.nix`, and `nixos-modules/options-realms-network.nix` removed only after `resources-network.nix` and controller reach parity; `d2b.envs` consumer migration guide updated | + +### ADR046-network-005 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-network-005` | +| Dependency/owner | ADR046-network-001–004; network-local controller owner; D-NETWORK-001, D-NETWORK-002, and D-NETWORK-003 resolved | +| Current source | `nixos-modules/network.nix` (tap/sysctl sections); `packages/d2b-host/src/{bridge_port,nftables,netlink,routes}.rs`; broker ops in `packages/d2b-contracts/src/broker_wire.rs`: **real runtime ops** `ApplyNftables`, `ApplyNmUnmanaged`, `ApplyRoute`, `ApplySysctl`, `CreatePersistentTap`, `SetBridgePortFlags`, `UpdateHostsFile`, `SeedDnsmasqLease` (all `implemented-and-reachable`); **new ops to author**: canonical `DeletePersistentTap` paired with `CreatePersistentTap`, plus `CreateBridge` and `DeleteBridge` (do not exist in v3 baseline; must be added to `broker_wire.rs` and implemented as `RealBrokerRequest` handlers in `packages/d2b-priv-broker/src/runtime.rs`); **NOT current ops**: no unsuffixed tap-deletion alias is valid, and `CreateMacvtap` does not exist — macvtap is created inside broker's `SpawnRunner` dispatch (`packages/d2b-priv-broker/src/runtime.rs` line 5097 `live_create_macvtap_fd`) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-network-local/src/controller.rs`: async NetworkReconciler; `packages/d2b-provider-network-local/src/plan.rs`: ReconcilePlan computation; `packages/d2b-provider-network-local/src/observe.rs`: drift-detection observe loop. Full crate layout required (see [Package and crate boundary](#package-and-crate-boundary)): `src/` (controller/plan/observe + colocated unit tests), `tests/` (hermetic conformance and state-machine tests), `integration/` (provider-system reconcile fixtures), `README.md` (Network ResourceType, controller binary, placement, RBAC, security invariants, build/test/integration commands). | +| Detailed design | Implements full async reconcile interface from `ADR-046-resource-reconciliation`. `plan()` computes desired vs. actual bridge-presence, sysctl, host-side nftables, hosts-file, NM-unmanaged, config Volume content, Guest, guest-agent Process, and mDNS-Process states. `reconcile()` dispatches in order: `CreateBridge` for each bridge not present (broker applies IPv6 sysctls atomically; `CreateBridge` failure sets `FabricReady=False/bridge-create-error` and aborts) → `ApplySysctl` (defense-in-depth IPv6) → `ApplyNftables` (host-side `inet d2b` table) → `ApplyNmUnmanaged` → `ApplyRoute` → `UpdateHostsFile` → `SeedDnsmasqLease` for new reservations → **Volume upsert** (two-phase): Phase 1 — create `Volume/net--config` with `kind: ephemeral`, `source.executionRef: Host/`, `source.settings.kind: tmpfs`, `quota: {maxBytes: 4194304, maxInodes: 128, enforcement: hard}` (tmpfs quota charged to Host memory budget), `layout` entries (root directory with `type: directory` and four config files each with `type: file`, `ownerRef: User/net-local-controller`, `groupRef: User/net-local-controller`, `mode: "0640"`, `accessAcl: []`, `defaultAcl: []`, `noFollow: true`, conservative create/repair/cleanup policies), `views: {guest-readonly: {path: "", rights: [read, traverse]}}`, `attachments: []` (no Guest attachment); abort on terminal error with `ConfigVolumeReady=False/config-volume-error`. Wait for Volume backing to reach `Ready`; requeue on `Degraded`/`Failed` with `ConfigVolumeReady=False/volume-not-ready`. Write rendered config content through Volume write service (no raw host paths). Phase 2 — create Guest upsert with `systemArtifactId` from REQUIRED `Network.spec.netVmSystemArtifactId`. Wait for Guest `Ready`. Then update Volume with Guest attachment: `attachments: [{executionRef: Guest/, transport: virtiofs, view: guest-readonly, access: read-only, mountPath: "/run/d2b/net-config", settings: {posixAcl: false, xattr: false, cache: auto, inodeFileHandles: never, threadPoolSize: null, socketGroup: null}}]`; wait for attachment `Ready`; requeue on `Degraded` with `ConfigVolumeReady=False/attachment-not-ready`. Create or update guest-agent Process `Process/net--agent` with `processClass: worker`, `sandbox: {namespaceClasses: [], capabilityClasses: [network-admin, network-bind, network-raw]}` (inherits Guest network namespace; `network-admin`→`CAP_NET_ADMIN`, `network-bind`→`CAP_NET_BIND_SERVICE`, `network-raw`→`CAP_NET_RAW`, all effective in Guest network namespace only; INV-NET-009), `mounts: [{volumeRef: Volume/net--config, view: guest-readonly, mountPath: "/run/d2b/net-config", access: read-only, required: true}]`. mDNS Process upsert when `spec.mdns.enable = true` (D-NETWORK-001) → `SetBridgePortFlags` per tap. Removed attachments first wait for Guest/VMM FD ownership to close, then issue `DeletePersistentTap` with the retained opaque attachment ID and current expected Network/attachment generations; the handle remains retained until confirmed effect or validated absence. Stale generation refreshes/requeues, transient kernel error retries, and foreign marker fails closed. Each broker op returns typed audit evidence. `observe()` re-reads `firewallDigest` (host-side), bridge isolation flags, IPv6 sysctls, and guest-agent Process status (`dnsmasq-bound`, `firewall-applied` predicates); queues reconcile on drift. Metrics use only the fixed semantic labels in §OTEL spans and metrics; Zone/Network identity remains in OTEL resource attributes and permitted audit fields and never enters metric labels or span attributes. **Finalizer** (strictly child-first): `NetworkDraining` → stop workload Guests and await VMM FD closure → generation-fenced `DeletePersistentTap` for each retained attachment, awaiting confirmation → delete guest-agent Process and mDNS Processes; wait for each Deleted watch event → update Volume to remove Guest attachment (`attachments: []`); wait for attachment removal confirmed → delete `Guest/`; wait for Deleted watch event → delete `Volume/net--config`; wait for Deleted watch event → `ApplyNftables` (empty) → `ApplyNmUnmanaged` (empty) → `UpdateHostsFile` (empty) → `DeleteBridge` for each bridge (idempotent) → clear finalizer. No USBIP rules installed by Network; device-usbip issues the existing `UsbipBindFirewallRule` request with action `Ensure` for apply or `Remove` for release (D-NETWORK-002). | +| Integration | Controller process registers descriptor, watches `Network` resources via d2b-bus/ComponentSession/ResourceClient. Owned Guest and Process mutations trigger owner reconciliation. Device-usbip watches only Network identity/readiness/generation; its Core adapter privately resolves relay/firewall effects (D-NETWORK-002). | +| QEMU launch integration | For every authorized QEMU attachment, the controller supplies only the opaque realization to `NetworkEffectPort`. The Core adapter performs `CreatePersistentTap → SetBridgePortFlags`, then transfers the connected CLOEXEC `OwnedFd` directly to ProviderSupervisor's Process LaunchTicket attachment. The qemu Provider/controller receives only opaque Network/Endpoint refs and no broker op/fd; no fd is serialized through ResourceAPI, ComponentSession, or d2b-bus. Adapter/supervisor parent copies remain CLOEXEC and close after spawn; ticket rejection, cancellation, or spawn failure closes all copies before generation-fenced `DeletePersistentTap`, retaining the opaque realization until confirmation. | +| Data migration | None after full reset | +| Validation | `ADR046-reconcile-001` toolkit conformance; latency gates (p95 ≤5 ms hint-to-handler); Network-specific: CIDR conflict blocks reconcile, `CreateBridge` failure sets `FabricReady=False`, Volume creation failure sets `ConfigVolumeReady=False/config-volume-error`, `User/net-local-controller` not Ready aborts with `ConfigVolumeReady=False/user-not-ready`, Volume schema round-trip (kind=ephemeral, source.settings.kind=tmpfs, quota.enforcement=hard, layout type=file entries, views.guest-readonly.rights=[read,traverse]), tmpfs quota charged to Host memory budget (test Volume creation fails when Host memory budget exceeded), Guest not created before Volume backing `Ready`, Guest attachment not added before Guest `Ready`, guest-agent Process created after attachment `Ready` (`processClass: worker`, `namespaceClasses: []`, `capabilityClasses: [network-admin, network-bind, network-raw]`, `access: read-only`, `required: true`), host-capability leakage test: no host-netns process gains `CAP_NET_ADMIN`/`CAP_NET_BIND_SERVICE`/`CAP_NET_RAW` as result of guest-agent launch (INV-NET-009; `tests/host-integration/guest-agent-cap-confinement.nix`), removed attachment and finalizer call `DeletePersistentTap` only after Guest/VMM FD closure with opaque ID/current generations, validated absence succeeds, transient failure retains handle/retries, stale generation refreshes, foreign marker blocks without deletion, request/audit contain no IfName/path, `DeleteBridge` called only after tap confirmations, Volume attachment removed before net-VM Guest deletion in finalizer (test order: workload FD closure → persistent taps deleted → agent Deleted → Volume attachment removed → net-VM Guest Deleted → Volume Deleted → bridges), east-west invariant (INV-NET-003), hostBlocklist enforcement (INV-NET-004), macvtap attachment status (delegated to runtime-ch), mDNS Process created/deleted with `spec.mdns.enable` toggle, broker INV-NET-002 tests, config-only spec change updates Volume content and triggers agent reload without Guest restart (INV-NET-008); golden tests updated for v3 IfNames; structural metric descriptor test asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `network`, and every resource-name-derived label and verifies a Network-name canary is absent from emitted label values | +| QEMU launch validation | `qemu_tap_launch_order` proves create → flags → ticket ordering; `qemu_tap_owned_fd_lifetime` proves CLOEXEC parent ownership, one intentional child slot, and closure on success/failure; `qemu_tap_failed_launch_cleanup` proves close-before-`DeletePersistentTap` and handle retention until confirmation; `qemu_tap_no_bus_serialization` proves the qemu Provider/controller and every ResourceAPI/ComponentSession/d2b-bus payload contain only opaque refs, never the fd or broker DTO. | +| Removal proof | Daemon-orchestrated network/bridge lifecycle removed only after controller passes conformance and parity tests | + +### ADR046-network-006 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-network-006` | +| Dependency/owner | ADR046-network-001, ADR046-network-005; test owner | +| Current source | `tests/unit/nix/cases/net-vm-network.nix`; `tests/golden/pinned/net-vm-bundle-gate.txt`; `tests/golden/pinned/net-canaries.txt`; `tests/golden/pinned/host-prepare-network.txt`; `tests/host-integration/bridge-isolation.nix`; `tests/integration/live/network-isolation.sh` | +| Reuse action | adapt | +| Destination | `tests/unit/nix/cases/net-vm-network.nix` (adapted to v3 resource API); updated golden pins; `tests/host-integration/bridge-isolation.nix` (adapted); `packages/d2b-priv-broker/tests/{bridge_lifecycle,persistent_tap_lifecycle}.rs` (new hermetic broker tests). Provider crate test directories: `packages/d2b-provider-network-local/tests/` — hermetic Cargo integration tests (conformance suite, controller state machine, CIDR validation vectors, IfName determinism, invariant tests INV-NET-001–007, reconcile/observe/finalize with deterministic clock, fault injection); `packages/d2b-provider-network-local/integration/` — container/Host/Guest lifecycle fixtures invoked by `make test-integration` (bridge isolation, east-west double opt-in, nftables drift detection, persistent-tap and macvtap lifecycle). Both directories required by package policy. | +| Detailed design | Rust integration tests: NetworkSpec CIDR validation golden vectors; AttachmentSpec index uniqueness; ExternalAttachmentSpec mutual-exclusion validation; IfName derivation determinism; CIDR overlap arithmetic; INV-NET-001 through INV-NET-009 invariant tests; reconcile/observe/finalize state machine (deterministic clock). Broker tests: `create_bridge_applies_ipv6_sysctl` (INV-NET-002 layer 1); `delete_bridge_is_idempotent`; `delete_bridge_never_cascades_attached_tap`; `create_bridge_parameters_match_spec` (MTU, STP disabled, multicast snooping disabled); `delete_persistent_tap_pairs_with_create`; `delete_persistent_tap_absent_is_idempotent_after_ownership_validation`; `delete_persistent_tap_rejects_stale_network_generation`; `delete_persistent_tap_rejects_stale_attachment_generation`; `delete_persistent_tap_foreign_marker_fails_closed`; `delete_persistent_tap_request_and_audit_have_no_ifname_or_path`. Controller tests: `reconcile_applies_sysctl_defense_in_depth` (INV-NET-002 layer 2); `volume_created_before_guest`; `guest_not_created_until_volume_ready`; `agent_process_created_after_guest`; `removed_attachment_waits_for_vmm_then_delete_persistent_tap`; `finalizer_order_vmm_then_taps_then_agent_then_guest_then_volume_then_bridges`; `delete_persistent_tap_transient_retry_retains_handle`; `delete_persistent_tap_generation_mismatch_refreshes`; `delete_persistent_tap_foreign_marker_blocks_finalizer`; `config_only_spec_change_updates_volume_no_guest_restart` (INV-NET-008); `finalizer_calls_delete_bridge`; `mdns_process_created_on_enable`; `mdns_process_deleted_on_disable`; `host_capability_leakage` (INV-NET-009). nix-unit: INV-NET-001 lib.mkForce assertion; net-VM artifact has no inline mDNS service and no per-Network dnsmasq/nftables data (INV-NET-008); Network emitter CIDR constraint assertions; no `systemd.network.netdevs` bridge entries emitted. Host integration: bridge isolation with east-west opt-in; nftables drift detection; persistent-tap and macvtap create/delete lifecycle; config Volume update propagates to guest-agent without Guest restart; `tests/host-integration/guest-agent-cap-confinement.nix` (INV-NET-009 zero leakage to host netns). | +| QEMU TAP tests | Add `qemu_tap_launch_order`, `qemu_tap_owned_fd_lifetime`, `qemu_tap_failed_launch_cleanup`, and `qemu_tap_no_bus_serialization` to the network-local/provider-supervisor integration fixture. Assert the exact `CreatePersistentTap → SetBridgePortFlags → LaunchTicket` chain, direct `OwnedFd` handoff, CLOEXEC discipline, close-before-generation-fenced-delete on every failed launch, no operation-scoped `CreateTapFd`, and no fd/broker DTO at the qemu controller or bus boundary. | +| Integration | Pinned tests registered in `tests/golden/pinned/`; nix-unit cases in `tests/unit/nix/cases/`; host integration in `tests/host-integration/` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | All listed tests must pass before `nixos-modules/network.nix` removal is eligible | +| Removal proof | Not applicable (this work item IS the test successor) | + +### ADR046-network-007 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-network-007` | +| Dependency/owner | ADR046-network-005; device-usbip Provider dossier; D-NETWORK-002 resolved | +| Current source | `nixos-modules/network.nix` lines 444–461 (USBIP host firewall); `packages/d2b-core/src/host.rs` lines 324–328 (usbip_backend_port, usbip_busid_locks in NetEnv); `packages/d2b-host/src/` usbip_argv.rs | +| Reuse action | adapt | +| Destination | `Provider/device-usbip` owns one relay Process/Endpoint authority per Network plus the typed EffectPort adapter for the existing closed `UsbipBindFirewallRule` request with closed action enum `Ensure|Remove`. The controller watches only the `networkRef` resource's identity/readiness/generation; Core privately resolves Network UID to relay attachment and firewall intent. Network spec/status is not mutated with USBIP fields. Full crate layout required for `packages/d2b-provider-device-usbip/` (see [Package and crate boundary](#package-and-crate-boundary)): `src/` (controller and usbip runner + unit tests), `tests/` (hermetic conformance, dependency-watch state machine, `UsbipBindFirewallRule` `Ensure|Remove` round-trip), `integration/` (Host/Guest USBIP attach/detach lifecycle fixtures), `README.md` (Provider identity, provider-neutral USB Service/Binding types, USBIP Processes/Endpoints, Network least-privilege dependency contract, RBAC, security invariants, standalone-repo path). | +| Detailed design | Device-usbip's typed EffectPort is the sole semantic owner of every USBIP TCP/3240 rule. Its Core adapter resolves the opaque per-Network/per-busid intent and issues the same `UsbipBindFirewallRule` request with action `Ensure` for apply and `Remove` for release; no separate release op exists. `Remove` is generation-bound, ownership-scoped, idempotent after validated absence, and foreign-marker fail-closed. The controller retains firewall token/status and the relay authority reference until the broker confirms `Remove`; its strict provider status owns firewall digest/drift. Network-local emits no generic host or net-VM TCP/3240 allow and ignores device-usbip ownership markers in Network drift. The device Provider owns exactly one multiplexed relay Endpoint authority per Network and supplies Binding proxies only authorized connected streams through LaunchTickets. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | device-usbip watches Network readiness → Core adapter resolves opaque Network attachment → `UsbipBindFirewallRule { action: Ensure, ... }` for apply or `{ action: Remove, ... }` for release + one relay Endpoint authority → Binding proxy LaunchTicket; release clears status/authority only after confirmed `Remove` | +| Data migration | Current network.nix USBIP carve-out replaced by UsbipBindFirewallRule broker op | +| Validation | device-usbip conformance tests cover the exact closed `Ensure|Remove` enum (unknown actions rejected), same-request broker mapping for apply/release, expected Network/Service generation binding, exact per-Network/per-busid scoping, idempotent validated-absence `Remove`, one relay Endpoint authority, ownership-scoped drift/status, foreign-marker rejection, transient retry, and retention of status/token/authority until effect confirmation; network-local nftables tests assert no TCP/3240/USBIP rule on host or net VM and prove USBIP rule churn does not change Network `FirewallReady`; the pinned USBIP firewall golden moves to device-usbip ownership | +| Removal proof | Network.nix USBIP sections removed only after UsbipBindFirewallRule mechanism passes conformance | + +### ADR046-network-008 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-network-008` | +| Dependency/owner | ADR046-network-004, ADR046-network-005; Zone runtime integrator | +| Current source | No current v3 source: generation lifecycle and `managedBy`/`configurationGeneration` classification do not exist on the pre-ADR45 v3 baseline. The v3 baseline uses NixOS activation scripts that atomically replace all host JSON artifacts; there is no per-resource generation tracking, no async cleanup queue, and no `managedBy` field. | +| Reuse action | create | +| Destination | `packages/d2b-core-controller/src/configuration.rs`: bundle application, diff, generation-transition logic (including per-item name-conflict handling), prior-bundle retention under `/var/lib/d2b/zones//configuration/prior/`; `packages/d2b-core-controller/src/cleanup.rs`: removal scheduling and `PendingCleanup` condition tracking; `packages/d2b-contracts/src/generation_bundle.rs`: `ZoneBundle`/`BundleResource`/`BundleMetadata` **input** DTOs — MUST NOT include `managedBy` or `configurationGeneration` (both are persisted resource metadata set by core at activation, not bundle input fields); `ManagedBy` closed enum `{ Configuration, Controller, Api }` and `configurationGeneration: u64` live in `packages/d2b-core-controller/src/resource_store.rs` as persisted resource metadata fields; `nixos-modules/resources-network.nix` (emits bundle with `managedBy`/`configurationGeneration` absent; core sets both at activation per ADR046-network-004); `d2b.zones..retainedGenerations` Nix/compiler-level Zone option (outside `Zone.spec`; default `3`, range `1..16`); `tests/unit/nix/cases/generation-cleanup-absent-network.nix`; `packages/d2b-contracts/tests/generation_bundle.rs`; `tests/host-integration/nix-generation-cleanup.nix` | +| Detailed design | **Core generation tracking** (`packages/d2b-core-controller/src/configuration.rs`): core maintains a monotone `configurationGeneration` counter per Zone in its durable state. On each bundle application it compares the incoming `contentHash` against the prior applied hash. If different, it increments `configurationGeneration`, sets `managedBy = "configuration"` and the new counter value on each resource in the bundle, and performs the resource diff (create/update/delete scheduling). The `managedBy` and `configurationGeneration` fields are absent from the Nix-emitted bundle and are set exclusively by core at activation time. **`managedBy` field and per-item name-conflict handling**: `ManagedBy` is a closed enum (`Configuration`, `Controller`, `Api`) persisted in resource metadata at `packages/d2b-core-controller/src/resource_store.rs`. It is NOT a field in `ZoneBundle`/`BundleResource` input DTOs; core sets it at activation. Controllers set `ManagedBy::Controller` when creating owned children (net-VM Guest, config Volume, guest-agent Process, mDNS Processes); exact controller identity/UID/generation are tracked in separate internal metadata, not embedded in the `managedBy` value. API-created resources carry `ManagedBy::Api` and persist until explicit delete with no bundle-driven lifecycle. Core's generation-transition logic only schedules bundle-driven Delete for `ManagedBy::Configuration` resources. **Per-item name-conflict handling**: when a bundle item's `(zone, name)` already exists with `managedBy ≠ "configuration"`, core skips that item and records it with `phase = Degraded, reason: name-conflict`; a `ResourceConflictSkipped` audit record is emitted for that item. All non-conflicting items in the bundle proceed normally (Provider-state contract). The existing resource is left completely untouched. The operator deletes the conflicting resource via the resource API; the next bundle application applies the item. **Removal scheduling**: on generation N+1 activation, core performs a set difference: `prev_configuration_managed - new_configuration_managed` = resources to delete. For each, it sets `metadata.deletionRequestedAt` in the resource store and emits a `ResourceDeletionScheduled` audit record. Normal finalizer-path Delete proceeds asynchronously. **`PendingCleanup` condition**: the Zone self resource carries a `PendingCleanup = True` condition while any `managedBy = Configuration` resource has `deletionRequestedAt` set and has not yet been atomically removed. Aggregate Zone `phase = Degraded` applies. The condition transitions to `False` and Zone phase returns to `Ready` when all scheduled deletions complete. **Prior generation bundle retention** (`cleanup.rs`): count-based (`d2b.zones..retainedGenerations`, outside `Zone.spec`, default 3, range 1..16); no TTL. Core copies prior bundles to `/var/lib/d2b/zones//configuration/prior/.json`. A generation is eligible for pruning when all configuration-managed resources from it have either been atomically removed or are present unchanged in a newer generation, AND the count would be exceeded. **`BundleActivated` audit record**: emitted at each generation transition with `contentHash`, `configurationGeneration`, `resourceCount`, and `providerSchemaDigests` map (digests from `type=provider` artifacts via `Provider.spec.artifactId`); no spec contents, CIDRs, or resource names appear in the record. Provider schema digests in the bundle are re-verified against installed Provider artifact digests at application time; a mismatch aborts application with a `BundleRejected` audit record. | +| Integration | ADR046-network-004 (emitter writes bundle format; core sets `managedBy`/`configurationGeneration` at activation) → ADR046-network-008 (runtime reads and applies). ADR046-network-005 (controller Delete path) is invoked by ADR046-network-008 removal scheduling for Network resources. Zone `PendingCleanup` condition and `Degraded` phase are read by CLI `d2b zone status`. | +| Data migration | None on v3 initial install (no prior generation state). Host upgrades from the pre-ADR45 v3 baseline perform a reset: core starts with `configurationGeneration = 1` and no prior bundle. All declared resources are treated as new Creates. | +| Validation | **nix-unit**: `tests/unit/nix/cases/generation-cleanup-absent-network.nix` — verifies that a Network resource present in generation N and absent from generation N+1 receives `deletionRequestedAt` and appears in the `PendingCleanup` condition; verifies that a controller-owned `Guest` (`managedBy = "controller"`) does NOT receive a direct bundle-driven Delete; verifies that a re-declared (identical spec) Network is NOT scheduled for Delete; verifies `retainedGenerations` default is 3. **Rust contract tests**: Two separate test files — (1) `packages/d2b-contracts/tests/generation_bundle.rs`: tests the **input** bundle DTO only: `ZoneBundle`/`BundleResource`/`BundleMetadata` JSON round-trip, `contentHash` stability across serialization, `providerSchemaDigests` presence, `managedBy` and `configurationGeneration` fields ABSENT from `BundleResource` input struct (verified by both compile-time type check: the fields must not exist on the `BundleResource` type, and runtime JSON serialization: the serialized object must not contain those keys). (2) `packages/d2b-core-controller/tests/resource_metadata.rs`: `ManagedBy` closed enum round-trip with `"configuration"`/`"controller"`/`"api"` values tested separately here since `ManagedBy` is persisted resource metadata in `resource_store.rs`, not a field of the input bundle DTO. **Controller integration tests**: async Delete triggered through finalizers for Network; mDNS Process child deleted before Network finalizer clears; bridge `DeleteBridge` broker call made exactly once during finalizer; controller waits for Deleted watch event (not a persistent phase=Deleted row) before proceeding. **Host integration**: `tests/host-integration/nix-generation-cleanup.nix` — runNixOSTest scenario: apply generation 1 with Network resource, then apply generation 2 with that Network absent; assert Zone enters `Degraded/PendingCleanup`; assert Network `phase = Degraded` with `NetworkDraining = True` and `deletionRequestedAt` set and `reason: configuration-generation-removed`; assert cleanup completes (single store transaction: Deleted REVISION event + row/index removal; dedup-guarded audit append follows committed transaction) and Zone returns to `Ready`; assert no controller-owned children deleted directly by core; assert prior bundle copied to `/var/lib/d2b/zones//configuration/prior/` and retained until cleanup complete; assert bundle pruned when `retainedGenerations` exceeded and generation eligible. **INV-NET-LIFECYCLE-001**: core never schedules bundle-driven Delete for `managedBy ≠ "configuration"` resources; verified by static analysis of core's generation-transition diff function, which is bounded at compile time to iterate only the `configuration_managed_resources` set. **INV-NET-LIFECYCLE-002**: per-item name-conflict — when a bundle item collides with `managedBy = "controller"` or `"api"`, that item is recorded as `Degraded/name-conflict`; the existing resource is left untouched; non-conflicting items continue to activate; tested by `packages/d2b-core-controller/tests/configuration_name_conflict.rs` (three cases: collision with a controller-owned child, an API-created resource, and a same-name configuration resource from a prior generation that completed deletion; each case asserts non-conflicting items still activate). | +| Removal proof | Not applicable (this is a new capability). The `PendingCleanup` condition and zone cleanup audit path have no prior equivalent to remove. | + +### ADR046-network-009 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-network-009` | +| Dependency/owner | D097 Host-global authority index; ADR046-network-001, ADR046-network-005; Provider/network-local and Core authority owners | +| Current source | Existing macvtap spawn path resolves `parentInterface` but has no cross-Zone authority admission or compatible-sharing contract | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/network.rs` external-attachment sharing schema/status; `packages/d2b-core-controller/src/authority.rs` Core-derived physical-NIC identity and Host-global claim; Provider/network-local descriptor/reconcile/finalizer | +| Detailed design | Resolve operator-declared `parentInterface` against trusted Host inventory and derive an opaque `external-physical-nic/v1` digest; index `(Host, external-physical-nic, opaqueKeyDigest)` before any macvtap/VMM effect. `passthru`, `private`, and `vepa` are exclusive. `bridge` defaults exclusive and is multiplexed only under explicitly authored compatible policy. Use typed `external-physical-nic-conflict`; expose only bounded authority availability/holder-count/queue/arbitration/update-currency and conditions; keep digest, interface identity, and owner proof private. Parent/mode/policy update drains and releases the old claim before replacement; deletion closes macvtap/VMM ownership before releasing the claim; restart adopts exact owner proof and quarantines ambiguity. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt the existing private macvtap-FD spawn path; add authority admission before it. | +| Integration | Network validation and Core authority preflight gate runtime-cloud-hypervisor LaunchTicket/`SpawnRunner`; the finalizer and D091 update planner release in dependency order | +| Data migration | Full d2b 3.0 reset; no authority ledger import | +| Validation | Hermetic authority tests cover same-Zone and cross-Zone exclusive collisions, mixed-policy conflicts, non-bridge multiplex rejection, explicit compatible bridge multiplex admission, Core-derived key equality for two selectors resolving to one fake NIC, caller-supplied digest rejection, no-effect conflict, owner-proof adoption/ambiguity, disruptive update, and release-after-close ordering. Nix eval covers schema and declared cross-Zone conflicts; host integration covers create/update/delete with a fake macvtap parent and status/condition transitions without raw identity exposure. | +| Removal proof | None — authority admission is new; existing direct macvtap spawn becomes unreachable without a claim. | diff --git a/docs/specs/ADR-046-resources-volume.md b/docs/specs/ADR-046-resources-volume.md new file mode 100644 index 000000000..121977f4d --- /dev/null +++ b/docs/specs/ADR-046-resources-volume.md @@ -0,0 +1,1709 @@ +# ADR 0046 Volume resource + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-resources-volume` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-provider-volume-local`, `d2b-provider-volume-virtiofs`, Zone resource API/store | +| Depends on | `ADR-046-resource-object-model`, `ADR-046-primitive-resource-composition`, `ADR-046-resource-reconciliation`, `ADR-046-provider-model-and-packaging`, `ADR-046-components-processes-and-sandbox` | +| Supersedes | `storage.json` v2, `sync.json` v1, per-VM hardlink-farm and store-view emitters in `nixos-modules/storage-json.nix`, `nixos-modules/store.nix` | + +## Purpose + +This spec exhaustively defines the Volume ResourceType: schema, entry types, ACL +contract, lifecycle policies, sensitivity, named views, Process mounts, same-Zone +Host/Guest attachments, the virtiofs transport and owned virtiofsd Process, both +Provider implementations, current `storage.json`/`sync.json`/store-view/TPM/runtime +path rows and their v3 successors, async reconciliation, owner triggers, RBAC, +Nix configuration examples, and implementation work items. + +Volume replaces `storage.json`/`sync.json` path rows as the single authoritative +storage layout/lifecycle contract (D032). It does not replace Host/Guest/Process +configuration or broker wire operations. + +## Terminology mapping (current baseline → v3 target) + +All evidence citations in this spec use current baseline symbol names. This table +records the canonical rename so every citation can be traced forward to its v3 +destination. + +| Current baseline name | Current location | v3 target name | Notes | +| --- | --- | --- | --- | +| `Realm` / `RealmId` | `packages/d2b-realm-core/src/ids.rs` | Zone | `RealmId::parse(label)` validates lowercase label shape | +| `RealmPath` | `packages/d2b-realm-core/src/realm.rs` | Zone self resource path | Runtime directory root for the realm | +| `NodeId` | `d2b-realm-core/src/ids.rs` | Not yet a first-class v3 ResourceType | Retained in Zone runtime contracts | +| `WorkloadId` | `d2b-realm-core/src/ids.rs`, `src/workload.rs` | Guest (VM/sandbox/cloud/remote) or Host (local physical) | Classified by `WorkloadProviderKind` below | +| `WorkloadProviderKind::LocalVm` | `d2b-realm-core/src/workload.rs` | Guest with isolation: VirtualMachine under a Zone | Locally supervised NixOS microVM | +| `WorkloadProviderKind::QemuMedia` | same | Guest with isolation: VirtualMachine (QEMU media path) | Locally supervised external-media QEMU runtime | +| `WorkloadProviderKind::ProviderManaged` | same | Guest under a Zone-local Provider resource | Runtime owned by a provider adapter | +| `WorkloadProviderKind::UnsafeLocal` | same | User-only Host under Provider/system-core | No isolation boundary; `IsolationPosture::UnsafeLocal` | +| `ProviderId` | `d2b-realm-core/src/ids.rs` | Provider ResourceRef | Provider resource identity | +| `VmProcessDag` | `d2b-core/src/processes.rs` | Set of Process/EphemeralProcess resources under a Guest | Currently emitted as `processes.json` bundle artifact | +| `ProcessRole` | `d2b-core/src/processes.rs` | Process/EphemeralProcess ResourceType classification | enum variant → resource spec template name | +| `ProcessRole::Virtiofsd` | same | Process resource, template `virtiofsd-worker`, owned by volume-virtiofs | Currently a dag node under Guest (WorkloadId) | +| `ProcessRole::Swtpm` | same | Process resource owned by a device-tpm Provider (not Volume) | TPM state belongs to Volume; swtpm lifecycle to device-tpm | +| `ProcessRole::CloudHypervisorRunner` | same | Process resource template `cloud-hypervisor-runner` under Guest | Core VM runner | +| `d2b.vms.` | `nixos-modules/options-vms.nix` | v3 target: flat `d2b.zones..resources.` with `type = "Guest"` | Current Nix option namespace for VM config | +| `d2b.realms.` | `nixos-modules/options-realms.nix` | v3 target: Zone configuration | Current Nix namespace for realm workload stateDir etc. | +| `storage.json` row `scope: "vm:"` | `nixos-modules/storage-json.nix` | `ownerRef: Guest/` in Volume LayoutEntry | Opaque bundle ID in v2; typed ResourceRef in v3 | +| `storage.json` row `scope: "host"` | same | Zone-level path; ownerRef absent or `Host/` | Host-global paths owned by Zone runtime | +| `d2b-priv-broker` (`d2b-priv-broker.service`) | `packages/d2b-priv-broker/` | Zone broker; child realm broker child process | Fixed local-root broker; realm brokers are separate child processes (ADR 0045) | +| `d2bd` (`d2bd.service`) | `packages/d2bd/` | Zone runtime controller | Fixed local-root controller; child realm controllers are separate child processes | + +## Resolved decisions + +All ten Volume design decisions are resolved in this revision. + +| ID | Resolution | +| --- | --- | +| DRVOL-001 | `block-image` is a first-class volume-local SourceKind. volume-local manages the image file; the Guest Provider (cloud-hypervisor/QEMU) receives a stable FD and attaches it as `virtio-blk`. | +| DRVOL-002 | `quota.enforcement: hard` means the Volume is set to Failed at creation if the backing filesystem cannot enforce byte/inode quotas. `enforcement: none` is always permitted. For `tmpfs` SourceKind, enforcement is always effectively hard (kernel-enforced mount limits). | +| DRVOL-003 | Volume snapshot and storage-content migration operations are modeled as EphemeralProcess resources owned by the Volume and surfaced through the resource API; they are not CLI-only jobs. | +| DRVOL-004 | `access: read-write` permits at most one simultaneous writer; the controller enforces the single-writer constraint. `access: shared-write` is a distinct mode requiring the Provider to declare `supportsSharedWrite: true`; write-ordering semantics are the Provider's responsibility. | +| DRVOL-005 | `tmpfs` is a first-class SourceKind. `quota.maxBytes` and `quota.maxInodes` are required; usage is charged against the Host or Guest memory budget. `kind` must be `ephemeral` or `tmp`. | +| DRVOL-006 | Maximum per Volume: 1024 layout entries, 64 Views, 64 attachments. | +| DRVOL-007 | `file`, `directory`, and `symlink` are each first-class LayoutEntry types with independent `createPolicy`/`repairPolicy`/`cleanupPolicy`. A `symlink` entry carries a required `target` field; the target must be a path relative to the Volume root with no `..` components, no leading `/`, and no null bytes. Absolute targets and escape attempts are rejected at schema validation time. | +| DRVOL-008 | ACL `principal.ref` is a typed `User/` ResourceRef in the same Zone. No numeric UID/GID migration path; d2b 3.0 is a clean reset. The controller resolves the stable UID from the User resource at reconciliation time and re-resolves on User resource revision changes. | +| DRVOL-009 | `accessAcl` and `defaultAcl` are continuously reconciled: the controller re-applies declared ACLs to all existing entries and children during every repair cycle. A `foreignChildPolicy` field on each directory entry governs children not covered by `defaultAcl`: `preserve` leaves foreign ACL entries unchanged; `fail` sets an `ForeignAclViolation` condition on the entry. | +| DRVOL-010 | The virtiofsd export socket path is a generated private implementation detail of volume-virtiofs. It is never exposed as a spec field, status field, or API surface. ADR046-volume-003 owns the safe path generation contract. | + +## Volume ResourceSpec + +### Three-layer spec shape (D089) + +D089 freezes Volume spec as three layers. Layer 1 is the universal Resource +envelope and metadata. Layer 2 is the Volume base spec at top-level `spec.*`, +including `spec.providerRef`; source, kind, layout, views, attachments, quota, +and lifecycle fields documented here are base fields. Layer 3 is the optional +canonical selected-Provider extension +`spec.provider = { schemaId, schemaVersion, settings }`; it is the only +Provider-specific desired extension. It omits `providerRef` and +`observedProviderGeneration`: `spec.providerRef` is base, and spec is desired +rather than observed. + +**D091 update policy.** The universal base spec carries `spec.updatePolicy` for +every Volume: disruptive changes default to manual, while automatic +non-disruptive upgrades are permitted by policy. A `spec.provider` extension MAY +add provider-specific knobs, but MUST NOT bypass or weaken base +`spec.updatePolicy`. + +**D090 expedited reconcile.** Authorized Volume `Create`, `UpdateSpec`, and +`Delete` calls MAY set `waitForReconcile`. Under one mutation ticket, +`operationId`, and deadline, Core admission and the reserved-revision redb commit +run in parallel with controller preflight/plan, but the controller MUST NOT +perform external effects, finalizer release, or status mutation until Core +supplies `CommittedRevisionProof {resourceUid, generation, revision, +operationId}`; DB failure aborts with no effect. The API returns the committed +object plus one-pass projected layered status, `disposition` +(`Converged|Progressing|Blocked|UpgradeRequired|Failed`), `statusPersistence` +(`pending|committed`), and the last persisted status revision. The durable +commit is never rolled back on reconcile timeout or failure; effect idempotency +keys derive from `(UID,generation,revision,operationId)`, and the expedited pass +uses a bounded priority lane in the same per-resource single-flight. + +Every Volume Provider `ResourceApiBinding` MUST implement the exact Volume base +spec schema version and fingerprint, accept the canonical minimal valid base +Spec, and pass base lifecycle/status/error/finalizer conformance. A Provider MAY +reject an optional base capability only through its signed standard capability +matrix and a typed provider-neutral `unsupported-capability` error; it MUST NOT +ignore, reinterpret, rename, duplicate, weaken, or require extension data for +base-required behavior. `spec.provider.settings` is strict deny-unknown, +bounded, schema-versioned and digested, validated against `spec.providerRef` at +Nix build and API admission, and fails with `spec-provider-schema-invalid` or +`spec-provider-shadow` when invalid or shadowing/restating/overriding/renaming/ +duplicating a base field. Shared Volume semantics are promoted to the Volume +base spec and never live in `spec.provider`; generic CLI/controllers operate on +base spec plus base status. For the same Provider, the `spec.provider` and +`status.provider` schemas align. + +`Volume.spec.source.settings` (including `kind` and `sourcePolicyId`) and the +per-attachment `settings` object (typed mount options) are Volume base +structures, not a Provider extension. Only genuinely +implementation-only desired settings use `spec.provider.settings`. +Provider resource dossiers in this file retain the D075 Provider +self-description shape (`spec.artifactId`, `spec.config`) because a Provider has +no non-circular `spec.providerRef`. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Volume +metadata: + name: work-state + zone: dev + uid: + generation: 1 + ownerRef: Guest/work-vm # optional + finalizers: [] +spec: + providerRef: Provider/volume-local + source: + executionRef: Host/host-system # where the backing storage lives + settings: + kind: local-path # see SourceKind below + sourcePolicyId: + kind: state # volume semantic kind (see below) + layout: + - path: "" # root of the volume + type: directory + ownerRef: User/example-system + groupRef: User/example-system + mode: "0700" + accessAcl: [] + defaultAcl: [] + noFollow: true + recursive: false + sensitivity: private + createPolicy: create-if-never-provisioned + repairPolicy: exact-owner + cleanupPolicy: owner-controlled + adoptionPolicy: adopt-with-live-owner-proof + restartPolicy: preserve-across-controller-restart + leaseClass: none + invariants: [no-symlink, broker-opaque-id-only] + views: + controller: + path: "" + rights: [read, write, create, delete, traverse] + reader: + path: "" + rights: [read, traverse] + attachments: + - executionRef: Guest/work-vm + transport: virtiofs + view: controller + access: read-write + mountPath: /state + settings: + posixAcl: false + xattr: false + cache: auto + inodeFileHandles: never + threadPoolSize: null # null → vcpu count + quota: null # null = no limit; see §Quota for enforcement options +status: {} +``` + +### Spec field reference + +| Field | Type | Required | Default | Constraints | +| --- | --- | --- | --- | --- | +| `providerRef` | ResourceRef | Yes | — | Must resolve to a Ready Provider in the same Zone implementing Volume | +| `source.executionRef` | ResourceRef | Yes | — | Must resolve to a Host or Guest in the same Zone | +| `source.settings` | object | Yes | — | Validated against Provider-specific source schema | +| `source.settings.kind` | SourceKind enum | Yes | — | `local-path`, `block-image`, `tmpfs` | +| `source.settings.sourcePolicyId` | string | conditional | — | Opaque bounded ID for `local-path`/`block-image`; references an entry in volume-local's private allowlisted root policy. Never a raw host path; never exposed in public status or audit. | +| `kind` | VolumeKind enum | Yes | — | `durable`, `ephemeral`, `state`, `tmp`, `cache` | +| `layout` | LayoutEntry[] | Yes | `[]` | Anchored relative paths; must be non-overlapping; max 1024 entries | +| `views` | map | Yes | `{}` | ViewName matches `^[a-z][a-z0-9-]*$`; max 64 Views | +| `attachments` | Attachment[] | No | `[]` | Max 64 attachments; at most one `read-write` at any time; `shared-write` requires Provider `supportsSharedWrite: true` | +| `quota` | QuotaSpec or null | No | null | `enforcement: hard` fails Volume if backing FS cannot enforce; `enforcement: none` always permitted | + +### VolumeKind semantics + +| Kind | Persistence | Cleanup trigger | Typical backing | +| --- | --- | --- | --- | +| `durable` | Across reboots and controller restarts | Never (operator-controlled) | Local persistent directory | +| `state` | Across reboots and controller restarts; content is role-state | Never; fail-closed on missing-after-provision | Local persistent directory + provisioning marker | +| `cache` | Best-effort; may be rebuilt | On controller restart or explicit flush | Local persistent or tmpfs | +| `ephemeral` | Boot-scoped; content is transient | On Host/Guest restart or Zone reset | `tmpfs` or `local-path` (boot-scoped subdir under `/run/d2b`) | +| `tmp` | Process-scoped; cleaned on process exit | Process exit with proof | Local subdirectory with process-pidfd lease | + +## Layout entries + +A LayoutEntry declares one path relative to the Volume root. Relative path `""` is +the Volume root itself. All paths are anchored; no `..`, absolute path, symlink +traversal (when noFollow is true), or drive-letter form is accepted. + +### LayoutEntry fields + +| Field | Type | Required | Default | Constraints | +| --- | --- | --- | --- | --- | +| `path` | relative path string | Yes | — | Anchored; `""` = volume root; no leading `/`; no `..` | +| `type` | EntryType enum | Yes | — | `directory`, `file`, `symlink`, `unix-socket`; each is first-class with independent lifecycle policies | +| `ownerRef` | ResourceRef | Yes | — | Must resolve to a `User/` ResourceRef in the same Zone; no numeric UID accepted | +| `groupRef` | ResourceRef | Yes | — | Must resolve to a `User/` ResourceRef in the same Zone; no numeric GID accepted | +| `mode` | octal string | Yes | — | Four-octet string, e.g. `"0700"`, `"0640"`, `"0660"` | +| `target` | relative path string | conditional | — | Required for `symlink` type only; relative to Volume root; no `..`, no leading `/`, no null bytes; must resolve within Volume root | +| `accessAcl` | AclGrant[] | No | `[]` | Named access ACL; continuously reconciled during every repair cycle | +| `defaultAcl` | AclGrant[] | No | `[]` | Default ACL applied to all new children; continuously reconciled; `foreignChildPolicy` governs unlisted children | +| `foreignChildPolicy` | `preserve` or `fail` | No | `preserve` | For `directory` entries: `preserve` retains unexpected child ACL entries; `fail` sets `ForeignAclViolation` condition | +| `noFollow` | bool | No | `true` | Reject symlink traversal during layout operations; may be false only for `symlink`-type entries | +| `recursive` | bool | No | `false` | Apply owner/mode/ACL recursively during repair; dangerous for large trees | +| `sensitivity` | SensitivityClass enum | No | `private` | Governs audit redaction and log handling | +| `createPolicy` | CreatePolicy enum | No | `create-if-absent` | When to create the entry | +| `repairPolicy` | RepairPolicy enum | No | `exact-owner` | How to reconcile drift from the declared state | +| `cleanupPolicy` | CleanupPolicy enum | No | `never` | When the entry is removed | +| `adoptionPolicy` | AdoptionPolicy enum | No | `adopt-with-live-owner-proof` | How an existing entry is treated on first bind | +| `restartPolicy` | RestartPolicy enum | No | `preserve-across-controller-restart` | Behavior across Volume controller restart | +| `leaseClass` | LeaseClass enum | No | `none` | Type of live-ownership lease checked during adoption | +| `invariants` | Invariant[] | No | `[no-symlink]` | Additional fail-closed checks | + +### EntryType + +| Value | Current baseline analog | Notes | +| --- | --- | --- | +| `directory` | `StoragePathKind::Directory` | Default and most common | +| `file` | `StoragePathKind::RegularFile` | Regular file; must be declared with an ownerRef/mode | +| `symlink` | `StoragePathKind::Symlink` | First-class entry type with independent lifecycle policies; `noFollow: false` required; `target` field required (relative to Volume root, no `..`, no absolute); target is validated at schema time | +| `unix-socket` | `StoragePathKind::UnixSocket` | Mode `0660` default; process-scoped cleanup required | + +`DeviceNode` and `ExternalGrantOnly` from the current `StoragePathKind` enum are not +exposed as Volume LayoutEntry types. Device nodes are a Device Provider concern; +`external-grant-only` becomes an observation-only entry with `repairPolicy: none`. + +### AclGrant + +```yaml +principal: + ref: User/example-system # typed User/ ResourceRef; always same Zone +permissions: rwx # POSIX ACL permission string +``` + +ACL principals are always typed `User/` ResourceRefs in the same Zone. +No numeric UID/GID form is accepted; d2b 3.0 is a clean reset with no numeric +migration path. The controller resolves the User's stable UID at reconciliation +time and re-resolves on any User resource revision change that affects the UID binding. + +### CreatePolicy + +| Value | Semantics | Baseline `StorageLifecycle` analog | +| --- | --- | --- | +| `create-if-absent` | Create the entry if it does not exist | `Config`, `Persistent` | +| `create-if-never-provisioned` | Create only if a prior-provision marker is absent; preserve existing content | — (swtpm/state hardening model) | +| `always-recreate` | Always remove and recreate; use only for process-scoped entries | `ProcessScoped` | +| `observe-only` | Do not create; observe and report phase but do not mutate | `ExternalObserveOnly` | + +### RepairPolicy + +| Value | Semantics | Baseline analog | +| --- | --- | --- | +| `none` | No repair; report drift as a condition | `RepairPolicy::None` | +| `nix-activation` | Repair is Nix activation system responsibility | `RepairPolicy::NixActivation` | +| `exact-owner` | Broker reconciles owner/group/mode to exact declared values; non-recursive by default | `RepairPolicy::BrokerReconcile` | +| `fail-closed` | Broker treats any drift as a fatal condition; sets Degraded/Failed; no repair | `RepairPolicy::BrokerFailClosed` | +| `operator-only` | No automated repair; operator must intervene | `RepairPolicy::OperatorOnly` | + +### CleanupPolicy + +| Value | Semantics | Baseline analog | +| --- | --- | --- | +| `never` | Entry is never removed by the Volume controller | `CleanupPolicy::Never` | +| `boot` | Removed on next host/Zone boot; entry is /run/-scoped | `CleanupPolicy::Boot` | +| `process-exit-with-proof` | Removed after the owning Process exits (verified by pidfd) | `CleanupPolicy::ProcessExitWithProof` | +| `vm-stop-with-proof` | Removed when the owning Guest stops (verified by controller) | `CleanupPolicy::VmStopWithProof` | +| `cutover-only` | Removed on cutover/generation switch | `CleanupPolicy::CutoverOnly` | +| `owner-controlled` | Lifecycle is owned by the controller that mounts/creates the Volume | `CleanupPolicy::External` | + +### AdoptionPolicy + +| Value | Semantics | Baseline analog | +| --- | --- | --- | +| `adopt-with-live-owner-proof` | Adopt existing entry if owner proof (pidfd/cgroup) is live | `StorageAdoptionPolicy::AdoptWithLiveOwnerProof` | +| `recreate-from-persistent` | Delete existing and recreate from persistent state | `StorageAdoptionPolicy::RecreateFromPersistent` | +| `quarantine-on-ambiguity` | Quarantine existing entry; set Degraded; do not destroy | `StorageAdoptionPolicy::QuarantineOnAmbiguity` | +| `delete-if-owner-dead` | Delete existing entry if the owner is no longer live | `StorageAdoptionPolicy::DeleteIfOwnerDead` | +| `not-adoptable` | Entry is not adoptable; always recreated on controller start | `StorageAdoptionPolicy::NotAdoptable` | + +### RestartPolicy + +| Value | Semantics | Baseline analog | +| --- | --- | --- | +| `preserve-across-controller-restart` | Entry is retained across Volume controller restart | `StorageRestartPolicy::PreserveAcrossDaemonRestart` | +| `recreate-after-owner-death` | Entry is recreated if the owning process exits | `StorageRestartPolicy::RecreateAfterOwnerDeath` | +| `cleanup-after-owner-death` | Entry is removed if the owning process exits | `StorageRestartPolicy::CleanupAfterOwnerDeath` | +| `manual-recovery` | Restart requires operator action; controller sets Degraded | `StorageRestartPolicy::ManualRecovery` | +| `not-applicable` | Entry has no process owner; restart policy is irrelevant | `StorageRestartPolicy::NotApplicable` | + +### LeaseClass + +| Value | Semantics | Baseline analog | +| --- | --- | --- | +| `none` | No live-ownership lease | `LeaseClass::None` | +| `process-pidfd` | Entry is leased to a process identified by pidfd | `LeaseClass::ProcessPidfd` | +| `cgroup-leaf` | Entry is leased to a cgroup leaf | `LeaseClass::CgroupLeaf` | +| `file-record` | Entry has an OFD file-record lock | `LeaseClass::FileRecord` | + +### SensitivityClass + +| Value | Semantics | Baseline analog | +| --- | --- | --- | +| `public` | Content may be mentioned in status/logs at bounded granularity | `SensitivityClass::Public` | +| `private` | Content path must not appear in public status/audit events | `SensitivityClass::Private` | +| `secret-adjacent` | Content path and size must not appear anywhere outside the broker audit trail | `SensitivityClass::SecretAdjacent` | +| `audit` | Entry is a tamper-evident audit segment; special repair/cleanup rules | `SensitivityClass::Audit` | +| `zone-scoped` | Sensitivity is bounded to the Zone boundary; Zone-link does not export metadata | `SensitivityClass::RealmScoped` | + +### Invariants + +| Value | Semantics | Baseline analog | +| --- | --- | --- | +| `no-symlink` | Broker rejects symlinks during path walk | `StorageInvariant::NoSymlink` | +| `no-magic-link` | Broker rejects magic links (`/proc/self/...`) | `StorageInvariant::NoMagicLink` | +| `no-recursive-mutation` | Broker does not recurse into children | `StorageInvariant::NoRecursiveMutation` | +| `same-filesystem` | Entry must share `st_dev` with the Volume root (hardlink farm constraint) | `StorageInvariant::SameFilesystem` | +| `hardlink-farm-no-recursion` | Entry is a hardlink farm node; broker does not recurse | `StorageInvariant::HardlinkFarmNoRecursion` | +| `broker-opaque-id-only` | Only broker-assigned identities may create children | `StorageInvariant::BrokerOpaqueIdOnly` | + +## Named views and rights + +A View maps a name to a subtree of the Volume and a bounded rights set. +Process mounts and attachments always select a named View; they do not +reference the raw Volume path. + +```yaml +views: + controller: + path: "" # subtree root relative to Volume root ("" = whole volume) + rights: [read, write, create, delete, traverse, execute] + reader: + path: data + rights: [read, traverse] + config: + path: config + rights: [read] +``` + +### Rights + +| Right | Meaning | +| --- | --- | +| `read` | Read file contents and directory entries | +| `write` | Modify file contents; create, delete, rename within the subtree | +| `create` | Create new files/directories directly in this subtree | +| `delete` | Remove files/directories directly in this subtree | +| `traverse` | Enter directories (needed to reach sub-paths) | +| `execute` | Execute files; implies `traverse` on parent directories | + +A View grants only rights that the Volume LayoutEntry ACLs permit for the +Process/Guest principal. The controller validates right intersection at attach time. + +ViewName must match `^[a-z][a-z0-9-]*$`. A Volume must have at least one View. +Views declared in a Process mount or attachment spec must exist in the Volume at +creation time. + +## Quota + +The `quota` field specifies storage limits for the Volume. `enforcement: hard` +requires the Provider to verify that the backing filesystem can enforce byte and +inode limits at Volume creation time; if it cannot, the Volume is set to Failed +immediately and no layout operations are performed. `enforcement: none` is always +accepted and records the limits for informational purposes only. + +```yaml +quota: + maxBytes: 10737418240 # 10 GiB; required when enforcement: hard + maxInodes: 1000000 # required when enforcement: hard + enforcement: none # none | hard + # hard: Volume fails if FS cannot enforce limits +``` + +For `tmpfs` SourceKind Volumes, `quota.maxBytes` maps to the `size=` mount option +and `quota.maxInodes` to `nr_inodes=`; the kernel enforces these limits so +enforcement is always effectively `hard` for tmpfs. `quota.maxBytes` and +`quota.maxInodes` are required for `tmpfs` source Volumes. + +## Process volume mounts + +Process and EphemeralProcess spec inline their Volume mounts: + +```yaml +mounts: + - volumeRef: Volume/work-state + view: controller + mountPath: /state + access: read-write # read-only | read-write + optional: false +``` + +| Field | Type | Required | Default | Constraints | +| --- | --- | --- | --- | --- | +| `volumeRef` | ResourceRef | Yes | — | Must resolve to a Ready Volume in the same Zone | +| `view` | ViewName | Yes | — | Must exist in the Volume spec | +| `mountPath` | absolute path string | Yes | — | Inside the Process sandbox; no overlap with other mounts | +| `access` | `read-only` or `read-write` | No | `read-only` | Must be compatible with View rights | +| `optional` | bool | No | `false` | If true, absent/Degraded Volume does not prevent Process start | + +The Process Provider (system-systemd or system-minijail) never resolves the +Volume root itself. ProviderSupervisor resolves it via `VolumeSourceEffectPort` +at launch time and delivers a bound FD in the LaunchTicket. The raw host +path never appears in Process ResourceSpec, status, or audit. + +## Volume source + +### SourceKind + +| Kind | Backing | Notes | +| --- | --- | --- | +| `local-path` | Host directory rooted at an allowlisted policy entry, referenced by opaque `sourcePolicyId` | Only accepted by volume-local with a policy-allowlisted root | +| `block-image` | Raw or qcow2 disk-image file under the allowlisted policy entry's root, referenced by opaque `sourcePolicyId` | volume-local manages the image file; Guest Provider attaches as `virtio-blk`; `kind: ephemeral` or `kind: durable`; `quota.maxBytes` required | +| `tmpfs` | Memory-backed tmpfs mount; no persistent backing | `quota.maxBytes` and `quota.maxInodes` required (charged to Host/Guest memory budget); `kind` must be `ephemeral` or `tmp`; cleanup unmounts the tmpfs | + +`source.settings.sourcePolicyId` is a required field for `local-path` and +`block-image`. It is an opaque bounded string ID (never a raw path) that +references one entry in volume-local's own private `config.allowedHostPaths` +policy catalog — each catalog entry carries its own `id` plus the actual root +path. The Provider process and its controller see only the ID; path +resolution happens exclusively inside volume-local's private Nix/bundle/effect +authority, and the resolved path is handed to the caller only as an opaque FD +via `VolumeSourceEffectPort` at attach/launch time. It never appears in +public status, audit records, or CLI output. The allowlisted roots in the +v3.0 initial policy are: + +- `id: state-root`, root `$stateDir` (default `/var/lib/d2b`) — durable and state Volumes +- `id: ephemeral-root`, root `/run/d2b` — ephemeral and tmp Volumes +- `id: cache-root`, root `/var/cache/d2b` — cache Volumes + +Operator root config binds `stateDir` at Nix compile time. + +## Same-Zone Host/Guest attachments + +An Attachment declares that the Volume is exported to an execution context. + +```yaml +attachments: + - executionRef: Guest/work-vm + transport: virtiofs + view: controller + access: read-write + mountPath: /state + settings: + posixAcl: false + xattr: false + cache: auto # auto | always | never + inodeFileHandles: never # never | prefer | mandatory + threadPoolSize: null # null → vcpu count of the target Guest + socketGroup: null # null → broker-default (runner gid) +``` + +| Field | Type | Required | Default | Constraints | +| --- | --- | --- | --- | --- | +| `executionRef` | ResourceRef | Yes | — | Host or Guest in same Zone | +| `transport` | AttachmentTransport enum | Yes | — | `virtiofs` for filesystem shares; `virtio-blk` for `block-image` source Volumes | +| `view` | ViewName | Yes | — | Must exist in the Volume spec | +| `access` | `read-only`, `read-write`, or `shared-write` | No | `read-only` | `read-write`: single writer enforced by controller; `shared-write`: requires Provider `supportsSharedWrite: true`; must be compatible with View rights | +| `mountPath` | absolute path string | Yes | — | Guest-side mount path | +| `settings` | typed attachment-options object | No | `{}` | Volume base nested attachment (mount) options defined by the Volume base schema (`posixAcl`, `xattr`, `cache`, `threadPoolSize`, `inodeFileHandles`, `socketGroup`) and validated against it; a ResourceType-common structure, not a Provider extension. Genuinely implementation-only tuning uses `spec.provider.settings`, never this base object. | + +The Volume controller creates one owned virtiofsd Process per attachment when +`transport: virtiofs`. Multiple attachments with distinct `executionRef` values +each get a separate virtiofsd Process. + +## virtiofs attachment controller (volume-virtiofs) + +`Provider/volume-virtiofs` is the controller that reconciles virtiofs attachments. +It is one Provider crate with one controller component and one worker Process +binary (virtiofsd itself). + +### Responsibilities + +1. For each Volume with at least one virtiofs attachment, volume-virtiofs + ensures exactly one owned virtiofsd `Process` exists per attachment. +2. On attachment create/repair, it emits a `Create` or `UpdateSpec` for the + virtiofsd Process resource. +3. On attachment delete, it requests virtiofsd Process deletion and observes + its finalizer drain before clearing the attachment finalizer. +4. Per-attachment status records export socket readiness and guest mount state. + +### Owned virtiofsd Process + +The virtiofsd Process resource is owned by the Volume (via `ownerRef`) and +managed by volume-virtiofs. Its spec follows the common Process spec with +Provider-specific fields: + +```yaml +type: Process +metadata: + name: vol-work-state-virtiofsd-work-vm + ownerRef: Volume/work-state +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + processClass: worker + template: virtiofsd-worker # resolves through Provider/volume-virtiofs registered as the Volume controller + sandbox: + namespaceClasses: [mount, user] + capabilityClasses: [] + startRoot: false + seccompClass: w1-virtiofsd + readOnlyRoot: true + userNamespace: + mappingClass: process-principal-root # maps in-NS UID/GID 0 to User/vol-work-state-vfd + mounts: [] +``` + +Every field above is one of the exact common Process/SandboxSpec fields (see +`ADR-046-resources-host-guest-process-user`); there is no Provider-private +`hostUidForZero`/`hostGidForZero`, `requiresStartRoot`, `seccompPolicyRef`, +`readOnlyPaths`/`writablePaths`, or `cgroupSubtree` field. `cgroupSubtree` is +never authored: cgroup placement is derived by ProviderSupervisor/the broker +from the declared Process, exactly as for any other Process. + +The virtiofsd worker has: + +- **Zero host capabilities** (`capabilityClasses: []`). All filesystem capabilities + are scoped inside the user namespace (ADR 0021). +- **ProviderSupervisor-mediated, broker-pre-established user namespace**: the + `system-minijail` Provider controller never calls the broker itself; it + resolves `userNamespace.mappingClass: process-principal-root` through + `ProcessLaunchEffectPort`, and ProviderSupervisor dispatches + `clone3(CLONE_NEWUSER)` to the broker to write a single-entry UID/GID map + (`in-NS 0 → stable principal UID`) before virtiofsd's first instruction + runs. The mapping principal is `User/vol--vfd` — a dedicated + per-Volume User resource. +- **`--sandbox=chroot`**: permitted because `CAP_SYS_ADMIN` is available inside + the user namespace. +- **`--inode-file-handles=never`**: `open_by_handle_at(2)` is not needed for + read-only or per-VM share serving. +- **`--posix-acl --xattr`** only when the attachment `settings.posixAcl` or + `settings.xattr` is true. These flags are omitted for the ro-store share + because `/nix/store` has no ACLs and per-VM hardlink farms are d2b-managed. +- **`--readonly`** when `access: read-only` or when the backing Volume kind + is `state/durable` and the View rights do not include `write`. +- **`--cache=`** from `settings.cache`, default `auto`. +- **`--thread-pool-size=`** from resolved `settings.threadPoolSize` or + the target Guest's vcpu count. +- The worker's own runtime/control state (export socket, control files) is a + private runtime path under the Zone/Guest runtime root directory (not a + Volume — see `path:vm-run:` in the current-code migration table), + computed and handed to the worker only through its LaunchTicket. It is + never an authored `mounts` entry and never a raw writable sandbox path. +- Read access to `/nix/store` for the virtiofsd binary's own execution is + inherent to standard sandbox namespace inheritance; it is never an authored + sandbox path list entry. + +virtiofsd is NOT started as root (`startRoot: false`). It never holds +ambient host capabilities. Any change to the virtiofsd sandbox profile that +introduces host capabilities, `startRoot: true`, or `--sandbox=namespace` +violates ADR 0021. This invariant is tested by `tests/minijail-validator-virtiofsd.sh` +and enforced by the `tests/unit/nix/cases/broker-caps.nix` policy gate. + +### Per-attachment status + +Volume status includes: + +```yaml +status: + observedGeneration: 1 + phase: Ready + conditions: + - type: LayoutReady + status: "True" + reason: layout-reconciled + observedGeneration: 1 + - type: AttachmentsReady + status: "True" + reason: all-attachments-ready + observedGeneration: 1 + attachmentStatuses: + - executionRef: Guest/work-vm + transport: virtiofs + virtiofsdProcessRef: Process/vol-work-state-virtiofsd-work-vm + exportReady: true + guestMountReady: true # observed via guest-control health probe + # export socket path is a private implementation detail; never a status field + phase: Ready + lastCheckedAt: 2026-07-22T00:00:01Z +``` + +The virtiofsd export socket path is an internal implementation detail of +volume-virtiofs and is never exposed as a status field, spec field, or API surface. +Export readiness is detected by the Unix socket listener check (current: +`unix-socket-exists` readiness kind). Guest mount readiness is observed via the +guest-control health protocol. + +## Store-view Volume + +The per-VM closure-only Nix store hardlink farm is modeled as a Volume with +`Provider/volume-local`, `kind: durable`, and `source.settings.kind: local-path` +rooted at `$storeStateDir//store-view`. + +The store-view Volume is always owned by the Guest that uses it. Its attachment +has `transport: virtiofs`, `view: ro-store`, `access: read-only`, and +`mountPath: /nix/.ro-store` in the guest. + +The Volume controller (or system-minijail broker, until volume-local is live) +enforces: + +1. **Hardlink farm layout**: the `live/` subdirectory contains only hardlinks + from the VM's Nix closure. It is never a direct bind of `/nix/store`. + `share.source == "/nix/store"` is the eval-time sentinel that triggers + store-view substitution; virtiofsd is pointed at `store-view/live`, not + at `/nix/store`. +2. **Same-filesystem invariant**: hardlinks require `st_dev` equality between + `/nix/store` and `$storeStateDir`. If they differ, the controller fails + closed and reports `storage-drift` condition. +3. **Marker file**: `store-view/live/.d2b-marker-` is a zero-length file + owned `d2bd:users 0444`, `invariants: [no-symlink, same-filesystem, + hardlink-farm-no-recursion, broker-opaque-id-only]`. Its existence is + checked by the virtiofsd readiness predicate before the virtiofsd worker + is considered ready. +4. **Generation meta**: `store-view/meta/current` is a symlink (noFollow: false) + pointing at `generations/`. The current generation contains `system`, + `store-paths`, `db.dump`, and `meta.json`. GC roots live in + `store-view/gcroots/generation-` (host-only, at store-view root; see + spec correction below). +5. **State directory**: `store-view/state/` is a host-only directory that holds + per-generation state entries (`state/generations//`). It is never served + to the guest via virtiofsd. +5. **Sync lock**: `store-view/sync.lock` is a regular-file OFD advisory lock + (`leaseClass: file-record`, owner `d2bd:users 0640`) used during store sync. + It is never unlinked (preserves OFD semantics across controller restarts). +6. **Private mount namespace**: the broker performs the hardlink operation inside + a private mount namespace where `/nix/store` is lazily detached from the + bind-mount shadow, avoiding `EXDEV` from cross-vfsmount `link(2)`. + A `EMLINK` fallback (saturated inode link count) copies the byte content. + +### Store-view LayoutEntry rows + +| Entry path (relative) | Type | Invariants | Notes | +| --- | --- | --- | --- | +| `` (root) | directory | no-symlink, scope-authorization-required | root: `d2bd:users 0755` | +| `live` | directory | no-symlink, broker-opaque-id-only | hardlink farm root; `cutover-only` cleanup | +| `live/.d2b-marker-` | file | no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only | zero-length readiness marker; `d2bd:users 0444` | +| `meta` | directory | no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only | generation meta tree; guest-served via virtiofsd | +| `meta/generations` | directory | no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only | cutover-only cleanup | +| `meta/current` | symlink | broker-opaque-id-only | noFollow: false; points at generations/ | +| `state` | directory | no-symlink, broker-opaque-id-only | host-only; NOT guest-served; `d2bd:users 0700`; holds `state/generations//` | +| `gcroots` | directory | no-symlink, same-filesystem, hardlink-farm-no-recursion, broker-opaque-id-only | host-only, at store-view root (NOT under `meta/`); cutover-only cleanup; `d2bd:users 0755` | +| `sync.lock` | file | no-symlink, broker-opaque-id-only | OFD lock; never unlink; leaseClass: none | + +**Spec correction**: `nixos-modules/storage-json.nix` (baseline `b5ddbed6`) declares +`path:store-view-gcroots` at `store-view/meta/gcroots` and omits `store-view/state/` +entirely. `packages/d2b-host/src/hardlink_farm.rs::gcroots_dir()` places gcroots at +`store-view/gcroots` (store-view root), and `packages/d2b-priv-broker/src/ops/store_view_posture.rs` +confirms the broker posturas `state/`, `gcroots/`, `sync.lock` at root level. Code wins; +the v3 Volume LayoutEntry spec follows `hardlink_farm.rs`. `storage-json.nix` path drift +will be resolved when Volume resources replace the path rows. + +## TPM Volume + +The per-VM swtpm state directory is modeled as a Volume with `Provider/volume-local`, +`kind: state`, `source.settings.kind: local-path`, and root `$storeStateDir//swtpm`. + +Key invariants enforced by the volume-local controller (pre-v3: broker `swtpm_dir.rs`): + +1. **Fail-closed owner**: any mismatch between declared ownerRef UID and `st_uid` + fails closed with a typed, path-free error. The controller never silently + chowns existing NVRAM. +2. **Provisioning marker**: `/var/lib/d2b/swtpm-markers/` is a root-owned + regular file (`0600`, `invariants: [no-symlink, root-owned-parent, + broker-opaque-id-only, scope-authorization-required]`). It records the trusted + `st_dev`/`st_ino` plus first-provision stamp. If the swtpm directory is absent + after the marker was written (`previously-provisioned-swtpm-state-missing`), + the controller sets Failed and refuses to re-provision. +3. **Stale socket cleanup**: a stale `tpm.sock` under the runtime dir is unlinked + before the swtpm Process is started. The socket path itself is not part of + the TPM Volume layout; it belongs to the TPM Device Provider runtime. +4. **Sensitivity**: `sensitivity: secret-adjacent`. The swtpm state path must never + appear in public status, audit, or log output. + +TPM Volume layout entry: + +```yaml +layout: + - path: "" + type: directory + ownerRef: User/d2b--swtpm # stable UID mapped by User resource + groupRef: User/d2b--swtpm + mode: "0700" + createPolicy: create-if-never-provisioned + repairPolicy: fail-closed + cleanupPolicy: never + adoptionPolicy: quarantine-on-ambiguity + sensitivity: secret-adjacent + invariants: [no-symlink, broker-opaque-id-only, scope-authorization-required] +``` + +## Volume status + +### Three-layer status shape (D088) + +D088 freezes `Volume` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `Volume`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +**D091 update currency.** Every Volume includes universal `status.update` with +`state` (`Current|UpdateAvailable|UpgradeRequired|Upgrading|Blocked|Unknown`), +`reasons` +(`CoreGenerationChanged|ProviderGenerationChanged|ArtifactChanged|ImageOrSystemGenerationChanged|SpecChanged|DependencyChanged|SecurityPolicyChanged`), +bounded non-secret observed/target generation and digest IDs, `disruption` +(`None|Reload|Restart|Recycle|Replace`), `preserveState`, optional +`operationId`, `lastAssessedAt`, and bounded/truncated `owned:{count,refs}` and +`dependencies:{count,refs}`. Volume-specific currency refinements live in +`status.resource` and never in `status.provider`; controllers set +`status.update` via `assess_update` on core/provider/artifact/spec/dependency/ +security-policy triggers and MUST report `UpgradeRequired` for disruptive +changes rather than applying them in place. Durable/state Volumes report +`preserveState: true` across upgrades; disruption `Replace` is allowed only with +explicit state transfer. + +`Volume` has multiple implementations (`volume-local` and `volume-virtiofs`). +Attachment base status, layout phase/conditions, marker/invariant observations, +quota observations, and per-attachment readiness are frozen in `status.resource` +and MUST be identical across all implementations. Implementation-specific +observation belongs only in that implementation's `status.provider.details`; +shared fields MUST NOT be duplicated there. + +Common resource status plus: + +| Field | Type | Notes | +| --- | --- | --- | +| `layoutPhase` | phase string | `Pending`, `Ready`, `Degraded`, `Failed` | +| `layoutConditions` | Condition[] | Per-entry condition types: `EntryMissing`, `EntryDrift`, `EntryQuarantined`, `InvariantViolated` | +| `attachmentStatuses` | AttachmentStatus[] | One per attachment; see per-attachment status above | + +Condition message is bounded (512 bytes), UTF-8/control-character validated, and +must not contain host paths, secret content, process data, or terminal bytes. + +Phase transitions: + +```text +Pending → Ready + → Degraded (recoverable drift or quarantine) + → Failed (fail-closed invariant, missing-after-provision) + → Unknown (controller/Host/link unreachable) +``` + +## Async reconciliation + +Volume reconciliation follows the common reconciliation loop +(`ADR-046-resource-reconciliation`): + +1. On Volume spec create/update, volume-local receives `spec-generation-changed`. +2. Controller reads current Volume spec and evaluates all layout entries. +3. For each entry, the controller resolves `ownerRef`/`groupRef` User UID/GID. +4. The controller calls `VolumeLayoutEffectPort` with the entry's declared + policy; ProviderSupervisor dispatches the layout operation + (create/repair/cleanup/adopt) to the broker via a bounded broker operation + with a path-free audit record. The controller itself never imports or + calls the broker. +5. Controller writes status batch with expected revision; conflict → re-read/retry. +6. On attachment create, volume-virtiofs receives `owned-resource-changed` from + the Volume. +7. volume-virtiofs emits or updates a virtiofsd Process resource owned by Volume. +8. volume-virtiofs observes virtiofsd Process readiness and writes attachment status. +9. On Volume deletion, both controllers finalize in parallel under their finalizers. + volume-virtiofs finalizes before volume-local (child finalizers first). + +Owner triggers: every Volume spec/status/finalizer mutation produces a +`owned-resource-changed` hint for the Volume's `ownerRef` (typically a Guest). +The Guest controller relist and re-asserts the full child graph. + +External drift observation: volume-local declares a bounded observe interval +(default 60 s) for durable/state Volumes, querying entry owner/mode/invariants. +Ephemeral/tmp Volumes observe only on start. + +## Volume ownership, sharing, and finalizers + +### Ownership + +A Volume may have `metadata.ownerRef` pointing to a Host, Guest, or another +Volume (nested). Owner deletion orders the Volume's finalization first. + +A Volume without an ownerRef is standalone; it persists until explicitly deleted. + +### Sharing + +Multiple Processes may mount the same Volume (with potentially different Views). +Multiple Guests may receive the same Volume via multiple attachments. At most one +attachment may use `access: read-write` at any time; the controller enforces the +single-writer constraint and rejects a second `read-write` attachment while one is +active. Multiple simultaneous writers require `access: shared-write` on all +writer attachments; the Provider must declare `supportsSharedWrite: true` in its +capabilities and is responsible for write-ordering semantics. + +A Volume may not be owned by two separate resources simultaneously (singular +ownerRef). Unrelated consumers use ordinary `volumeRef` without ownership. + +### Finalizers + +volume-local adds finalizer `volume-local/layout` when any layout entry has +`cleanupPolicy != never`. It is cleared after cleanup completes or is skipped. + +volume-virtiofs adds finalizer `volume-virtiofs/attachments` when any attachment +exists. It is cleared after all owned virtiofsd Processes are deleted and their +guest mounts confirmed absent. + +### Snapshots and migrations + +Volume snapshot and storage-content migration operations are modeled as +EphemeralProcess resources owned by the Volume and surfaced through the resource +API. They are not CLI-only jobs. The `volume-local` Provider exposes snapshot +and migration operations as EphemeralProcess templates in its Provider catalog. +Current baseline has no evidence for these operations; ADR046-volume-005 carries +the implementation work item. + +## RBAC + +Standard resource verbs apply. Typical Role rules: + +```yaml +# Zone controller creating Volumes +rules: + - resourceTypes: [Volume] + verbs: [create, update-spec, update-status, update-finalizers, get, list, watch] + zones: [dev] + +# Guest Provider reading/mounting volumes +rules: + - resourceTypes: [Volume] + verbs: [get, list, watch] + zones: [dev] + executionRefs: [Guest/work-vm] + +# Process mounting (read-only) +rules: + - resourceTypes: [Volume] + verbs: [get] + zones: [dev] +``` + +No subject may write spec for a Volume they do not own. Status may be written only +by the current controller lease for the declared `providerRef`. The +`sourcePolicyId` field in `source.settings` is an opaque ID; resolving it to an +actual host path is rejected for any caller without the +`volume-local/source-policy-resolve` permission claim, which authorizes only a +`VolumeSourceEffectPort` call. This permission is granted only to +ProviderSupervisor acting on behalf of `Provider/volume-local`'s controller +process — never to the controller process performing the resolution itself. + +## Security invariants + +1. **No raw host path in public surface**: `source.settings` never carries a + raw host path field; it carries only the opaque `sourcePolicyId`, which + never appears in resource list/watch responses, status, audit records, + error messages, CLI output, or telemetry. The controller reads the ID once + from spec and calls `VolumeSourceEffectPort` to resolve it to a validated + FD; subsequent broker operations use the FD, never a path. +2. **Anchored relative paths**: all layout entry paths are validated as + relative, non-absolute, with no `..` component, no drive letter, and no null + byte. The validator rejects Unicode homoglyphs of path separator characters. +3. **noFollow default true**: symlink traversal in layout operations is disabled + by default. Only `symlink`-type entries with explicit `noFollow: false` may + traverse. +4. **Broker-opaque-id-only**: entries with this invariant reject children created + by non-broker actors. This prevents arbitrary file injection into controlled + subtrees (swtpm state, store-view, lock files). +5. **No recursive mutation without explicit flag**: `recursive: false` is the + default. Enabling recursion for large trees (store-view) is rejected unless + the entry is explicitly declared with `recursive: true` and `repairPolicy` + is `exact-owner` or `fail-closed`. +6. **ADR 0021 virtiofsd invariant**: every virtiofsd worker process must declare + `capabilityClasses: []` and `startRoot: false`. A policy test rejects any + virtiofsd Profile that includes a non-empty capability set or a true + `startRoot`. `--sandbox=namespace` is never emitted. +7. **TPM never re-provisioned**: after the swtpm provisioning marker exists, + a missing or replaced swtpm directory is a hard failure. The controller never + silently creates a new empty TPM directory. +8. **Store isolation**: virtiofsd serving `access: read-only` for the ro-store + attachment always uses `store-view/live` as `--shared-dir`, never the host's + `/nix/store`. `share.source == "/nix/store"` is the compile-time sentinel only. + +## Audit and redaction + +Volume audit records include: + +- subject/Zone +- `Volume/` reference (never spec body) +- verb: `create`, `update-spec`, `update-status`, `delete` +- expected/current/result revision +- authorization outcome +- operation/correlation ID + +Excluded from audit: `source.settings.sourcePolicyId`, entry paths, ACL grant +values, layout entry content, virtiofsd socket paths, secret-adjacent entry +paths, guest mount paths, process data, terminal bytes, credential material. + +Broker path-free audit ops (current: `PrepareSwtpmDir`): + +| Op | Fields logged | +| --- | --- | +| `ProvisionLayoutEntry` | Volume UID, entry type, owner UID digest (not path) | +| `RepairLayoutEntry` | Volume UID, entry type, repair action class | +| `CleanupLayoutEntry` | Volume UID, entry type, cleanup trigger | +| `PrepareSwtpmDir` | VM UID, result class (provisioned/reconciled/quarantined) | +| `VirtiofsdLaunch` | Volume UID, attachment executionRef digest | +| `StoreSyncComplete` | Volume UID, generation number | + +## Provider dossiers + +> **Workspace policy**: every `packages/d2b-provider--/` crate must contain +> `src/`, `tests/`, `integration/`, and `README.md`. Missing any path fails the workspace/package +> policy gate. `src/` owns implementation binaries and colocated unit tests. `tests/` owns hermetic +> Cargo integration, ResourceType/controller/conformance, and fault tests. `integration/` owns +> heavier container/Host/Guest/cross-process/provider-system fixtures and scenarios invoked by +> existing test orchestration. `README.md` documents Provider identity, config schema, ResourceTypes, +> controllers/services/workers/binaries, placement, dependencies/RBAC, security/state/telemetry, +> build/test/integration commands, and future standalone-repo usage. + +Neither `volume-local` nor `volume-virtiofs` declares a Provider state Volume: +their bounded non-secret operational state (reconcile stage, per-attachment +readiness, adoption observations, last-successful checkpoints) lives in the +owning resource's `status` subresource and the core Operation ledger (D087), +and per-Volume provisioning markers for `state`-kind Volumes are broker- +maintained outside any Volume tree. Their `ProviderStateSet` is therefore +empty. The "State" rows below describe the ResourceType-owned data each +Provider keeps — in resource status and the layout it manages for *other* +Providers' declared Volumes — not a state Volume of its own. +`ProviderStateSet` is an optional query-time grouping of a Provider's declared +Volumes, not a separate stored artifact, and it never duplicates the resource +store's own authority over layout/attachment status. + +### Provider/volume-local + +| Field | Value | +| --- | --- | +| Crate | `packages/d2b-provider-volume-local/` | +| ResourceTypes | Volume (layout + views; no attachment transport) | +| Source kinds | `local-path`, `block-image`, `tmpfs` | +| Controller component | `volume-local-controller`; Process under Host/system-core | +| Broker ops (dispatched via `VolumeLayoutEffectPort`/ProviderSupervisor, never called by the Provider process itself) | `ProvisionLayoutEntry`, `RepairLayoutEntry`, `CleanupLayoutEntry`, `StoreSyncComplete`, `PrepareSwtpmDir` | +| State | Volume's own layout root; per-Volume provisioning marker for `state` kind | +| Permissions | `volume-local/source-policy-resolve` (authorizes only a `VolumeSourceEffectPort` FD resolution call); never ambient path access; never a broker import in the Provider process | +| Finalizers | `volume-local/layout` | +| Supported Host capabilities | Local NixOS Host; bare-metal; ACA if filesystem is accessible | +| Supported Guest capabilities | Not applicable (volume-local does not attach to Guests) | +| Required crate layout | `src/` (controller, broker op adapters, layout engine, store_view.rs, swtpm_volume.rs, colocated unit tests); `tests/` (hermetic: layout provision/repair/cleanup/adopt, store-view invariants, ACL reconciliation, swtpm fail-closed, quota enforcement, block-image lifecycle, tmpfs mount/unmount, symlink target validation, foreignChildPolicy preserve/fail); `integration/` (container fixtures: Host path access, store-view FS boundary enforcement, quota FS fixture, swtpm marker, block-image virtio-blk attachment); `README.md` (identity, allowedHostPaths config schema, owned ResourceTypes, broker op catalogue, placement, deps/RBAC, security invariants, state/telemetry, build/test/integration commands) | + +volume-local is the sole reconciler of every other Provider's *declared* +optional state Volume, but it declares no state Volume of its own. Because the +first `volume-local-controller` instance on each execution target keeps its +bounded non-secret operational state in `status`/the core Operation ledger and +declares no state Volume, no component needs a Volume before that instance is +Ready — so there is no bootstrap state-Volume cycle, no per-execution-target +local bootstrap storage mechanism, and no bootstrap-storage exception (D086, +superseded by D087; see "No bootstrap state Volume" in +`ADR-046-components-processes-and-sandbox`). A Guest bootstraps its own +Guest-local `volume-local` instance without any parent-Host dirfd or resource +handle, and that instance reaches Ready from Guest-local primitives and its own +status alone. Every Provider's declared state Volume — on any target — is +provisioned only through the normal Core ProviderDeployment → volume-local +create/reconcile path. + +volume-local controller reconcile flow: + +1. Resolve `source.settings.sourcePolicyId` by calling `VolumeSourceEffectPort`; + ProviderSupervisor validates it against the private allowlist policy and + returns an `OwnedFd`. The controller never opens the host path itself and + never sees the raw path. +2. For each layout entry (topological order, parent before child): + a. Resolve `ownerRef`/`groupRef` → UID/GID from User resource. + b. Call `VolumeLayoutEffectPort.provision`/`.repair`; ProviderSupervisor + dispatches the corresponding broker `ProvisionLayoutEntry` or + `RepairLayoutEntry` op based on `createPolicy`/`repairPolicy`. + c. Apply and continuously reconcile ACLs if `accessAcl` or `defaultAcl` is non-empty; enforce `foreignChildPolicy` (`preserve` or `fail`) for directory children not covered by declared ACL entries. +3. Check store-view specific invariants (marker, sync.lock) if `kind: durable` + and `source.settings.kind: local-path` with storeView mode. +4. Write status batch with layout conditions. + +### Provider/volume-virtiofs + +| Field | Value | +| --- | --- | +| Crate | `packages/d2b-provider-volume-virtiofs/` | +| ResourceTypes | Volume (attachment lifecycle and status only) | +| Attachment transport | `virtiofs` | +| Controller component | `volume-virtiofs-controller`; Process under Host | +| Worker binary | `virtiofsd` (upstream Rust virtiofsd from `pkgs/virtiofsd/`) | +| Worker Process template | `virtiofsd-worker` | +| Owned Process naming | `vol--virtiofsd-` | +| Broker ops (dispatched via `ProcessLaunchEffectPort`/`VolumeSourceEffectPort`/ProviderSupervisor, never called by the Provider process itself) | `SpawnRunner` (virtiofsd), `VirtiofsdLaunch`, `ProvideFdToWorker` | +| State | Per-attachment virtiofsd export socket (boot-scoped; path is a private implementation detail of volume-virtiofs; never exposed in spec/status/API) | +| Permissions | `volume-virtiofs/spawn-virtiofsd` (authorizes only a `ProcessLaunchEffectPort` call); receives source Volume FD from volume-local via ProviderSupervisor, never a direct cross-Provider or broker call | +| Finalizers | `volume-virtiofs/attachments` | +| Required crate layout | `src/` (controller, virtiofsd argv generation, attachment lifecycle, socket readiness, ADR 0021 user-NS pre-establishment, colocated unit tests); `tests/` (hermetic: argv golden/pinned vectors, ADR 0021 invariant rejection, attachment create/ready/delete lifecycle, single-writer enforcement, shared-write capability gate, read-only flag per access mode, multi-attachment isolation, socket path never-in-status invariant); `integration/` (container fixtures: virtiofsd launch, guest-mount readiness, finalizer drain under Guest restart); `README.md` (identity, virtiofsd argv options, owned ResourceTypes, ADR 0021 invariant summary, socket path privacy contract, placement, deps/RBAC, security invariants, state/telemetry, build/test/integration commands) | + +virtiofsd argv shape (baseline: `packages/d2b-host/src/virtiofsd_argv.rs`): + +``` +virtiofsd + --socket-path= + --socket-group= + --shared-dir= + --thread-pool-size= + --sandbox=chroot + --inode-file-handles=never + --cache= + [--posix-acl] # only if settings.posixAcl + [--xattr] # only if settings.xattr + [--readonly] # only if access: read-only + [] # Provider root config only; empty by default +``` + +The virtiofsd worker is tested by: + +- `packages/d2b-host/src/virtiofsd_argv.rs` (14 unit tests; golden/pinned/argv.txt) +- `tests/tools/gen-migration-ledger.sh` → `virtiofsd-argv-shape` gate +- `tests/tools/gen-migration-ledger.sh` → `minijail-validator-virtiofsd` gate +- `tests/unit/smoke/smoke-eval-tpm.nix` (swtpm ordering only) + +## Current-code fit + +| Item | Evidence class | Treatment | +| --- | --- | --- | +| `packages/d2b-core/src/storage.rs`: `StorageJson`, `StoragePathSpec`, all policy enums (`CleanupPolicy`, `RepairPolicy`, `StorageRestartPolicy`, `StorageAdoptionPolicy`, `LeaseClass`, `SensitivityClass`, `StorageInvariant`, `StoragePathKind`, `PrincipalRef`, `ActorRef`, `AclGrant`) | `generated-or-eval-contract` | Extract and adapt to Volume LayoutEntry; enum values preserved with renames where noted | +| `packages/d2b-core/src/sync.rs`: `SyncJson`, `LockSpec` | `generated-or-eval-contract` | OFD lock rows become Volume layout entries with `leaseClass: file-record`; per-process advisory lock semantics preserved | +| `packages/d2b-core/src/storage_lifecycle.rs`: `StorageLifecycleReport`, `StorageLifecycleIssue`, `StorageContractValidationReason`, `SyncContractValidationReason` | `implemented-and-reachable` | Daemon startup lifecycle report; migrated to Volume controller phase/condition reporting | +| `packages/d2b-core/src/processes.rs`: `VmProcessDag`, `ProcessRole::Virtiofsd`, `ProcessRole::Swtpm`, `ProcessRole::CloudHypervisorRunner` | `generated-or-eval-contract` | `ProcessRole::Virtiofsd` → v3 Process resource template `virtiofsd-worker` owned by volume-virtiofs; `VmProcessDag` → per-Guest set of Process resources | +| `packages/d2b-realm-core/src/ids.rs`: `RealmId`, `WorkloadId`, `NodeId`, `ProviderId` (newtype label-validated types) | `implemented-and-reachable` | Current identifier layer; maps to Zone/Guest/Provider `/` ResourceRef in v3 | +| `packages/d2b-realm-core/src/workload.rs`: `WorkloadId`, `WorkloadProviderKind` (LocalVm/QemuMedia/ProviderManaged/UnsafeLocal), `IsolationPosture`, `WorkloadExecutionPosture` | `implemented-and-reachable` | Current VM/workload classification layer; LocalVm/QemuMedia → Guest (VirtualMachine isolation); ProviderManaged → Guest under Provider; UnsafeLocal → user-only Host under Provider/system-core | +| `nixos-modules/storage-json.nix` (1086 lines): all path rows with `scope:"vm:"`/`scope:"host"`, owner, mode, cleanup/repair/restart/adoption/lease/sensitivity/invariants | `generated-or-eval-contract` | Each path row maps to a Volume LayoutEntry or a non-Volume host path (see migration table below); `scope:"vm:"` → `ownerRef: Guest/` | +| `nixos-modules/store.nix`: per-VM hardlink farm activation, private-NS sync algorithm | `generated-or-eval-contract` | Extracted to volume-local store-view mode; sync algorithm and private-mount-NS invariant preserved | +| `packages/d2b-host/src/hardlink_farm.rs`: `build_store_view`, `build_farm`, `GenerationMarker`, `BuildStoreViewRequest`, `gcroots_dir`, `state_dir`, `meta_dir`, `live_dir`, `sync_lock_path`, `generation_id` | `implemented-and-reachable` | Canonical store-view layout implementation; confirms `gcroots/` and `state/` at store-view root (not under `meta/`); marker is zero-length; migrates to volume-local store-view mode | +| `packages/d2b-priv-broker/src/ops/storage_contract.rs`: `reconcile_storage_scope`, `validate_lock_spec`, `validate_storage_scope` | `implemented-and-reachable` | Live broker handler; accepts only opaque BundleOpId; resolves path/owner/mode from broker's trusted bundle; path-hash in errors (never raw path); migrates to volume-local broker ops | +| `packages/d2b-priv-broker/src/ops/store_sync.rs`: `run_store_sync`, `run_store_sync_repair`, `StoreSyncOutcome`, `cleanup_store_view`, `prune_gcroots` | `implemented-and-reachable` | Live StoreSync broker op; calls `d2b_host::hardlink_farm`; confirms gcroots at store-view root; migrates to volume-local `StoreSyncComplete` broker op | +| `packages/d2b-priv-broker/src/ops/store_sync_audit.rs`, `store_sync_export.rs`: audit fields and export wire | `implemented-and-reachable` | Co-located with `store_sync.rs`; migrate to volume-local audit/export ops | +| `packages/d2b-priv-broker/src/ops/store_view_posture.rs`: `posture_store_view_matrix_paths`, `plant_live_marker_with_matrix_posture` | `implemented-and-reachable` | No-recursion posture for `state/`, `gcroots/`, `sync.lock` at store-view root; hardlink-farm-no-recursion invariant enforced here; migrates to volume-local repair policy | +| `packages/d2b-priv-broker/src/ops/state_dir.rs`: `PrepareStateDir`, `PrepareRuntimeDir`, `PrepareDirRequest`, `DirKind` | `implemented-and-reachable` | Broker op that fchown/fchmod per-VM state/runtime dirs without ambient path traversal; migrates to volume-local `ProvisionLayoutEntry` op | +| `packages/d2b-priv-broker/src/ops/swtpm_dir.rs`: swtpm provisioning, fail-closed marker, reconcile-in-place, ancestor traverse ACL, `seccomp_policy_ref: "w1-swtpm"` | `implemented-and-reachable` | Migrated to volume-local `create-if-never-provisioned` + fail-closed repair for TPM Volume | +| `packages/d2b-host/src/virtiofsd_argv.rs`: `VirtiofsdArgvInput`, `generate_virtiofsd_argv` (14 unit tests, golden `argv.txt` lines 166–184) | `implemented-and-reachable` | Extracted to volume-virtiofs virtiofsd-worker template; all 14 existing tests migrated | +| `nixos-modules/minijail-profiles.nix`: `virtiofsdProfiles`; principal `d2b--runner` (normal shares); principal `d2b--gctlfs` (d2b-gctl share); exception `"ADR 0021 v1.1.1fu14 virtiofsd fake-root via broker pre-established user NS"` | `generated-or-eval-contract` | Becomes virtiofsd worker sandbox spec; ADR 0021 invariants preserved; principal names → `User/` ResourceRef (typed ResourceRef only; no numeric form) | +| `nixos-modules/processes-json.nix`: `virtiofsdRunner` shape; `roStoreSharedDir` redirect sentinel `share.source == "/nix/store"` → `store-view/live` | `generated-or-eval-contract` | Replaced by volume-virtiofs controller-owned Process resource; store-view/live redirect preserved | +| `packages/d2bd/src/supervisor/dag.rs`: virtiofsd `VmProcessDag` node supervised as `ProcessRole::Virtiofsd` dag entry under a WorkloadId (current `d2b-realm-core::WorkloadId`-keyed dag) | `implemented-and-reachable` | Replaced by Process controller lifecycle in v3 | +| `packages/d2b-contract-tests/tests/storage_sync_contracts.rs`: `storage_and_sync_emitters_are_wired_into_private_bundle`, `broker_storage_and_sync_requests_stay_opaque_only`, `host_mutation_sources_are_registered_with_storage_or_sync_policy`, `tmpfiles_host_mutable_paths_are_covered_by_storage_contract_roots` | `implemented-and-reachable` | Live gate asserting storage.json/sync.json bundle wiring and opaque-id contract; adapted to Volume resource parity gate in v3 | +| `tests/unit/nix/cases/per-vm-state-ownership.nix` | `implemented-and-reachable` | Adapted to v3 Volume LayoutEntry matrix | +| `tests/unit/smoke/smoke-eval-tpm.nix` | `implemented-and-reachable` | Migrated to volume-local TPM Volume conformance test | + +### Current storage.json path rows → Volume migration + +| Current id prefix | Current path | v3 Volume | Notes | +| --- | --- | --- | --- | +| `path:etc-root` | `/etc/d2b` | Not a Volume (config root; Nix activation) | Remains in system-core Host activation | +| `path:state-root` | `$stateDir` | Not a Volume (host tree root) | system-core Host state root | +| `path:run-root` | `/run/d2b` | Not a Volume (host runtime root) | system-core Host runtime root | +| `path:daemon-state` | `$stateDir/daemon-state` | Not a Volume (daemon internal) | d2bd internal state; Zone store replaces | +| `path:run-locks` | `/run/d2b/locks` | Not a Volume | system-core bootstrap path | +| `path:run-locks-usbip` | `/run/d2b/locks/usbip` | Device Provider state | device-usbip Provider owns | +| `path:vm-state:` | `$storeStateDir/` | Volume root for per-Guest state | Guest owns as Volume with `kind: state` | +| `path:vm-run:` | `/run/d2b/vms/` | Zone/Guest runtime root directory; not a Volume | system-core Host runtime path; volume-virtiofs stores export sockets here as a private implementation detail | +| `path:store-view:` | `$storeStateDir//store-view` | `Volume/store-view-` root | volume-local, kind: durable | +| `path:store-view-live:` | `.../store-view/live` | LayoutEntry `live` | cutover-only, broker-opaque-id-only | +| `path:store-view-marker:` | `.../store-view/live/.d2b-marker-` | LayoutEntry `live/.d2b-marker-` | zero-length file, hardlink-farm invariants | +| `path:store-view-meta:` | `.../store-view/meta` | LayoutEntry `meta` | same-filesystem, hardlink-farm; guest-served | +| `path:store-view-generations:` | `.../store-view/meta/generations` | LayoutEntry `meta/generations` | same | +| _(absent from storage-json.nix)_ | `.../store-view/state` | LayoutEntry `state` | host-only state dir; present in `hardlink_farm.rs::state_dir()`; see spec correction | +| `path:store-view-gcroots:` | `.../store-view/meta/gcroots` _(storage-json.nix)_ | LayoutEntry `gcroots` at store-view root | **Spec correction**: `hardlink_farm.rs::gcroots_dir()` places gcroots at `store-view/gcroots`, NOT `meta/gcroots`; code wins | +| `path:store-view-current:` | `.../store-view/meta/current` | LayoutEntry `meta/current` | symlink, noFollow: false | +| `path:store-sync-lock:` | `.../store-view/sync.lock` | LayoutEntry `sync.lock` | file, leaseClass: none (OFD) | +| `path:swtpm-state:` | `$storeStateDir//swtpm` | `Volume/swtpm-` root | volume-local, kind: state, secret-adjacent | +| `path:swtpm-marker:` | `$stateDir/swtpm-markers/` | LayoutEntry in swtpm-markers host path | volume-local or system-core path | +| `path:vm-audio-state-dir:` | `$storeStateDir//state` | LayoutEntry in Guest state Volume | audio-pipewire Provider mounts view | +| `path:vm-audio-state-file:` | `.../state/audio-state.json` | LayoutEntry file in Guest state Volume | same | +| `path:vm-audio-lock:` | `/run/d2b/locks/audio-.lock` | OFD lock file in Zone runtime; LayoutEntry with `leaseClass: file-record`; path not exposed in spec/status | audio-pipewire Provider owns | + +sync.json lock rows migrate to Volume LayoutEntry `leaseClass: file-record` entries +or to controller-internal OFD locks not exposed as Volume resources. + +## Nix configuration + +> **Note**: The Nix option namespace `d2b.zones.` shown below is the +> **v3 ADR-only target API** — it does not exist in the current baseline +> (`b5ddbed6`). Current Nix API uses `d2b.vms.` (options-vms.nix) and +> `d2b.realms.` (options-realms.nix). The v3 resource compiler +> (ADR046-volume-004) emits Volume resource JSON from an evolved Nix surface. + +### Resource shape + +All resources — Volume, Provider, Credential, User, Guest, Host — use one +uniform Nix shape that mirrors the canonical ResourceSpec JSON nearly identically: + +```nix +d2b.zones."".resources."" = { + type = "ResourceType"; # required; matches canonical spec + spec = { + # exact ResourceType spec fields — same keys and nesting as canonical JSON + }; + # Optional authoritative metadata fields: + # metadata.ownerRef = "ResourceType/name"; # authoritative ownership + # metadata.labels = { key = "value"; }; # optional presentation + # metadata.annotations = { key = "value"; }; # optional presentation + # Derived (not authored): metadata.name, metadata.zone, apiVersion + # Omitted (read-only / core-managed): status, UID, generation, revision, + # timestamps, managedBy, configurationGeneration +}; +``` + +Nix option types, defaults, and docs for `spec.*` are generated from the same +ResourceTypeSchema JSON the runtime uses. Build validation compares the canonical +rendered JSON against the schema — there is no second bespoke Nix vocabulary and +Provider-specific fields are never renamed or renested. The Zone self-resource is +runtime-created with `spec = {}`; it is not authored through Nix. Child resources +live under `d2b.zones."".resources.*`. + +### Artifact catalog + +Derivation-valued inputs — Provider binaries, NixOS system images, and other +executables — are registered in a separate global artifact catalog, not inside +any ResourceSpec. ResourceSpecs use plain bounded IDs to reference artifacts. +`Artifact` is not a ResourceType; `artifactId` is not a `*Ref` field. + +```nix +# Artifact catalog — derivations and their type/trust metadata live here only. +# Store paths are private catalog implementation data; they never appear in any +# resource spec, status field, or audit record. +d2b.artifacts."volume-local-provider" = { + package = pkgs.d2b-provider-volume-local; + type = "provider"; +}; + +d2b.artifacts."volume-virtiofs-provider" = { + package = pkgs.d2b-provider-volume-virtiofs; + type = "provider"; +}; +``` + +The Nix build validates the catalog: each entry must have a unique ID, a valid +`type`, and a trusted derivation. The build emits a private integrity-pinned +artifact catalog mapping each ID to its type, digest, and closure metadata. +Store paths are used only at activation time by the runtime; they never flow +into resource specs, status objects, or audit records. + +### Provider installation + +Both volume Providers must be installed as Provider resources in the Zone. The +`spec.artifactId` field is a plain bounded string matching a catalog entry with +`type = "provider"`. No derivation appears in the ResourceSpec. + +```nix +d2b.zones."dev".resources."volume-local" = { + type = "Provider"; + spec = { + artifactId = "volume-local-provider"; # must exist in d2b.artifacts with type = "provider" + config = { + # Root config validated against volume-local's signed root-config.schema.json. + # Each entry's "id" is the opaque value a Volume references via + # source.settings.sourcePolicyId. Raw prefixes are resolved only inside + # ProviderSupervisor's private VolumeSourceEffectPort adapter state; the + # Provider controller itself never opens these paths directly. + allowedHostPaths = [ + { id = "state-root"; prefix = config.d2b.site.stateDir; volumeKinds = [ "durable" "state" "cache" ]; } + { id = "ephemeral-root"; prefix = "/run/d2b"; volumeKinds = [ "ephemeral" "tmp" ]; } + ]; + # No secrets in Provider root config; any credential must use Credential refs. + }; + }; +}; + +d2b.zones."dev".resources."volume-virtiofs" = { + type = "Provider"; + spec = { + artifactId = "volume-virtiofs-provider"; # must exist in d2b.artifacts with type = "provider" + config = {}; # no root config; per-attachment settings come from Volume spec + }; +}; +``` + +### Volume resource configuration — minimal state Volume + +```nix +d2b.zones."dev".resources."work-state" = { + type = "Volume"; + spec = { + providerRef = "Provider/volume-local"; + source = { + executionRef = "Host/host-system"; + settings = { + kind = "local-path"; + sourcePolicyId = "state-root"; + # sourcePolicyId references the matching "id" in volume-local's + # allowedHostPaths config; the operator authors only the opaque ID. + # The raw prefix is resolved solely inside the private effect-adapter + # state and never appears in public status. + }; + }; + kind = "state"; + layout = [ + { + path = ""; + type = "directory"; + ownerRef = "User/d2b-work-vm-runner"; + groupRef = "User/d2b-work-vm-runner"; + mode = "0700"; + sensitivity = "private"; + createPolicy = "create-if-never-provisioned"; + repairPolicy = "fail-closed"; + cleanupPolicy = "never"; + } + ]; + views.controller = { + path = ""; + rights = [ "read" "write" "create" "delete" "traverse" ]; + }; + attachments = [ + { + executionRef = "Guest/work-vm"; + transport = "virtiofs"; + view = "controller"; + access = "read-write"; + mountPath = "/state"; + } + ]; + }; +}; +``` + +### Canonical ResourceSpec JSON + +The Nix resource compiler emits one canonical JSON object per Volume with +all fields normalized, defaults applied, and unknown fields absent. The example +above renders as: + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Volume", + "metadata": { + "name": "work-state", + "zone": "dev", + "ownerRef": null, + "finalizers": [] + }, + "spec": { + "providerRef": "Provider/volume-local", + "source": { + "executionRef": "Host/host-system", + "settings": { "kind": "local-path", "sourcePolicyId": "state-root" } + }, + "kind": "state", + "layout": [ + { + "path": "", + "type": "directory", + "ownerRef": "User/d2b-work-vm-runner", + "groupRef": "User/d2b-work-vm-runner", + "mode": "0700", + "sensitivity": "private", + "createPolicy": "create-if-never-provisioned", + "repairPolicy": "fail-closed", + "cleanupPolicy": "never", + "adoptionPolicy": "adopt-with-live-owner-proof", + "restartPolicy": "preserve-across-controller-restart", + "leaseClass": "none", + "noFollow": true, + "recursive": false, + "foreignChildPolicy": "preserve", + "accessAcl": [], + "defaultAcl": [], + "invariants": ["no-symlink"] + } + ], + "views": { + "controller": { + "path": "", + "rights": ["create", "delete", "read", "traverse", "write"] + } + }, + "attachments": [ + { + "executionRef": "Guest/work-vm", + "transport": "virtiofs", + "view": "controller", + "access": "read-write", + "mountPath": "/state", + "settings": { + "cache": "auto", + "inodeFileHandles": "never", + "posixAcl": false, + "socketGroup": null, + "threadPoolSize": null, + "xattr": false + } + } + ], + "quota": null + } +} +``` + +Rights are sorted lexicographically. All keys are sorted. Defaults are always +present. `sourcePolicyId` is a plain opaque bounded string in the emitted +JSON, exactly like any other spec field; there is no raw `hostPath` field, +injected or otherwise — a raw host path never exists anywhere in the Volume +ResourceSpec, envelope JSON, or emitted bundle. + +### Nix eval/build validation + +Every Volume resource is fully validated during NixOS eval (before `nix build`). +A failed assertion emits a structured `throw` that includes the Volume name, +field path, and error class — never host paths or secret values. + +Validation steps in order: + +1. **providerRef resolution**: `Provider/` must appear as a resource of type `Provider` in `d2b.zones..resources`. +2. **executionRef resolution**: `Host/` or `Guest/` must appear in `d2b.zones..resources.*`. +3. **Provider artifact resolution**: the Provider resource's `spec.artifactId` must appear in `d2b.artifacts` with `type = "provider"`. A missing or wrong-type `artifactId` aborts the build; the error names the Provider resource and the missing/mismatched catalog ID. +4. **Provider source-kind schema**: `source.settings` is validated against the volume-local Provider's signed `root-config.schema.json`. The schema is read from the private artifact catalog entry for the Provider's `artifactId`; no store path appears in the resource spec. Provider-specific settings that fail the schema abort the build; the error includes the schema version and violated constraint, not the field value. +5. **Layout bounds**: ≤ 1024 entries; path uniqueness (no duplicates or overlaps); each path is relative, contains no `..` components, no leading `/`, no null bytes, no Unicode path-separator homoglyphs. +6. **Layout entry ownerRef/groupRef**: each `User/` must appear as a resource of type `User` in `d2b.zones..resources`. +7. **symlink target validation**: every entry with `type = "symlink"` must declare `target`; target is validated as a relative path with no `..` and no leading `/`; target path must resolve to a path under the Volume root. +8. **ACL principal validation**: every `accessAcl`/`defaultAcl` principal `ref` must be a `User/` resolving to a configured User; bare numeric forms abort. +9. **Views bounds**: ≤ 64 Views; ViewName matches `^[a-z][a-z0-9-]*$`. +10. **Attachment bounds**: ≤ 64 attachments; each attachment `executionRef` resolves; each `view` name exists in the Volume's `views`; at most one `read-write` attachment; `shared-write` only if Provider declares `supportsSharedWrite: true`. +11. **block-image quota**: `source.settings.kind == "block-image"` requires `quota.maxBytes != null`. +12. **tmpfs quota**: `source.settings.kind == "tmpfs"` requires `quota.maxBytes != null` and `quota.maxInodes != null`. +13. **Attachment provider-settings schema**: virtiofs attachment `settings` is validated against volume-virtiofs's signed `attachment.schema.json`; `virtio-blk` attachment settings against volume-local's `block-attachment.schema.json`. Both schemas are read from the private artifact catalog entries for the respective Provider `artifactId`s; no store paths in the spec. +14. **Credential refs**: no secret values (raw keys, passwords, tokens) appear in Volume spec. If a future layout entry requires a secret (e.g., an encrypted-at-rest key), it must use `credentialRef: Credential/`. +15. **Conflict detection**: two `local-path`/`block-image` Volumes bound to the + same `sourcePolicyId` root may not declare overlapping resolved subtrees. + The Nix resource compiler checks the set of all resolved host paths across + Volumes in the Zone — using the private `allowedHostPaths` catalog entry + each `sourcePolicyId` resolves to, never a spec-authored path — and aborts + the build on overlap. + +All validation errors are fatal and prevent `nix build`. They produce a structured JSON error block written to stderr, never to a path. + +### Integrity-pinned Zone resource bundle + +The Nix build emits one sorted, integrity-pinned Zone resource bundle per +NixOS generation: + +```json +{ + "schemaVersion": "resources.d2bus.org/zone-bundle/v1", + "zone": "dev", + "generationId": "", + "emittedAt": "2026-07-22T21:00:00Z", + "resources": { + "Provider/volume-local": { "digest": "" }, + "Provider/volume-virtiofs": { "digest": "" }, + "User/d2b-work-vm-runner": { "digest": "" }, + "Volume/work-state": { "digest": "" }, + "Volume/store-view-work-vm":{ "digest": "" } + }, + "resourceOrder": [ + "Provider/volume-local", + "Provider/volume-virtiofs", + "User/d2b-work-vm-runner", + "Volume/store-view-work-vm", + "Volume/work-state" + ], + "bundleDigest": "" +} +``` + +Resources are sorted by `"/"` lexicographically. The bundle digest +covers all resource digests and ordering. The configuration publication handler +verifies the bundle digest before activation and rejects any modified or +partially-applied bundle. + +The full resource JSON bodies are embedded in the bundle (or in a +content-addressed sidecar file keyed by digest); the `digest` field in the +index is the SHA-256 of the canonical JSON body. When core activates the bundle +it sets `metadata.managedBy = "configuration"` and +`metadata.configurationGeneration = ` on each activated resource. +These are the authoritative markers for configuration-owned resources; there is +no `configOwned` field in the bundle index. + +### Store-view Volume (Nix resource compiler output) + +```nix +# Generated automatically per Guest; not written by operators +d2b.zones."dev".resources."store-view-work-vm" = { + type = "Volume"; + spec = { + providerRef = "Provider/volume-local"; + source = { + executionRef = "Host/host-system"; + settings.kind = "local-path"; + settings.sourcePolicyId = "state-root"; + # Resolves via ProviderSupervisor's private VolumeSourceEffectPort + # adapter state to "/work-vm/store-view"; the raw path + # is never written to this spec by the compiler or the operator. + }; + kind = "durable"; + layout = [ + { path = ""; type = "directory"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } + { path = "live"; type = "directory"; invariants = [ "no-symlink" "broker-opaque-id-only" ]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } + { path = "live/.d2b-marker-work-vm"; type = "file"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0444"; invariants = [ "no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only" ]; repairPolicy = "exact-owner"; } + { path = "meta"; type = "directory"; invariants = [ "no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only" ]; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } + { path = "meta/generations"; type = "directory"; invariants = [ "no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only" ]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } + { path = "meta/current"; type = "symlink"; target = "generations/0"; noFollow = false; invariants = [ "broker-opaque-id-only" ]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0777"; } + { path = "state"; type = "directory"; invariants = [ "no-symlink" "broker-opaque-id-only" ]; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0700"; } # host-only; NOT guest-served + { path = "gcroots"; type = "directory"; invariants = [ "no-symlink" "same-filesystem" "hardlink-farm-no-recursion" "broker-opaque-id-only" ]; cleanupPolicy = "cutover-only"; repairPolicy = "exact-owner"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0755"; } # at store-view root, NOT under meta/ + { path = "sync.lock"; type = "file"; ownerRef = "User/d2bd"; groupRef = "User/users"; mode = "0640"; leaseClass = "none"; invariants = [ "no-symlink" "broker-opaque-id-only" ]; restartPolicy = "preserve-across-controller-restart"; } + ]; + views = { + ro-store = { path = "live"; rights = [ "read" "traverse" ]; }; + meta = { path = "meta"; rights = [ "read" "traverse" ]; }; + }; + attachments = [ + { + executionRef = "Guest/work-vm"; + transport = "virtiofs"; + view = "ro-store"; + access = "read-only"; + mountPath = "/nix/.ro-store"; + settings = { posixAcl = false; xattr = false; cache = "auto"; inodeFileHandles = "never"; }; + } + ]; + }; +}; +``` + +## Cleanup contract + +> Generic bundle lifecycle (non-blocking activation, `metadata.managedBy`, +> `configurationGeneration`, generation retention count) is specified in the Zone +> resource API spec. This section covers Volume-specific finalizer behavior, +> status, and tests only. + +### Configuration-owned vs controller-created + +A Volume's resource row carries `metadata.managedBy = "configuration"` and +`metadata.configurationGeneration = ` when it was created or last +updated by the configuration publication handler activating a Nix-emitted bundle. +Core sets these fields at activation time. + +A Volume is **configuration-owned** if its resource row has +`metadata.managedBy = "configuration"`. It is **controller-created** if a +running controller created it (e.g., a Provider creating a nested Volume); such +rows carry `metadata.managedBy = "controller"` with the controller identity tracked +separately as an internal field. + +The configuration handler manages the lifecycle of configuration-owned Volumes +only. It never issues Delete for controller-created Volumes in response to a +Nix generation change. Owner controllers handle their own children as their +parent resource changes. + +### Removed Volume lifecycle + +When a Volume is absent from the new Nix generation but its resource row carries +`metadata.managedBy = "configuration"` at the prior generation: + +1. The configuration handler issues a normal async `Delete` for the Volume + (sets `deletionRequestedAt`). +2. The Volume's phase becomes `Degraded` with a condition: + ```yaml + type: ConfigurationRemoved + status: "True" + reason: absent-from-configuration + message: "Volume removed from Nix generation; pending finalizer drain" + ``` +3. volume-virtiofs finalizer runs: it deletes all owned virtiofsd Process + resources. Guest mounts become unavailable. Each attachment's `state` field + in the attachment status becomes `detaching`. +4. volume-local finalizer runs (after volume-virtiofs completes): it executes + layout cleanup per each entry's `cleanupPolicy`. Entries with `cleanupPolicy: + never` are preserved. Entries with `cleanupPolicy: boot` or `cleanupPolicy: + process-exit-with-proof` are removed. +5. When all finalizers drain, the resource-store transaction writes an + event-only `Deleted` revision and removes the row and index atomically. + The audit subsystem appends the deletion audit record afterward, using a + dedup/exactly-once recovery key so a retried recovery never produces a + duplicate audit entry. +6. Prior generation retention is governed by the Zone's `priorGenerationCount` + (default 3, range 1..16). No time-based TTL applies. + +### Guest/Process children during Volume cleanup + +When a Volume is being deleted: +- The Guest that used the Volume (via attachment) receives an + `owned-resource-changed` trigger when the attachment `state` changes to + `detaching`. +- The Guest controller reconciles: it marks its own status as Degraded if the + Volume was required (`optional: false` in the Process mount). +- If a Process had the Volume mounted, the Process Provider (system-minijail/ + system-systemd) detects mount failure at the next health check or is notified + via the Volume attachment status, and sets the Process phase to Failed/Degraded. +- Controller-created children (snapshot EphemeralProcess etc.) owned by the + Volume receive Delete through the normal owner-child finalizer flow before + the Volume itself is deleted. + +### Controller-created resources are not touched by config cleanup + +If a Volume owns controller-created children (e.g., a virtiofsd Process), and +the Volume's spec changes to remove an attachment while the Volume itself remains +in Nix config, the volume-virtiofs controller reconciles its child Process list +and deletes the Process for the removed attachment only. The configuration handler +does not touch controller-created children. + +A controller-created Volume has `metadata.managedBy = "controller"` and is +invisible to the configuration cleanup pass regardless of whether the new +generation bundle includes it. + +### Prior generation retention + +The Zone retains the last `priorGenerationCount` prior generations (default 3, +range 1..16). Within the retained set, an operator may reactivate any prior +generation: +- The configuration handler re-activates the prior bundle (atomic single Zone + revision) via `ActivateGeneration`. +- Resources absent from the new generation but present in the prior one (currently + being deleted) have their `deletionRequestedAt` cleared if they have not yet + reached `phase: Deleted`. +- Resources added by the aborted new generation receive Delete. +- Reactivation is non-blocking and follows the same activation flow. + +When the retained count is exceeded the oldest generation's bundle record is +pruned from the store with a tamper-evident audit record. Pruning removes only +that historical bundle record. It never force-clears an undrained resource +finalizer: a resource that has not completed deletion remains in its current +non-terminal phase (Pending or Degraded) indefinitely, exactly as it would +outside a generation-retention prune, until its owning Provider controller +drains the outstanding effects/children and clears the finalizer through the +normal deletion flow. + +### Status, errors, and audit for cleanup + +Volume status during cleanup: + +```yaml +status: + phase: Degraded + conditions: + - type: ConfigurationRemoved + status: "True" + reason: absent-from-configuration + observedGeneration: 4 + - type: FinalizersBlocked + status: "False" + reason: finalizers-draining + attachmentStatuses: + - executionRef: Guest/work-vm + state: detaching + exportReady: false + guestMountReady: false +``` + +Audit record on removed-resource Delete: + +```json +{ + "subject": "configuration-publication-handler", + "zone": "dev", + "verb": "delete", + "resourceRef": "Volume/work-state", + "triggerGenerationId": "", + "priorGenerationId": "", + "reason": "absent-from-configuration", + "outcome": "delete-requested", + "correlationId": "" +} +``` + +No host paths, secret content, spec body, or layout entry paths appear in the +audit record. + +### Tests for removed-resource cleanup + +| Test | Layer | Coverage | +| --- | --- | --- | +| `volume_removed_from_nix_generation_receives_async_delete` | unit (configuration handler) | Configuration-owned delete path | +| `controller_created_volume_not_deleted_by_config_change` | unit (configuration handler) | `managedBy` authority; controller-created invisible to cleanup | +| `volume_cleanup_does_not_block_new_generation_ready` | integration | Non-blocking activation | +| `volume_finalizer_drain_order_virtiofs_before_local` | integration | Child-first finalizer order | +| `volume_cleanup_never_policy_preserves_entries` | integration | `cleanupPolicy: never` honored | +| `prior_generation_reactivation_cancels_in_flight_delete` | integration | Generation reactivation | +| `config_owned_volume_with_controller_children_cleans_children_first` | integration | Owner-child flow | +| `cleanup_audit_record_contains_no_host_paths` | unit (audit) | Redaction | +| `guest_phase_degraded_when_required_volume_detaching` | integration | Guest response to Volume detach/deletion | +| `volume_status_degraded_with_configuration_removed_condition` | unit (status) | Status shape during cleanup | + +## Implementation work items + +### ADR046-volume-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0 shared contract root; `d2b-contracts` | +| Current source | `packages/d2b-core/src/storage.rs`, `sync.rs`; `nixos-modules/storage-json.nix` | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/volume.rs`, `volume_layout.rs`, `volume_attachment.rs` | +| Detailed design | Complete Volume ResourceSpec, LayoutEntry, all policy enums (values preserved from baseline), AclGrant, ViewSpec, AttachmentSpec, quota placeholder, strict serde unknown-field rejection, canonicalization, bounds Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Provider dossiers/controller descriptors bind exact types; Nix resource compiler emits canonical JSON | +| Data migration | Full d2b 3.0 reset; storage.json rows migrated per table above | +| Validation | Golden JSON spec vectors; serde unknown-field; path anchor/depth/traversal validators; ACL grant bounds; policy enum coverage | +| Removal proof | `d2b-core/src/storage.rs` and `sync.rs` removed only after all Volume-successor consumers are live | + +### ADR046-volume-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-volume-001; volume-local Provider owner | +| Current source | `nixos-modules/storage-json.nix`, `nixos-modules/store.nix`, `packages/d2b-priv-broker/src/ops/swtpm_dir.rs`, `packages/d2b-priv-broker/src/ops/storage_contract.rs` (`reconcile_storage_scope`, `validate_lock_spec`), `packages/d2b-priv-broker/src/ops/store_sync.rs` (`run_store_sync`, `run_store_sync_repair`, `cleanup_store_view`, `prune_gcroots`), `packages/d2b-priv-broker/src/ops/store_sync_audit.rs`, `packages/d2b-priv-broker/src/ops/store_sync_export.rs`, `packages/d2b-priv-broker/src/ops/store_view_posture.rs` (`posture_store_view_matrix_paths`, `plant_live_marker_with_matrix_posture`), `packages/d2b-priv-broker/src/ops/state_dir.rs` (`PrepareStateDir`, `PrepareRuntimeDir`), `packages/d2b-host/src/hardlink_farm.rs` (`build_store_view`, `GenerationMarker`, `gcroots_dir`, `state_dir`), `packages/d2b-core/src/storage_lifecycle.rs` (`StorageLifecycleReport`, `StorageLifecycleIssue`), `packages/d2b-contract-tests/tests/storage_sync_contracts.rs`, `packages/d2bd/src/ownership_preflight.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-volume-local/src/` (layout engine, store_view.rs, swtpm_volume.rs, broker op adapters); `packages/d2b-provider-volume-local/tests/` (hermetic layout/store-view/swtpm tests); `packages/d2b-provider-volume-local/integration/` (container Host-path and store-view FS fixtures); `packages/d2b-provider-volume-local/README.md` | +| Detailed design | volume-local controller: layout engine (provision/repair/cleanup/adopt per policy), store-view mode (hardlink farm from `hardlink_farm.rs`, private-NS sync, zero-length marker, `gcroots/` and `state/` at store-view root, `sync.lock` OFD), swtpm volume hardening (provisionIfNeverProvisioned + marker + fail-closed repair as in `swtpm_dir.rs`), path-free broker audit ops, storage lifecycle report, opaque BundleOpId contract preserved from `storage_contract.rs` Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | volume-local controller registered under Host/system-core; resource status written via ResourceClient | +| Data migration | Per-VM `storage.json` rows (scoped `"vm:"` → `ownerRef: Guest/`) replaced by Volume resources generated by Nix resource compiler; broker continues to own path operations | +| Validation | `tests/unit/nix/cases/per-vm-state-ownership.nix` adapted to Volume LayoutEntry matrix; `tests/unit/smoke/smoke-eval-tpm.nix` migrated to TPM Volume invariant; `d2b-contract-tests/tests/storage_sync_contracts.rs` parity tests adapted; new: store-view same-filesystem, zero-length marker existence, sync.lock preserve-OFD, gcroots at store-view root (not meta/), state/ dir existence, swtpm fail-closed-on-missing-after-provision, anchored-path validators | +| Removal proof | `nixos-modules/storage-json.nix` removed only after Volume resources replace all path rows and all consumers verified | + +### ADR046-volume-003 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-volume-001; volume-virtiofs Provider owner | +| Current source | `packages/d2b-host/src/virtiofsd_argv.rs` (`VirtiofsdArgvInput`, `generate_virtiofsd_argv`), `nixos-modules/minijail-profiles.nix` (virtiofsdProfiles; principals `d2b--runner`, `d2b--gctlfs`), `nixos-modules/processes-json.nix` (virtiofsdRunner shape; `roStoreSharedDir` sentinel), `packages/d2b-core/src/processes.rs` (`ProcessRole::Virtiofsd`, `VmProcessDag`; the virtiofsd dag node is a `ProcessRole::Virtiofsd` entry in a WorkloadId-keyed `VmProcessDag`), `packages/d2b-priv-broker/src/ops/spawn_runner.rs` (`SpawnRunnerPlan` for virtiofsd; current `SpawnRunnerPlanInput` carries `adr_carve_out` for virtiofsd swtpm path), `packages/d2b-priv-broker/src/sys.rs` (clone3/user-NS pre-establishment), ADR 0021 | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-volume-virtiofs/src/` (controller, virtiofsd_argv.rs); `packages/d2b-provider-volume-virtiofs/tests/` (hermetic argv/lifecycle/ADR-0021 tests); `packages/d2b-provider-volume-virtiofs/integration/` (virtiofsd launch and guest-mount fixtures); `packages/d2b-provider-volume-virtiofs/README.md` | +| Detailed design | volume-virtiofs controller: attachment lifecycle, owned virtiofsd Process create/update/delete, argv generation (reuse current 14 tests), ADR 0021 invariant (`capabilityClasses: []`, `startRoot: false`, `sandbox: chroot`, user-NS via `userNamespace.mappingClass: process-principal-root`), per-attachment export socket readiness check (`unix-socket-exists` readiness kind; current v2 socket path: `/run/d2b/vms//-virtiofs-.sock`; v3: stable hash-derived private path under Zone runtime directory, never exposed in spec/status/API), guest-mount status observation, finalizer drain Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | volume-virtiofs registered under Host; virtiofsd Process resource owned by Volume; guest-control health integration for mount readiness | +| Data migration | Current `processes-json.nix` virtiofsd `VmProcessDag` nodes (keyed by `WorkloadId` = current VM name, role `ProcessRole::Virtiofsd`) replaced by virtiofsd Process resources owned by Volume | +| Validation | Migrated `virtiofsd_argv` unit tests (14 tests); `tests/tools/gen-migration-ledger.sh` virtiofsd-argv-shape gate adapted; `minijail-validator-virtiofsd` gate adapted to Process sandbox spec; new: attachment lifecycle (create/ready/delete), ADR 0021 invariant rejection test, multi-attachment isolation, readOnly flag per access mode, store-view shared-dir = store-view/live (never /nix/store) | +| Removal proof | `nixos-modules/processes-json.nix` virtiofsdRunner block removed only after virtiofsd Process resources pass parity; `packages/d2bd/src/supervisor/dag.rs` `ProcessRole::Virtiofsd` path removed after controller lifecycle covers all cases | + +### ADR046-volume-004 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-volume-001; Nix integrator | +| Current source | `nixos-modules/storage-json.nix`, `nixos-modules/store.nix`, `nixos-modules/options-vms.nix` (`d2b.vms..*` — current VM Nix option namespace; virtiofs shares and TPM enable are configured here), `nixos-modules/options-realms-workloads.nix` (`d2b.realms..stateDir` — current realm workload state root), `packages/d2b-realm-core/src/workload.rs` (`WorkloadProviderKind::LocalVm`/`QemuMedia`/`UnsafeLocal` — informs which WorkloadIds need store-view Volumes vs. no Volume) | +| Reuse action | adapt | +| Destination | `nixos-modules/resources-volume.nix`, `nixos-modules/options-volumes.nix` | +| Detailed design | Nix resource compiler for Volume/LayoutEntry/View/Attachment from d2b.zones config; strict schema validation; emit canonical JSON per Volume; generate store-view Volume per Guest (from current `d2b.vms.` → future flat `d2b.zones..resources.` with `type = "Guest"`) with hardlink-farm layout (gcroots/, state/ at root per `hardlink_farm.rs`); generate swtpm Volume for TPM-enabled Guests; emit volume-virtiofs attachment spec per virtiofs share; migration: store-view stateDir root configuration | +| Integration | `nixos-modules/default.nix` wires resources-volume.nix; Nix evaluation tests verify canonical output | +| Data migration | `d2b.vms..shares` (virtiofs entries) → Volume attachments; `d2b.vms..tpm.enable` → swtpm Volume | +| Validation | nix-unit cases for store-view Volume output (gcroots at root), TPM Volume spec, virtiofs attachment spec, anchored-path rejection; render parity with current storage.json path rows; canonical JSON golden vector; Provider schema validation rejection; symlink target validation; bundle digest coverage | +| Removal proof | Old `storage-json.nix` and `store.nix` emitters removed only after rendered Volume JSON passes all drift-check gates | + +### ADR046-volume-005 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-volume-002, ADR046-volume-003; respective Provider owners | +| Current source | N/A (no baseline evidence for block-image, quota enforcement, snapshots, or tmpfs Volume paths) | +| Reuse action | create | +| Destination | `packages/d2b-provider-volume-local/src/` (block-image, quota, snapshots, tmpfs, ACL reconciliation); `packages/d2b-provider-volume-local/tests/` (hermetic quota/tmpfs/ACL/block-image/snapshot tests); `packages/d2b-provider-volume-local/integration/` (block-image virtio-blk, FS-without-quota fixture, tmpfs memory-budget); `packages/d2b-provider-volume-virtiofs/src/` (single-writer enforcement, shared-write, private socket path contract); `packages/d2b-provider-volume-virtiofs/tests/` (single-writer rejection, shared-write capability gate, socket-path invariant); `packages/d2b-provider-volume-virtiofs/integration/` (shared-write cross-Guest fixture) | +| Detailed design | (1) **block-image SourceKind**: add `SourceKind::BlockImage` to volume-local; manage raw/qcow2 image file lifecycle; emit virtio-blk attachment spec consumed by Guest Provider; `quota.maxBytes` required; add store-overlay.img migration path for current `DiskInit` plan-op. (2) **Quota hard enforcement**: implement `enforcement: hard` capability check in volume-local at Volume creation time; query backing FS for quota/limits support; test with no-project-quota fixture; enforce `maxBytes`/`maxInodes` via xfs project quota or ext4 per-dir quota where available. (3) **Volume snapshots/migrations**: design and implement EphemeralProcess templates in volume-local catalog for snapshot (copy-on-write or rsync capture) and content migration (atomic rename + sync); no CLI-only path; all operations surface through resource API. (4) **Single-writer enforcement**: volume-virtiofs controller tracks active `read-write` attachment count; rejects a second `read-write` attachment while one is active (returns `ResourceConflict` error); `shared-write` mode accepted only if Provider declares `supportsSharedWrite: true`. (5) **tmpfs source**: implement tmpfs mount/unmount lifecycle in volume-local; `maxBytes` → `size=`, `maxInodes` → `nr_inodes=` mount options; charge memory against Host/Guest budget; cleanup unmounts on Volume deletion or restart. (6) **Bounds enforcement**: enforce max 1024 layout entries, 64 Views, 64 attachments at schema validation layer; add corresponding row to API request-size limit table in `ADR-046-resource-api-and-authorization`. (7) **File/symlink first-class lifecycle**: implement independent `createPolicy`/`repairPolicy`/`cleanupPolicy` for `file` and `symlink` entries; implement `target` field validation (relative, no `..`, must resolve within Volume root); `symlink` create writes the target link. (8) **ACL principal ResourceRef**: remove bare `{type,ref}` struct from AclGrant; implement `User/` ResourceRef resolution with User resource watch and re-reconcile on User revision change. (9) **Continuous ACL reconciliation**: implement `foreignChildPolicy: preserve|fail` in broker reconcile loop; re-apply `accessAcl`/`defaultAcl` to all existing entries and children on every repair cycle; emit `ForeignAclViolation` condition when `foreignChildPolicy: fail` and unexpected entries found. (10) **virtiofsd socket path contract**: implement stable hash-derived private socket path in volume-virtiofs (deterministic hash of Zone name + Volume name + attachment executionRef); assert path never appears in public status, spec, audit, or CLI output; validate with a dedicated security invariant test. | +| Integration | Each sub-item produces a focused spec amendment; resolved decisions already reflected in spec revision 2 | +| Data migration | Per-sub-item; block-image and tmpfs are new capabilities with no legacy migration required | +| Validation | (1) `VirtioblkArgvInput` unit tests; block-image integration fixture. (2) Quota-enforcement fixture with FS-without-quota; hard-enforcement failure test. (3) EphemeralProcess snapshot lifecycle test; content-migration parity test. (4) Single-writer rejection test; shared-write capability gate test. (5) tmpfs mount/unmount lifecycle test; memory-budget accounting assertion. (6) Schema bound rejection tests (1025 entries, 65 views, 65 attachments). (7) File/symlink independent lifecycle tests; target validation (absolute rejected, `..` rejected, escape rejected). (8) ACL principal ResourceRef validation; numeric form rejected; User revision trigger test. (9) foreignChildPolicy preserve/fail tests; continuous repair cycle test. (10) Socket path invariant test; no-status-leak assertion. | +| Removal proof | None — net-new capabilities; no prior owner to remove | + +### ADR046-volume-006 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-volume-001, ADR046-volume-004; Nix integrator and core-controller config-publication owner | +| Current source | `nixos-modules/storage-json.nix` (current Nix eval-time validation via `lib.asserts`; schema checked by `d2b-contract-tests/tests/storage_sync_contracts.rs`); `packages/d2bd/src/` (current config activation / host-prepare dispatch) | +| Reuse action | create | +| Destination | `nixos-modules/resources-volume.nix` (Nix eval-time schema validation, canonical JSON emission, bundle digest); `packages/d2b-core-controller/src/configuration.rs` (config-publication handler cleanup logic); `packages/d2b-contracts/src/v3/zone_bundle.rs` (bundle index schema) | +| Detailed design | **Nix eval/build validation**: implement all 15 validation steps in §Nix eval/build validation as Nix assertions; abort build on any failure with structured error (Volume name + field path + error class); provider-specific settings schema (`root-config.schema.json`, `attachment.schema.json`) read from the private artifact catalog entry for each Provider's `artifactId` by the resource compiler; validate against `lib.evalModules`-compatible schema; emit canonical sorted JSON with all defaults materialized; compute SHA-256 per resource; emit Zone resource bundle with `generationId` and `bundleDigest`. **Config-publication handler cleanup**: on new bundle activation, diff resources with `metadata.managedBy = "configuration"` between new and prior bundle; issue async Delete for resources absent from new bundle; mark deleted resources with `ConfigurationRemoved` condition; track pending-cleanup set in Zone status; Zone status is `Degraded/PendingCleanup` while prior-generation deletions are in progress; activation is immediate but Zone readiness reflects cleanup completion. **Config-owned vs controller-created distinction**: `metadata.managedBy = "configuration"` is the authoritative marker set by core at activation; the bundle index carries only digests; controller-created resources have `metadata.managedBy = "controller"` and are never touched by the configuration cleanup pass. **Prior generation retention**: retain `priorGenerationCount` prior generations (default 3, range 1..16); no time-based TTL; when count is exceeded prune oldest generation from the store with a tamper-evident audit record. **Generation reactivation**: re-activate any retained prior bundle via `ActivateGeneration` operation; cancel in-flight Deletes for resources being reinstated; issue Deletes for resources added by the aborted new generation. | +| Integration | `nixos-modules/default.nix` wires resources-volume.nix; `d2b-core-controller` config-publication handler consumes the bundle; all Volume controllers observe `ConfigurationRemoved` condition and respond to finalizer triggers | +| Data migration | Full d2b 3.0 reset; no partial import of prior generation state | +| Validation | Tests per §Cleanup contract — Tests for removed-resource cleanup table (10 tests); nix-unit: `volume_canonical_json_golden_vector`, `volume_bundle_digest_covers_all_resources`, `provider_schema_validation_rejects_unknown_fields`, `symlink_target_escape_rejected_at_eval`, `tmpfs_without_quota_rejected_at_eval`, `layout_bounds_1025_entries_rejected`, `attachment_bounds_65_rejected`, `conflicting_host_paths_rejected`; integration: cleanup audit redaction, generation reactivation, prior-generation pruning | +| Removal proof | Old `storage-json.nix` schema assertions removed only after Nix eval-time Volume validation covers all prior `lib.asserts` paths; old config-activation code in `d2bd` removed after config-publication handler is live | diff --git a/docs/specs/ADR-046-resources-zone-control.md b/docs/specs/ADR-046-resources-zone-control.md new file mode 100644 index 000000000..a439917c6 --- /dev/null +++ b/docs/specs/ADR-046-resources-zone-control.md @@ -0,0 +1,4661 @@ +# ADR 0046 ResourceTypes: Zone, ZoneLink, Provider, Role, RoleBinding, Quota, EmergencyPolicy, ResourceExport, and ResourceImport + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-resources-zone-control` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-contracts` (schemas), `d2b-core-controller` (handlers), Zone runtime, Nix resource compiler | +| Depends on | `ADR-046-decision-register`, `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-store-redb`, `ADR-046-core-controllers`, `ADR-046-provider-model-and-packaging`, `ADR-046-resource-reconciliation` | +| Supersedes | None | + +## 1. Scope + +This spec defines the complete normative contract for the nine core control +ResourceTypes: `Zone`, `ZoneLink`, `Provider`, `Role`, `RoleBinding`, `Quota`, +`EmergencyPolicy`, `ResourceExport`, and `ResourceImport` (the last two added by +D096 for cross-Zone sharing of scarce singleton resources). + +For each type it provides: + +- complete `metadata`/`spec`/`status` field schemas with bounds and defaults; +- provider/controller ownership; +- phase/condition/outcome transitions and timestamps; +- ownerRef/finalizer/deletion behavior; +- native RBAC and bootstrap authorization; +- Zone self-resource and parent/child ZoneLink semantics; +- core controller algorithms and async reconciliation triggers; +- security, audit, OTEL, and error requirements; +- normative Nix authoring examples; +- conformance requirements and exact tests; +- current-code fit tables; +- implementation work items. + +This revision resolves all design choices for the covered ResourceTypes. New +design decisions are tracked in +[`docs/specs/ADR-046-decision-register.md`](ADR-046-decision-register.md). + +All common resource fields (universal envelope, generation, revision, UID, +condition and outcome shapes, phase values, deletion protocol) are governed by +[`ADR-046-resource-object-model`](ADR-046-resource-object-model.md). This spec +extends that contract for these nine types only. + +--- + +## 2. Zone + +### 2.1 Role + +`Zone` is the self-resource of a Zone store. Every Zone store contains exactly +one authoritative: + +```text +Zone/ +``` + +`` must equal the redb `store_meta.zone_name` key and the resource's +own `metadata.zone` field. There is no cross-Zone resource reference; a +`Zone/` resource resolves only inside the store where `` equals +the store's own `zone_name`. The Zone resource is not replicated, shadowed, or +referenced from other Zones. + +**Controller/owner**: The `configuration publication` handler of the fixed +core-controller process (Provider/system-core) owns Zone reconciliation. No +other controller may update Zone spec or status. + +### 2.2 Metadata + +Zone metadata follows the common envelope. Additional Zone-specific rules: + +| Field | Rule | +| --- | --- | +| `metadata.name` | Must equal `store_meta.zone_name`; validated at every open/upgrade; immutable after first commit | +| `metadata.zone` | Must equal `metadata.name` for the self resource | +| `metadata.uid` | Immutable; equals `store_meta.zone_uid`; generated once at store creation | +| `metadata.generation` | Starts at 1; increments only on spec change | +| `metadata.ownerRef` | Must be `null`; Zone cannot be owned by another resource | +| `metadata.finalizers` | Core may add `core.zone-drain` finalizer during shutdown/reset; no other controller may add a Zone finalizer | +| `metadata.deletionRequestedAt` | Set only by deliberate reset/shutdown; normal operations never delete Zone | + +### 2.3 Spec + +`Zone.spec` is `{}` — an empty object. Zone identity, API catalog, policy +revision, and configuration revision are entirely derived from the store +metadata and other installed resources (Provider, Role, RoleBinding, Quota, +EmergencyPolicy). The Zone resource is a pure identity anchor. + +**D091 update policy.** The universal base spec carries `spec.updatePolicy` for +every Zone-control ResourceType (`Zone`, `ZoneLink`, `Provider`, `Role`, +`RoleBinding`, `Quota`, and `EmergencyPolicy`): disruptive changes default to +manual, while automatic non-disruptive upgrades are permitted by policy. A +`spec.provider` extension MAY add provider-specific knobs, but MUST NOT bypass +or weaken base `spec.updatePolicy`. + +**D090 expedited reconcile.** Authorized `Create`, `UpdateSpec`, and `Delete` +calls on these ResourceTypes MAY set `waitForReconcile`. Under one mutation +ticket, `operationId`, and deadline, Core admission and the reserved-revision +redb commit run in parallel with the owning controller's preflight/plan, but the +controller MUST NOT perform external effects, finalizer release, or status +mutation until Core supplies `CommittedRevisionProof {resourceUid, generation, +revision, operationId}`; DB failure aborts with no effect. The API returns the +committed object plus one-pass projected layered status, `disposition` +(`Converged|Progressing|Blocked|UpgradeRequired|Failed`), `statusPersistence` +(`pending|committed`), and the last persisted status revision. The durable +commit is never rolled back on reconcile timeout or failure; effect idempotency +keys derive from `(UID,generation,revision,operationId)`, and the expedited pass +uses a bounded priority lane in the same per-resource single-flight. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Zone +metadata: + name: dev + zone: dev + uid: + generation: 1 + revision: + ownerRef: null + finalizers: [] + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:00:00Z +spec: {} +status: + observedGeneration: 1 + phase: Ready + conditions: [] + lastReconciledAt: 2026-07-22T00:00:01Z + startedAt: 2026-07-22T00:00:00Z + completedAt: null + outcome: null +``` + +### 2.4 Status + +#### Three-layer status shape (D088) + +D088 freezes `Zone` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `Zone`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +**D091 update currency.** Every Zone-control ResourceType includes universal +`status.update` with `state` +(`Current|UpdateAvailable|UpgradeRequired|Upgrading|Blocked|Unknown`), `reasons` +(`CoreGenerationChanged|ProviderGenerationChanged|ArtifactChanged|ImageOrSystemGenerationChanged|SpecChanged|DependencyChanged|SecurityPolicyChanged`), +bounded non-secret observed/target generation and digest IDs, `disruption` +(`None|Reload|Restart|Recycle|Replace`), `preserveState`, optional +`operationId`, `lastAssessedAt`, and bounded/truncated `owned:{count,refs}` and +`dependencies:{count,refs}`. ResourceType-specific currency refinements, +including Quota, EmergencyPolicy, Role, and RoleBinding currency, live in +`status.resource` and never in `status.provider`; controllers set +`status.update` via `assess_update` on core/provider/artifact/spec/dependency/ +security-policy triggers and MUST report `UpgradeRequired` for disruptive +changes rather than applying them in place. A Zone-level rollout trigger reports +reason `CoreGenerationChanged`; Core aggregates self, owned, and dependency +currency for list/get. + +Zone.status extends common status with: + +| Field | Type | Rules | +| --- | --- | --- | +| `apiCatalogRevision` | u64 | Current Zone api_catalog revision from store_meta | +| `policyRevision` | u64 | Current authorization policy revision from store_meta | +| `configurationRevision` | u64 | Active configuration revision from store_meta | +| `coreControllerPhase` | phase enum | Aggregate phase of all mandatory core handlers | +| `handlers` | bounded list | Per-handler name/phase/lastReconciledAt (no resource names, no secrets) | +| `installedProviderCount` | u32 | Count of installed (non-Deleted) Provider resources | +| `readyProviderCount` | u32 | Count of Ready Provider resources | +| `totalResourceCount` | u32 | Total non-deleted resource count | +| `activeConfigurationGeneration` | u64 | Generation number of the active Nix-authored resource bundle | +| `generationCleanupPending` | bool | True while prior-generation config-owned resources are completing deletion | +| `cleanupPendingCount` | u32 | Count of config-owned resources from prior generations awaiting deletion | + +`coreControllerPhase` is the stricter of all mandatory handler phases. Optional +handlers (ZoneLink maintenance, backup cleanup) may report Degraded without +changing `coreControllerPhase` to Failed. + +`handlers` entries contain only: handler name (stable closed enum), phase, and +lastReconciledAt. No resource names, counts by type, provider diagnostics, or +store paths appear here. + +### 2.5 Phase and conditions + +| Phase | Meaning | +| --- | --- | +| `Pending` | Store open; core handlers initializing | +| `Ready` | All mandatory handlers Ready; Zone fully operational | +| `Degraded` | Core handlers Ready but optional handlers impaired, or config-owned resources are awaiting deletion (`GenerationCleanupPending=True`) | +| `Failed` | One or more mandatory handlers in Failed; Zone non-operational | +| `Unknown` | Store unavailable or Zone runtime cannot determine state | +| `Deleted` | Final revision event emitted; store closing/closed | + +Zone never reaches `Succeeded` (it is long-lived, not a one-shot resource). + +Closed condition types for Zone: + +| Condition type | Meaning | +| --- | --- | +| `StoreReady` | Underlying redb database is open and healthy | +| `ConfigurationCurrent` | Active configuration generation is current | +| `ApiCatalogReady` | API catalog bound and valid | +| `AuthorizationReady` | Role/RoleBinding index ready and authorization operational | +| `ProvidersHealthy` | All required Provider processes are Ready | +| `CoreHandlerReady` | Fixed core-controller process is fully initialized | +| `GenerationCleanupPending` | Prior-generation config-owned resources have not yet completed deletion | +| `GenerationCleanupFailed` | One or more prior-generation config-owned resources are stuck awaiting deletion beyond threshold | + +Condition `status` values follow the common `True|False|Unknown` rule. +Transition times are RFC 3339 UTC. + +`GenerationCleanupPending=True` changes the Zone `phase` to `Degraded` while +any prior-generation config-owned resources are awaiting deletion; this is the +normal pending-cleanup posture and does not indicate a fault. `GenerationCleanupFailed=True` +is additionally set when a candidate is stuck beyond `cleanupStuckThreshold` (default +5 minutes) with no controller progress. + +### 2.6 ownerRef, finalizers, and deletion + +`metadata.ownerRef` is always `null`. Zone is the root resource; it cannot be +owned. + +Core adds `core.zone-drain` finalizer to Zone only when an explicit reset/ +shutdown request is in progress. During drain: + +1. `metadata.deletionRequestedAt` is set. +2. Core stops admitting new resource/service requests. +3. Each non-Zone resource receives a delete request in reverse dependency order + under finalizer protocol. +4. After all other resources are deleted, `core.zone-drain` is cleared. +5. Final transaction emits `phase=Deleted` event and closes the store. + +A Zone resource is never deleted except during a deliberate destructive reset. +Normal daemon restart, upgrade, or Provider lifecycle changes do not delete Zone. + +--- + +## 3. ZoneLink + +### 3.1 Role + +`ZoneLink/` represents one parent/child Zone delegation as the child +Zone's local uplink. It is authored and stored in the **child** Zone. It carries +that local child identity, transport/session requirements, cursor tracking for +resource synchronization, and connection health. `spec.childZoneName` MUST +equal the enclosing Zone's self-name. + +A parent accesses child Zone resources exclusively through the child's +`d2b.resource.v3` service over a ComponentSession routed via the transport +Provider named in ZoneLink. The parent never receives a database handle, +credential, token, or cross-Zone ResourceRef. + +The provisioning parent allocator owns privileged listener creation, placement, +and route-namespace effects. The Nix compiler selects that allocator through +the enclosing Zone's compiler-only `parentZone` setting. The allocator binds +the child-local ZoneLink UID and child identity to that one parent edge in +sealed bootstrap state and keeps only allocator/route-engine state in the +parent; it does not create a reciprocal parent-store resource. +**Controller/owner**: the child Zone's `zone +link/delegation` handler in the fixed core-controller process owns the +ZoneLink resource and its status. It consumes allocator observations through +the authenticated session and has no direct privileged-effect path. No other +controller may update ZoneLink spec or status. + +### 3.2 Metadata + +| Field | Rule | +| --- | --- | +| `metadata.name` | Operator-assigned; locally unique in the child Zone; no structural constraint beyond ResourceName regex | +| `metadata.zone` | Child Zone self-name; ZoneLink is always local to that child | +| `metadata.ownerRef` | Optional same-Zone ref to the managing transport/runtime Provider; never a parent-Zone ref | +| `metadata.finalizers` | Core adds `core.zone-link-drain` before deletion; transport Provider may add its own finalizer | +| `metadata.deletionRequestedAt` | Normal deletion by operator or owning resource | + +### 3.3 Spec + +```yaml +apiVersion: resources.d2bus.org/v3 +type: ZoneLink +metadata: + name: guest-uplink + zone: guest + uid: + generation: 1 + revision: + ownerRef: null + finalizers: + - core.zone-link-drain + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:00:00Z +spec: + childZoneName: guest + transportProviderRef: "Provider/transport-unix" + transportSettings: {} + transportCredentials: [] + disabled: false + limits: + maxPendingIntents: 256 + maxActiveStreams: 32 + reconnectMaxAttempts: 10 + reconnectWindowSecs: 300 +``` + +**`spec.transportProviderRef`** (required; no default): + +- Must match `^Provider/transport-[a-z][a-z0-9-]*$`; any other form is rejected at admission. +- References a Provider installed in the same child Zone whose name begins with `transport-`; that Provider must be in `Ready` phase before any connection is attempted. +- The named transport Provider exports a ZoneLink schema extension (`transportSettings` schema); the resource compiler validates `spec.transportSettings` against that schema at build time. +- No transport Provider is pre-installed; an operator must install one before creating ZoneLinks that require it. + +**`spec.transportSettings`** (required; default `{}`): + +- Provider-specific configuration object; validated against the transport Provider's ZoneLink schema extension at build time (Phase 2) and at admission time (runtime). +- Must not contain credential bytes; use `spec.transportCredentials` for secrets. + +**`spec.transportCredentials`** (list of Credential refs; default `[]`): + +- Each entry is a string `"Credential/"` in the same Zone. +- Max 8 entries; evaluated at ComponentSession establishment time. +- Referenced Credentials must be declared in `d2b.zones..resources.*` with `type = "Credential"`. + +**`spec.limits`**: ZoneLink connection and queue limits. All fields have normative defaults (shown above). Bounds enforced at admission: +- `maxPendingIntents`: max 1024 +- `maxActiveStreams`: max 128 + +### 3.4 Status + +#### Three-layer status shape (D088) + +D088 freezes `ZoneLink` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `ZoneLink`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + + +ZoneLink.status extends common status with: + +| Field | Type | Rules | +| --- | --- | --- | +| `childZoneUid` | UID string or null | Immutable UID of this local child Zone as acknowledged by the parent allocation; null until first successful connection | +| `connected` | bool | Whether the allocator-bound ComponentSession to the parent route endpoint is established | +| `lastConnectedAt` | RFC 3339 UTC or null | Most recent successful session establishment | +| `lastDisconnectedAt` | RFC 3339 UTC or null | Most recent disconnect | +| `lastSentRevision` | u64 or null | Highest child advertisement/local intent revision sent to the parent | +| `lastAckedRevision` | u64 or null | Highest child revision acknowledged by the parent | +| `lastReceivedRevision` | u64 or null | Highest parent route/export advertisement revision received | +| `lastAppliedRevision` | u64 or null | Highest parent advertisement revision applied to child-local routing/import state | +| `linkEpoch` | u64 | Monotonic counter; increments on every reconnect establishing a new session generation | +| `pendingLocalIntents` | u32 | Count of locally queued intents while disconnected; bounded | +| `childAuthorized` | bool | Whether the parent allocator accepted and authorized this child Zone subject | + +`childZoneUid` lets both ends detect that this child Zone was destroyed and +recreated. If it changes across reconnects, the child-local ZoneLink handler +clears local cursor state and the parent allocator discards the corresponding +route allocation before both sides relist/rewatch from revision 0. + +Cursor fields (`lastSentRevision`, `lastAckedRevision`, `lastReceivedRevision`, +`lastAppliedRevision`) reflect the `zone_link_cursors` redb table entry for +this ZoneLink. They are bounded u64 monotonic values. Their exact semantics +are defined by the ZoneLink session and cursor protocol. + +### 3.5 Phase and conditions + +| Phase | Meaning | +| --- | --- | +| `Pending` | Transport not yet connected or child not yet authorized | +| `Ready` | ComponentSession established; child authorized; resources accessible | +| `Degraded` | Connected but some capability unavailable (e.g. watch quota exceeded) | +| `Failed` | Cannot connect after retry policy exhausted, or child permanently denied | +| `Unknown` | Transport state cannot be determined | + +The parent Zone has no ZoneLink resource phase. The child-local ZoneLink alone +reports the edge's resource phase from authenticated allocator observations. + +Closed condition types for ZoneLink: + +| Condition type | Meaning | +| --- | --- | +| `TransportReachable` | Transport provider reports the endpoint is reachable | +| `SessionEstablished` | ComponentSession handshake completed successfully | +| `ChildAuthorized` | Parent allocator accepted the authenticated child subject and route allocation | +| `CursorSynchronized` | Parent/child revision cursors are within expected bounds | +| `LocalIntentsDrained` | No pending locally queued intents remain | +| `DisabledByOperator` | `spec.disabled: true`; link is intentionally inactive | + +### 3.6 Local intents while disconnected + +When the parent route endpoint is unreachable, the child may record bounded +local ZoneLink intents — operator intent that cannot be forwarded until +reconnection. These are not resource writes in the parent. They are persisted +in the child store as ZoneLink-owned metadata-only entries. + +Rules: + +- bounded count: `pendingLocalIntents` must not exceed 256; new intents are + rejected with `backpressure` when at limit; +- intents are ordered FIFO; no deduplication or priority; +- on reconnect the ZoneLink handler applies pending intents in order under + expected-revision preconditions; +- failed intent application clears the intent queue and records a + `IntentApplicationFailed` condition; the operator must relist and retry; +- the queue is cleared on ZoneLink deletion or successful relist/resync. + +### 3.7 ownerRef, finalizers, and deletion + +`metadata.ownerRef` is optional and always same-Zone. A local transport/runtime +Provider may own the uplink it manages. A parent allocator, parent Provider, or +parent Zone resource can never be the owner because that would require a +cross-Zone ref. When the local owner is deleted, ZoneLink deletion follows +standard child-first finalizer protocol. + +Core adds `core.zone-link-drain` to `metadata.finalizers` before processing a +delete request. The ZoneLink handler: + +1. Closes the allocator-bound ComponentSession to the parent route endpoint. +2. Clears the cursor table entry. +3. Removes any pending local intents. +4. Clears `core.zone-link-drain`. +5. Clears any transport-Provider finalizer (by calling delete on the provider's + owned endpoint resource if applicable under the resolved transport Provider + contract). + +After all finalizers clear, core removes the ZoneLink resource and its +`zone_link_cursors` entry in one transaction. + +--- + +## 4. Provider + +### 4.1 Role + +`Provider/` is the installed representation of one independently +buildable Provider package in a Zone. A `providerRef` resolves only if the +named Provider resource exists and is `Ready` in the same Zone. + +Provider resources are always Zone-local. There are no cross-Zone Provider +refs. Provider installation is a Zone-local administrative act. + +> **`unsafe-local` is NOT a Provider.** The current `kind = "unsafe-local"` Nix +> workload option (and the baseline `WorkloadProviderKind::UnsafeLocal` / +> `IsolationPosture::UnsafeLocal` in `d2b-realm-core/src/workload.rs`) maps to +> a **Host** resource with `spec.defaultDomain=user`, `spec.allowedDomains=[user]`, +> and `spec.defaultUserRef=User/`, reconciled by `Provider/system-core`. +> No Provider resource with name or type `unsafe-local` exists in ADR 0046. +> Child processes of an unsafe-local Host use normal Process Providers. No-isolation +> posture and warnings are preserved in Host status, CLI output, and audit records. +> They must not appear in OTEL metric label values or structured log labels. See ADR046-zone-control-008 (§17) and §16.2 subsection +> "Workload posture and unsafe-local types". + +**Controller/owner**: The `provider lifecycle` handler of the fixed +core-controller process owns Provider reconciliation. Only Provider/system-core +and Provider/system-minijail are fixed bootstrap exceptions with no Process +resource — all other Provider controllers are Process resources owned by their +Provider resources. + +### 4.2 Metadata + +| Field | Rule | +| --- | --- | +| `metadata.name` | Must equal the Provider's declared identity; no installer can select an arbitrary name independent of the package | +| `metadata.zone` | Zone where Provider is installed | +| `metadata.ownerRef` | `null` for directly installed Providers; may point to a managing resource for dependent Providers | +| `metadata.finalizers` | Core adds `core.provider-api-binding` before Provider deletion begins | +| `metadata.deletionRequestedAt` | Set by operator delete or owning resource delete | + +Finalizer `core.provider-api-binding` ensures the API catalog handler +withdraws all exported ResourceType schemas and verifies no resources of those +types remain before the Provider resource is removed. + +### 4.3 Spec + +Provider.spec carries the complete desired installation. All digests are +`sha256:` prefixed lowercase hex. An empty or missing digest field is +rejected at admission. + +```yaml +spec: + artifactId: "runtime-cloud-hypervisor" # bounded ID; references d2b.artifacts catalog + # PackageIdentity fields (digest, manifestDigest, configSchemaDigest, signatureId, + # trustEpoch, conformanceAttestationDigest, compatibility, support, etc.) are + # resolved from the artifact catalog by the resource compiler; they do not appear + # in operator-authored spec or in the canonical JSON envelope. + config: {} # root config; validated against catalog's configSchemaDigest schema + exports: + resourceTypes: + - name: Host # short Zone-unique ResourceType name (or vendor.qualified.Name) + version: 1 + schemaDigest: "sha256:<64 hex>" + - name: User + version: 1 + schemaDigest: "sha256:<64 hex>" + components: + controllers: + - id: host-controller + binaryRef: d2b-core-controller + resourceTypes: [Host, User] + supportedHostProviderCapabilities: [] + supportedGuestProviderCapabilities: [] + allowedDomains: [system] + specVerbs: [create, update-spec] + statusVerbs: [update-status] + finalizerIds: [core.host-teardown] + watchSelectors: + - resourceType: Host + - resourceType: User + dependencySelectors: [] + ownerChildTriggers: [owned-resource-changed] + reconcileConcurrency: 8 + observeConcurrency: 4 + maxPendingResources: 256 + observeIntervalSeconds: null # null = no periodic observe; non-null = seconds + resyncPolicy: on-dependency-change + resourceTypeDeadlineSeconds: + Host: 30 + User: 10 + services: [] + workers: [] + dependencies: + # aliases bound to Provider names; closed set defined per Provider manifest + # example entries (actual aliases declared in signed manifest): + # runtime: Provider/runtime-cloud-hypervisor + # volume: Provider/volume-local + permissionClaims: + - verb: create + resourceType: Host + - verb: update-status + resourceType: Host + - verb: create + resourceType: User + - verb: update-status + resourceType: User + lifecycle: + upgradePolicy: drain-then-replace # drain-then-replace | rolling | immediate + drainTimeoutSeconds: 120 + restartPolicy: on-failure + maxRestarts: 5 + restartBackoffSeconds: 10 +``` + +#### 4.3.1 Artifact identity and catalog fields + +`spec.artifactId` is a bounded label (`^[a-z][a-z0-9-]*$`, max 128 chars) +referencing the `d2b.artifacts.` catalog entry with `type = "provider"`. +All PackageIdentity sub-fields are resolved from the artifact catalog at build +time and are **not** present in the operator-authored spec or canonical JSON +envelope. They are validated by the resource compiler and stored in the private +artifact catalog; the resource store holds only `artifactId`. + +| Artifact catalog field | Type | Rules | +| --- | --- | --- | +| `name` (from manifest) | string | Must equal Provider identity declared in manifest; must equal `metadata.name` | +| `version` (from manifest) | string | Semver `major.minor.patch`; informational; exact digest is binding | +| `digest` | sha256 | Content digest of signed Provider package; required; validated at build | +| `executableDigests` | map[name]sha256 | One entry per built binary; validated at build | +| `manifestDigest` | sha256 | Digest of the Provider's signed manifest artifact; validated at build | +| `configSchemaDigest` | sha256 | Digest of the root config JSON Schema; used to validate `spec.config` | +| `publisher` | string | Stable publisher/organization label; `^[a-z][a-z0-9-]*$` | +| `signatureId` | string | Stable opaque signature reference; non-empty | +| `trustEpoch` | u32 | Trust root epoch; must not be revoked | +| `revocationRef` | null or string | Stable revocation check token; null if no revocation mechanism | +| `conformanceAttestationDigest` | sha256 | Digest of Provider API conformance attestation | +| `compatibility.apiVersion` | u32 | Provider API major version; exact match required; no downgrade | + +Unknown artifact catalog fields are rejected at build time. Store paths and +Nix closure metadata are private catalog implementation data and do not appear +in resource spec, status, audit records, or OTEL attributes. + +#### 4.3.2 Root configuration + +`spec.config` is an object validated against the JSON Schema identified by +the artifact catalog's `configSchemaDigest` for `spec.artifactId`. Validation +occurs at build time (resource compiler) and is re-validated at Provider spec +admission by the Zone runtime. + +Rules: + +- all validation is schema-strict; unknown fields in config are rejected unless + the schema explicitly permits `additionalProperties: true` in a bounded + vendor extension object; +- no credential bytes, raw host paths, PIDs, process arguments, or ambient + authority values are permitted in `config`; +- secrets are `Credential/` ResourceRefs; config values that need + resolution reference Credentials or Volumes; +- config digest (derived from canonical JSON representation of `config`) is + bound to every Process resource spawned from this Provider's components; +- config replacement (spec.generation increment) triggers all owned Process + resources to be drained and replaced; +- the empty config `{}` is valid for Providers with no config. + +#### 4.3.3 Component descriptors + +A Provider may declare at most one set of each component type (controllers, +services, workers). Component IDs are unique within the Provider. + +The following component bounds are normative and enforced at admission: + +| Field | Maximum | +| --- | --- | +| Controllers per Provider | 8 | +| Services per Provider | 8 | +| Worker templates per Provider | 32 | +| ResourceTypes per controller | 16 | +| Watch selectors per controller | 32 | +| Dependency selectors per controller | 16 | +| Finalizer IDs per controller | 8 | + +**Controller descriptor fields** (normative): + + +| Field | Type | Rules | +| --- | --- | --- | +| `id` | ComponentId | `^[a-z][a-z0-9-]*$`; unique within Provider | +| `binaryRef` | string | Key in `package.executableDigests`; identifies the built binary | +| `resourceTypes` | list of ResourceType names | ResourceTypes this controller reconciles; declared in `exports.resourceTypes`; max 16 | +| `supportedHostProviderCapabilities` | list of capability tokens | Host Provider capabilities required for this controller to operate | +| `supportedGuestProviderCapabilities` | list of capability tokens | Guest Provider capabilities required | +| `allowedDomains` | list of `system|user` | Process domains this controller instance supports | +| `specVerbs` | list of resource verbs | Verbs on spec subresource; bounded set from common verb enum | +| `statusVerbs` | list of resource verbs | Verbs on status subresource; controller is status owner if `update-status` present | +| `finalizerIds` | list of FinalizerId | Finalizers this controller adds/clears; `^[a-z][a-z0-9.-]*$` | +| `watchSelectors` | list of selector objects | Exact ResourceTypes and optional name/owner/zone filters this controller watches | +| `dependencySelectors` | list of selector objects | ResourceTypes watched as read-only dependencies | +| `ownerChildTriggers` | list of trigger reason tokens | Subset of common triggers; must include `owned-resource-changed` if controller uses owner index | +| `reconcileConcurrency` | u32 ≥ 1 | Max concurrent reconcile handlers for distinct resources | +| `observeConcurrency` | u32 ≥ 1 | Max concurrent observe handlers | +| `maxPendingResources` | u32 ≥ 1 | Max resources queued for reconcile before backpressure | +| `observeIntervalSeconds` | null or u32 ≥ 1 | Periodic observe period; null means no periodic observe | +| `resyncPolicy` | enum | `on-dependency-change | manual | period-only` | +| `resourceTypeDeadlineSeconds` | map[ResourceType]u32 | Per-type per-reconcile deadline; 0 means use global default | + +**Service descriptor fields**: + +| Field | Type | Rules | +| --- | --- | --- | +| `id` | ComponentId | Unique within Provider | +| `binaryRef` | string | Key in `package.executableDigests` | +| `serviceMethods` | list of method IDs | ttrpc method names this service exports | +| `serviceSchemaDigest` | sha256 | Digest of ttrpc service descriptor | +| `allowedDomains` | list of `system|user` | | +| `endpointRequirements` | object | ComponentSession purpose class/transport class required | +| `subjectConstraints` | list | Closed set of ResourceType subject classes allowed to connect | + +**Worker descriptor fields**: + +| Field | Type | Rules | +| --- | --- | --- | +| `id` | ComponentId | Unique within Provider | +| `binaryRef` | string | Key in `package.executableDigests` | +| `templateName` | string | Name of the Process template used for this worker | +| `allowedDomains` | list of `system|user` | | +| `configProjection` | list of config field paths | Config fields visible to this worker; bounded | +| `maxInstances` | u32 ≥ 1 | Cardinality; owner/controller enforces | +| `executionConstraints` | object | Allowed executionRef ResourceTypes (Host or Guest) for this worker | + +#### 4.3.4 Dependencies + +`spec.dependencies` is a flat map from alias to Provider ResourceRef: + +```yaml +dependencies: + runtime: Provider/runtime-cloud-hypervisor + volume: Provider/volume-local + network: Provider/network-local + credential: Provider/credential-secret-service + transport: Provider/transport-unix +``` + +Rules: + +- alias names are defined in the Provider's signed manifest; the Zone config + binds each alias to an exact installed Provider; +- alias resolution is request-time; the Provider lifecycle handler re-checks + all aliases when a referenced Provider enters or leaves `Ready`; +- synchronous dependency cycles fail Provider configuration; +- an optional dependency's absence produces `Degraded` in the Provider's + component that requires it; the Provider may still publish other components; +- a required dependency's absence prevents the Provider reaching `Ready`; +- alias keys must be `^[a-z][a-z0-9-]*$`; max 16 aliases per Provider. + +#### 4.3.5 Permission claims + +`spec.permissionClaims` is a bounded list of operations the Provider controller +or service requests. Each claim has: + +```yaml +- verb: create # resource verb or runtime verb + resourceType: Host # ResourceType name + subresource: null # null or bounded subresource name + resourceNames: [] # [] = all; non-empty = explicit names + zones: [] # [] = this Zone only; non-empty = named linked Zones + executionRefs: [] # [] = all; non-empty = explicit Host/Guest refs +``` + +Rules: + +- the Zone API binding handler intersects permission claims with Zone policy + derived from installed resources and static admission rules before granting + any Role; +- a Provider cannot claim permissions not declared in its signed manifest; +- cross-Zone claims require an explicit Zone in `zones`; a ZoneLink must exist; +- wildcard permission claims (empty `resourceNames` and `executionRefs`) are + only permitted for verified system Providers with explicit review; +- permission claims are inputs to generated Roles; they are not + self-executing grants. + +#### 4.3.6 Upgrade and lifecycle policy + +| Field | Values | Default | +| --- | --- | --- | +| `lifecycle.upgradePolicy` | `drain-then-replace | rolling | immediate` | `drain-then-replace` | +| `lifecycle.drainTimeoutSeconds` | u32; 0 < t ≤ 3600 | 120 | +| `lifecycle.restartPolicy` | `on-failure | always | never` | `on-failure` | +| `lifecycle.maxRestarts` | u32; ≤ 20 | 5 | +| `lifecycle.restartBackoffSeconds` | u32; 1 ≤ b ≤ 3600 | 10 | + +`drain-then-replace`: All controlled processes are gracefully stopped before +new version starts. No overlap window. Safe for stateful controllers. + +`rolling`: Controller reconciliation continues; workers are replaced +concurrently under cardinality constraints. Not safe for controllers that hold +exclusive locks. + +`immediate`: Old processes stop concurrently with new start. Only for +stateless workers. + +### 4.4 Status + +#### Three-layer status shape (D088) + +D088 freezes `Provider` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `Provider`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +Provider.status extends common status with: + +| Field | Type | Rules | +| --- | --- | --- | +| `trustResult` | enum or null | `trusted | revoked | expired-epoch | attestation-failed | conformance-failed`; null until first check | +| `conformanceResult` | enum or null | `passed | failed | skipped`; null until first check | +| `lastTrustCheckedAt` | RFC 3339 UTC or null | | +| `activePackageDigest` | sha256 or null | Currently running package digest; differs from spec during upgrade | +| `activeConfigDigest` | sha256 or null | Currently running config digest | +| `providerGeneration` | u64 | Monotonic counter incremented by core on every trust+config+component re-validation | +| `components` | bounded list | Per-component status entries | +| `exportedResourceTypes` | list | ResourceTypes currently published to API catalog | +| `dependencyHealth` | map | alias → `Ready|Degraded|Missing`; no Provider details | +| `controllerLeases` | bounded list | Per-controller lease ID / phase / lastCheckpointAt | +| `stateSchemaMigration` | enum | `current | pending | in-progress | failed`; null if no state schema | +| `disabled` | bool | Whether Provider is operator-disabled | +| `quarantined` | bool | Whether core has quarantined Provider due to trust/conformance failure | + +**Component status entry**: + +```yaml +- componentId: host-controller + type: controller # controller | service | worker + phase: Ready # common phases + processRef: Process/system-core-host-controller # null for bootstrap exceptions + lastReconciledAt: 2026-07-22T00:00:01Z + conditions: [] +``` + +`processRef` is null for `Provider/system-core` and `Provider/system-minijail` +components only. All other component `processRef` values must resolve to a +current Process resource owned by this Provider. + +### 4.5 Phase and conditions + +| Phase | Meaning | +| --- | --- | +| `Pending` | Provider spec admitted; trust/conformance/component checks not yet complete | +| `Ready` | All required components Ready; all exported ResourceTypes published | +| `Degraded` | Optional components impaired; required components Ready; exported types published | +| `Failed` | One or more required components Failed; exported types withdrawn | +| `Unknown` | Core cannot determine Provider state | + +Provider never reaches `Succeeded` (long-lived resource). + +Closed condition types for Provider: + +| Condition type | Meaning | +| --- | --- | +| `PackageTrusted` | Package digest/signature/epoch/conformance verified | +| `ConfigValid` | Root config validated against configSchemaDigest schema | +| `DependenciesReady` | All required dependency aliases resolved to Ready Providers | +| `ComponentsReady` | All required component Processes in Ready phase | +| `ApiPublished` | All exported ResourceTypes bound in Zone API catalog | +| `ControllerLeaseActive` | At least one active controller lease exists | +| `DrainComplete` | Upgrade drain complete; `True` only during drain cycle | +| `Quarantined` | Trust/conformance failure has caused quarantine | + +### 4.6 ownerRef, finalizers, and deletion + +**ownerRef**: null for directly installed Providers. A managing resource +(e.g. a future Provider catalog manager) may own a Provider, making it a +generated child that is deleted when the owner is deleted. + +**Finalizer `core.provider-api-binding`**: Added by core when a delete request +arrives. The API catalog handler: + +1. Marks all exported ResourceTypes as `withdrawing`. +2. Verifies no resources of those types remain (returns Pending if they do, + with a `PendingResourceDeletion` condition). +3. Atomically unbinds types from api_schemas. +4. Clears `core.provider-api-binding`. + +**Additional Provider finalizers**: A Provider controller may add finalization +logic through its own finalizer ID (declared in the controller descriptor +`finalizerIds`). Core only coordinates finalizer ordering; it does not execute +Provider-specific finalization logic. + +**Deletion sequence**: + +1. Operator requests delete; `deletionRequestedAt` is set. +2. All owned child resources (Process, Volume, etc.) receive delete requests + child-first. +3. `core.provider-api-binding` awaits ResourceType draining. +4. Provider-specific controller finalizers execute. +5. All finalizers cleared; `phase=Deleted` emitted; resource removed. + +### 4.7 Bootstrap exceptions + +`Provider/system-core` and `Provider/system-minijail` are the two Provider +bootstrap exceptions: + +- they have no `processRef` in their component status; +- their Process binaries are embedded in or co-launched with the Zone runtime; +- they are pre-created in the store during zone initialization before any + other resource exists; +- they are granted only the compiled bootstrap authorization (§6); +- they cannot be deleted while the Zone is operational; +- their trust/package/conformance fields must still pass validation; they + are not exempt from trust checks, only from having Process resource children. + +`Provider/system-minijail` is the fixed second bootstrap exception because: + +- the first Process controller (`system-minijail`) cannot depend on a Process + controller to launch itself; +- `system-minijail` reconciles all later Process resources including + `system-systemd`; it is not a general-purpose minijail executor outside + this bootstrap role. + +After bootstrap both Providers share the same core-controller process process +boundary but use distinct authenticated subjects and closed RBAC grants. + +### 4.8 Provider crate layout (normative) + +Every Provider implementation lives in its own workspace member crate under +`packages/` with the name `d2b-provider--` (following +AGENTS.md naming: base before implementation, alphanumerically sorted in the +workspace member list). The crate **MUST** contain all four of the following +paths or the workspace/package policy check fails the build: + +#### 4.8.1 Required paths + +| Path | Required contents | Missing → | +| --- | --- | --- | +| `src/` | All implementation source files and binaries. Colocated unit tests (`#[cfg(test)]` modules) live here. Must contain at least one `lib.rs`, `main.rs`, or named binary entry point. | policy failure | +| `tests/` | Hermetic Cargo integration tests. Must cover: ResourceType admission/lifecycle, controller reconcile and phase transitions, conformance checks, and one fault-injection test (e.g. trust failure, config-schema mismatch, component crash). No external I/O or container dependencies; runs inside the Nix sandbox build. | policy failure | +| `integration/` | Heavier fixtures and scenarios invoked by the existing test orchestration (`make test-integration` / `make test-host-integration`). Must cover at least one cross-process scenario (Provider process spawn + component lifecycle) and, if the Provider creates Host or Guest resources, one Host or Guest attachment scenario. Scenarios must declare which orchestration target invokes them (container or host-integration). May not import symbols from `src/` directly; communicates through the Zone API or test harness fixtures. | policy failure | +| `README.md` | Provider identity (name, publisher, version, trust/conformance attestation pointers); full `spec.config` schema with field descriptions and defaults; list of exported ResourceTypes with brief description; list of controllers, services, workers, and binaries (role, placement, resource quotas); placement constraints and dependencies; RBAC requirements (Roles and RoleBindings the Provider requires pre-installed); security posture (capabilities, namespace isolation, state roots, credential handling); state and telemetry (audit events emitted, OTEL span/metric names); build, unit-test, integration-test commands; note on future standalone-repo packaging if applicable. | policy failure | + +#### 4.8.2 Policy enforcement + +The workspace policy check (`packages/d2b-contract-tests/tests/policy_provider_crate_layout.rs`, +implemented in work item ADR046-pkg-001) walks every `packages/d2b-provider-*` +crate directory in the workspace and asserts all four paths exist. A missing +`src/`, `tests/`, `integration/`, or `README.md` in any Provider crate is a +**hard policy failure** that blocks `make test-policy` (and therefore +`make check`) on the same basis as a workspace member sort violation or a +`Command::new("bash")` site. + +The check is gated by the same `D2B_FIXTURES` step in +`tests/tools/rust-workspace-checks.sh` as the existing crate-naming and +member-sort checks. It does not require building the crate; it inspects +the filesystem only. + +#### 4.8.3 README minimum sections + +A Provider `README.md` must contain all of the following section headings +(exact casing not required; matched case-insensitively after stripping `#` +whitespace): + +1. `Provider identity` +2. `Config schema` +3. `Exported resource types` +4. `Controllers / services / workers / binaries` +5. `Placement and dependencies` +6. `RBAC requirements` +7. `Security posture` +8. `State and telemetry` +9. `Build and test` + +The policy check verifies all nine headings are present. A missing heading +is a policy failure with the message: +`"d2b-provider-/README.md: missing required section ''"`. + +#### 4.8.4 Integration scenario declaration + +Each file under `integration/` must declare its orchestration target in a +top-level comment or doc attribute within the first 20 lines: + +```rust +//! integration-target: container // runs under make test-integration +//! integration-target: host-integration // runs under make test-host-integration +``` + +The policy check verifies every `integration/*.rs` file carries exactly one +such declaration. Missing or multiple declarations are a policy failure. + +#### 4.8.5 Example skeleton + +``` +packages/d2b-provider-runtime-cloud-hypervisor/ +├── Cargo.toml +├── README.md # §4.8.3 sections required +├── src/ +│ ├── lib.rs # provider registration, component factory +│ ├── guest.rs # Guest ResourceType controller +│ ├── process.rs # Process ResourceType controller +│ └── bin/ +│ └── d2b-provider-runtime-cloud-hypervisor # provider agent binary +├── tests/ +│ ├── guest_lifecycle.rs # admission → Pending → Ready → deletion +│ ├── process_lifecycle.rs # Process component phase transitions +│ ├── conformance.rs # check_provider_conformance/check_descriptor_conformance +│ └── fault_trust_failure.rs # quarantine on trust failure +└── integration/ + ├── guest_spawn.rs # integration-target: host-integration + └── guest_network_attach.rs # integration-target: container +``` + +--- + +## 5. Role + +### 5.1 Role and ownership + +`Role/` declares a bounded set of permission rules. RoleBindings grant +one Role to one or more subjects. + +**Controller/owner**: The `authorization` handler of the fixed core-controller +process owns Role reconciliation. No other controller may update Role spec or +status. + +### 5.2 Metadata + +| Field | Rule | +| --- | --- | +| `metadata.name` | Zone-unique ResourceName | +| `metadata.ownerRef` | Optional; may be owned by a Provider (for generated Roles), by a ZoneLink (for core-generated relay Roles), or null for operator-created Roles | +| `metadata.finalizers` | Core adds `core.role-binding-drain` when a Role with active RoleBindings is deleted | +| `metadata.deletionRequestedAt` | Set on operator delete or owning resource delete | + +### 5.3 Spec + +The complete Role spec is the `rules` array: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Role +metadata: + name: process-controller + zone: dev + uid: + generation: 1 + revision: + ownerRef: null + finalizers: [] + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:00:00Z +spec: + rules: + - resourceTypes: [Process, EphemeralProcess] + verbs: [get, list, watch, create, update-spec, update-status, update-finalizers, delete] + subresources: [] + resourceNames: [] + zones: [] + executionRefs: [] + sessionVerbs: [] +status: + observedGeneration: 1 + phase: Ready + conditions: [] + lastReconciledAt: 2026-07-22T00:00:01Z + startedAt: null + completedAt: null + outcome: null +``` + +#### 5.3.1 Rule schema + +Each rule in `spec.rules` has: + +| Field | Type | Default | Semantics | +| --- | --- | --- | --- | +| `resourceTypes` | list of ResourceType names | required, non-empty | ResourceTypes this rule covers; short Zone-unique names or qualified vendor names | +| `verbs` | list of resource verb tokens | `[]` = no resource verbs | Resource operation verbs (see §5.3.2); at least one of `verbs` or `sessionVerbs` must be non-empty | +| `subresources` | list of subresource or exact service/method selectors | `[]` = no subresource restriction | Empty means all subresources; non-empty means exactly those subresources or qualified service/method selectors | +| `resourceNames` | list of ResourceName | `[]` = all | Empty means all names; non-empty means exactly those names; max 64 | +| `zones` | list of Zone names | `[]` = this Zone only | Empty means only the evaluating Zone; non-empty means any listed Zone | +| `executionRefs` | list of ResourceRefs | `[]` = no restriction | Restricts to operations whose target resource has one of these executionRefs; `[]` means unrestricted | +| `sessionVerbs` | list of session verb tokens | `[]` = no session verbs | ComponentSession/service verbs granted to this rule (see §5.3.2) | + +All list fields are deduplicated at admission. Order is not significant. + +#### 5.3.2 Verb sets + +**Resource verbs** (exact closed set): + +| Verb | Semantics | +| --- | --- | +| `get` | Single resource read | +| `list` | Multi-resource read with pagination | +| `watch` | Streaming change subscription | +| `create` | Create a new resource | +| `update-spec` | Full spec replacement | +| `update-status` | Full status replacement (controller-only) | +| `update-metadata` | Bounded labels/annotations/ownerRef metadata change | +| `update-finalizers` | Add/remove finalizers (ownership constrained) | +| `delete` | Request resource deletion | +| `use-credential` | Invoke a Credential operation selected by one exact Credential allowed-operation subresource | +| `admin-credential` | Supplement matching ordinary Credential create/update-spec/delete authority using the same exact lifecycle subresource | + +Unknown verbs are rejected at admission. +`use-credential` and `admin-credential` are valid only for `Credential` rules +with the exact subresources defined by the Credential contract. + +**Session verbs** (exact closed set): + +| Verb | Semantics | +| --- | --- | +| `connect` | Open a ComponentSession to a service | +| `invoke` | Call a ttrpc method | +| `open-stream` | Open a named stream | +| `relay` | Forward an already-admitted invocation or stream to one authorized next ZoneLink hop | +| `attach` | Transfer a local file descriptor | +| `cancel` | Cancel an in-progress operation | +| `observe` | Subscribe to service health/event notifications | +| `audit-export` | Invoke only `d2b.audit.v3.AuditService/Export` (admin-only) | +| `support-bundle` | Invoke only `d2b.support.v3.SupportService/GenerateBundle` (admin-only) | + +Session verbs in a Role rule grant ComponentSession/d2b-bus access to the +services bound by the same rule's `resourceTypes` and `zones` constraints. +They are evaluated by the same native RBAC engine as resource verbs. +`audit-export` and `support-bundle` require exact qualified service/method +selectors in `subresources`, may appear only in `sessionVerbs`, and imply no +resource read or mutation verb. + +`relay` is transport forwarding authority only. It permits the exact +authenticated ZoneLink/transport subject to forward an invocation or named +stream that has already passed admission to the next route-selected hop. The +same hop independently evaluates the invocation's original target verb; a +`relay` allow never satisfies that check. `relay` grants no resource read, +create, update, or delete operation, identity mapping, capability widening, +attachment right, credential access, or local lifecycle authority. + +A relay-bearing rule is admitted only when all of the following hold: + +- `relay` appears in `sessionVerbs`, never `verbs`; +- `resourceTypes`, `resourceNames`, and `zones` exactly bound the forwarded + target; empty/all-name scope and every wildcard form are rejected. Named + methods match one immutable resource name. Nameless `List`/`Watch` requests + retain a non-empty exact `resourceNames` allowlist and bounded filters whose + possible result set is a subset of that allowlist at every hop; +- the Role and RoleBinding are core-generated with `ownerRef` naming the + governing `ZoneLink`, and the binding's trusted external-principal selector + matches the exact enrolled adjacent-`Zone` transport subject; or an + already-authorized local administrator explicitly permits the same bounded + grant through the durable admin-policy path; +- the request payload, Provider descriptor, and Provider/operator-authored + resource cannot assert core-generated or admin-policy provenance. + +Permission to create a Role or RoleBinding is not itself permission to grant +`relay`. Missing `relay`, missing target-verb authority, stale policy, or +unavailable policy state denies forwarding. There is no implicit relay grant. + +#### 5.3.3 Explicit wildcard form + +The foundation specifies: "no implicit wildcard is granted; a reviewed explicit +wildcard may exist only for fixed core-controller roles." + +An explicit wildcard in `resourceNames` is represented as a single-element +list `["*"]`. This form is: + +- permitted only for fixed core-controller generated Roles; +- prohibited in operator-created or Provider-generated Roles (admission + rejects at creation time); +- evaluated strictly as "all names in the current Zone catalog"; it does not + grant future ResourceTypes not yet bound; +- recorded in the authorization audit log with the wildcard marker. + +No other wildcard form exists. `[]` (empty list) means "all" for +`resourceNames` — this is equivalent to a wildcard for names but is the +default form available to all subjects. An `executionRefs: []` means +"unrestricted by executionRef", not a wildcard requiring `["*"]`. + +The distinction between `resourceNames: []` (unrestricted, default) and +`resourceNames: ["*"]` (explicit reviewed wildcard, only for core-controller +Roles) must be enforced at admission. For non-core-controller Roles, +`resourceNames: ["*"]` is rejected with `resource-schema-invalid`. + +#### 5.3.4 Bounds + +| Field | Maximum | +| --- | --- | +| Rules per Role | 32 | +| `resourceTypes` per rule | 16 | +| `verbs` per rule | 16 (bounded by verb enum: currently 11 verbs) | +| `sessionVerbs` per rule | 9 (bounded by session verb enum: currently 9 verbs) | +| `subresources` per rule | 16 | +| `resourceNames` per rule | 64 | +| `executionRefs` per rule | 32 | +| `zones` per rule | 8 | + +All bounds are enforced at admission. Exceeding any bound returns `resource-schema-invalid`. + +### 5.4 Status + +#### Three-layer status shape (D088) + +D088 freezes `Role` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `Role`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + + +Role.status contains only common fields. There are no Role-specific status +fields beyond: + +| Field | Type | Rules | +| --- | --- | --- | +| `activeBindingCount` | u32 | Number of RoleBindings currently referencing this Role; maintained by authorization handler | +| `lastValidatedAt` | RFC 3339 UTC or null | Most recent time the authorization handler validated this Role | + +`activeBindingCount` is informational only. Deletion is blocked (via finalizer) +when this count > 0. + +### 5.5 Phase and conditions + +| Phase | Meaning | +| --- | --- | +| `Pending` | Role spec admitted; authorization handler has not yet built index entry | +| `Ready` | Role validated; index built; authorization operational | +| `Degraded` | Role valid but one or more rules reference unresolvable executionRefs (warn only) | +| `Failed` | Role spec fails validation (invalid verb, unknown ResourceType, bad ref) | + +Closed condition types for Role: + +| Condition type | Meaning | +| --- | --- | +| `RuleSetValid` | All rules have valid verbs, ResourceType names, and ref formats | +| `IndexBuilt` | Authorization index entry is current | +| `ActiveBindings` | `True` when activeBindingCount > 0; informational | +| `PendingBindingDrain` | `True` during deletion while active bindings exist | + +### 5.6 ownerRef, finalizers, and deletion + +**ownerRef**: null for operator-created Roles; may be a Provider for +generated core-controller Roles. + +**Finalizer `core.role-binding-drain`**: Added when deletion is requested and +`activeBindingCount > 0`. The authorization handler: + +1. Sets `PendingBindingDrain=True` condition; the Role remains in its current phase (typically `Ready`) while awaiting binding drain. +2. Awaits all RoleBindings referencing this Role to be deleted first. +3. Clears the index entry. +4. Clears `core.role-binding-drain`. + +A Role with active bindings cannot be deleted until all bindings are removed. +This prevents authorization gaps from dangling RoleBinding references. + +--- + +## 6. RoleBinding + +### 6.1 Role and ownership + +`RoleBinding/` grants a Role to one or more subjects. It may optionally +narrow the Role's rules and carries revocation state. + +**Controller/owner**: The `authorization` handler of the fixed core-controller +process owns RoleBinding reconciliation. No other controller may update +RoleBinding spec or status. + +### 6.2 Metadata + +| Field | Rule | +| --- | --- | +| `metadata.name` | Zone-unique ResourceName | +| `metadata.ownerRef` | Optional; may be owned by the Provider that generates this binding, or by the governing ZoneLink for a core-generated relay binding | +| `metadata.finalizers` | No standard finalizer; core clears the binding on deletion without deferral (no downstream finalizer chain) | +| `metadata.deletionRequestedAt` | Set on operator delete or owning resource delete | + +### 6.3 Spec + +```yaml +apiVersion: resources.d2bus.org/v3 +type: RoleBinding +metadata: + name: process-controller-binding + zone: dev + uid: + generation: 1 + revision: + ownerRef: null + finalizers: [] + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:00:00Z +spec: + roleRef: Role/process-controller + subjects: + - Provider/system-minijail + externalPrincipalSelector: null + scopeNarrowing: null # see §6.3.5 +status: + observedGeneration: 1 + phase: Ready + conditions: [] + lastReconciledAt: 2026-07-22T00:00:01Z + startedAt: null + completedAt: null + outcome: null +``` + +#### 6.3.1 roleRef + +`spec.roleRef` is a canonical ResourceRef `Role/` in the same Zone. + +Rules: + +- `roleRef` is required; an absent or empty roleRef is rejected at admission; +- the referenced Role must exist; if it does not, RoleBinding enters `Failed` + with condition `RoleNotFound`; +- `roleRef` is immutable after creation; changing the role requires deleting + and recreating the RoleBinding; +- authorization evaluates the referenced Role's current generation at decision + time; the authorization cache is invalidated when the Role changes. + +#### 6.3.2 Subjects + +`spec.subjects` is a bounded ordered list of canonical same-Zone ResourceRefs: + +```yaml +subjects: + - Provider/system-minijail + - User/alice + - Host/host-system + - Process/wayland-proxy + - Guest/dev-vm +``` + +Rules: + +- each subject is a `/` ResourceRef in this Zone; +- supported subject ResourceTypes: `Zone`, `User`, `Provider`, `Host`, `Guest`, + `Process`; a ResourceRef `Zone` subject can name only the store's self Zone, + while an enrolled adjacent-Zone transport subject is matched by + `externalPrincipalSelector`; other ResourceTypes are rejected at admission with + `resource-schema-invalid`; +- subjects list must be non-empty except for a core-generated, ZoneLink-owned + relay binding with one exact trusted `externalPrincipalSelector`; +- duplicate subjects are rejected at admission; +- a subject that does not currently exist as a resource causes `SubjectNotFound` + condition (warning only, not Failed; the subject may be created later); +- the resolved subject UID is stored for change detection; if a subject is + deleted and a new resource of the same name is created, the authorization + handler detects the UID change, removes the old binding entry, and emits a + `SubjectIdentityChanged` condition; +- max subjects per RoleBinding: 128. + +#### 6.3.3 External principal selector + +`spec.externalPrincipalSelector` is `null` or a bounded object generated by +trusted enrollment/config. It selects ComponentSession subjects identified by +external identity evidence (e.g. enrolled Noise KK keys, vsock CID, enrollment +token digests) rather than by local ResourceRef. + +Rules: + +- external selectors may only appear in RoleBindings generated by trusted + enrollment config (configuration publication or Provider bootstrap); + operator-created RoleBindings may use external selectors only when the + operator has permission to create enrollment records; +- an external selector contains no credential bytes; it contains only opaque + enrollment digests or stable external identity tokens; +- external selectors are evaluated by the ComponentSession authentication step + before d2b-bus routing; they never appear in `subjects`; +- external selectors are bounded in size (max 512 bytes canonical JSON); +- `externalPrincipalSelector` and `subjects` may both be present; a request + satisfies the binding if it matches either. +- a core-generated relay binding uses an exact adjacent-Zone enrollment + selector and no broad subject-class selector; a peer cannot supply it in a + request. + +#### 6.3.4 (removed — no expiry field) + +`RoleBinding` has no `spec.expiry` field. Revoke, update, or delete a +RoleBinding via normal resource lifecycle operations. Time-limited grants +are implemented by operator-scheduled deletion of the RoleBinding resource, +not by an expiry field inside the spec. + +#### 6.3.5 Scope narrowing + + +`spec.scopeNarrowing` is null or a subset of the referenced Role's rules: + +```yaml +scopeNarrowing: + rules: + - resourceTypes: [Process] + verbs: [get, list, watch] + executionRefs: [Host/host-system] +``` + +Rules: + +- scope narrowing may only restrict; it cannot grant verbs or resourceTypes + not present in the referenced Role; +- an attempt to grant a verb absent from the Role is rejected at admission + with `resource-schema-invalid`; +- narrowed rules are the intersection of the Role rules and the narrowing + rules; +- `sessionVerbs`, including `relay`, `audit-export`, and `support-bundle`, are + intersected exactly like resource verbs; narrowing cannot add one or remove + its exact target or service/method bounds; +- `scopeNarrowing: null` means the full Role is granted without restriction; +- scope narrowing affects only this RoleBinding; the referenced Role is + unchanged. + +### 6.4 Status + +#### Three-layer status shape (D088) + +D088 freezes `RoleBinding` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `RoleBinding`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +| Field | Type | Rules | +| --- | --- | --- | +| `roleResolved` | bool | Whether `roleRef` currently resolves to a Ready Role | +| `subjectCount` | u32 | Number of `subjects` entries | +| `unresolvedSubjects` | bounded list | Subject refs that currently have no matching resource; max 8 entries shown; overflow shown as count | +| `revoked` | bool | Whether operator revocation is set | +| `revokedAt` | RFC 3339 UTC or null | When revocation was set | + +### 6.5 Phase and conditions + +| Phase | Meaning | +| --- | --- | +| `Pending` | Admitted; authorization handler not yet built index entry | +| `Ready` | Role resolved; at least one subject matches; index entry built | +| `Degraded` | Role resolved but some subjects unresolvable (warning) | +| `Failed` | Role not found or revocation set | + +Closed condition types for RoleBinding: + +| Condition type | Meaning | +| --- | --- | +| `RoleFound` | Referenced Role exists and is Ready | +| `SubjectsResolved` | All listed subjects resolve to current resources | +| `SubjectNotFound` | One or more listed subjects do not exist (warning) | +| `SubjectIdentityChanged` | A subject UID changed due to delete/recreate | +| `IndexBuilt` | Authorization index entry is current | +| `RoleBindingRevoked` | Operator revocation set | + +### 6.6 ownerRef, finalizers, and deletion + +**ownerRef**: null for operator-created bindings; may point to a Provider +that generated the binding. + +**No standard finalizer**: RoleBinding deletion is immediate. When +`deletionRequestedAt` is set: + +1. Authorization handler drains in-flight request contexts: ongoing requests + that were admitted under this binding retain their original context until + their deadline; no new authorizations are granted to these subjects under + this binding. +2. Authorization caches for all subjects in this binding are invalidated. +3. Role's `activeBindingCount` decrements. +4. **Final atomic transaction**: remove the RBAC index entry, emit the + `Deleted` revision event, and remove the resource row from the Zone store + as one redb write transaction. No observable intermediate state exists + between index removal and row removal. + +There is no RoleBinding finalizer chain; the binding is not needed to safely +remove. + +--- + +## 7. Quota + +### 7.1 Role and ownership + +`Quota/` declares Zone-wide aggregate resource ceilings and optionally +per-resource-type limits. Host, Guest, and Process resources may carry a +`spec.quotaRef: "Quota/"` pointing to the governing Quota, and may +declare inline resource requests (cpu, memory, storage) within that Quota's +bounds. + +**Controller/owner**: The `quota` handler of the fixed core-controller process +owns Quota reconciliation. No other controller may update Quota spec or status. + +### 7.2 Metadata + +| Field | Rule | +| --- | --- | +| `metadata.name` | Zone-unique ResourceName | +| `metadata.ownerRef` | null for operator-created Quotas | +| `metadata.finalizers` | Core adds `core.quota-drain` when Quota is the subject of a quotaRef before deletion | +| `metadata.deletionRequestedAt` | Set on operator delete | + +### 7.3 Spec + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Quota +metadata: + name: default-quota + zone: dev + uid: + generation: 1 + revision: + ownerRef: null + finalizers: [] + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:00:00Z +spec: + ceilings: + maxResources: 4096 # max total non-Deleted resources in Zone + maxResourcesPerType: 512 # max non-Deleted resources per ResourceType + maxOwnerDepth: 8 # max owner chain depth + maxCpu: null # optional: aggregate vCPU ceiling across all quotaRef resources (null = unlimited) + maxMemoryMib: null # optional: aggregate memory MiB ceiling (null = unlimited) + maxStorageGib: null # optional: aggregate storage GiB ceiling (null = unlimited) + perTypeCeilings: {} # map ResourceType → {maxResources, maxCpu, maxMemoryMib, maxStorageGib}; empty = use global ceilings + scope: zone # "zone" (only value in v3; reserved for future sub-Zone scopes) + enforcementPolicy: hard # "hard" (reject over-quota) | "soft" (warn only, still admit) +``` + +**Spec field rules**: + +- `ceilings.maxResources`: `1..=65536`; default 4096 +- `ceilings.maxResourcesPerType`: `1..=65536`; default 512 +- `ceilings.maxOwnerDepth`: `1..=32`; default 8 +- `ceilings.maxCpu`, `ceilings.maxMemoryMib`, `ceilings.maxStorageGib`: null or positive integer; null means no ceiling +- `perTypeCeilings`: max 64 entries; ResourceType names must be resolvable in the Zone API catalog +- `scope`: exactly `"zone"` in v3 +- `enforcementPolicy`: `"hard"` | `"soft"` + +Multiple Quota resources may coexist in a Zone; individual Host/Guest/Process +resources reference exactly one Quota via `spec.quotaRef` (or none). The quota +handler aggregates usage by quotaRef independently. A resource with no quotaRef +is counted against all `scope: zone` Quotas for the `maxResources` ceiling check +but is not subject to cpu/memory/storage ceilings. + +### 7.4 Status + +#### Three-layer status shape (D088) + +D088 freezes `Quota` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `Quota`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +| Field | Type | Rules | +| --- | --- | --- | +| `usedResources` | u32 | Count of non-Deleted resources referencing this Quota | +| `usedCpu` | u32 or null | Aggregate vCPU in use; null if no cpu ceiling | +| `usedMemoryMib` | u32 or null | Aggregate memory MiB in use; null if no memory ceiling | +| `usedStorageGib` | u32 or null | Aggregate storage GiB in use; null if no storage ceiling | +| `overQuota` | bool | True if any ceiling is currently exceeded (only possible with enforcementPolicy=soft) | +| `overQuotaTypes` | bounded list | ResourceTypes currently over their perTypeCeiling; max 16 shown | +| `lastCheckedAt` | RFC 3339 UTC or null | Most recent quota check | +| `dependentCount` | u32 | Count of resources referencing this Quota via `spec.quotaRef`; 0 when safe to delete | + +### 7.5 Phase and conditions + +| Phase | Meaning | +| --- | --- | +| `Pending` | Admitted; quota handler initializing | +| `Ready` | Quota active; ceilings enforced | +| `Degraded` | Quota over soft limit (enforcementPolicy=soft), or deletion requested with dependents remaining | +| `Failed` | Spec invalid (bad ceiling value, unknown ResourceType in perTypeCeilings) | + +Closed condition types: + +| Condition type | Meaning | +| --- | --- | +| `CeilingsValid` | All ceiling values and perTypeCeilings entries are valid | +| `OverQuota` | One or more ceilings exceeded (soft enforcement only) | +| `QuotaDrainPending` | Deletion in progress; `dependentCount` resources still reference this Quota; waiting for authorized owners/operators to reassign or delete them | + +### 7.6 ownerRef, finalizers, and deletion + +Quota deletion with active `quotaRef` assignments uses `core.quota-drain`: + +1. `deletionRequestedAt` set; Zone transitions to `Degraded` with `QuotaDrainPending` condition. +2. Quota controller sets `QuotaDrainPending=True` condition with message + `" resources still reference this Quota; reassign or delete them before deletion completes"`. +3. `status.dependentCount` reflects the current count of resources with `spec.quotaRef` pointing to this Quota. +4. The quota controller does NOT mutate any other resource's `spec.quotaRef`. + Authorized owners/operators must manually reassign dependents to another Quota or + delete them; the controller blocks `core.quota-drain` clearance until `dependentCount = 0`. +5. The quota controller re-checks `dependentCount` on every reconcile triggered by + resource creation/deletion/update; it emits `quota-drain-complete` audit record when + the count reaches 0, then clears `core.quota-drain`. +6. Final atomic transaction: emit `Deleted` revision event; remove resource row and index. + +A Quota with `enforcementPolicy=hard` and active resource ceilings blocks +resource creation over-quota; it never blocks deletion. + +### 7.7 Nix authoring + +```nix +d2b.zones.dev.resources.default-quota = { + type = "Quota"; + spec = { + ceilings = { + maxResources = 4096; + maxResourcesPerType = 512; + maxOwnerDepth = 8; + maxCpu = null; + maxMemoryMib = null; + maxStorageGib = null; + }; + perTypeCeilings = {}; + scope = "zone"; + enforcementPolicy = "hard"; + }; +}; +``` + +Eval-time assertions: `ceilings.maxResources` in `1..=65536`; `ceilings.maxOwnerDepth` in `1..=32`; `scope` equals `"zone"`; `enforcementPolicy` in `{"hard","soft"}`. Phase 2: `perTypeCeilings` ResourceType names resolved against Zone Provider catalogs. + +### 7.8 Controller algorithm + +1. **Validate** `spec.ceilings` and `spec.perTypeCeilings`; set `CeilingsValid`. +2. **Build usage index**: scan all non-Deleted Zone resources; aggregate by `quotaRef` and ResourceType. +3. **Enforce**: on every resource admission (`create` verb), check current usage + 1 against ceilings; reject with `quota-exceeded` if over ceiling and `enforcementPolicy=hard`; warn if `soft`. +4. **Update status**: write `usedResources`, `usedCpu`, `usedMemoryMib`, `usedStorageGib`, `overQuota`, `overQuotaTypes`. +5. **Trigger**: on any resource creation, deletion, or `spec.quotaRef` change, trigger quota reconcile. + +### 7.9 RBAC and audit + +Quota creates/updates generate audit event kind `resource-mutated` with redacted spec. Quota admission checks generate `quota-check` audit event with resource type, name, result (admitted/rejected), and usage snapshot. No subject names, resource names, or store paths in audit payload. + +--- + +## 8. EmergencyPolicy + +### 8.1 Role and ownership + +`EmergencyPolicy/` declares a Zone-wide emergency disable scope, +permitted actions during disable, and current disable status. Multiple +EmergencyPolicy resources may coexist; their scopes are evaluated as +a union (most restrictive wins per-action). + +**Controller/owner**: The `emergency-policy` handler of the fixed +core-controller process owns EmergencyPolicy reconciliation. Multiple +EmergencyPolicy resources with `enabled=true` may coexist; the handler +computes the most-restrictive union of all active scope flags: any scope +flag set to `true` in any enabled policy applies Zone-wide. The effective +`drainDeadlineSeconds` is the minimum across all enabled policies. + +### 8.2 Metadata + +| Field | Rule | +| --- | --- | +| `metadata.name` | Zone-unique ResourceName | +| `metadata.ownerRef` | null for operator-created EmergencyPolicies | +| `metadata.finalizers` | `core.emergency-drain` added when policy has `active=true` at deletion time | +| `metadata.deletionRequestedAt` | Set on operator delete | + +### 8.3 Spec + +```yaml +apiVersion: resources.d2bus.org/v3 +type: EmergencyPolicy +metadata: + name: zone-lockdown + zone: dev + uid: + generation: 1 + revision: + ownerRef: null + finalizers: [] + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:00:00Z +spec: + enabled: false # operator activates/deactivates disable + scope: + stopNewAdmissions: true # block all new resource API admissions + disconnectZoneLinks: true # gracefully disconnect all ZoneLinks + stopProviderProcesses: false # stop all Provider Process resources + drainOngoingOperations: true # drain in-flight operations to deadline + drainDeadlineSeconds: 30 # max seconds to drain; default 30 + reason: "" # operator-supplied rationale (max 256 chars); visible in resource spec and audit; never emitted in status, OTEL labels, or log labels +``` + +**Spec field rules**: + +- `enabled`: boolean; `false` means policy exists but is not active +- `scope.stopNewAdmissions`: boolean; when true, new resource API admissions are rejected while active +- `scope.disconnectZoneLinks`: boolean; when true, ZoneLinks receive graceful disconnect signal +- `scope.stopProviderProcesses`: boolean; when true, the runtime suppresses new Provider component Process launches and signals running non-bootstrap Provider component Processes to stop; Process resources are **not** deleted and no `deletionRequestedAt` is set; on deactivation the provider lifecycle controller resumes reconciliation and re-launches stopped Processes +- `scope.drainOngoingOperations`: boolean; when true, in-flight operations drain to deadline before service stops +- `drainDeadlineSeconds`: `1..=300`; default 30 +- `reason`: string max 256 chars; visible in resource spec; included in audit record body; must not appear in status fields, OTEL metric label values, or structured log labels + +### 8.4 Status + +#### Three-layer status shape (D088) + +D088 freezes `EmergencyPolicy` status as three layers. The universal `ResourceStatus` +base (Layer 1) lives at top-level `status` and owns `observedGeneration`, +`phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, and +bounded `outcome`. The `EmergencyPolicy`-specific status fields documented in this +section constitute the ResourceType-common `status.resource` (Layer 2) object +and never restate the universal base. Optional implementation-only observation +belongs in `status.provider` (Layer 3) with exactly `providerRef`, qualified +immutable `schemaId`, semver `MAJOR.MINOR` `schemaVersion`, numeric +`observedProviderGeneration`, and strict, bounded, redacted, +unknown-field-denied `details`. Generic API, CLI, and controllers MUST consume +only the base-only projection (`status` base plus `status.resource`). +Controllers MUST write all present layers atomically in one status mutation with +one expected revision. D087/D088 mapping is: shared observations go to +`status.resource`; implementation-specific bounded non-secret observations go to +`status.provider.details`; secret, large, or private observations go to an +optional Volume. D088 bounds apply: total status <= 64 KiB, `status.resource` <= +32 KiB, `status.provider.details` <= 32 KiB, 32 conditions, 64-entry lists/maps, +and 4 KiB strings; violations use `status-oversize`, +`status-provider-schema-invalid`, or `status-provider-overlap`. + +Mapping convention: within this spec a reference to `status.` denotes the ResourceType-common `status.resource.` unless `` is a universal base field (`observedGeneration`, `phase`, `conditions`, `lastReconciledAt`, `startedAt`, `completedAt`, `outcome`). + +| Field | Type | Rules | +| --- | --- | --- | +| `active` | bool | Whether this policy is currently enforced (`enabled=true` and effects applied) | +| `activatedAt` | RFC 3339 UTC or null | When most recently activated | +| `deactivatedAt` | RFC 3339 UTC or null | When most recently deactivated | +| `drainCompletedAt` | RFC 3339 UTC or null | When in-flight drain completed after most recent activation | + +### 8.5 Phase and conditions + +| Phase | Meaning | +| --- | --- | +| `Pending` | Admitted; handler initializing | +| `Ready` | Policy spec valid; handler operational (regardless of `enabled` state; see `status.active` for enforcement state) | +| `Degraded` | Policy enabled and drain in progress, or partial scope effect failure | +| `Failed` | Spec invalid | +| `Unknown` | Handler cannot determine state | + +Closed condition types: + +| Condition type | Meaning | +| --- | --- | +| `PolicyValid` | Spec fields are valid | +| `Enforced` | `True` when `enabled=true` and all effective scope actions are applied | +| `DrainComplete` | Post-activation drain completed; `True` after drain, reset on next activation | +| `EmergencyDrainPending` | Deletion is requested and `active=true`; drain must complete before finalizer clearance | + +### 8.6 ownerRef, finalizers, and deletion + +`core.emergency-drain` is added when deletion is requested while `status.active=true`: + +1. The controller sets `enabled=false` internally to begin deactivation and scope-effect reversal. +2. Drain completes per the effective `drainDeadlineSeconds` (minimum across all enabled policies, or this policy's own value if alone). +3. `core.emergency-drain` cleared; final atomic transaction emits `Deleted` revision event; resource row removed. + +Deletion of an `enabled=false` EmergencyPolicy has no finalizer; the final +atomic transaction emits `Deleted` and removes the row immediately. + +### 8.7 Nix authoring + +```nix +d2b.zones.dev.resources.zone-lockdown = { + type = "EmergencyPolicy"; + spec = { + enabled = false; + scope = { + stopNewAdmissions = true; + disconnectZoneLinks = true; + stopProviderProcesses = false; + drainOngoingOperations = true; + }; + drainDeadlineSeconds = 30; + reason = ""; + }; +}; +``` + +Eval-time assertions: `drainDeadlineSeconds` in `1..=300`; `reason` max 256 chars. `reason` is visible in the resource spec; it must not appear in status fields, OTEL metric label values, or structured log labels. + +### 8.8 Controller algorithm + +1. **Validate** spec; set `PolicyValid`. +2. **Compute effective scope union**: at each reconcile, iterate all EmergencyPolicy resources with `enabled=true` (including self). For each scope flag, set the effective flag to `true` if ANY enabled policy has it `true`. Effective `drainDeadlineSeconds` = minimum across all enabled policies. Apply/maintain the effective scope set. +3. **On `enabled` transition to `true`**: set `active=true`, `activatedAt=now()`, `Enforced=True`; apply this policy's contribution to the scope union: + - `stopNewAdmissions`: signal API admission gate to reject new requests (if not already gated by another policy) + - `disconnectZoneLinks`: emit graceful disconnect to all active ZoneLinks (if not already disconnected) + - `stopProviderProcesses`: suppress new Provider component Process launches; signal running non-bootstrap Provider component Processes to stop without setting `deletionRequestedAt` on their Process resources + - `drainOngoingOperations`: set effective drain deadline; await in-flight drain completion or deadline +4. **On `enabled` transition to `false`**: set `active=false`, `deactivatedAt=now()`, `Enforced=False`; recompute scope union from remaining enabled policies; if no other policy gates an action, restore that action (admit new requests, allow ZoneLinks to reconnect, resume Provider lifecycle reconciliation for stopped Processes). +5. **Update status** on every reconcile. + +### 8.9 RBAC and audit + +EmergencyPolicy activation/deactivation generates `emergency-policy-activated` / +`emergency-policy-deactivated` audit events carrying: policy ResourceRef/UID, +effective scope flags (union or individual), and outcome. `reason` is included +in the audit record body (it is a resource spec field); it must not appear in +audit span attribute labels, OTEL metric label values, or structured log labels. +Principal identity is carried at the ComponentSession authentication layer and +is not duplicated in the audit payload. + +--- + +## 8A. ResourceExport and ResourceImport (D096) + +`ResourceExport` and `ResourceImport` are the two standard ResourceTypes (D096) +that let a Provider's cross-Zone capability serve multiple Zones through one +authority, without a direct cross-Zone `ResourceRef` or duplicate open of a +physical device/backend. They carry the D089 three-layer spec and D088 +three-layer status like every other standard type. + +### 8A.1 General model + +- Every exportable capability has a qualified semantic/provider-neutral + `*Service` ResourceType and matching qualified semantic `*Binding` + ResourceType. These are not Provider implementation namespaces and are not + additions to the 19 standard ResourceTypes. +- The owner-Zone `*Service` is the one real authority. Its same-Zone spec + references the local `Device`, `Endpoint`, or qualified semantic backend + allowed by the Provider's signed projection factory. No consumer Zone opens + that backing. +- There is **no cross-Zone `ResourceRef`**. The owner Zone declares + `ResourceExport/` whose `resourceRef` targets only the local owner + `*Service`. It never targets a `Device`, `Endpoint`, or `*Binding`. The consumer + Zone declares `ResourceImport/` that references only its local + `zoneLinkRef` plus a bounded remote `exportKey` and signed fingerprints. +- The core ZoneLink **export/import controller** (this spec, `d2b-core-controller`) + owns routing and base lifecycle. It delegates semantic admission and + observations to the selected Provider's signed **export/import adapter** and + projection factory. A missing, unsigned, or mismatched factory fails closed. +- Core creates exactly one local projection **Service** per import. It has the + same qualified `*Service` type as the owner Service and + `metadata.ownerRef: ResourceImport/`. Core never creates a Device, + Endpoint, or Binding projection. +- Operator/Nix configuration authors one or more same-Zone matching `*Binding` + resources. Each Binding references the projection's `serviceRef` plus a + consuming `Guest`, `User`, or `Zone` allowed by the signed factory. Its + spec is desired consumer intent only; all observations belong in `status`. + Its Provider controller creates owned `Process`/`Endpoint` children. Binding is + never exported and never auto-created, owned, or deleted by the import + controller. +- Sharing is **opt-in on both sides**, over the **same Nix-compiled + `parentZone` hierarchy and child-local ZoneLink only**. Every hop applies RBAC + and a capability ceiling and requires an enrolled Noise_KK session. **No FD + or resource grant crosses a Zone.** Payload bytes flow only over bounded, + encrypted named streams with a + per-import session generation, credits/backpressure, cancel, deadline, and + idempotency (D096, ComponentSession/bus spec). Intermediate controllers see + ciphertext only. +- Export removal or ZoneLink loss revokes outstanding leases and degrades the + local projection Service; reconnect revalidates the remote generation and both + fingerprints so no stale authority survives. D091 update currency propagates + owner Service → export → import → projection Service → authored Binding → owned + children. Per D092, leases, sessions, ceremonies, transfers, named streams, + and stream handles stay internal/high-churn. + +#### 8A.1.1 Signed projection factory + +Every exportable Provider capability declares this metadata in its signed +descriptor: + +| Field | Contract | +| --- | --- | +| `serviceType` | Exact qualified semantic/provider-neutral `*Service` type used by owner and projection | +| `bindingType` | Exact qualified semantic/provider-neutral `*Binding` type permitted to consume that Service | +| `allowedBackingRefTypes` | Closed same-Zone types the owner Service may reference (`Device`, `Endpoint`, or qualified semantic backend types) | +| `allowedBindingTargetRefTypes` | Closed subset of `Guest`, `User`, and `Zone` | +| `projectionSchema` | Strict deny-unknown semantic base schema for the local projection Service; contains standard `providerRef` plus semantic base/import fields and excludes `spec.provider`, implementation-specific fields, FD, secret, raw path/locator, credential, and payload bytes | +| `projectionSchemaFingerprint` | SHA-256 of the canonical projection schema | +| `factoryFingerprint` | SHA-256 binding all fields above and the semantic projection-protocol version, never Provider/adapter identity | + +Provider install, Nix build, and API admission verify this metadata. The export's +Service type and fingerprints, the import's expectations, and the installed +consumer Provider factory must match exactly. There is no generic fallback and +no "project the exported resource's type" behavior. The selected implementation +remains the local `providerRef`; strict `spec.provider` contains only +implementation-specific settings on authored owner Services and Bindings. +Core-generated projection Services never contain `spec.provider`; their route +derives from the signed local Provider descriptor, `providerRef`, and +ResourceImport record, and implementation observation belongs only in +`status.provider`. Every conformant Provider MUST accept the +canonical minimal Service/Binding base without `spec.provider`. Base spec/status, +conditions, errors, and fingerprints never contain PipeWire, OTEL, USBIP, +CTAPHID, package, binary, or adapter details; `providerRef` is the sole opaque +implementation selector. + +### 8A.2 ResourceExport + +`ResourceExport/` lives in the owner/authority Zone. The core +export/import controller owns its base lifecycle; the named Provider's export +adapter owns semantic admission and per-consumer arbitration. + +#### 8A.2.1 Base spec (Layer 2, D089) + +| Field | Type | Required | Default | Bounds/notes | +| --- | --- | --- | --- | --- | +| `providerRef` | ResourceRef | yes | — | `Provider/` — the local authority Provider that mediates the resource | +| `resourceRef` | ResourceRef | yes | — | **Local qualified owner `*Service` only**; a Device, Endpoint, Binding, or other backing is rejected | +| `serviceType` | string | yes | — | Exact qualified type of `resourceRef`; must equal the factory `serviceType` | +| `projectionSchemaFingerprint` | string | yes | — | Must equal the signed factory fingerprint for the projection Service schema | +| `factoryFingerprint` | string | yes | — | Must equal the installed signed projection factory | +| `operations` | `[string]` | yes | — | Closed operation/capability set consumers may request; 0..64, deny-unknown | +| `arbitration` | enum | yes | — | `exclusive` \| `shared` \| `multiplexed` | +| `quota` | object | no | `{}` | fairness/quota/deadline knobs: `maxConsumers`, `perConsumerRate`, `fairness` (`fifo\|priority\|weighted`), `leaseDeadlineMs`; all bounded | +| `consumerZonePolicy` | object | yes | — | Allowed consumer-Zone selector (child Zones only) plus a capability ceiling that no import may exceed | +| `visibility` | enum | no | `child-zones` | `child-zones` \| `named-zones`; never host-global | +| `updatePolicy` | object | no | manual-disruptive | D091 update/revocation policy (manual disruptive default; auto non-disruptive permitted) | +| `revocationPolicy` | object | no | `{}` | grace window and forced-revoke behavior on export delete / ZoneLink loss | + +`spec.provider = { schemaId, schemaVersion, settings }` (D089) adds strict, +deny-unknown, type-specific export policy. It never restates a base field and +never carries raw bytes, paths, device nodes, sockets, tokens, or a backing ref. + +#### 8A.2.2 Base status (Layer 2, D088) + +`status` base carries advertised/ready/revoking state, `exportGeneration`, +active and pending consumer counts, bounded per-consumer **lease summaries** +(consumer Zone, capability subset, lease state, monotonic lease id digest — no +bytes), owner Service readiness/generation, the two verified fingerprints, and +the D091 `status.update` currency object. `status.provider.details` carries +bounded, redacted implementation observations. **No backing ref, raw bytes, +path, device node, socket, token, or endpoint locator** is advertised. + +#### 8A.2.3 Conditions, ownerRef, finalizer, deletion + +Closed conditions: `ExportAdvertised`, `AuthorityReady` (owner Service Ready), +`ConsumersAdmitted`, `Revoking`. `metadata.ownerRef` may be the +authority Provider or owner Service. Finalizer ordering on delete: quiesce new +imports → revoke each active lease with the grace window → confirm the owner +Service released all internal per-consumer records/streams → withdraw the +advertisement → clear finalizer. The owner Service and its backing are not +deleted by export cleanup. + +### 8A.3 ResourceImport + +`ResourceImport/` lives in the consumer (child) Zone. It never names a +remote resource; it names its local `ZoneLink` and a bounded `exportKey`. + +#### 8A.3.1 Base spec (Layer 2, D089) + +| Field | Type | Required | Default | Bounds/notes | +| --- | --- | --- | --- | --- | +| `providerRef` | ResourceRef | yes | — | **Local** Provider implementation whose import adapter builds the semantic projection | +| `zoneLinkRef` | ResourceRef | yes | — | **Local** `ZoneLink/` to the parent/authority Zone; the only routing anchor | +| `exportKey` | string | yes | — | Bounded opaque key naming the remote export (not a Ref); ≤128 chars | +| `expectedServiceType` | string | yes | — | Expected qualified `*Service`; must equal both factories' `serviceType` | +| `expectedProjectionSchemaFingerprint` | string | yes | — | Must equal the export and local factory projection schema fingerprint | +| `expectedFactoryFingerprint` | string | yes | — | Must equal the export and installed local factory fingerprint | +| `projectionName` | string | yes | — | Stable local name of the one projection Service core creates | +| `requestedCapabilities` | `[string]` | yes | — | Subset of the export `operations`; bounded by the capability ceiling | +| `requestedQuota` | object | no | `{}` | Requested rate/weight/deadline, clamped to the export quota | +| `updatePolicy` | object | no | manual-disruptive | D091 propagation policy | +| `disconnectPolicy` | object | no | `{}` | Behavior on ZoneLink loss/revocation: `degrade` (default) vs `teardown` | + +`spec.provider` adds strict type-specific import policy (deny-unknown). No remote +Ref, FD, path, or token appears anywhere. + +#### 8A.3.2 Base status (Layer 2, D088) + +`status` base: state (`pending\|reachable\|bound\|degraded\|revoked`), observed +remote `exportGeneration` and verified factory/projection fingerprints, local +projection-Service Ref, lease state/count, session generation digest, and +`status.update` currency. Degraded and revoked states propagate to the +projection Service, every Binding that references it, and their children. No raw +locator, backing ref, or bytes. + +#### 8A.3.3 Projection ownership, conditions, finalizer + +Core creates one `expectedServiceType` projection +(`ownerRef: ResourceImport/`) through the local Provider's import adapter +and keeps its readiness synchronized with the lease. Closed conditions: +`ExportReachable`, `FactoryMatched`, `SchemaMatched`, `Bound`, +`ProjectionReady`, `BindingReferencesRemain`, `Degraded`. + +On revoke/link loss, core first marks the projection Service draining/revoked +and refuses new sessions. Binding controllers observe that dependency change, +stop their owned Process/Endpoint children, and report degraded status; the +Binding rows remain because they are operator-owned. On import deletion, the +finalizer waits until all referencing Bindings are deleted or retargeted, then +releases the remote lease, deletes the projection Service and its remaining +provider-owned children, and clears only the import finalizer. It never deletes +or synthesizes a Binding. A remaining Binding produces visible pending cleanup, +not an implicit cascade. Reconnect revalidates generation and both fingerprints +before rebinding. + +### 8A.4 Reconcile (core routing + provider adapter) + +1. Core validates opt-in on both sides; resolves `resourceRef` to a same-Zone + qualified owner Service; rejects Device/Endpoint/Binding targets; validates + local `zoneLinkRef`; and rejects every cross-Zone Ref. +2. Core advertises the export over the ZoneLink to the exact `consumerZonePolicy` + selector. The advertisement carries only export key, Service type, factory + and projection fingerprints, operations, arbitration, and capability ceiling. +3. The importing Zone matches all three signed factory values. Missing metadata, + a mismatch, or an unauthorized Zone fails closed before a lease or projection. +4. The selected Provider's export adapter admits the consumer (arbitration, + quota, fairness, consent), issues an internal lease, and the import adapter + builds/updates exactly one local projection Service. Bytes flow only over a + bounded encrypted named stream. +5. Separately authored Bindings reconcile against that Service and own their + Process/Endpoint children. D091 currency propagates owner Service → export → + import → projection Service → Binding → children. Desired intent stays in + Binding spec; observed realization stays only in status. + +### 8A.5 Non-exportable defaults + +Credential/token resources are **non-exportable by default**. Per D093, Entra ID +identity stays a same-Zone identity Guest; there is no `ResourceExport` for it +unless a future, explicitly reviewed export capability is added. + +The frozen semantic pairs and initial implementing Providers are: + +| Semantic pair | Initial Provider | Export policy | +| --- | --- | --- | +| `audio.d2bus.org.AudioService` / `audio.d2bus.org.AudioBinding` | `Provider/audio-pipewire` | exportable | +| `security-key.d2bus.org.SecurityKeyService` / `security-key.d2bus.org.SecurityKeyBinding` | `Provider/device-security-key` | exportable | +| `telemetry.d2bus.org.TelemetryService` / `telemetry.d2bus.org.TelemetryBinding` | `Provider/observability-otel` | exportable | +| `usb.d2bus.org.UsbService` / `usb.d2bus.org.UsbBinding` | `Provider/device-usbip` | policy-gated: Provider, Zone, export, and device policy all opt in | + +Every other frozen Provider remains non-exportable. A matching Binding is always +non-exportable even when its Service is approved. Provider dossiers bind their +implementation and strict extensions to these names; they do not own or alias +the semantic namespaces. + +### 8A.6 Nix authoring example + +```nix +# The semantic AudioService name is Provider-independent. First declare the one +# real owner Service over local Device/Endpoint backing. +d2b.zones.local-root.resources.host-audio = { + type = "audio.d2bus.org.AudioService"; + spec = { + providerRef = "Provider/audio-pipewire"; + backingRefs = [ "Device/host-mic" "Endpoint/audio-local" ]; + }; +}; + +# Export only that owner Service. +d2b.zones.local-root.resources.mic-export = { + type = "ResourceExport"; + spec = { + providerRef = "Provider/audio-pipewire"; + resourceRef = "audio.d2bus.org.AudioService/host-audio"; + serviceType = "audio.d2bus.org.AudioService"; + projectionSchemaFingerprint = "sha256:..."; + factoryFingerprint = "sha256:..."; + operations = [ "capture" ]; + arbitration = "exclusive"; + consumerZonePolicy = { zones = [ "Zone/work" ]; capabilityCeiling = [ "capture" ]; }; + visibility = "named-zones"; + }; +}; + +# Consumer child Zone: declare the one local uplink used by imports. The +# compiler-only setting chooses the allocator owner but is never emitted into +# Zone.spec or the resource bundle. +d2b.zones.work.parentZone = "local-root"; + +# The child-local ZoneLink supplies transport and local route/session state; +# the selected parent allocator owns privileged listener and routing effects. +d2b.zones.work.resources.work-uplink = { + type = "ZoneLink"; + spec = { + childZoneName = "work"; + transportProviderRef = "Provider/transport-unix"; + transportSettings = {}; + transportCredentials = []; + disabled = false; + }; +}; + +# Consumer Zone: import exactly one local AudioService projection. +d2b.zones.work.resources.mic-import = { + type = "ResourceImport"; + spec = { + providerRef = "Provider/audio-pipewire"; + zoneLinkRef = "ZoneLink/work-uplink"; + exportKey = "host/mic-export"; + expectedServiceType = "audio.d2bus.org.AudioService"; + expectedProjectionSchemaFingerprint = "sha256:..."; + expectedFactoryFingerprint = "sha256:..."; + projectionName = "host-audio"; + requestedCapabilities = [ "capture" ]; + }; +}; + +# Operator-authored local consumption Binding. The import controller never +# creates this resource; its controller owns the resulting Process/Endpoint. +d2b.zones.work.resources.work-mic = { + type = "audio.d2bus.org.AudioBinding"; + spec = { + providerRef = "Provider/audio-pipewire"; + serviceRef = "audio.d2bus.org.AudioService/host-audio"; + targetRef = "Guest/workstation"; + mode = "capture"; + }; +}; +``` + +These declarations serialize to canonical ResourceEnvelopes with only local refs; +the resource compiler rejects a non-Service export target, cross-Zone Ref, +factory/schema mismatch, unauthorized consumer Zone, disallowed backing or Binding +target ref, or capability outside the export ceiling. Optional Nix sugar may +lower to these resources only when the canonical ResourceExport, +ResourceImport, projection-Service name, and Binding are stable and inspectable. + +### 8A.7 Conformance and tests + +Fast hermetic tests (fake ZoneLink/stream/clock/factory) MUST cover: signed +factory absent/mismatch/tamper fail-closed; only an owner Service is exportable; +exactly one same-type projection Service; no Device/Endpoint/Binding projection; +Binding is neither exported nor auto-created; Binding spec is intent-only and +observations are status-only; backing/target allowlists; stable canonical Nix +lowering; finalizer waiting on authored Bindings; update propagation; +classification (audio/security-key/observability approved, USBIP policy-gated, +all others forbidden); canonical minimal base acceptance; semantic-type +preservation across independently selected implementations; rejection of +implementation detail in the base; quota/fairness/deadline/reconnect/revocation; +and no FD/secret/path/raw locator. Slower integration tests use real bounded +encrypted streams for audio, security-key, observability, and policy-gated +USBIP, proving that intermediaries see ciphertext and high-churn sessions/ +streams remain internal records. + +--- + +## 8B. Core and singleton authorities (D097) + +Per D097, every scarce or singleton backing declares a signed +`AuthorityDescriptor` (schema in +[`ADR-046-resource-object-model` §Authority and cardinality](ADR-046-resource-object-model.md)), +and **core owns the authority index** keyed by `(Zone/scope, authorityClass, +opaqueKeyDigest)`. Core rejects a conflicting authority Resource or Process with +the typed `duplicateConflict` before any external effect; config activation that +would create a second authority goes `Degraded` naming the exact incumbent owner +digest, and restart adopts the exact authority by `ownerProof` (ambiguity +quarantines). + +The core/singleton control-plane authorities and their classification: + +| Authority | Scope | Cardinality | Owning Resource/service | Exportability | +| --- | --- | --- | --- | --- | +| Zone self-resource | zone | exactly-one | `Zone` self-resource | forbidden | +| Resource store (redb) | zone | exactly-one | core resource store | forbidden | +| Resource API + runtime | zone | exactly-one | core resource API/runtime | forbidden | +| Core controller | zone | exactly-one | `d2b-core-controller` | forbidden | +| Bus `Endpoint` (d2b-bus) | zone | exactly-one | core bus listener `Endpoint` | forbidden | +| Privileged broker | host | exactly-one per Zone | fixed local-root broker | forbidden | +| **Daemon** audit authority | zone | exactly-one | core daemon audit writer | forbidden (Zone-local system of record) | +| **Broker** audit authority | host | exactly-one | broker audit writer (**separate** chain from the daemon) | forbidden (separate system of record) | +| Configuration publisher | zone | exactly-one | core configuration controller | forbidden | +| Artifact catalog | zone | exactly-one | Nix-emitted catalog | forbidden | +| Host substrate allocator / effect authority | host | exactly-one | `Host` + `Provider/system-core` | forbidden | +| Network authority (net Guest + DHCP/DNS/NAT) | zone | exactly-one **per `Network`** | `Network` net-VM authority | forbidden | +| Provider controller | zone | exactly-one per Zone (observability **at-most-one**/zero-or-one) | `Provider` | forbidden | +| `ResourceExport` authority | owner Zone | zero-or-one per exported owner Service | `ResourceExport` | n/a (is the export mechanism) | +| `Quota` scope | zone/scope | exactly-one per scope | `Quota` | forbidden | +| `EmergencyPolicy` scope | zone/scope | exactly-one per scope | `EmergencyPolicy` | forbidden | + +The **daemon audit authority and the broker audit authority are two separate +authorities** (distinct writers, distinct chains); neither is transferable, and +exporting audit copies requires a separate explicit D096 export that transfers no +authority. **Provider controller cardinality** is `exactly-one` per Zone for most +Providers; the observability Provider is `at-most-one` (zero-or-one) per Zone. +The network authority is `exactly-one` per `Network` (the net Guest plus its +DHCP/DNS/NAT owner); a second DHCP/DNS/NAT owner on the same `Network` is a +`duplicateConflict`. `Quota`/`EmergencyPolicy` scope uniqueness is +`exactly-one`-per-scope; a second policy/quota claiming the same scope is a +`duplicateConflict`. + +**Cross-Zone exportability is deny-by-default.** Core singletons and ordinary +Provider resources/backings are `exportability: forbidden`. D096 +`ResourceExport`/`ResourceImport` are the sole typed bridge, and only a signed +factory-bound qualified owner Service may carry `explicit-export`. The initial +approved Provider families are audio, security-key, observability, and +policy-gated USBIP; all others remain forbidden. Their Devices, Endpoints, +Bindings, Credentials, secrets, and backend resources remain non-exportable. A +Zone needing another Zone's telemetry exports the observability owner Service, +which transfers capability/data but never audit-chain/store authority. + +**Name disambiguation.** The Volume/virtiofs **`Export`** resource (the virtiofs +share lifecycle owner referenced by D092, which owns its `Endpoint`) is a +**distinct concept** from the D096 **`ResourceExport`** standard ResourceType +(the cross-Zone sharing declaration). They are never conflated: a virtiofs +`Export` is a local Volume-share owner; a `ResourceExport` is the cross-Zone +bridge. A virtiofs share or its Endpoint is not directly exportable; a future +reviewed Provider would need a qualified Service/Binding pair and signed +projection factory, and `ResourceExport` would target only that Service. + +### 8B.2 D097 core-audit migration findings + +The D097 core-singleton audit surfaced the following missing migration work. +Core-owned items are work items in §17 (ADR046-zone-control-021…023); items whose +implementation destination is a downstream (foreign) crate/spec are recorded here +as findings for that scope to convert into its own work item: + +- **Process-global statics → per-Zone.** `USBIP_BACKGROUND_RECONCILE_ACTIVE`, + `FORCE_SHUTDOWN_GENERATIONS`, and `activation_locks()` are today process-global + and MUST move to per-Zone provider/resource status or a per-Zone coordinator + (core-owned; ADR046-zone-control-021). +- **Configuration publisher per-VM → per-Zone staging.** The current per-VM + configuration staging symbols move to per-Zone staging under the single + configuration-publisher authority (core-owned; ADR046-zone-control-021). +- **ZoneLink cursor/adoption.** ZoneLink cursor persistence and restart adoption + are an authority owned by the ZoneLink handler (core-owned; + ADR046-zone-control-021; see also `ADR-046-zone-routing`). +- **Provider cardinality admission.** Admission MUST enforce Provider controller + cardinality via the core authority index (core-owned; + ADR046-zone-control-022; `ADR-046-resource-api-and-authorization`). +- **Quota and EmergencyPolicy implementation/tests.** Scope-uniqueness authority + implementation and tests (core-owned; ADR046-zone-control-023). +- **`NetworkEffectPort`.** The per-`Network` DHCP/DNS/NAT authority needs a + `NetworkEffectPort` (D077) rather than ad hoc effects — finding for the + `resources-network`/`network-local` downstream scope. +- **activation-helper disposition.** The current activation-helper needs an + explicit v3 disposition — finding for the `activation-nixos` downstream scope. +- **OTEL `vm`-label migration + `d2b-telemetry` bounded emitter** — finding for + the `telemetry-audit-and-support`/`observability-otel` downstream scope. + +Physical/scarce and per-user/session authorities (mic/speaker, security key, +GPU/render-node, video decoder, TPM/swtpm, NIC/uplink/macvtap, Wayland portal, +clipboard, notification sink, PipeWire mediator, Secret Service, systemd-user +manager, Entra login authority, shell supervisor, SigNoz ingest, cloud +subscription control) carry their qualified `AuthorityDescriptor` in the owning +Provider dossier; those per-Provider classifications are refined by evidence with +conservative existing-behavior defaults. + +### 8B.1 Authority conformance tests + +Hermetic (fake clock/index/adapter) fast tests and slower integration tests MUST +cover, provider-neutrally: a **duplicate race** (two concurrent authority +claimants → exactly one wins, the other gets `duplicateConflict`, no second +effect); a **config collision** (a second configuration-managed authority for the +same `(scope, authorityClass, opaqueKeyDigest)` → hard eval error / `Degraded` +activation naming the incumbent owner digest); each `arbitration` mode +(`exclusive` denies a second holder; `shared`/`multiplexed` admit bounded holders +through the single owner; `partitioned` isolates partitions); **adoption +ambiguity** (restart with two candidate owners or an unverifiable index entry → +quarantine, no open); **cross-Zone import** binds through the single owner with +no duplicate open; **non-exportable rejection** (an `exportability: forbidden` +authority declared as a `ResourceExport` target is rejected); **update drain** +(D091 upgrade drains consumers before recycling the authority); and **each +initial singleton** (Zone/store/API/bus/controller/broker/audit/config +publisher/allocator/net-VM-per-Network/`Quota`/`EmergencyPolicy` scope) admits +exactly one owner and rejects a second. + +### 8B.3 Hardware singleton authorities (D097) + +The D097 hardware audit classifies every scarce/physical/kernel backing. These +are **Host-global** authorities (keyed by `(Host, authorityClass, +opaqueKeyDigest)`): two Zones on the same host collide over one physical backing, +so the index admits exactly one owner across all Zones on the host. + +| Hardware authority | Scope | Cardinality | Owner | Arbitration | Exportability | +| --- | --- | --- | --- | --- | --- | +| GPU/DRM full device (primary/VFIO) | physical-device (Host-global) | zero-or-one per GPU | `Device`/`Provider/device-gpu` | exclusive | forbidden (FD/local kernel) | +| GPU render-node | physical-device (Host-global) | bounded-many per render node | `Provider/device-gpu` | **shared** (explicitly) | forbidden | +| GPU-owned `udmabuf`/video subresources | subresource of the GPU authority | n/a (internal) | GPU authority | n/a | not a separate resource — declared **authority subresource/DeviceGrant**, never a new Provider | +| Per-Guest swtpm state + marker | physical-device (Host-global) | exactly-one per Guest | `Provider/device-tpm` | exclusive | forbidden; **state never wiped** (device-tampering signal) | +| Physical TPM | physical-device (Host-global) | exactly-one (host singleton) | `Provider/device-tpm` | exclusive | forbidden | +| Physical USB backing | physical-device (Host-global) | zero-or-one per Core-derived trusted identity digest | Any authority Service implemented by a USB or security-key Provider, initially `Provider/device-usbip` and `Provider/device-security-key` | exclusive through the identical `(Host, physical-usb-backing, opaqueKeyDigest)` tuple | forbidden directly; a policy-gated semantic USB Service or security-key Service may mediate it | +| `usbip-host` kernel module | host (Host-global) | exactly-one host-global | `Provider/device-usbip` | exclusive | forbidden | +| Per-Network USBIP listener + firewall | host (Host-global) | exactly-one per Core-derived Network UID/signed-policy-port digest | `Provider/device-usbip` relay `Endpoint` (never `Network` authority) | multiplexed; conflict `usbip-network-relay-authority-conflict` | forbidden | +| External NIC / macvtap `parentInterface` | physical-device (Host-global) | zero-or-one per interface | `Network`/`Provider/network-local` | `passthru` **globally exclusive across all Zones**; `bridge`/`private`/`vepa` per explicit policy | forbidden | +| Host-shared KVM (`/dev/kvm`) | host (Host-global) | shared grant, one grant authority | **`Provider/system-core`** (host substrate/effect authority) | shared | forbidden | +| Host-shared vhost-vsock (`/dev/vhost-vsock`) | host (Host-global) | shared grant, one grant authority | **`Provider/system-core`** | shared | forbidden | +| vsock CID allocation | host (Host-global) | globally-unique per CID | core allocator | exclusive | forbidden (CID never crosses a Zone) | +| Fixed listener port namespace | host (Host-global) | exactly-one per port | the listener's `Endpoint` | exclusive | forbidden (fixed ports are `Endpoint` resources) | +| Host Nix store | host (Host-global) | exactly-one | Host substrate | shared read | forbidden | +| Per-Guest store-view writer | physical-device (Host-global) | exactly-one writer per Guest | store-view writer | exclusive | forbidden | +| Network TAP / bridge | zone | exactly-one per TAP/bridge | `Network` authority | exclusive | forbidden | + +**KVM/vhost-vsock ownership (no 28th Provider).** `/dev/kvm` and +`/dev/vhost-vsock` are **host-shared kernel devices owned by +`Provider/system-core`** (the existing Host substrate/effect authority, which +already declares the `kvm` `HostCapabilityClass`) and granted to runtime +Providers via the D077 EffectPort/LaunchTicket DeviceGrant. They are **not** a +`Device` `busClass` (the closed set stays `usb|hidraw|drm|pci|tpm`) and do **not** +require a `Provider/device-kvm` — any such reference resolves to a +`Provider/system-core` host-shared grant (finding for the foreign dossiers that +still name `Provider/device-kvm`). GPU-owned `udmabuf`/video and per-session +`vhost-vsock` tokens stay **authority subresources / DeviceGrants**, never new +Providers. + +**D096 exportability of hardware.** GPU, KVM, physical/emulated TPM, host Nix +store, store-view writer, and macvtap/NIC `parentInterface` require an FD or +local-kernel authority and are **non-exportable** (`forbidden`). **USBIP capability is policy-gated exportable** only through the +factory-bound qualified USBIP Service/Binding pair and typed CTAPHID-free USBIP +protocol. The physical Device and fixed listener Endpoint remain non-exportable. +This supersedes any stale direct-hardware export claim. + +### 8B.4 D097 hardware-audit consistency findings + +- **TPM flush TTL** — corrected in `ADR-046-resources-device` to the D094 + canonical successful-EphemeralProcess TTL (`1h`), matching the device-tpm + dossier; a shorter `15m` would need an explicit justified override. +- **`Provider/device-kvm` stale reference** — resolves to a + `Provider/system-core` host-shared grant (no 28th Provider, no `kvm` busClass); + finding for the foreign dossiers/validation that still name it. +- **`device-tpm` physical TPM** — the device-tpm dossier MUST explicitly + implement or explicitly reject physical TPM (no second TPM Provider); finding + for that downstream dossier. +- **macvtap `passthru`** conflict is Host-global across all Zones (covered by the + Host-global index); **`Create`/`DeleteBridge`** must be `NetworkEffectPort` + ops (finding for `resources-network`/`network-local`). +- **USBIP port 3240** multiplex behavior and the fixed port become an `Endpoint` + resource; finding for the usbip/network downstream scope. +- **vsock CID hardcoded `2`** migration to the global CID allocation authority — + corrected in `ADR-046-zone-routing`. +- **store-view gcroots path** code-vs-spec: **code wins**; finding for the store + spec to align its prose to the implemented gcroots path. +- **ZoneLink range capacity/quota** — an explicit bounded quota; corrected in + `ADR-046-zone-routing`. + +--- + +## 9. Bootstrap authorization + +Before any Role/RoleBinding resources exist, the Zone runtime has one compiled +non-configurable bootstrap authorization policy. + +### 9.1 Subjects + +Exactly two subjects are permitted under bootstrap: + +- `Provider/system-core` +- `Provider/system-minijail` + +Any other subject is denied. There is no bootstrap fallback for any other +Provider, controller, service, operator, or CLI connection. + +### 9.2 Verbs + +Bootstrap grants only: + +| Verb | ResourceTypes | Scope | +| --- | --- | --- | +| `get`, `list` | all | Store recovery read | +| `create` | Zone | Initial self-resource creation | +| `create` | Provider | Initial system-core and system-minijail records | +| `create` | Host | First Host creation | +| `create` | User | Local User creation | +| `create` | Role | Initial core-controller Roles | +| `create` | RoleBinding | Initial core-controller RoleBindings | +| `update-spec`, `update-status` | Zone, Provider, Host, User | Initial reconciliation | +| `update-status` | Role, RoleBinding | Initial authorization index publication | +| `connect`, `invoke` | store-lifecycle, core-controller services | Core process launch only | + +No wildcard Provider, resource, runtime authority, or cross-Zone access is +granted. No config field widens bootstrap authorization. + +### 9.3 Properties + +- Bootstrap policy is compiled into the Zone runtime binary; it is not + configurable, overridable, or loaded from any file or resource at runtime. +- Bootstrap admission checks all structural constraints (Zone/session/route + correctness, installed Provider/API binding, ref integrity, generation/revision, + budget) as if they were normal requests. +- Every bootstrap action is structurally audited. +- Bootstrap authorization is superseded completely and immediately after the + first full set of Role/RoleBinding resources is published and the + authorization handler reports `IndexBuilt=True` for all initial bindings. +- After bootstrap supersession, any request using only bootstrap subjects/verbs + that are not covered by stored Roles is denied. +- The transition from bootstrap to stored RBAC is atomic: the authorization + handler swaps its in-memory policy in one transaction; there is no window + where both policies are active. + +### 9.4 Recovery + +If the authorization store is corrupted or the Role/RoleBinding index is +unreadable: + +- no new requests are admitted (fail-closed); +- the out-of-band safety path (see §12.6) allows a privileged local operator + to trigger a destructive reset that re-enters bootstrap for a fresh + initialization; +- the out-of-band path is authenticated by OS-level mechanisms (uid=0 or + equivalent); it cannot be invoked remotely or through d2b-bus. + +--- + +## 10. Zone self-resource and ZoneLink relationship + +### 10.1 Zone self-resource invariant + +Every Zone store contains exactly one `Zone/` resource where +`` equals `store_meta.zone_name`. This invariant is enforced: + +- at store open: if `Zone/` does not exist, bootstrap creates it; +- at every mutation: create/update requests cannot change `metadata.name` or + `metadata.zone` to violate the self-resource constraint; +- at every open: the Zone runtime reads and verifies the self resource before + admitting any API request; +- at upgrade: if `store_meta.zone_uid` differs from the Zone resource UID, + the store is quarantined (this indicates replacement or corruption); +- a second `Zone/` resource in the same store cannot be + created; the ResourceType `Zone` is restricted to cardinality 1 within a + store. + +### 10.2 No cross-Zone resource references + +Ordinary resource refs (`/`) always resolve in +the evaluating resource's own Zone. There is no URI scheme, Zone prefix, or +cross-Zone address in any ref field. + +Prohibited: + +```yaml +ownerRef: Zone/other-zone::Process/foo # REJECTED +subjectRef: other-zone:User/alice # REJECTED +``` + +A parent Zone accesses child Zone resources by calling the child Zone's +`d2b.resource.v3` API over the allocator-bound ComponentSession represented by +the child-local ZoneLink. The parent's +local resources (e.g. a local `Process` that represents a child Zone workload) +do not contain cross-Zone ResourceRefs; they contain only local refs valid in +the parent Zone. + +### 10.3 ZoneLink parent/child access model + +``` +Parent Zone / allocator Child Zone + private route allocation Zone/guest + (privileged listeners/routes) ZoneLink/guest-uplink + | (resource, cursor, intent state) + | allocator-bound ComponentSession | + |------- d2b.resource.v3 Get ----------> | + |<------- response (child data) --------- | +``` + +The parent receives only data the child's authorization engine permits. The +parent's mapped subject in the child Zone is established through the +transport Provider's authenticated ComponentSession using +`spec.transportProviderRef`, `spec.transportSettings`, and resolved +`spec.transportCredentials`. The parent cannot read child resources it is not +authorized for; the child authorization engine is the sole arbiter. The +ZoneLink spec and all of its refs resolve in the child store. Parent-side +allocation/route state is not a resource and is not a reciprocal ZoneLink. +The enclosing child's compiler-only `parentZone` chooses which parent allocator +appears on the left side of the diagram; no ZoneLink field can select or +override that owner. + +A disconnected child uplink: + +- records outbound local ZoneLink intents in the child but does not claim + parent resource state changed; +- on reconnect, re-authenticates and applies/rejects pending intents against + the current parent revision; +- never lets either endpoint use cached remote resource state for authorization + decisions. + +--- + +## 11. Core controller algorithms + +### 11.1 Zone controller algorithm + +The `configuration publication` handler reconciles Zone: + +1. **Startup relist**: List `Zone/`; verify `metadata.uid` matches + `store_meta.zone_uid`; quarantine if mismatch. +2. **Spec validation**: Validate that `Zone.spec` is exactly `{}`; any + non-empty object is rejected with `zone-spec-invalid` and `ConfigValid=False` condition (handler sets Phase `Failed` for this resource). +3. **Configuration activation**: Read integrity-pinned candidate bundle; + validate all Provider packages/APIs/config/refs/owners/RBAC/budgets; + stage inactive resources in bounded transactions; atomically activate one + configuration revision; trigger affected resources/providers/controllers. +4. **Status update**: Write current `apiCatalogRevision`, `policyRevision`, + `configurationRevision` from store_meta; aggregate mandatory handler phases + into `coreControllerPhase`. +5. **Reconcile result**: `converged` when store_meta revisions match status and + all mandatory handlers are Ready; `pending` during activation; `degraded` if + optional handlers impaired. + +The Zone controller never directly touches Provider, Role, RoleBinding, Host, +Guest, or Process resources. Those are owned by their respective handlers. + +### 11.2 ZoneLink controller algorithm + +The child Zone's `zone link/delegation` handler reconciles its local uplink: + +1. **Startup relist**: List all ZoneLink resources; for each, read cursor from + the child-local `zone_link_cursors` table; verify `childZoneName` equals the + local Zone self-name and `childZoneUid` against the local Zone UID. +2. **Transport resolution**: Resolve `spec.transportProviderRef`; wait for + that same-child Provider to be `Ready`. Submit the ZoneLink UID, child + identity, and transport binding to the parent allocator selected by the + compiler-only `parentZone` map. The allocator alone creates privileged + listeners/route namespace and returns the pre-bound transport through + sealed bootstrap authority. +3. **Authentication**: Establish the allocator-bound ComponentSession; perform + the Noise handshake; authenticate the parent allocator and present the + child-local subject. +4. **Child authorization check**: Require the parent to acknowledge that the + child subject and requested ceiling fit the sealed allocation; set + `ChildAuthorized`. Parent access to `d2b.resource.v3` is independently + checked by the child authorization engine on every call. +5. **Cursor recovery**: Compare `childZoneUid` with the local Zone UID; if it + changed, reset local cursor state and require a fresh parent allocation. +6. **Watch/relist**: Receive parent route/export advertisements from + `lastAppliedRevision`; if the cursor expired, relist and re-watch. +7. **Status update**: Write connection state, cursor values, link epoch. +8. **Reconnect loop**: On disconnect, attempt reconnect with exponential backoff; + write `lastDisconnectedAt`, `connected=false`, `Unknown` phase until + reconnected. +9. **Drain on delete**: On `deletionRequestedAt`, gracefully close session, + apply drain finalizer, update status to reflect drain. + +ZoneLink does not copy parent or child resources across stores. The child store +contains only the local ZoneLink row/cursors/intents; parent allocator and +route-engine state is session/allocation scoped. + +### 11.3 Provider lifecycle algorithm + +The `provider lifecycle` handler reconciles Provider: + +1. **Trust/conformance**: Resolve the artifact catalog entry for `spec.artifactId`; + verify `digest`, `signatureId`, `trustEpoch` against the installed trust store; + verify `conformanceAttestationDigest` against known attestation store. + Set `PackageTrusted` and `trustResult`. +2. **Config validation**: Validate `spec.config` against the JSON Schema + identified by the artifact catalog's `configSchemaDigest`. Set `ConfigValid`. +3. **Dependency resolution**: For each alias in `spec.dependencies`, resolve + to a Ready Provider; verify service fingerprint compatibility. Set + `DependenciesReady`. +4. **API binding**: Pass component descriptors and exported ResourceTypes to + the API catalog handler; verify no name collisions; intersect permission + claims with Zone policy; install schemas. +5. **Component launch**: For each component in `spec.components`, create or + update the owned Process resource with the corresponding template/placement. + Bootstrap exception components (`system-core`, `system-minijail`) skip this + step. +6. **Readiness**: Once all required component Processes reach `Ready`, write + Provider status to `Ready` with `ApiPublished=True`. Write `providerGeneration`. +7. **Upgrade**: On spec change (generation increment), execute lifecycle policy + (`drainAndReplace`, `rolling`, `immediate`); update component Processes. +8. **Quarantine**: On trust or conformance failure, set `quarantined=true`; stop + all component Processes; withdraw exported ResourceTypes; leave state Volumes + intact for incident investigation. + +### 11.4 Authorization algorithm (Role and RoleBinding) + +The `authorization` handler reconciles Role and RoleBinding: + +1. **Role reconciliation**: + a. Validate rules (verbs, ResourceType names, ref formats, explicit wildcard + restriction, and relay origin/ZoneLink/target bounds). + b. Build index entry: for each (subject, Zone, ResourceType, verb, subresource, + resourceNames, executionRefs) tuple, add an allow entry. + c. Write `IndexBuilt=True`, `phase=Ready`. + d. Invalidate authorization caches for all subjects covered by changed rules. + +2. **RoleBinding reconciliation**: + a. Resolve `roleRef` to current Role UID and generation. + b. Resolve each subject in `subjects` to current UID; note unresolved. + c. Resolve `externalPrincipalSelector` if present. + d. Apply `scopeNarrowing` as an intersection with Role rules. + e. For a relay-bearing Role, verify the exact adjacent-Zone enrollment + selector, governing ZoneLink owner, and core-generated or explicit + admin-policy provenance. + f. Honor operator revocation state and any pending deletion request. + g. Build index entry: for each subject × (narrowed rule) add allow entry. + h. Increment Role `activeBindingCount`. + i. Write `phase=Ready`, `IndexBuilt=True`, `roleResolved=true`. + j. Invalidate authorization caches for all covered subjects. + +3. **Index swap**: After every batch of Role/RoleBinding commits, the + authorization handler atomically swaps the in-memory evaluator to the new + generation. The swap is MVCC-safe: in-flight requests retain their original + context; new requests use the new index. + +4. **Cache invalidation**: The authorization cache (positive decisions cached + under exact attributes and short expiry) is invalidated per subject upon: + a. Any Role change affecting that subject. + b. Any RoleBinding change affecting that subject. + c. Zone policy revision change. + d. Provider/API catalog revision change. + Denials are never cached as allows. + +5. **Deletion handling**: + - **Role deletion**: Blocked by `core.role-binding-drain` until all + RoleBindings referencing this Role are deleted. The handler decrements + `activeBindingCount` on each binding deletion; clears the finalizer when + count reaches 0. + - **RoleBinding deletion**: The final atomic transaction removes the RBAC + index entry, emits the `Deleted` revision event, and removes the resource + row from the Zone store in one redb write; no observable intermediate state + exists between index removal and row removal (§6.6). + +### 11.5 Configuration generation cleanup algorithm + +The `configuration publication` handler executes the following sequence when +activating a new Nix-authored resource bundle generation. See §14.11 for the +full normative cleanup contract including status/conditions/audit. + +1. **Diff computation**: Compare the new bundle's config-owned resource set + against all config-owned resources currently in the store. Resources present + in the new bundle are upserted (created if new; spec-updated if changed). + Resources that were config-owned in any prior active generation but absent + from the new bundle are "cleanup candidates". + +2. **Immediate generation activation**: The new bundle generation is written as + the active configuration revision in `store_meta` atomically. The Zone runtime + begins serving requests under the new generation without waiting for cleanup. + `Zone.status.activeConfigurationGeneration` updates to the new value. + `ConfigurationCurrent=True` is set. + +3. **Async Delete for absent config-owned resources**: For each cleanup candidate: + a. If `metadata.deletionRequestedAt` is null, set it to the current timestamp. + b. Add the resource-type-specific core finalizer if not already present. + c. Emit an async reconcile trigger to the owning handler. + Deletes are issued concurrently, bounded by the store transaction limit. + Resources with `managedBy=controller` (including bootstrap-created resources + such as `Provider/system-core`, `Provider/system-minijail`, and the Zone + self-resource) or `managedBy=api` are never cleanup-deleted by generation change. + +4. **Controller-created resource preservation**: The handler tracks only bundle + records. Controller-created children — process instances, ephemeral records, + dynamic volumes — have no bundle entry and are never deleted by generation + change. The owning controller reconciles its dynamic children when the parent + resource's spec or phase changes via watch trigger. + +5. **Owner controller child cascade**: When a config-owned resource has + `deletionRequestedAt` set, its owning controller cascades deletion to owned children: + - `Provider` awaiting deletion: drains component Processes per `upgradePolicy`; state + Volumes preserved unless the policy specifies destruction. + - `Role` awaiting deletion: blocked by `core.role-binding-drain` until all dependent + RoleBindings are deleted or re-bound to another Role. + - `RoleBinding` awaiting deletion: atomic RBAC index removal (see §6.6); resource row + removed in same transaction. + - `ZoneLink` awaiting deletion: graceful session close; pending-intent drain; + `core.zone-link-drain` finalizer cleared. + +6. **Cleanup status tracking**: The configuration publication handler tracks + resources pending deletion via the standard watch mechanism: + - While any remain: `generationCleanupPending=true`, + `cleanupPendingCount=`, `GenerationCleanupPending=True` condition with + reason `PendingCleanup`; Zone.status.phase = `Degraded`. + - When all complete: fields reset, condition cleared, Zone.status.phase + reverts to the aggregate mandatory handler phase. + +7. **Stuck-cleanup Degraded**: If any resource awaiting deletion has not + completed finalizer drain within `cleanupStuckThreshold` (default 5 minutes), + `GenerationCleanupFailed=True` is additionally set. Zone.status.phase remains + `Degraded` until the stuck resource clears. The condition message names the + ResourceType only (not the resource name or any spec content). The operator + inspects the resource's finalizer list and owning controller conditions. + +8. **Prior generation retention**: Prior bundle files are retained in the Zone + store bundle directory up to the configured retention count (default 3, range + 1..16). When the count is exceeded, the oldest retained prior bundle beyond + the retention window is de-referenced from the rollback target (resources + with `deletionRequestedAt` set from that generation continue their individual + finalizer drain but lose rollback association). + +9. **Rollback**: An operator with `verb=zone.config-rollback` may restore the + retained prior generation bundle atomically: re-creates deleted resources from + the prior spec, issues async Deletes for superseded-generation additions. The + rollback itself triggers a new cleanup cycle. + +--- + +## 12. Async reconciliation triggers + +### 12.1 Trigger reasons + +These trigger reasons are relevant for Zone control ResourceTypes. All are +from the common closed set in `ADR-046-resource-reconciliation`. + +| Trigger | Zone | ZoneLink | Provider | Role | RoleBinding | +| --- | --- | --- | --- | --- | --- | +| `spec-generation-changed` | ✓ | ✓ | ✓ | ✓ | ✓ | +| `owned-resource-changed` | ✓ | — | ✓ | — | — | +| `dependency-changed` | — | ✓ | ✓ | — | ✓ | +| `dependency-ready` | — | ✓ | ✓ | — | — | +| `deletion-requested` | ✓ | ✓ | ✓ | ✓ | ✓ | +| `finalizer-required` | ✓ | ✓ | ✓ | ✓ | — | +| `controller-generation-changed` | ✓ | ✓ | ✓ | ✓ | ✓ | +| `Provider-generation-changed` | — | — | — | — | — | +| `policy-changed` | ✓ | — | ✓ | — | — | +| `execution-status-changed` | — | — | — | — | — | +| `scheduled-observe` | ✓ | ✓ | ✓ | — | ✓ | +| `retry-due` | — | ✓ | ✓ | ✓ | ✓ | +| `manual-reconcile` | ✓ | ✓ | ✓ | ✓ | ✓ | +| `startup-relist` | ✓ | ✓ | ✓ | ✓ | ✓ | + +`dependency-changed` for RoleBinding is triggered when the referenced Role +changes (subject resolved status changes). `dependency-ready` for Provider +is triggered when any alias dependency Provider transitions to Ready. + +### 12.2 Cross-type triggers + +| Event | Triggers reconcile of | +| --- | --- | +| Zone spec/status change | No automatic cross-type trigger; handlers watch their own types | +| ZoneLink status Connected | Provider lifecycle handler if transport Provider is ZoneLink-dependent | +| Provider reaches Ready | Every RoleBinding that includes this Provider as a subject | +| Provider quarantined | Zone status update; no cascade to Roles/RoleBindings | +| Role spec change | All RoleBindings referencing this Role (`dependency-changed`) | +| RoleBinding subject UID change | RoleBinding itself (`scheduled-observe` detects UID mismatch) | +| Role deletion finalizer drain | RoleBindings referencing it receive `deletion-requested` from core | + +### 12.3 Convergence and suppression + +Core may suppress a reconcile hint for a Zone control resource when: + +- spec generation equals status `observedGeneration`; +- controller generation is current; +- no dependency/provider/policy generation change is pending; +- no deletion/finalizer/owner/retry cause exists; +- conditions do not require re-evaluation. + +Core must NOT suppress: +- any trigger for `policy-changed` affecting Role/RoleBinding; +- any `deletion-requested` or `finalizer-required`; +- any scheduled-observe for RoleBinding subject re-resolution or revocation state refresh; +- any Provider trust expiry check. + +### 12.4 Observe intervals + +| ResourceType | `observeIntervalSeconds` | Rationale | +| --- | --- | --- | +| Zone | 60 | Periodic store health snapshot | +| ZoneLink | 30 | Transport reachability and cursor health | +| Provider | 300 | Trust epoch and conformance re-check | +| Role | null | No external drift; reconcile only on change | +| RoleBinding | 300 | Subject UID drift detection; revocation state refresh | + +These are the declared `observeIntervalSeconds` values for core-controller +handlers. Provider controllers declare their own intervals. + +--- + +## 13. Security, audit, OTEL, and errors + +### 13.1 Authorization attributes for Zone control types + +Every decision on a Zone/ZoneLink/Provider/Role/RoleBinding resource evaluates: + +```text +Zone (from authenticated session) +subject (from AuthenticatedSubjectContext.subjectRef) +ResourceType (Zone | ZoneLink | Provider | Role | RoleBinding) +subresource (spec | status | metadata | finalizers | deletion) +verb (get | list | watch | create | update-spec | update-status | ...) +resource name +executionRef / domain / userRef scope (not applicable to Zone control types) +Provider/controller generation +``` + +Zone control type modifications are restricted: + +| Operation | Permitted subjects | +| --- | --- | +| Create/delete Zone | Bootstrap only (zone-drain is core-initiated) | +| Update Zone spec | Bootstrap; then stored RBAC via core-controller Role | +| Create/delete ZoneLink | Admin subjects per Zone policy | +| Create/delete Provider | Admin subjects per Zone policy | +| Create/delete Role | Admin subjects per Zone policy; core-generated Roles owned by system-core | +| Create/delete RoleBinding | Admin subjects per Zone policy; core-generated bindings owned by system-core | +| Update-status (any) | Owning core-controller handler only | +| Update-finalizers | Core only (no external finalizer owners for Zone control types except Provider controllers per §4.3.3) | + +### 13.2 Audit records + +Audit records for Zone control type operations contain: + +- `subject` (ResourceRef/UID of the authenticated subject) +- `zone` (name) +- `resourceType` (Zone | ZoneLink | Provider | Role | RoleBinding) +- `resourceRef` (`ResourceType/name`) and `resourceUid` of the affected resource +- `verb`/`subresource` +- `expectedRevision`/`currentRevision`/`resultRevision` +- authorization decision (`allowed | denied`) and policy revisions consulted +- `operationId`/`correlationId` +- fixed outcome/error class + +Audit records for these types MUST NOT contain: + +- Role rule payloads (verbs/resourceTypes/resourceNames); +- RoleBinding subject lists; +- Provider package digests or config; +- Zone policy fields; +- ZoneLink transport credentials or cursor values; +- any credential byte, process data, or terminal byte. + +The audit sink is the same fixed OTEL-separate audit path described in the +parent ADR. Zone control type audit records share no sink with runtime +operational audit. + +### 13.3 OTEL telemetry + +Metrics for Zone control types use closed label sets: + +| Metric | Labels | +| --- | --- | +| `d2b.zone.reconcile.duration_seconds` | `handler`, `resource_type`, `outcome` | +| `d2b.zone.authorization.decisions_total` | `resource_type`, `verb`, `decision` | +| `d2b.zone.provider.component_phase` | `component_type`, `phase` | +| `d2b.zone.zoneLink.connected` | (no label; gauge per link reported as aggregate) | + +Never label: subject names, resource names, provider package digests, Zone +names, RoleBinding subject counts, config values, or any credential-adjacent +string. + +Spans for Zone control reconciliation: + +- one span per reconcile attempt: `d2b.zone.reconcile` +- child spans for trust check, config validation, component launch, API binding +- no resource payload in span attributes; only stable codes and durations +- parent trace propagated from d2b-bus into reconcile context + +### 13.4 Errors + +Stable error codes for Zone control types: + +| Error code | Meaning | +| --- | --- | +| `zone-self-resource-mismatch` | Store zone_name/zone_uid differs from Zone resource | +| `zone-spec-invalid` | Zone spec is not exactly `{}` | +| `zone-link-transport-unavailable` | Transport Provider unavailable or unreachable | +| `zone-link-child-auth-denied` | Parent allocator rejected the authenticated child subject or requested allocation ceiling | +| `zone-link-child-uid-changed` | Child Zone UID changed; cursor reset required | +| `provider-package-digest-mismatch` | Package content differs from declared digest | +| `provider-trust-revoked` | Trust epoch revoked or signature invalid | +| `provider-conformance-failed` | API conformance attestation check failed | +| `provider-config-invalid` | Config failed schema validation | +| `provider-api-name-collision` | Exported ResourceType name already bound by another Provider | +| `provider-dependency-cycle` | Provider dependency alias creates a cycle | +| `role-wildcard-denied` | Explicit wildcard attempted by non-core-controller Role | +| `role-unknown-verb` | Rule contains an unknown verb token | +| `role-relay-grant-restricted` | Relay is unbounded, self-asserted, lacks ZoneLink ownership/exact adjacent-Zone enrollment selector, or lacks explicit admin-policy provenance | +| `role-unknown-resource-type` | Rule names a ResourceType not in Zone API catalog | +| `rolebinding-role-not-found` | `roleRef` does not resolve to an existing Role | +| `rolebinding-subject-type-invalid` | Subject ResourceType is not a permitted subject type | +| `rolebinding-scope-exceeds-role` | scopeNarrowing attempts to grant more than Role allows | +| `bootstrap-subject-denied` | Non-bootstrap subject attempted a bootstrap-only verb | + +All error messages are bounded (max 512 bytes), UTF-8 validated, +control-character sanitized, and must not contain secrets, credentials, paths, +process data, or any other sensitive content. + +### 13.5 Authentication boundary + +Zone control type mutations require: + +- authenticated ComponentSession with verified `AuthenticatedSubjectContext`; +- the subject must be current (its resource UID must match the last seen UID + for that subject ref in the authorization index); +- subject UID changes invalidate existing sessions — any session whose subject + resource UID has changed is denied on next method call and must re-establish; +- bootstrap Providers (system-core, system-minijail) are authenticated by + the compiled bootstrap policy through their fixed process identity, not by + an external token. + +### 13.6 Emergency disable + +An emergency disable is declared by creating or updating an `EmergencyPolicy` +resource (§8) with `spec.enabled=true`. See §8 for the full EmergencyPolicy +spec and controller algorithm. When active, it applies to Zone control types as +follows: + +- Zone emergency disable stops all new resource API admissions; ongoing bounded + operations drain to their effective deadline (minimum `drainDeadlineSeconds` across + all enabled policies); +- active ZoneLinks receive a graceful disconnect signal; +- Provider component Processes are stopped without deleting their Process resources; + reconciliation resumes when all effective `stopProviderProcesses` policies are deactivated; +- Role/RoleBinding evaluation continues for ongoing admitted operations until + deadline; +- Zone status reflects the admission stop via a fixed condition; it is not a Zone + spec change and does not directly set Zone `phase`. + +--- + +## 14. Nix authoring and resource bundle + +This section is normative for the d2b 3.0 Nix configuration surface, including Quota and EmergencyPolicy resources. + +**Unified authoring syntax**: All Zone control ResourceTypes use a single +uniform structure. `metadata.name` is derived from the resource attrset key, +`metadata.zone` from the Zone attrset key, and `apiVersion` defaults to +`"resources.d2bus.org/v3"`. `status` is omitted — it is read-only; the Zone runtime fills +all status fields. Core fills `uid`, `generation`, `revision`, `createdAt`, +`updatedAt`, `ownerRef` (where applicable), `finalizers`, and +`deletionRequestedAt`: + +```nix +d2b.zones..resources. = { + type = ""; # "Zone"|"ZoneLink"|"Provider"|"Role"|"RoleBinding"|"Quota"|"EmergencyPolicy"|"Credential"|... + spec = { + # Exact ResourceType spec fields — mirrors ResourceSpec.spec for this type. + # No renaming or re-nesting of field names. + }; +}; +``` + +**Artifact catalog**: Derivation-valued inputs (Provider packages, NixOS +system closures, and similar build outputs) are configured exclusively in the +separate artifact catalog: + +```nix +d2b.artifacts. = { + package = ; # Nix derivation; required + type = "provider" # "provider" | "nixos-system" | ... + | "nixos-system" | ...; # closed enum; further types added via ADR +}; +``` + +`` is a bounded label (`^[a-z][a-z0-9-]*$`, max 128 chars). The resource +compiler builds/includes/hashes each derivation, validates the catalog for +type consistency and ID uniqueness, checks trust/conformance, and emits a +**private integrity-pinned artifact catalog** mapping each ID to its type, +content digest, and closure metadata. Store paths are private catalog +implementation data and are **never** exposed in resource spec fields, status +fields, audit records, or OTEL attributes. + +ResourceSpecs remain **pure direct schema mirrors**: they reference artifacts +by plain bounded IDs. Provider spec uses `artifactId` (string); Guest system +spec uses `systemArtifactId` (string). These are not `*Ref` fields because +`Artifact` is not a ResourceType. A missing or wrong-type artifact ID is a +NixOS build failure. + +**Schema-driven Nix options**: For each registered ResourceType, the Nix module +generator reads the corresponding ResourceTypeSchema JSON and emits Nix option +declarations (types, defaults, documentation strings) for every field in `spec`. +There is no second hand-authored Nix option vocabulary for resource spec fields. + +**Build validation**: The resource compiler serializes each Nix `spec` attrset +to canonical JSON and validates it against the ResourceTypeSchema for the given +`type`. Provider `spec.config` is additionally validated against the exact signed +Provider's config schema (located via the artifact catalog's `configSchemaDigest` +for the named `artifactId`). The validated canonical JSON is the input to +per-resource digest computation and the Zone resource bundle. + +See §14.8 for the canonical envelope format, §14.9 for the bundle format, and +§14.10 for the full three-phase validation pipeline. + +### 14.1 Zone declaration + +```nix +d2b.zones.local-root = {}; # parentZone is forbidden on this Zone + +d2b.zones.dev = { + parentZone = "local-root"; + resources = {}; +}; +``` + +`parentZone` is a compiler-only plain Zone name, not a `ResourceRef`. It is +required on every non-root Zone, forbidden on `local-root`, and never emitted +into a ResourceEnvelope or `Zone.spec`, which remains exactly `{}`. The +compiler resolves it to one declared parent and writes that edge only into the +sealed private allocator-bootstrap topology. It rejects a missing or unknown +parent, self-parenting, conflicting scalar definitions, cycles, and ancestry +paths longer than 16 Zone names. + +The Zone self-resource name derives from the Zone attrset key. It is created by +the Zone runtime on first initialization with `managedBy=controller` and is +never included in the resource bundle. Authors do not declare a `type = "Zone"` +resource under `resources`. + +### 14.2 ZoneLink declaration + +```nix +d2b.zones.guest.parentZone = "local-root"; + +d2b.artifacts.transport-unix = { + package = pkgs.d2b-provider-transport-unix; + type = "provider"; +}; + +d2b.zones.guest.resources.transport-unix = { + type = "Provider"; + spec = { artifactId = "transport-unix"; config = {}; }; +}; + +d2b.zones.guest.resources.guest-uplink = { + type = "ZoneLink"; + spec = { + childZoneName = "guest"; + transportProviderRef = "Provider/transport-unix"; + transportSettings = {}; + transportCredentials = []; + disabled = false; + }; +}; +``` + + +`metadata.name` (`guest-uplink`) and `metadata.zone` (`guest`) derive from the +attrset keys. Eval-time checks: `spec.childZoneName` matches +`^[a-z][a-z0-9-]*$` and equals the enclosing Zone key; the local root has no +uplink; at most one ZoneLink resource (enabled or disabled) exists per non-root +Zone; `spec.limits.*` remain within bounds. `parentZone` selects the allocator +owner and is compiled into sealed bootstrap topology; the child-local ZoneLink +supplies transport and local route/session state for that selected edge. + +### 14.3 Provider installation + +```nix +# Step 1: declare the derivation in the artifact catalog (global, not zone-local). +# Artifact IDs are zone-scoped at use, but the catalog is a top-level Nix attrset. +d2b.artifacts.runtime-cloud-hypervisor = { + package = pkgs.d2b-provider-runtime-cloud-hypervisor; + type = "provider"; +}; + +# Step 2: install the Provider in a Zone; spec.artifactId references the catalog +# by ID. All PackageIdentity sub-fields (digest, signatureId, trustEpoch, +# manifestDigest, conformanceAttestationDigest, configSchemaDigest) are resolved +# from the artifact catalog at build time — they do not appear in the spec. +d2b.zones.dev.resources.runtime-cloud-hypervisor = { + type = "Provider"; + spec = { + artifactId = "runtime-cloud-hypervisor"; # plain bounded ID; NOT a ResourceRef + + # spec.config: operator-authored; validated at build time against the config + # schema identified by the artifact catalog's configSchemaDigest for this ID. + config = { + defaultCpuCount = 2; + defaultMemoryMib = 1024; + # Secrets via Credential ref: + # apiToken = d2b.zones.dev.credentialRef "api-token"; + }; + + # spec.exports, spec.components, spec.dependencies, spec.permissionClaims, + # spec.upgradePolicy, spec.restartPolicy: populated from the signed manifest + # embedded in the artifact at build time. These are NOT writable Nix options; + # the Nix module generator does not emit settable options for them. + # Attempting to set them is an eval assertion error. + }; +}; +``` + +Bootstrap Providers (`system-core`, `system-minijail`) are auto-generated by +the resource compiler from the Zone runtime's artifact catalog entries (type +`"provider"`, IDs `"system-core"` and `"system-minijail"`). Authoring +`d2b.zones..resources.system-core` or `system-minijail` with +`type = "Provider"` is an eval assertion error: +`"system-core and system-minijail are bootstrap-only providers and cannot be +hand-authored"`. + +### 14.4 Role authoring + +```nix +d2b.zones.dev.resources.process-controller = { + type = "Role"; + spec = { + rules = [ + { + resourceTypes = [ "EphemeralProcess" "Process" ]; + verbs = [ "create" "delete" "get" "list" "update-finalizers" + "update-spec" "update-status" "watch" ]; + subresources = []; + resourceNames = []; # empty = all names + zones = []; # empty = this Zone only + executionRefs = []; # empty = unrestricted + sessionVerbs = [ "connect" "invoke" "open-stream" ]; + } + ]; + }; +}; + +d2b.zones.dev.resources.zone-reader = { + type = "Role"; + spec = { + rules = [ + { + resourceTypes = [ "Guest" "Host" "Process" "Provider" "Zone" ]; + verbs = [ "get" "list" "watch" ]; + subresources = []; + resourceNames = []; + zones = []; + executionRefs = []; + sessionVerbs = []; + } + ]; + }; +}; +``` + +Eval-time validations: `spec.rules[*].verbs` against the closed verb enum; +`spec.rules[*].sessionVerbs` against the closed session verb enum; +`spec.rules[*].executionRefs` format if non-empty. `spec.rules[*].resourceTypes` +validation against the Zone API catalog is deferred to Phase 2 (catalog requires +loading Provider manifests; core types are known at eval time). +Generated option help lists all nine session verbs, describes `relay` as +ZoneLink-scoped forwarding only, and binds `audit-export` and `support-bundle` +to their exact admin-only service/method selectors without granting resource +authority. Nix recognizes all three as session-verb tokens, but Phase 2 +admission still rejects an unbounded, wildcard, Provider-asserted, or ordinary +operator-authored relay grant unless explicit admin policy permits that exact +bounded grant. + +### 14.5 RoleBinding authoring + +```nix +d2b.zones.dev.resources.process-controller-binding = { + type = "RoleBinding"; + spec = { + roleRef = "Role/process-controller"; + subjects = [ "Provider/system-minijail" ]; + externalPrincipalSelector = null; + scopeNarrowing = null; + }; +}; + +d2b.zones.dev.resources.zone-reader-alice = { + type = "RoleBinding"; + spec = { + roleRef = "Role/zone-reader"; + subjects = [ "User/alice" ]; + externalPrincipalSelector = null; + scopeNarrowing = null; + }; +}; + +d2b.zones.dev.resources.zone-reader-bob = { + type = "RoleBinding"; + spec = { + roleRef = "Role/zone-reader"; + subjects = [ "User/bob" ]; + externalPrincipalSelector = null; + scopeNarrowing = null; + }; +}; +``` + +Eval-time validations: `spec.roleRef` format; each `spec.subjects[*]` format +and ResourceType; no duplicate subjects. + +### 14.6 Bootstrap provider records + +`Provider/system-core` and `Provider/system-minijail` are generated +automatically by the Zone runtime on first initialization as `managedBy=controller` +resources. They are not included in the resource bundle and are never subject to +generation cleanup deletion. + +Attempting to author `d2b.zones..resources.system-core` or +`d2b.zones..resources.system-minijail` with `type = "Provider"` emits the +eval assertion: +`"system-core and system-minijail are bootstrap-only providers and cannot be +hand-authored"`. + +--- + +### 14.7 Credential references and secret handling + +Secrets must never appear as inline string values in the resource bundle. The +Nix authoring surface uses Credential resources to hold secret references; any +Provider `config` field that requires a secret is expressed using the canonical +Credential ref object: + +```json +{ "$credentialRef": "Credential/" } +``` + +Nix authoring syntax: + +```nix +# Declare a Credential resource using the unified syntax: +d2b.zones.dev.resources.api-token = { + type = "Credential"; + spec = { + source = "systemd-credential"; # "systemd-credential"|"host-secret"|"derivation-secret" + credentialName = "d2b-api-token"; + }; +}; + +# Reference it in a Provider spec.config: +d2b.zones.dev.resources.some-provider = { + type = "Provider"; + spec = { + artifactId = "some-provider"; + config = { + apiToken = d2b.zones.dev.credentialRef "api-token"; + # ↑ compiles to: { "$credentialRef": "Credential/api-token" } + }; + }; +}; +``` + +Normative rules: + +- `$credentialRef` is the only permitted `$`-prefixed key in config JSON; any + other `$`-prefixed key is rejected at build time. +- The referenced Credential must be declared in `d2b.zones..resources.*` + with `type = "Credential"` within the same Zone; cross-Zone Credential refs + are rejected at eval time. +- Credential resources are operator-authored and appear in the resource bundle; + the Zone runtime creates them as `managedBy=configuration` resources subject to + the generation cleanup contract (§14.11). +- The Zone runtime resolves Credential refs at activation time by reading the + declared secret source. Resolved secret values are never written to the redb + store, emitted in logs, audit records, or status fields. +- The resource compiler applies a heuristic inline-secret lint (PEM headers, + base64 payload > 32 bytes, `sk-*`/`ecdsa-*` SSH key prefixes, UUIDs ≥ 128 bits + as raw hex) and rejects the build when `--strict-secrets` is set or emits a + warning otherwise. This lint is heuristic and does not replace the requirement + to use Credential refs for all actual secrets. +- When a Credential is deleted by generation cleanup, the Zone runtime revokes + the resolved secret binding before clearing the finalizer. + +### 14.8 Canonical ResourceSpec JSON envelope format + +All resources emitted by the Nix resource compiler follow this exact envelope +structure (normative). + +**Nix-to-envelope mapping**: `type` in the Nix record maps to `resourceType` +in the envelope; `spec` maps to `spec`. `metadata.name` derives from the attrset +key; `metadata.zone` derives from the Zone attrset key; `apiVersion` defaults to +`"resources.d2bus.org/v3"`. `metadata.generation` is initialized to 1 for new resources and +incremented by the compiler whenever `spec` changes. All other metadata fields +and `status` are filled by the Zone runtime (null in the bundle). + +``` +{ + "apiVersion": "resources.d2bus.org/v3", // fixed; not per-type versioned + "resourceType": "", // PascalCase registered type name + "metadata": { + "name": , // ResourceName; ^[a-z][a-z0-9-]*$; ≤128 chars + "zone": , // equals the Zone name this bundle is for + "uid": null, // null in bundle; set by runtime at first create + "generation": , // 1 for new resources; compiler-incremented on spec change + "revision": null, // null in bundle; set by runtime on each write + "labels": {}, // string→string; empty by default + "annotations": {}, // string→string; empty by default + "ownerRef": , // "ResourceType/name" or null + "finalizers": [], // pre-set core finalizer strings (see per-type rules) + "deletionRequestedAt": null, // null in bundle; set by runtime on deletion + "createdAt": null, // null in bundle; set by runtime on first create + "updatedAt": null // null in bundle; set by runtime on each write + }, + "spec": { ... }, // type-specific; fully determined by Nix; see per-type examples + "status": null // always null in bundle; written by runtime only +} +``` + +Invariants: + +- `metadata.zone` must equal the Zone name of the enclosing bundle. +- Runtime-assigned fields (`uid`, `revision`, `createdAt`, `updatedAt`) are `null` + in the bundle; the runtime fills them on first activation and preserves stored + values on subsequent generations. +- `metadata.generation` in the bundle is the spec-generation counter maintained + by the resource compiler: starts at 1, incremented each time the operator + changes the spec for that resource. The runtime preserves and independently + increments this counter after activation. +- `metadata.finalizers` pre-populated in the bundle instruct the runtime to add + those finalizers when the resource is first created. Values must be from the + closed per-type core finalizer set. +- `spec` is serialized to canonical JSON (RFC 8785 key ordering: UTF-16 code-unit + sort; no insignificant whitespace) by the resource compiler. +- Per-resource `digest` = `sha256()` in lowercase hex + prefixed `sha256:`. + +#### Zone canonical envelope + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "resourceType": "Zone", + "metadata": { + "name": "dev", "zone": "dev", "uid": null, "generation": 1, + "revision": null, "labels": {}, "annotations": {}, + "ownerRef": null, "finalizers": [], + "deletionRequestedAt": null, "createdAt": null, "updatedAt": null + }, + "spec": {}, + "status": null +} +``` + +Zone is the self-resource; it is created by the Zone runtime on initialization +(`managedBy=controller`) and is not included in the resource bundle. + +#### ZoneLink canonical envelope + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "resourceType": "ZoneLink", + "metadata": { + "name": "guest-uplink", "zone": "guest", "uid": null, "generation": 1, + "revision": null, "labels": {}, "annotations": {}, + "ownerRef": null, "finalizers": [], + "deletionRequestedAt": null, "createdAt": null, "updatedAt": null + }, + "spec": { + "childZoneName": "guest", + "transportProviderRef": "Provider/transport-unix", + "transportSettings": {}, + "transportCredentials": [], + "disabled": false, + "limits": { + "maxPendingIntents": 256, + "maxActiveStreams": 32, + "reconnectMaxAttempts": 10, + "reconnectWindowSecs": 300 + } + }, + "status": null +} +``` + +`childZoneUid` is `null` in the bundle; the child-local ZoneLink controller +writes the local Zone UID after the parent allocator acknowledges it and uses +it to detect replacement across reconnects. + +#### Provider canonical envelope + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "resourceType": "Provider", + "metadata": { + "name": "runtime-cloud-hypervisor", "zone": "dev", "uid": null, "generation": 1, + "revision": null, "labels": {}, "annotations": {}, + "ownerRef": null, "finalizers": ["core.provider-drain"], + "deletionRequestedAt": null, "createdAt": null, "updatedAt": null + }, + "spec": { + "artifactId": "runtime-cloud-hypervisor", + "config": { "defaultCpuCount": 2, "defaultMemoryMib": 1024 }, + "exports": [ + { "resourceType": "Guest", "schemaDigest": "sha256:445566..." } + ], + "components": [ + { + "name": "primary", "role": "controller", "placement": "HostLocal", + "processTemplate": { + "executableRef": "d2b-provider-runtime-cloud-hypervisor", + "args": ["--zone-bus-fd", "3"], + "environmentPolicy": "isolated" + }, + "required": true + } + ], + "dependencies": [], + "permissionClaims": [ + { "resourceType": "Guest", + "verbs": ["create","update-spec","update-status","delete"], + "level": "standard" } + ], + "upgradePolicy": "drain-then-replace", + "restartPolicy": "on-failure" + }, + "status": null +} +``` + +`exports`, `components`, `dependencies`, `permissionClaims`, `upgradePolicy`, and +`restartPolicy` are loaded from the signed manifest embedded in the artifact +identified by `artifactId`. The operator supplies only `artifactId` (a plain +bounded ID referencing the `d2b.artifacts.*` catalog) and `config`. The resource +compiler resolves all manifest-derived fields and PackageIdentity sub-fields from +the artifact catalog at build time. Store paths and raw closure metadata are +**private catalog implementation data** and do not appear in the spec, status, +audit records, or OTEL attributes. + +#### Role canonical envelope + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "resourceType": "Role", + "metadata": { + "name": "process-controller", "zone": "dev", "uid": null, "generation": 1, + "revision": null, "labels": {}, "annotations": {}, + "ownerRef": "Provider/system-minijail", + "finalizers": ["core.role-binding-drain"], + "deletionRequestedAt": null, "createdAt": null, "updatedAt": null + }, + "spec": { + "rules": [ + { + "resourceTypes": ["EphemeralProcess", "Process"], + "verbs": ["create","delete","get","list","update-finalizers", + "update-spec","update-status","watch"], + "subresources": [], + "resourceNames": [], + "zones": [], + "executionRefs": [], + "sessionVerbs": ["connect","invoke","open-stream"] + } + ] + }, + "status": null +} +``` + +Note: `verbs` and `resourceTypes` arrays in the canonical envelope are sorted +ascending by the resource compiler (RFC 8785 array-of-string sort). + +#### RoleBinding canonical envelope + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "resourceType": "RoleBinding", + "metadata": { + "name": "process-controller-binding", "zone": "dev", "uid": null, "generation": 1, + "revision": null, "labels": {}, "annotations": {}, + "ownerRef": null, "finalizers": [], + "deletionRequestedAt": null, "createdAt": null, "updatedAt": null + }, + "spec": { + "externalPrincipalSelector": null, + "roleRef": "Role/process-controller", + "scopeNarrowing": null, + "subjects": ["Provider/system-minijail"] + }, + "status": null +} +``` + + +### 14.9 Zone resource bundle: generation, canonical sort, and integrity pinning + +The Nix resource compiler emits one `zone-resources.json` per configured Zone +per build. This is the Zone resource bundle. + +```json +{ + "bundleVersion": "1", + "zoneUid": null, + "zoneName": "dev", + "generation": 7, + "generatedAt": "2026-07-22T21:25:43Z", + "nixRevision": "abc123def456", + "resources": [ + { + "resourceType": "Credential", "name": "api-token", + "digest": "sha256:...", + "envelope": { "..." } + }, + { + "resourceType": "Provider", "name": "runtime-cloud-hypervisor", + "digest": "sha256:...", + "envelope": { "..." } + }, + { + "resourceType": "Role", "name": "process-controller", + "digest": "sha256:...", + "envelope": { "..." } + }, + { + "resourceType": "RoleBinding", "name": "process-controller-binding", + "digest": "sha256:...", + "envelope": { "..." } + } + ], + "bundleDigest": "sha256:..." +} +``` + +| Bundle field | Type | Rules | +| --- | --- | --- | +| `bundleVersion` | string | Fixed `"1"`; the resource compiler's bundle schema version | +| `zoneUid` | string\|null | `null` at build time; the runtime validates against `store_meta.zone_uid` at activation; first activation stores the newly generated UID back into the bundle record in the store | +| `zoneName` | string | Must match the Zone self-resource `metadata.name` | +| `generation` | u64 | Monotonically increasing; compiler-incremented on every config change; starts at 1; rejected at runtime if not strictly greater than `store_meta.active_configuration_revision` | +| `generatedAt` | RFC 3339 UTC | Build-time timestamp from `builtins.currentTime` (impure); not used for security decisions | +| `nixRevision` | string | Git rev of the NixOS config, if available; opaque; traceability only | +| `resources` | array | Sorted ascending by `(resourceType, name)` lexicographically; contains only operator-authored resources (`managedBy=configuration`); bootstrap resources (`Provider/system-core`, `Provider/system-minijail`, `Zone/`) are runtime-created and never present in the bundle | +| `digest` | sha256 hex | `sha256()`; lowercase hex prefixed `sha256:` | +| `bundleDigest` | sha256 hex | `sha256()` over the sorted array; computed by the compiler; verified by the runtime before applying any resource | + +**Canonical sort order**: `(resourceType, name)` lexicographic ascending. Examples: +`Credential/api-token` < `Provider/runtime-cloud-hypervisor` +< `Role/process-controller` < `RoleBinding/process-controller-binding`. + +**Integrity pinning**: The runtime verifies `bundleDigest` against the `resources` +array before applying any resource from the bundle. A digest mismatch rejects the +entire bundle; the prior generation remains active. + +**Bundle file location**: Installed at +`/var/lib/d2b/zones//bundles/generation-.json` (path managed by the +Zone runtime's state directory). The `store_meta.active_configuration_revision` +pointer names the active generation. Retained prior generation bundles use the +same path scheme. + +### 14.10 Nix eval and build-time validation pipeline + +Three ordered phases validate the Nix configuration before a bundle is activated. + +#### Phase 1 — NixOS eval (pure; runs on `nixos-rebuild` evaluation and `nix flake check`) + +| Check | Mechanism | Failure mode | +| --- | --- | --- | +| Resource attrset key (→ `metadata.name`) matches `^[a-z][a-z0-9-]*$` | Nix `assert` | eval error | +| Name length ≤ 128 chars | Nix `assert` | eval error | +| `type` is a recognized core ResourceType or a non-empty string (non-core types validated at Phase 2) | Nix `assert` | eval error if empty or non-string | +| `spec.rules[*].verbs` tokens in closed verb enum (§5.3.2) — Role only | Nix `assert` | eval error | +| `spec.rules[*].sessionVerbs` tokens in closed session verb enum — Role only | Nix `assert` | eval error | +| `spec.roleRef` format `^Role/[a-z][a-z0-9-]*$` — RoleBinding only | Nix `assert` | eval error | +| `spec.subjects[*]` format `^[A-Za-z][A-Za-z0-9]*/[a-z][a-z0-9-]*$` — RoleBinding only | Nix `assert` | eval error | +| `spec.subjects[*]` ResourceType in permitted subject set (§6.3.2) — RoleBinding only | Nix `assert` | eval error | +| No duplicate entries in `spec.subjects` — RoleBinding only | Nix `assert` | eval error | +| No cross-Zone refs anywhere in `spec` (no `/ResourceType/name` form) | Nix `assert` | eval error | +| `type = "Provider"` and name `system-core` or `system-minijail` rejected | Nix `assert` | eval error with message | +| Any authored `type = "Zone"` entry under `resources` | Nix `assert`; Zone self-resource is runtime-created | eval error | +| `parentZone` omitted on a non-root Zone or set on `local-root` | Nix `assert` over Zone options | eval error | +| `parentZone` does not resolve to a declared Zone or equals the child | Nix `assert` over Zone options | eval error | +| `parentZone` graph cycles or exceeds 16 Zone names on an ancestry path | Nix graph walk using retained `MAX_REALM_LABELS` bound | eval error | +| conflicting `parentZone` scalar definitions | Standard Nix module merge | eval error | +| ZoneLink: `spec.childZoneName ==` Zone attrset key | Nix `assert` | eval error | +| Non-root Zone has at most one ZoneLink resource; local root has none | Nix `assert` | eval error | +| `spec.transportProviderRef` matches `^Provider/transport-[a-z][a-z0-9-]*$` — ZoneLink only | Nix `assert` | eval error | +| `$credentialRef` target in any `spec.*` string must name a `type = "Credential"` resource in the same Zone | Nix `assert` | eval error | +| No `$`-prefixed keys in `spec` other than `$credentialRef` | Nix `assert` | eval error | +| `spec.limits.maxPendingIntents ≤ 1024` — ZoneLink only | Nix `assert` | eval error | +| `spec.limits.maxActiveStreams ≤ 128` — ZoneLink only | Nix `assert` | eval error | +| `spec.artifactId` format `^[a-z][a-z0-9-]*$`, max 128 chars — Provider only | Nix `assert` | eval error | +| `type = "Provider"` without a `spec.artifactId` field is rejected | Nix `assert` | eval error | +| `spec.artifactId` value must name an entry in `d2b.artifacts.*` (attrset key lookup at eval time) with `type = "provider"` — Provider only | Nix `assert` | eval error if ID absent from catalog | +| Manifest-derived Provider spec fields (`spec.exports`, `spec.components`, `spec.dependencies`, `spec.permissionClaims`, `spec.upgradePolicy`, `spec.restartPolicy`) not set by operator | Nix `assert` | eval error with message | + +#### Phase 2 — Nix build (impure; runs on `nixos-rebuild build` and `nix build`) + +| Check | Mechanism | Failure mode | +| --- | --- | --- | +| Each resource's `spec` validated against the ResourceTypeSchema for its `type`; canonical JSON compared against schema (`genSchemaOptions` emits Phase 1 Nix options; Phase 2 performs the definitive comparison against the committed schema JSON) | Resource compiler | build failure naming type and failing field | +| `spec.artifactId` names an existing `d2b.artifacts.` catalog entry with `type = "provider"` — Provider only | Resource compiler | build failure: "artifact ID not found or wrong type" | +| Artifact catalog entry has required derivation outputs (manifest, config schema, executable) — Provider only | Resource compiler | build failure | +| Artifact catalog `configSchemaDigest` matches SHA-256 of schema file in derivation output — Provider only | Resource compiler | build failure | +| Operator `spec.config` passes JSON Schema validation against catalog-resolved config schema — Provider only | Resource compiler | build failure naming failing field and schema path | +| Artifact catalog `manifestDigest` matches SHA-256 of manifest file in derivation output — Provider only | Resource compiler | build failure | +| Artifact manifest signature chain valid against installed trust store — Provider only | Resource compiler | build failure | +| Artifact `conformanceAttestationDigest` present in known attestation store — Provider only | Resource compiler | build failure | +| No duplicate `d2b.artifacts.` entries (IDs are unique across the catalog) | Resource compiler | build failure naming duplicate ID | +| All declared dependency aliases resolve within the same Zone's Providers — Provider only | Resource compiler | build failure naming unresolved alias | +| ResourceType short-name collision among installed Providers in the same Zone | Resource compiler | build failure naming conflicting Providers | +| `spec.rules[*].resourceTypes` resolved against installed Provider catalogs in the Zone bundle — Role only | Resource compiler | build failure | +| `spec.roleRef` names an existing `type = "Role"` resource in the same Zone bundle — RoleBinding only | Resource compiler | build failure | +| `spec.subjects[*]` names resolve to declared resources or known external principal types in the same Zone bundle — RoleBinding only | Resource compiler | build failure | +| Inline-secret heuristic lint on config string values | Resource compiler | build warning; build failure with `--strict-secrets` | +| `resources` array sorted by `(resourceType, name)` | Resource compiler | auto-sorted; not an operator error | +| Per-resource `digest` = `sha256(canonical-JSON-of-spec)` | Resource compiler | computed and written; mismatch is a compiler bug | +| `bundleDigest` over sorted `resources` array | Resource compiler | computed and written | + +#### Phase 3 — Runtime activation (on daemon restart or `d2b zone config apply`) + +| Check | Mechanism | Failure mode | +| --- | --- | --- | +| `bundleDigest` integrity re-verified against `resources` array | Zone runtime | bundle rejected; prior generation stays active | +| `zoneUid` consistency: null = first activation; non-null must match `store_meta.zone_uid` | Zone runtime | bundle rejected | +| `zoneName` must match Zone self-resource `metadata.name` | Zone runtime | bundle rejected | +| `generation` strictly greater than `store_meta.active_configuration_revision` | Zone runtime | bundle rejected (prevents replay/downgrade) | +| All resource `digest` values re-verified against `envelope.spec` | Zone runtime | bundle rejected | +| Provider `config` re-validated against the config schema identified by the artifact catalog entry for `spec.artifactId` | Zone runtime | affected Provider set to `Failed` with `ConfigValid=False` condition; generation proceeds for other resources | +| `zoneUid=null` only on store's first activation (no prior Zone UID stored) | Zone runtime | bundle rejected if non-null UID expected | + +If Phase 3 rejects the bundle, no resources are applied atomically and the prior +generation remains active. A per-Provider `config` failure does not block other +resources from activating; the affected Provider is set to `Failed` with `ConfigValid=False` condition. + +### 14.11 Configuration generation lifecycle and cleanup contract + +This section is the normative specification for resource lifecycle across Nix +configuration generation changes. + +#### Configuration ownership + +The Zone runtime stores two internal resource metadata fields on every resource +in the redb store (not exposed in the public resource API envelope): +- `managedBy`: one of `configuration | controller | api` — set by the runtime at create time +- `configurationGeneration`: the bundle generation when this resource was last confirmed present in the bundle + +Every resource present in the bundle is created (or updated) by the runtime as +`managedBy=configuration`. The Zone runtime also creates bootstrap resources +(`Provider/system-core`, `Provider/system-minijail`, and the Zone self-resource) +directly on initialization as `managedBy=controller`; these are not included in +the bundle. The cleanup authority is the stored `managedBy` and +`configurationGeneration` values on the resource, **never** inferred from bundle +absence, `ownerRef`, labels, or any other field. + +| Class | `managedBy` | In bundle | Cleanup eligible | +| --- | --- | --- | --- | +| Config-owned | `configuration` | Yes | Yes — absent from new bundle (`configurationGeneration < new generation`) → async Delete | +| Bootstrap-created | `controller` | No | No — `managedBy=controller` resources untouched by generation cleanup | +| Controller-created | `controller` | No | No — `managedBy=controller` resources untouched by generation cleanup | +| API-created | `api` | No | No — `managedBy=api` resources untouched by generation cleanup | + +The resource compiler must reject any operator attempt to name a controller-created +resource in config (e.g., placing `Process/provider-primary-0` under `d2b.zones.*`). +This is enforced by a build-time exclusion list of reserved name prefixes per +controller-managed ResourceType. + +#### Generation activation sequence (non-blocking) + +1. Resource compiler emits bundle with `generation=N+1`. +2. Zone runtime reads and integrity-verifies the bundle (Phase 3 checks, §14.10). +3. `store_meta.active_configuration_revision` advances to `N+1` atomically. +4. The Zone begins serving all requests under generation `N+1` immediately. + No cleanup gate; no operator wait. +5. `Zone.status.activeConfigurationGeneration = N+1`. +6. `Zone.status.conditions[ConfigurationCurrent].status = "True"`. +7. Absent config-owned resources are identified and queued for async Delete + (see below). Activation does not wait for them. + +#### Absent resource deletion (normative) + +After generation activation, for each resource in the store with +`managedBy=configuration` whose `configurationGeneration` does not match the +new bundle generation (i.e., absent from the new bundle): + +1. `metadata.deletionRequestedAt` is set to the activation timestamp (if null). +2. The resource-type-specific core finalizer is added if absent: + - `Provider`: `core.provider-drain` + - `Role`: `core.role-binding-drain` + - `ZoneLink`: `core.zone-link-drain` + - `Credential`: `core.credential-revoke` + - `RoleBinding`: no pre-set finalizer (immediate atomic deletion) +3. An async reconcile trigger is emitted to the owning controller handler. +4. `Zone.status.cleanupPendingCount` is incremented per candidate. +5. `Zone.status.generationCleanupPending = true`. +6. `Zone.status.conditions[GenerationCleanupPending]` is set with reason + `PendingCleanup` and message `" config-owned resources from generation + completing deletion"`. +7. Zone.status.phase transitions to `Degraded` (remains until cleanup completes). + +The Zone runtime never force-removes finalizers. If an owning controller fails +to clear its finalizer, `GenerationCleanupFailed` is eventually set and Zone +remains `Degraded`. Resources with `managedBy=controller` or `managedBy=api` +are never touched by the generation cleanup path. + +#### Controller-created child preservation (normative) + +Controllers reconcile their dynamic children in response to parent resource +deletion requests (`deletionRequestedAt` set) and phase changes, not in response +to generation changes directly: + +- A `Provider` with `deletionRequestedAt` set triggers the provider lifecycle + handler to stop owned component Processes. State Volumes are preserved unless + `upgradePolicy=immediate`. +- A `Role` with `deletionRequestedAt` set is blocked by `core.role-binding-drain` + until all dependent RoleBindings are deleted or re-bound to another Role. + The authorization handler triggers dependent RoleBinding deletion automatically. +- `RoleBinding` deletion is the atomic final transaction (§6.6): RBAC index + removal + row removal + `Deleted` event in one redb write. +- `ZoneLink` deletion closes the session gracefully, drains pending intents, then + clears `core.zone-link-drain`. +- Controller-created resources with `managedBy=controller` (e.g., `Process/provider-primary-0`, + `Volume/provider-state`) are **never** automatically deleted on generation change, + even if absent from the new Nix config. They are managed exclusively by their + owning controller in response to parent resource phase transitions. + +#### Cleanup status, conditions, and audit (normative) + +**While cleanup is pending**: + +``` +Zone.status.generationCleanupPending = true +Zone.status.cleanupPendingCount = +Zone.status.conditions: + GenerationCleanupPending: + status: "True" + reason: "PendingCleanup" + message: " config-owned resources from generation completing deletion" + lastTransitionTime: +Zone.status.phase: "Degraded" (transitions to Degraded while any cleanup is pending) +``` + +**After all cleanup candidates complete**: + +``` +Zone.status.generationCleanupPending = false +Zone.status.cleanupPendingCount = 0 +Zone.status.conditions[GenerationCleanupPending].status: "False" +Zone.status.conditions[GenerationCleanupFailed]: absent or status="False" +Zone.status.phase: reverts to aggregate mandatory handler phase (typically "Ready") +``` + +**On stuck cleanup** (resource awaiting deletion with no controller progress for +`cleanupStuckThreshold`, default 5 minutes): + +``` +Zone.status.phase: "Degraded" (already set from pending cleanup; remains Degraded) +Zone.status.conditions: + GenerationCleanupFailed: + status: "True" + reason: "CleanupStuck" + message: " resource has been awaiting deletion beyond threshold" + lastTransitionTime: +``` + +The message names the ResourceType only; it does not include the resource name, +spec content, or any configuration value. + +**Audit records** (all carry `zone_name` and `generation`; no resource names, +spec content, or secret values): + +| Audit kind | Emitted when | +| --- | --- | +| `zone.config.generation.activate` | New bundle generation becomes active | +| `zone.config.cleanup.complete` | All cleanup candidates for a generation complete deletion | +| `zone.config.cleanup.stuck` | A cleanup candidate exceeds `cleanupStuckThreshold` | +| `zone.config.generation.rollback` | Prior generation is restored by operator | + +**Error handling**: The configuration publication handler retries stuck finalizer +notifications with exponential backoff, bounded by `cleanupStuckThreshold`. After +threshold, it records the `GenerationCleanupFailed` condition without further +retries. The runtime never force-removes finalizers. An operator may resolve a +stuck cleanup by: (a) fixing or restarting the owning controller so it can +complete its finalizer, or (b) performing a full Zone reset. + +#### Prior generation retention and rollback + +Prior generation bundle files are retained in the Zone store bundle directory +up to the configured `retainedPriorGenerationCount` (default 3, range 1..16). +A bundle file for generation M is eligible for pruning when: + +1. All resources from generation M with `managedBy=configuration` that were + absent from generation M+1 have completed deletion; AND +2. No rollback lock is outstanding (`d2b zone config rollback-lock set`); AND +3. Retaining this file would exceed `retainedPriorGenerationCount`. + +When the count is exceeded, the oldest eligible prior bundle file is pruned; +resources with `deletionRequestedAt` already set from that generation continue +their individual finalizer drain but lose rollback association. + +An operator with `verb=zone.config-rollback` may restore the retained prior +generation: + +``` +d2b zone config rollback --zone dev --to-generation +``` + +Rollback atomically: +1. Re-activates the prior bundle as the new active generation. +2. Re-creates any config-owned resources deleted by the superseded generation's + cleanup, using the prior bundle's resource envelopes. +3. Issues async Deletes for resources that appeared in the superseded generation + but not the prior generation. +4. Emits `zone.config.generation.rollback` audit record. +5. Triggers a new cleanup cycle for the superseded-generation additions. + +--- + +## 15. Conformance and tests + +### 15.1 Zone self-resource tests + +| Test | Assertion | +| --- | --- | +| `zone-self-resource-enforced` | Store with no Zone resource fails to open | +| `zone-uid-mismatch-quarantine` | Opening a store where Zone UID differs from store_meta.zone_uid quarantines the store | +| `zone-name-mismatch-rejected` | Zone resource with `metadata.name != store_meta.zone_name` is rejected | +| `zone-cardinality-one` | Attempt to create a second Zone resource is rejected with `resource-already-exists` | +| `zone-cross-zone-ref-rejected` | Any resource with a ref containing a Zone prefix is rejected | +| `zone-owner-rejected` | Create request with non-null Zone `ownerRef` is rejected | +| `zone-deletion-only-on-drain` | Zone deletion without `core.zone-drain` finalizer path is rejected | + +### 15.2 ZoneLink tests + +| Test | Assertion | +| --- | --- | +| `zonelink-reconnect-child-uid-change` | On reconnect after local child Zone UID replacement, local cursor resets to 0, the parent allocation is recreated, and `childZoneUid` updates | +| `zonelink-disconnect-unknown-phase` | On transport disconnect ZoneLink phase becomes Unknown within one reconcile | +| `zonelink-intent-queue-limit` | Queuing more than 256 local intents returns `backpressure` | +| `zonelink-disabled-no-reconnect` | `spec.disabled=true` prevents reconnection and sets `DisabledByOperator` condition | +| `zonelink-child-auth-denied-failed` | Parent allocator rejecting the child subject/allocation sets `Failed` phase and `ChildAuthorizationDenied` condition | +| `zonelink-drain-closes-session` | Deletion drains local intents and closes session before removing resource | +| `zonelink-transport-provider-ref-required` | ZoneLink without `spec.transportProviderRef` fails admission | +| `zonelink-transport-ref-pattern-enforced` | `spec.transportProviderRef = "Provider/network-local"` fails admission because it does not match the required `transport-*` pattern | +| `zonelink-transport-credentials-max` | ZoneLink with 9 `transportCredentials` entries fails admission | +| `zonelink-child-name-matches-store` | ZoneLink whose `childZoneName` differs from its enclosing child Zone is rejected | +| `zonelink-one-child-local-uplink` | A second ZoneLink (even disabled) in one child Zone and any uplink in local root are rejected | +| `zonelink-parent-bootstrap-binding` | Child ZoneLink allocation uses exactly the parent chosen by compiler-only `parentZone`; no transport setting can override it | +| `zonelink-parent-has-no-reciprocal-row` | Reconcile creates parent allocator/route state but no parent-store ZoneLink resource | + +### 15.3 Provider tests + +| Test | Assertion | +| --- | --- | +| `provider-trust-check-required` | Provider with invalid signature is rejected before any component launch | +| `provider-conformance-check-required` | Provider without attestation digest is rejected | +| `provider-config-schema-validation` | Config object not matching configSchemaDigest schema is rejected | +| `provider-api-name-collision-rejected` | Two Providers exporting the same short ResourceType name: second rejected | +| `provider-dependency-cycle-rejected` | Provider with cyclic alias dependency is rejected | +| `provider-quarantine-on-trust-failure` | Trust check failure after install quarantines Provider and stops components | +| `provider-bootstrap-no-process` | system-core and system-minijail report no processRef; all others require processRef | +| `provider-wildcard-permission-restricted` | Non-bootstrap Provider with wildcard permission claim is rejected | +| `provider-upgrade-drain-then-replace` | `upgradePolicy=drain-then-replace` completes old component drain before new launch | +| `provider-component-bound-limits` | Provider with 9 controllers fails admission; 8 is the maximum | +| `provider-crate-layout-src-required` | A `d2b-provider-*` workspace crate without `src/` fails `make test-policy` with message naming the crate and missing path | +| `provider-crate-layout-tests-required` | A `d2b-provider-*` workspace crate without `tests/` fails `make test-policy` | +| `provider-crate-layout-integration-required` | A `d2b-provider-*` workspace crate without `integration/` fails `make test-policy` | +| `provider-crate-layout-readme-required` | A `d2b-provider-*` workspace crate without `README.md` fails `make test-policy` | +| `provider-readme-sections-all-present` | A Provider `README.md` missing any of the nine required headings (§4.8.3) fails policy with the exact missing heading name | +| `provider-readme-sections-partial-missing` | A Provider `README.md` with 8 of 9 sections fails policy; message names the one missing section | +| `provider-integration-target-declared` | An `integration/*.rs` file without an `integration-target:` declaration in the first 20 lines fails policy | +| `provider-integration-target-unique` | An `integration/*.rs` file with two `integration-target:` declarations fails policy | +| `provider-integration-target-valid-values` | An `integration-target:` value other than `container` or `host-integration` fails policy | +| `provider-crate-naming-convention` | A crate named `d2b--` (implementation before base) fails the workspace member name policy | +| `provider-crate-layout-non-provider-exempt` | A non-`d2b-provider-*` workspace crate is exempt from the §4.8 layout check | + +### 15.4 Role tests + +| Test | Assertion | +| --- | --- | +| `role-unknown-verb-rejected` | Rule with an unknown verb token fails admission | +| `role-relay-core-zonelink-admitted` | Core-generated, ZoneLink-owned Role/RoleBinding with an exact adjacent-Zone enrollment selector and exact target bounds admits `relay` | +| `role-relay-missing-denied` | A forwarding hop without `relay` fails closed even when the target verb is allowed | +| `role-relay-target-verb-required` | `relay` alone cannot authorize the forwarded invocation/stream target verb | +| `role-relay-provider-self-assertion-rejected` | A Provider- or payload-asserted relay grant is rejected | +| `role-relay-wildcard-rejected` | Relay with empty/all-name or wildcard target scope is rejected, including for a core-generated Role | +| `role-unknown-resource-type-rejected` | Rule naming a ResourceType not in Zone API catalog is rejected | +| `role-wildcard-non-core-rejected` | Non-core-controller Role with `resourceNames: ["*"]` is rejected | +| `role-wildcard-core-permitted` | Core-generated Role with `resourceNames: ["*"]` is admitted | +| `role-index-built-before-ready` | Role does not reach Ready until `IndexBuilt=True` | +| `role-deletion-blocked-by-bindings` | Role with active bindings cannot be deleted until all bindings removed | +| `role-bounds-enforced` | Role with 33 rules fails admission; 32 is the maximum | + +### 15.5 RoleBinding tests + +| Test | Assertion | +| --- | --- | +| `rolebinding-role-not-found-failed` | RoleBinding with non-existent `roleRef` reaches `Failed` phase | +| `rolebinding-invalid-subject-type-rejected` | Subject with non-permitted ResourceType rejected at admission | +| `rolebinding-scope-exceeds-role-rejected` | `scopeNarrowing` with verb absent from Role rejected at admission | +| `rolebinding-subject-uid-change-detected` | Subject deleted/recreated triggers `SubjectIdentityChanged` condition | +| `rolebinding-deletion-immediate` | RoleBinding deletion is one atomic transaction: RBAC index removal, `Deleted` revision event, and row removal occur simultaneously; no intermediate state is observable | +| `rolebinding-subject-bounds-enforced` | RoleBinding with 129 subjects fails admission | + +### 15.6 Bootstrap tests + +| Test | Assertion | +| --- | --- | +| `bootstrap-only-system-core-minijail` | Any subject other than system-core/system-minijail is denied under bootstrap | +| `bootstrap-no-runtime-authority` | Bootstrap cannot grant exec/shell/process-outside-spec operations | +| `bootstrap-non-configurable` | No config field widens bootstrap authorization | +| `bootstrap-supersession-atomic` | After stored RBAC publishes, bootstrap is fully superseded with no overlap window | +| `bootstrap-recovery-out-of-band` | Corrupt authorization store triggers fail-closed; reset requires privileged local operator | + +### 15.7 Cross-cutting tests + +| Test | Assertion | +| --- | --- | +| `zone-control-audit-no-payload` | Audit records for Zone control type mutations contain no rule/subject/config/digest content | +| `zone-control-otel-no-sensitive-labels` | OTEL metrics carry no subject names, resource names, or credential-adjacent labels | +| `zone-control-error-bounded` | All Zone control error messages ≤ 512 bytes, UTF-8, no secrets | +| `zone-control-status-owner-only` | Only core-controller handler may update-status for any Zone control resource | +| `zone-control-cross-zone-ref-rejected` | Any ref with cross-Zone notation is rejected at admission | + +### 15.8 Configuration generation and cleanup tests + +#### Phase 1 — Nix eval tests + +| Test | Assertion | +| --- | --- | +| `nix-eval-name-regex-enforced` | Zone/Provider/Role/RoleBinding with name `"Bad_Name"` fails eval with assertion error | +| `nix-eval-name-length-enforced` | Resource name of 129 characters fails eval | +| `nix-eval-verb-closed-enum` | Rule with verb `"delete-all"` (unknown) fails eval | +| `nix-eval-session-verb-closed-enum` | Rule with `sessionVerbs=["sudo"]` fails eval | +| `nix-eval-relay-session-verb-known` | `sessionVerbs=["relay"]` reaches Phase 2 as the canonical token; placing `relay` in `verbs` fails eval | +| `nix-build-relay-scope-restricted` | Unbounded, wildcard, or Provider/self-asserted relay configuration fails before bundle activation | +| `nix-eval-roleref-format` | `roleRef="role/foo"` (wrong case) fails eval | +| `nix-eval-subject-type-restricted` | Subject `"Device/foo"` (non-permitted type) fails eval | +| `nix-eval-no-duplicate-subjects` | Two identical subjects in one RoleBinding fails eval | +| `nix-eval-no-cross-zone-ref` | Subject `"dev/Role/foo"` (Zone-prefixed) fails eval | +| `nix-eval-bootstrap-provider-rejected` | `d2b.zones.dev.resources.system-core = { type = "Provider"; ... }` fails eval with named assertion | +| `nix-eval-provider-missing-artifact-id` | `d2b.zones.dev.resources.p = { type = "Provider"; spec = { config = {}; }; }` (no `artifactId`) fails eval | +| `nix-eval-artifact-id-not-in-catalog` | `spec.artifactId = "nonexistent"` for a Provider where `d2b.artifacts` has no `nonexistent` entry fails eval | +| `nix-eval-artifact-wrong-type` | `d2b.artifacts.foo = { package = pkgs.hello; type = "nixos-system"; }` used as `spec.artifactId` in a Provider fails eval (type mismatch) | +| `nix-eval-artifact-id-format` | `spec.artifactId = "Bad_ID"` fails eval with label regex assertion | +| `nix-eval-credentialref-declared` | `d2b.zones.dev.credentialRef "missing"` for undeclared Credential fails eval | +| `nix-eval-dollar-key-rejected` | Config `{ "$secret" = "x"; }` fails eval | +| `nix-eval-parent-zone-required-root-forbidden` | Missing `parentZone` on a non-root Zone and any definition on `local-root` fail eval | +| `nix-eval-parent-zone-resolves` | Unknown and self-valued `parentZone` settings fail eval | +| `nix-eval-parent-zone-one-parent` | Conflicting scalar `parentZone` module definitions fail through Nix merging | +| `nix-eval-parent-zone-cycle-rejected` | A two-Zone or longer `parentZone` cycle fails eval | +| `nix-eval-parent-zone-depth-bound` | A topology path containing 17 Zone names fails eval; 16 succeeds | +| `nix-eval-zonelink-child-name-mismatch-rejected` | Child-local ZoneLink with `childZoneName` unequal to its enclosing Zone key fails eval | +| `nix-eval-zonelink-second-uplink-rejected` | A second child-local uplink (even disabled), or any local-root uplink, fails eval | +| `nix-eval-zonelink-limits-maxpendingintents-bound` | `maxPendingIntents = 1025` fails eval | + +#### Phase 2 — Build tests + +| Test | Assertion | +| --- | --- | +| `nix-build-artifact-id-missing-from-catalog` | Provider `spec.artifactId = "unknown"` fails build with "artifact ID not found" | +| `nix-build-artifact-wrong-type-rejected` | Provider `spec.artifactId` pointing to a `nixos-system` catalog entry fails build | +| `nix-build-duplicate-artifact-id` | Two `d2b.artifacts` entries with the same ID fail build with duplicate error | +| `nix-build-artifact-store-path-absent-from-bundle` | Emitted bundle JSON contains no Nix store path strings | +| `nix-build-artifact-store-path-absent-from-config` | Emitted config JSON in bundle contains no Nix store path strings | +| `nix-build-config-schema-failure` | Provider config field of wrong type fails build with field path in error | +| `nix-build-schema-digest-mismatch` | Schema file with SHA-256 not matching `configSchemaDigest` in artifact catalog fails build | +| `nix-build-manifest-digest-mismatch` | Manifest file with SHA-256 not matching `manifestDigest` in artifact catalog fails build | +| `nix-build-resourcetype-collision` | Two Providers in the same Zone exporting the same short ResourceType name fail build | +| `nix-build-bundle-sorted` | Emitted `resources` array is sorted by `(resourceType, name)` ascending | +| `nix-build-bundle-digest-stable` | Same Nix config on two builds produces identical `bundleDigest` | +| `nix-build-per-resource-digest-correct` | Per-resource `digest` matches `sha256(canonical-JSON-of-spec)` | +| `nix-build-credential-ref-survives-build` | `{ "$credentialRef": "Credential/api-token" }` appears verbatim in emitted bundle | +| `nix-build-inline-secret-lint-warning` | Config string matching PEM header emits build warning | +| `nix-build-inline-secret-strict-failure` | Same with `--strict-secrets` fails build | + +#### Phase 3 — Runtime activation tests + +| Test | Assertion | +| --- | --- | +| `nix-runtime-bundledigest-integrity` | Bundle with tampered `bundleDigest` is rejected; prior generation stays active | +| `nix-runtime-generation-monotone` | Bundle with `generation ≤ active_configuration_revision` is rejected | +| `nix-runtime-zoneuid-mismatch-rejected` | Bundle with `zoneUid` not matching `store_meta.zone_uid` is rejected | +| `nix-runtime-zonename-mismatch-rejected` | Bundle with `zoneName != Zone self-resource name` is rejected | +| `nix-runtime-activation-nonblocking` | New generation activates and serves requests before cleanup of prior generation completes | +| `nix-runtime-provider-config-invalid-continues` | Provider `config` failing schema re-check sets `Failed` with `ConfigValid=False` condition but does not block other resources from activating | + +#### Cleanup tests + +| Test | Assertion | +| --- | --- | +| `cleanup-config-owned-absent-resource-deleted` | Provider present in generation N but absent from N+1 receives async Delete | +| `cleanup-controller-created-resource-preserved` | Controller-created `Process/provider-primary-0` (not in bundle) is NOT deleted on generation change | +| `cleanup-bootstrap-provider-preserved` | `Provider/system-core` absent from operator config is NOT deleted on any generation change | +| `cleanup-role-deletion-blocked-by-binding` | Config-owned Role awaiting cleanup is blocked by `core.role-binding-drain` until dependent RoleBinding is deleted | +| `cleanup-rolebinding-auto-deleted-when-role-deleted` | Authorization handler triggers RoleBinding deletion when parent Role has `deletionRequestedAt` set and is awaiting `core.role-binding-drain` clearance | +| `cleanup-provider-stops-processes-on-delete` | Config-owned Provider with `deletionRequestedAt` set stops owned component Processes via provider lifecycle handler | +| `cleanup-credential-revoke-on-delete` | Credential with `deletionRequestedAt` set triggers runtime to revoke resolved secret binding before finalizer clearance | +| `cleanup-status-pending-count-accurate` | `Zone.status.cleanupPendingCount` equals exact count of cleanup candidates with `deletionRequestedAt` set | +| `cleanup-zone-degraded-while-pending` | Zone.status.phase is Degraded while any cleanup candidate has `deletionRequestedAt` set; reverts when all complete | +| `cleanup-condition-clears-on-completion` | `GenerationCleanupPending` condition clears, count resets to 0, and Zone.status.phase reverts after all cleanup candidates complete | +| `cleanup-stuck-sets-degraded` | Resource awaiting deletion beyond `cleanupStuckThreshold` keeps Zone.status.phase=Degraded and additionally sets `GenerationCleanupFailed=True` | +| `cleanup-stuck-message-no-content` | `GenerationCleanupFailed` condition message contains ResourceType but no resource name, spec content, or secret value | +| `cleanup-audit-activate-emitted` | Generation activation emits `zone.config.generation.activate` audit record | +| `cleanup-audit-complete-emitted` | Cleanup completion emits `zone.config.cleanup.complete` audit record | +| `cleanup-audit-stuck-emitted` | Stuck cleanup emits `zone.config.cleanup.stuck` audit record | +| `cleanup-audit-no-resource-names` | All cleanup audit records contain no resource names, spec content, or secret values | +| `rollback-restores-prior-generation` | `d2b zone config rollback` re-creates deleted config-owned resources from prior bundle | +| `rollback-deletes-superseded-additions` | Rollback issues async Delete for resources added by the superseded generation but absent from prior bundle | +| `rollback-requires-prior-generation-retained` | Rollback fails if prior generation bundle has been pruned | +| `rollback-emits-audit-record` | Rollback emits `zone.config.generation.rollback` audit record | + + +### 15.9 Quota and EmergencyPolicy tests + +| Test | Assertion | +| --- | --- | +| `quota-ceiling-hard-reject` | Resource creation over `maxResources` ceiling with `enforcementPolicy=hard` rejected with `quota-exceeded` | +| `quota-ceiling-soft-warn` | Same but `enforcementPolicy=soft`; resource admitted; `overQuota=true` in status | +| `quota-ceiling-pertype` | Resource creation over `maxResourcesPerType` ceiling for a specific ResourceType rejected | +| `quota-drain-blocks-on-dependents` | Quota deletion with `dependentCount > 0` keeps `core.quota-drain`; `QuotaDrainPending=True` condition message names the count; no quotaRef on other resources is modified | +| `quota-over-quota-status` | Quota status `usedResources` and `overQuota` reflect current usage | +| `quota-nix-eval-bounds` | `ceilings.maxOwnerDepth = 33` fails eval; `33 > 32` | +| `quota-nix-build-pertype-unknown-type` | `perTypeCeilings` entry for a ResourceType not in Zone API catalog fails build | +| `emergency-policy-activates-gate` | EmergencyPolicy with `enabled=true` and `stopNewAdmissions=true` causes new resource admissions to return admission-denied | +| `emergency-policy-disconnects-zonelinks` | EmergencyPolicy activation with `disconnectZoneLinks=true` triggers ZoneLink graceful disconnect | +| `emergency-policy-multiple-enabled-union` | Two enabled EmergencyPolicy resources with `stopNewAdmissions=true` and `disconnectZoneLinks=true` respectively produce a combined union where both effects apply Zone-wide | +| `emergency-policy-union-deactivate-partial` | Deactivating one of two enabled EmergencyPolicy resources removes only its contribution; the other policy's effects remain | +| `emergency-policy-deactivation-restores-gate` | Setting `enabled=false` restores normal admissions | +| `emergency-policy-stop-processes-no-delete` | EmergencyPolicy with `stopProviderProcesses=true` stops running Provider component Processes without setting `deletionRequestedAt`; Process resources remain; reconciliation resumes on deactivation | +| `emergency-policy-drain-finalizer-on-active-delete` | Deleting an EmergencyPolicy with `active=true` adds `core.emergency-drain`; drain completes before final atomic deletion | +| `emergency-reason-visible-in-spec` | EmergencyPolicy `reason` is readable in the resource spec via API; it does not appear in `status.*`, OTEL metric label values, or structured log labels | + +--- + +## 16. Current-code fit + +All seven Zone control ResourceTypes as stored resource objects are **`ADR-only`**: +no Zone, ZoneLink, Provider, Role, or RoleBinding resource schema, store table, +or controller handler exists in the v3 baseline (`b5ddbed6`). The subsections +below record every Realm-related baseline symbol that maps to Zone control +concepts, with exact evidence class and architectural mapping notes. + +### 16.1 Why Realm→Zone is not a textual rename + +ADR 0043 (`docs/adr/0043-realm-native-control-plane.md`, Accepted) defines a +Realm as a **runtime control-plane process pair**: each active realm boundary is +served by its own `d2bd` instance and its own privileged broker, with a distinct +socket, state directory, audit log, cgroup slice, nftables partition, and +identity key. A Realm is a running process boundary, not a data record. + +A **Zone** (ADR 0046) is a typed ResourceType stored in a redb object store, +reconciled by the core controller. It is a resource object representing the +local isolation and policy domain of the store that owns it — one Zone per +store, cardinality-1 enforced. It is not itself a process; the Zone runtime +that hosts the store and core controller is the process, but the Zone resource +is just the authoritative metadata record in that runtime. + +The key architectural differences that implementations must track: + +| Dimension | Baseline Realm (ADR 0043) | Target Zone (ADR 0046) | +| --- | --- | --- | +| Identity model | `RealmPath(Vec)`, a tree path; realms may nest up to `MAX_REALM_LABELS=16` | `metadata.name` (single label); compiler-only `parentZone` selects the parent while a child-local ZoneLink supplies transport/route state | +| Process model | Each realm = one `d2bd` + one broker (multiple pairs on one host) | Zone is a resource record; Zone runtime is the surrounding process | +| Hierarchy representation | Tree path embedded in identity; parent realm is a routing ancestor in `RouteTreeEngine`/`RealmEntrypointTable` | `parentZone` compiles to sealed private allocator topology; the child store holds only its local ZoneLink; no path encoding or reciprocal parent row | +| Config loading | `realm-controllers.json` loaded at daemon startup (`load_realm_controllers_config` at `d2bd/src/lib.rs:1408`); routing remains "inert" but metadata is live | Zone self resource read from redb store; no external JSON config file for Zone identity | +| Access resolution | `RealmAccessResolverRequest/Response` + `RealmEntrypointTable` loaded from `/run/current-system/sw/share/d2b/realm-entrypoints.json`; reachable in CLI routing path (`d2b/src/target_routing.rs`) | ZoneLink resource with ComponentSession transport binding; access resolution is a resource-lifecycle operation | +| Provider placement | `RealmControllerPlacement` enum (`HostLocal`, `GatewayVm`, `CloudFullHost`, `ProviderController`, `ProviderAgent`) — runtime property of where realm's d2bd runs | Provider component `placement` field — descriptor in Provider resource spec; different semantics (component placement, not realm process placement) | +| Host-resource allocation | `LocalRootAllocatorEngine` in `d2b-realm-core/src/allocator_engine.rs:332`; typed leases for cgroup subtrees, nftables partitions, socket paths, bridges; leases tied to realm broker lifecycle | Resource store lifecycle + broker operations; allocator concepts move into Zone runtime internal mechanics, not ResourceTypes | +| Routing | `RouteTreeEngine`/`DescendantRoute`/`TreeRoutePath`/`RouteAdvertisement` (tree discovery); `OperationRouter` dedup; `RemoteNodeRegistry` (full-host nodes) — all `implemented-but-unwired` | Not scope of Zone control types; ZoneLink establishes point-to-point connection, not tree route advertisements | +| Auth/admission | Coarse `PeerRole::{Admin,Launcher,HostShutdown}` per-connection; `DaemonAccessPolicyRole::RealmAdmin` | Native RBAC: Role resource (rule table) + RoleBinding resource (subject binding); per-operation rather than per-connection | + +### 16.2 Exhaustive symbol evidence matrix + +Evidence classes: +- `implemented-and-reachable` — in live call path, not gated by dead-code allow +- `implemented-but-unwired` — compiles and is tested but not called from live daemon/CLI code +- `generated-or-eval-contract` — Nix emitter or generated JSON artifact; consumed at runtime +- `test-only-or-preview` — only in test harnesses + +#### Identity and path types + +| Symbol | File | Evidence class | ADR-0046 mapping | +| --- | --- | --- | --- | +| `RealmId` (label-shaped, `is_label()`) | `d2b-realm-core/src/ids.rs:211` | `implemented-and-reachable` | Zone `metadata.name`; ResourceName shape | +| `WorkloadId` (label-shaped) | `d2b-realm-core/src/ids.rs:225` | `implemented-and-reachable` | `metadata.name` for Guest or Host resources; `WorkloadProviderKind::LocalVm`/`QemuMedia` → Guest `metadata.name`; **`WorkloadProviderKind::UnsafeLocal` → Host `metadata.name` (user-only, `defaultDomain=user`, reconciled by `Provider/system-core`)** — NOT a Guest; `WorkloadProviderKind::ProviderManaged` → Guest or Host per provider semantics | +| `ProviderId` (label-shaped) | `d2b-realm-core/src/ids.rs:232` | `implemented-and-reachable` | Provider `metadata.name` | +| `NodeId` (label-shaped) | `d2b-realm-core/src/ids.rs:218` | `implemented-and-reachable` | Host `metadata.name` | +| `LABEL_PATTERN = "^[a-z][a-z0-9-]*$"`, `is_label()`, `MAX_ID_LEN = 128`, `IdError` | `d2b-realm-core/src/ids.rs:31,76,28,54` | `implemented-and-reachable` | ResourceName validator; extracted for ResourceRef parser | +| `RealmIdentityRef` (opaque, redacts debug) | `d2b-realm-core/src/ids.rs:312` | `implemented-and-reachable` | Opaque identity ref pattern; maps to Zone key-ref in ZoneLink key-pin model | +| `RealmPath(Vec)` | `d2b-realm-core/src/realm.rs:64` | `implemented-and-reachable` | **Not** a target type; encodes hierarchy structurally (path-in-identity). Compiler-only `parentZone` replaces the path topology; the child-local ZoneLink carries transport/route state. `RealmPath` depth (`MAX_REALM_LABELS=16`) maps to the compiled parent graph's ancestry bound | +| `MAX_REALM_LABELS = 16` | `d2b-realm-core/src/realm.rs:67` | `implemented-and-reachable` | Compiler-only `parentZone` ancestry bound | +| `RealmControllerPlacement::{HostLocal, GatewayVm, CloudFullHost, ProviderController, ProviderAgent}` | `d2b-realm-core/src/realm.rs:26` | `implemented-and-reachable` | **Partially reused**: these placement labels map to Provider component `placement` descriptor semantics, but the target is a Provider resource field, not a realm process property. Not a 1:1 rename. | +| `EntrypointMode::{HostResident, GatewayBacked}` | `d2b-realm-core/src/realm.rs:12` | `implemented-and-reachable` | Informs ZoneLink transport binding (host-local socket vs. remote transport); maps to the resolved `transportProviderRef` selector semantics | +| `RealmTarget { workload: WorkloadId, realm: RealmPath }` | `d2b-realm-core/src/target.rs:39` | `implemented-and-reachable` (CLI routing path) | `RealmTarget` is the current addressable unit (`..d2b`); maps to `ResourceRef` (target scoped to Zone); NOT a Zone identity. `WorkloadTarget = RealmTarget` alias in `d2b-core/src/workload_identity.rs:55` | +| `TargetName`, `RealmTargetParser`, `RealmTargetParseError` | `d2b-realm-core/src/target.rs:122,373,280` | `implemented-and-reachable` | Maps to ResourceRef `/` parser in ADR046-identities-001 | +| `LegacyNodeQualifiedTarget` | `d2b-realm-core/src/target.rs:242` | `implemented-and-reachable` | Migration artifact; removed after target format normalizes to ResourceRef | + +#### Routing and access resolution types + +| Symbol | File | Evidence class | ADR-0046 mapping | +| --- | --- | --- | --- | +| `RealmTreeEdge { parent: RealmPath, child: RealmPath }` | `d2b-realm-core/src/routing.rs:543` | `implemented-but-unwired` | Tree edge between two zones; ZoneLink resource replaces this structural edge with a stored resource object | +| `DescendantRoute`, `TreeRoutePath`, `TreeRouteHop` | `d2b-realm-core/src/routing.rs:581,854,798` | `implemented-but-unwired` | Multi-hop tree routing metadata; ADR 0046 replaces with ZoneLink cursor model (no multi-hop discovery in Zone control types) | +| `RouteAdvertisement`, `RouteAdvertisementEnvelope`, `RouteNamespaceAllocation` | `d2b-realm-core/src/routing.rs:609,697,716` | `implemented-but-unwired` | Route advertisement for peer discovery; no equivalent in Zone control types (ZoneLink uses ComponentSession, not route advertisements) | +| `RouteRealmClass::{LocalRoot, StaticConfigured, HostLocalPeer, GatewayBacked, CloudFullHost, ProviderManaged, EphemeralDiscovered, Unknown}` | `d2b-realm-core/src/routing.rs:1170` | `implemented-but-unwired` | Telemetry/audit realm class. Maps to ZoneLink low-cardinality transport class labels in OTEL metrics (§13); NOT a stored resource field | +| `RoutePlacementClass` | `d2b-realm-core/src/routing.rs:1201` | `implemented-but-unwired` | Low-cardinality placement label for telemetry; maps to Provider component placement OTEL label | +| `RouteAuditLabels`, `RouteTelemetryLabels`, `RouteTelemetrySample`, `RouteTelemetryBatch` | `d2b-realm-core/src/routing.rs:1244,1300,1313,1322` | `implemented-but-unwired` | Audit/telemetry shape precedent; maps to ZoneLink OTEL metrics label constraints in §13.2 | +| `RouteTreeEngine`, `RouteEngineEvent`, `RouteAdvertisementAdmission`, `DiscoveryQueueDecision`, `RoutePruneReport`, `DirectShortcutAuthorizationRequest` | `d2b-realm-core/src/route_engine.rs:161,30,37,51,70,78` | `implemented-but-unwired` | Live routing engine with dedup, pruning, shortcut auth; exported from `d2b_realm_core::lib.rs:122` but has **zero call sites in live daemon or CLI code** — only used in tests within `route_engine.rs`. ZoneLink does not use route advertisements; the `decide_route` algorithm is an unwired precedent only | +| `RealmEntrypointTable`, `RealmEntrypoint`, `DispatchTarget::{HostResident, GatewayBacked}`, `ResolveError` | `d2b-realm-router/src/target_resolver.rs:103,25,54` | `implemented-and-reachable` (CLI only) | The CLI (`d2b/src/lib.rs:5239`, `target_routing.rs:430`) loads the realm entrypoints file from `/run/current-system/sw/share/d2b/realm-entrypoints.json` (Nix-generated at `nixos-modules/host-daemon.nix:385`) and resolves targets. The daemon (`d2bd`) only uses it in `realm_stubs.rs` (dead code). Compiler-only `parentZone` topology plus child-local ZoneLink ComponentSession transport replace the entrypoint table | +| `OperationRouter`, `RouteDecision`, `OperationRoutePlan`, `ReconcilableLease` | `d2b-realm-router/src/lib.rs:306,95,179,636` | `implemented-but-unwired` | In `d2bd` only via `realm_stubs.rs` (`#![allow(dead_code)]`; comment: "not called from the running daemon"); `OperationRouter` dedup model maps to resource API idempotency layer | +| `RemoteNodeRegistry`, `RemoteFullHostAdapter`, `RemotePeerClient`, `RemoteDispatchOutcome` | `d2b-realm-router/src/remote_node.rs:252,644,608,627` | `implemented-but-unwired` | Remote full-host node management; in d2bd only via realm_stubs dead-code. No Zone control type equivalent; future Host/node resource design | +| `RealmAccessTargetInput`, `RealmAccessAliasBinding`, `RealmAccessResolverRequest`, `RealmAccessResolverResponse`, `RealmAccessBinding`, `RealmTransportBinding`, `HostLocalPeerCredentialSemantics`, `AccessBindingRef` | `d2b-realm-core/src/access.rs:170,285,322,542,597,569,364,96` | `implemented-and-reachable` (CLI path) | Used actively in `d2b/src/target_routing.rs:21–26` which is called from live CLI routing. `RealmTransportBinding::{LocalUnixSocket,RemoteRealmTransport,ProviderRealmTransport}` maps to the resolved ZoneLink transport binding variants | +| `realm_access_resolver` module (`resolve_local_root_realm_access`, `host_local_capability_preflight`, `realm_controllers_config_generation`) | `d2bd/src/realm_access_resolver.rs:20,159,190` | `implemented-but-unwired` | `pub mod realm_access_resolver` in `d2bd/src/lib.rs:117`; no call sites in live daemon code (only within the module itself). Capability preflight model maps to Zone/ZoneLink capability surface in §13 | +| `realm_stubs` module (`ApiFrontend`, `ApiService`, `TargetResolver`, `PeerOperationRouter`, `ProviderExecutor`, `PeerDaemon`, `DaemonMode`, `SharedRouter`) | `d2bd/src/realm_stubs.rs` | `implemented-but-unwired` | Explicitly `#![allow(dead_code)]`; module comment: "not called from the running daemon — the local CLI→daemon path is unchanged (zero behavior change)". Seam for future gateway work. Maps to: `ApiFrontend`→ComponentSession framing; `ApiService`→Zone runtime API surface; `TargetResolver`→ZoneLink access | + +#### Session types + +| Symbol | File | Evidence class | ADR-0046 mapping | +| --- | --- | --- | --- | +| `PeerSession` | `d2b-realm-router/src/session.rs` | `implemented-but-unwired` | No call sites in live d2bd/d2b code (d2bd imports `d2b_realm_router` only for `realm_stubs` dead code; d2b CLI imports only `RealmEntrypointTable` from router). Noise handshake/session framing model; reuse source for ComponentSession | +| `SecurePeerSession` (Noise-based) | `d2b-realm-router/src/secure_session.rs` | `implemented-but-unwired` | Same reachability as `PeerSession`; Noise KK handshake implementation; **design precedent** for ComponentSession Noise KK (copy/adapt from main `a1cc0b2d`) | +| `MuxSession` | `d2b-realm-router/src/mux_session.rs` | `implemented-but-unwired` | Stream multiplexing over a session; maps to ZoneLink named-stream framing model (copy/adapt from main `a1cc0b2d` d2b-bus) | +| `SessionLifecycle`, `SessionPhase` | `d2b-realm-router/src/session_lifecycle.rs` | `implemented-but-unwired` | FSM phases (current evidence: `Unknown`, `Connecting`, `Established`, `Failed`); design precedent for ZoneLink session reconnect loop; `Connecting`/`Established` are current baseline FSM states that map to ZoneLink `status.connected` detail field, not to `status.phase`; ZoneLink `status.phase` uses common Resource phases (`Pending`/`Ready`/`Degraded`/`Failed`/`Unknown`) | + +#### Identity store and enrollment types + +| Symbol | File | Evidence class | ADR-0046 mapping | +| --- | --- | --- | --- | +| `RealmIdentityStore`, `EnrollmentRecord`, `ChildKeyPin`, `ControllerGenerationId`, `RevocationList`, `RecoveryProcedure` | `d2b-realm-core/src/identity_store.rs:141,207,177,182,197,202` | `implemented-and-reachable` (d2b-realm-core is a live dependency) | Zone identity anchoring and child enrollment model. The identity store tracks enrolled realm key-pins, controller generations, and revocations. ZoneLink child key-pin binding adapts `ChildKeyPin` + `EnrollmentRecord`. **Not** a ResourceType; internal Zone runtime mechanism | +| `RealmIdentityConfigJson`, `RealmIdentityConfigEntry`, `RealmIdentityConfigInvariants`, `RealmIdentityConfigError` | `d2b-realm-core/src/identity_config.rs:19,69,89,124` | `implemented-and-reachable` | Deserialized by `load_realm_identity_config()` in live daemon startup (`d2bd/src/lib.rs:1425`); runtime trust sessions logged as "inert". Maps to Zone identity key-ref fields loaded from bundle | +| `RealmIdentityConfigRuntimeState::MetadataOnly` | `d2b-realm-core/src/identity_config.rs:63` | `implemented-and-reachable` | `runtimeState = "metadata-only"` invariant retained; all identity config validated as non-secret at startup | + +#### Realm controller config types (generated-and-consumed) + +| Symbol | File | Evidence class | ADR-0046 mapping | +| --- | --- | --- | --- | +| `RealmControllersJson`, `RealmControllerConfig`, `RealmControllerPlacement`, `RealmControllerLocalRuntime`, `RealmControllerLocalWorkload`, `RealmAllocatorBinding`, `RealmDaemonConfig`, `RealmBrokerConfig` | `d2b-core/src/realm_controller_config.rs:144,190,256,278,434,541,497,511` | `implemented-and-reachable` | Loaded in live daemon startup at `d2bd/src/lib.rs:1408` and in serve path at line `16741`; `WorkloadTargetIndex::build_from_controllers` is called live. Contains per-realm socket/state paths, placement, providers, workloads. Maps to: Zone self resource (the realm metadata row); compiler-only `parentZone` compiled into the private allocator binding (per-realm parent socket/edge data); child-local ZoneLink transport/route state; Provider (per-realm provider config rows); NOT replaced by a single file — replaced by resource store entries plus sealed allocator state | +| `WorkloadTargetIndex`, `TargetResolution`, `TargetResolutionError` | `d2bd/src/workload_target_index.rs:93,28,56` | `implemented-and-reachable` | Built from `RealmControllersJson` in live serve path (`d2bd/src/lib.rs:16745`); resolves `..d2b` to VM name. Maps to resource API `resourceRef` lookup table in ADR 0046 API layer | + +#### Allocator types + +| Symbol | File | Evidence class | ADR-0046 mapping | +| --- | --- | --- | --- | +| `AllocatorLease`, `LeaseAllocationRequest`, `AllocatorLeaseState`, `LeaseOwner`, `AllocatorReasonCode`, `HostResourceKind`, `ResourceShareMode` | `d2b-realm-core/src/allocator.rs:274,151,241,71,310,41,84` | `implemented-and-reachable` | Core allocator data model. Leases typed by `HostResourceKind` (`HostFilePartition`, `CgroupSubtree`, `NftablesPartition`, `Bridge`, `NamespaceBoundary`). Maps to Zone runtime internal resource management; these are **mechanisms, not ResourceTypes** | +| `LocalRootAllocatorEngine` | `d2b-realm-core/src/allocator_engine.rs:332` | `implemented-and-reachable` | Test-facing fake engine; drives idempotency/reconciliation proof. Maps to Zone runtime allocator internals; NOT a ResourceType or Zone store table | +| `AllocatorEngineDecision`, `AllocatorEngineOutcome`, `AllocatorAllocationDecision`, `AllocatorReconciliationAction` | `d2b-realm-core/src/allocator_engine.rs:22,78,152,170` | `implemented-and-reachable` | Allocation decision types; maps to Zone runtime startup allocation invariants | + +#### Provider trait types + +| Symbol | File | Evidence class | ADR-0046 mapping | +| --- | --- | --- | --- | +| `HostSubstrateProvider`, `RuntimeProvider`, `WorkloadProvider`, `DurableExecutionProvider`, `InfrastructureProvider`, `NodeProvider`, `TransportProvider`, `RelayProvider`, `CredentialProvider`, `PersistentShellProvider`, `DisplayProvider` traits | `d2b-realm-provider/src/provider.rs:31,41,65,89,268,333,197,324,301,137,169` | `implemented-and-reachable` (`WorkloadProvider` is imported in live `d2bd/src/lib.rs:93` for ACA) | Provider trait surface. Each trait axis maps to a Provider component service definition; `WorkloadProvider` is the only one with a live call path (ACA gateway). Remaining traits are reachable via `d2b-realm-provider` dependency but have no live non-ACA call sites | +| `RuntimeCapabilitySet`, `WorkloadCapabilitySet`, `NodeCapabilitySet` | `d2b-realm-provider/src/capabilities.rs` | `implemented-and-reachable` | Maps to Provider `spec.components[].supportedCapabilities` | +| `workload_lists_and_advertises()`, `display_fails_closed_when_unsupported()` | `d2b-realm-provider/src/conformance.rs` | `implemented-and-reachable` | Provider conformance check behavior; reused in Provider trust check (ADR046-zone-control-003) | +| `ProviderError`, `ErrorKind`, `RetryHint`, `ProviderDiagnostic` | `d2b-realm-provider/src/error.rs` | `implemented-and-reachable` | Provider lifecycle error/retry schema; maps to Provider `status.conditions[].message` and error bounds | + +#### Process and supervisor types + +| Symbol | File | Evidence class | ADR-0046 mapping | +| --- | --- | --- | --- | +| `ProcessRole::{CloudHypervisorRunner, Virtiofsd, Swtpm, GpuRenderNode, Audio, Video, QemuMediaRunner, VsockRelay, OtelHostBridge, Usbip}` | `d2b-core/src/processes.rs:194` | `implemented-and-reachable` | Each `ProcessRole` variant maps to a declared Process or EphemeralProcess resource under a Provider component | +| `VmProcessDag`, `ProcessNode` | `d2b-core/src/processes.rs:20,57` | `implemented-and-reachable` | Provider component DAG shape; maps to Process/EphemeralProcess dependency graph | +| `DagExecutor`, `NodeRunner`, `topo_sort()`, `NodeOutcome`, `NodeBudget` | `d2bd/src/supervisor/dag.rs:302,181,222,38,148` | `implemented-and-reachable` | DAG lifecycle executor; maps to Process/EphemeralProcess launch ordering in Provider component lifecycle | + +#### Authorization and admission types + +| Symbol | File | Evidence class | ADR-0046 mapping | +| --- | --- | --- | --- | +| `PeerRole::{Admin, Launcher, HostShutdown}`, `PeerIdentity`, `authorize_peer()`, `verb_requires_admin()`, `verb_allowed_for_host_shutdown()` | `d2bd/src/admission.rs:16,10,37,123,157` | `implemented-and-reachable` | **Current bootstrap auth system**. Called in live daemon request path. Maps to: `PeerRole::Admin`→`system-core` subject; `PeerRole::Launcher`→`system-minijail` subject; `PeerRole::HostShutdown`→narrow bootstrap exception; verb tables → Role rule verb enum | +| `LocalUnixAllowlistRole::{Admin, Launcher, Denied}`, `DaemonAccessPolicyRole::RealmAdmin`, `DaemonAccessDecision::{Authorized,Denied}`, `DaemonAccessAdmissionSource`, `MappedDaemonAccessPrincipal::{LocalAdmin,LocalLauncher,LocalDenied}`, `map_local_unix_daemon_access()`, `map_remote_daemon_access()` | `d2b-daemon-access/src/lib.rs:361,370,390,415,468,487` | `implemented-and-reachable` | **Current local auth surface**. `LocalUnixAllowlistRole` determines `PeerRole` by group membership (`Admin`→`LocalAdmin`, `Launcher`→`LocalLauncher`). Maps to Role/RoleBinding RBAC engine (§6–§9). `DaemonAccessPolicyRole::RealmAdmin` maps to a future Role grant for realm operators; `MappedDaemonAccessPrincipal` maps to RoleBinding subject identity model | + +#### Nix authoring and generated artifacts + +| Symbol | File | Evidence class | ADR-0046 mapping | +| --- | --- | --- | --- | +| `d2b.realms..*`, `providerType` submodule, `providerKind = "^[a-z][a-z0-9-]*$"`, `placementProvider`, `providerSpecificPlacement` | `nixos-modules/options-realms.nix:26,39,55,65,170` | `generated-or-eval-contract` | Authoring interface for realm/provider declarations. Label regex identical to `LABEL_PATTERN` in `ids.rs`. Adapted to `d2b.zones..providers.*` in ADR046-zone-control-007 | +| `d2b.realms..workloads.*`, `kind`, `placement`, `legacyVmName` | `nixos-modules/options-realms-workloads.nix` | `generated-or-eval-contract` | Workload authoring submodule; `kind = "local-vm"` / `"qemu-media"` → Guest resource Nix options; **`kind = "unsafe-local"` → Host resource Nix options** (`defaultDomain=user`, `allowedDomains=[user]`); `kind = "provider-placeholder"` → Guest/Host per provider semantics | +| `d2b.realms..network.*` | `nixos-modules/options-realms-network.nix` | `generated-or-eval-contract` | Network/bridge authoring; maps to Network resource (not Zone control types) | +| `realm-controllers.json` (`RealmControllersJson`), `schemaVersion = "v2"`, `runtimeState = "metadata-only"`, per-realm rows | `nixos-modules/realm-controller-config-json.nix` (emitter); `d2b-core/src/realm_controller_config.rs:144` (deserializer) | `generated-or-eval-contract` (Nix); `implemented-and-reachable` (consumption in daemon) | **Loaded live in daemon** at `d2bd/src/lib.rs:1408,16741`. Runtime routing "remains inert" but `WorkloadTargetIndex` is built from it. Maps to: Zone self resource (identity row); compiler-only `parentZone` plus private parent allocator binding (topology/socket data); child-local ZoneLink transport/route state; Provider (provider config rows). The bundle artifact is replaced by child resource-store entries and sealed allocator state; until then, the schema is the live authority | +| `realm-identity.json` (`RealmIdentityConfigJson`), identity refs, fingerprints | `nixos-modules/realm-identity-config-json.nix` (emitter); `d2b-realm-core/src/identity_config.rs:19` (deserializer) | `generated-or-eval-contract` (Nix); `implemented-and-reachable` (consumption in daemon) | Loaded live at `d2bd/src/lib.rs:1425`; trust sessions "remain inert". Maps to Zone runtime identity key-ref state and store metadata, not Zone.spec | +| `allocator.json` resource request rows (per-realm: cgroup, nftables, bridges, socket paths), `LocalRootAllocatorEngine` | `nixos-modules/allocator-json.nix` (emitter); `d2b-realm-core/src/allocator_engine.rs:332` (engine) | `generated-or-eval-contract` (Nix); `implemented-and-reachable` (engine) | Allocator resource requests are generated per realm from `d2b.realms.*` Nix config and consumed by the allocator at runtime. Maps to compiler-only `parentZone` topology plus Zone runtime startup resource claiming. **Mechanism, not a ResourceType** | +| `realm-entrypoints.json`, loaded from `/run/current-system/sw/share/d2b/realm-entrypoints.json` | `nixos-modules/host-daemon.nix:385` (emitter); `d2b/src/lib.rs:5239` (loader) | `generated-or-eval-contract` (Nix); `implemented-and-reachable` (CLI consumption) | Loaded by `d2b` CLI in live routing path for realm-qualified targets. Replaced by the sealed `parentZone` topology plus child-local ZoneLink Resource/ComponentSession lookup | + +#### Workload posture and unsafe-local types + +> These symbols ground the `unsafe-local` → **Host** ResourceType mapping. +> `WorkloadProviderKind::UnsafeLocal` / `IsolationPosture::UnsafeLocal` do NOT +> map to a Guest resource or a Provider resource; they map exclusively to a +> user-domain Host resource reconciled by `Provider/system-core`. + +| Symbol | File | Evidence class | ADR-0046 mapping | +| --- | --- | --- | --- | +| `WorkloadProviderKind::{LocalVm, QemuMedia, ProviderManaged, UnsafeLocal}` | `d2b-realm-core/src/workload.rs:13` | `implemented-and-reachable` | `LocalVm`/`QemuMedia` → Guest resource kind; **`UnsafeLocal` → Host resource (`defaultDomain=user`, `allowedDomains=[user]`)** — NOT a Provider, NOT a Guest; `ProviderManaged` → Guest or Host per provider semantics | +| `IsolationPosture::{VirtualMachine, ProviderManaged, UnsafeLocal}` | `d2b-realm-core/src/workload.rs:27` | `implemented-and-reachable` | `VirtualMachine` → Guest isolation; **`UnsafeLocal` → Host `spec.isolationPosture=no-isolation`**; no-isolation posture is preserved as a mandatory warning in Host status conditions, CLI/UI output, and audit records (`isolation=no-isolation`); it is never emitted as an OTEL metric label or span attribute | +| `WorkloadExecutionPosture { isolation, environment, display_environment, execution_identity, session_persistence }` | `d2b-realm-core/src/workload.rs:83` | `implemented-and-reachable` | Typed posture carried by launcher entries for CLI/desktop display. Canonical unsafe-local tuple at line 206–211: `isolation=unsafe-local`, `environment=systemd-user-manager-ambient`, `displayEnvironment=wayland-proxy-only`, `executionIdentity=authenticated-requester-uid`, `sessionPersistence=user-manager-lifetime`. Tuple maps to Host `status.observedPosture` and mandatory CLI/UI warnings; posture details appear in audit record body but are not OTEL labels | +| `EnvironmentPosture::{RuntimeManaged, SystemdUserManagerAmbient}` | `d2b-realm-core/src/workload.rs:39` | `implemented-and-reachable` | `SystemdUserManagerAmbient` is the unsafe-local environment posture; maps to Host `status.observedPosture.environment` field and audit record body; not emitted as an OTEL label | +| `ExecutionIdentityPosture::{WorkloadUser, ProviderManaged, AuthenticatedRequesterUid}` | `d2b-realm-core/src/workload.rs:61` | `implemented-and-reachable` | `AuthenticatedRequesterUid` is the unsafe-local identity posture; maps to Host `spec.defaultUserRef=User/` and audit label `execution_identity=authenticated-requester-uid` | +| `SessionPersistencePosture::{RuntimeManaged, UserManagerLifetime}` | `d2b-realm-core/src/workload.rs:73` | `implemented-and-reachable` | `UserManagerLifetime` is the unsafe-local session posture; maps to Host status field noting session lifetime bound to the systemd user manager | +| `UnsafeLocalWorkloadsJson`, `UnsafeLocalWorkload`, `UnsafeLocalLauncherItem::{Exec, Shell}`, `UnsafeLocalExecItem`, `UnsafeLocalShellItem`, `UnsafeLocalShellPolicy` | `d2b-core/src/unsafe_local_workloads.rs:16,47,106,129,141,150` | `implemented-and-reachable` (consumed by `d2b-core/src/bundle_resolver.rs:85,106`) | Private configured-item contract loaded via bundle resolver. `UnsafeLocalWorkload.identity` → Host `metadata.name` + `spec.defaultUserRef`; `UnsafeLocalShellPolicy.{defaultName, maxSessions}` → Host `spec.shellPolicy`; `UnsafeLocalExecItem` → Host launcher item contract. Constants: `MAX_UNSAFE_LOCAL_WORKLOADS=256`, `MAX_LAUNCHER_ITEMS_PER_WORKLOAD=64`, `MAX_UNSAFE_LOCAL_SHELL_SESSIONS=64` → Host cardinality bounds | +| `HelperRegistry`, `bind_helper_socket`, `dispatch_launch`, `HelperReply`, `HelperSnapshot`, `active_generation()` | `d2bd/src/unsafe_local_helper.rs:41,62,149,167,176,208,221` | `implemented-and-reachable` (live in `d2bd/src/lib.rs:1346–1468`) | Per-uid launch broker for unsafe-local helper sessions, bound at daemon startup (`d2bd/src/lib.rs:1356`). Maps to Zone runtime Host/Process broker: `dispatch_launch` → Process launch request; `HelperRegistry.allowed_uids` → Host subject UID allowlist derived from `spec.defaultUserRef` | +| `d2b.realms..policy.allowUnsafeLocal` | `nixos-modules/options-realms.nix:346` | `generated-or-eval-contract` | Gate option that permits `kind = "unsafe-local"` workloads in a realm; assertion at `nixos-modules/assertions.nix:730` blocks unsafe-local without this flag. Maps to a separate Host admission gate for user-domain unsafe-local resources, not Zone.spec | +| `kind = "unsafe-local"` enum value, doc "Host-user process runtime with no isolation boundary" | `nixos-modules/options-realms-workloads.nix:221,233` | `generated-or-eval-contract` | Nix workload `kind` enum value for unsafe-local Host resources. Maps to Host resource with `spec.defaultDomain=user` authored via `d2b.zones..resources. = { type = "Host"; spec = { ... }; };` (ADR046-zone-control-008) | +| Assertion: `!unsafeLocal \|\| realm.policy.allowUnsafeLocal` | `nixos-modules/assertions.nix:730` | `generated-or-eval-contract` | Eval-time gate; maps to Host admission enforcement for unsafe-local creation outside Zone.spec | + +### 16.3 Required delta + +None of the following exist in baseline: + +- Zone, ZoneLink, Provider, Role, RoleBinding resource schemas (Rust structs + JSON schemas) +- redb store physical tables for all seven types +- Core-controller handlers for all seven types +- Native RBAC engine (Role index + RoleBinding evaluation) +- Bootstrap authorization (compiled constant policy superseded by stored RBAC) +- `d2b.zones.*` Nix options and resource compiler +- Audit/OTEL instrumentation per §13 + +### 16.4 Reuse path summary + +| Baseline symbol | Reuse action | Destination | +| --- | --- | --- | +| `is_label()`, `LABEL_PATTERN`, `MAX_ID_LEN`, `IdError` | extract | `d2b-contracts/src/v3/resource_ref.rs` ResourceName validator | +| `RealmControllerPlacement` label set | adapt (semantics change: process placement → component placement) | Provider `spec.components[].placement` field enum | +| `EntrypointMode::{HostResident,GatewayBacked}` | adapt | ZoneLink transport binding selector | +| `RealmTransportBinding::{LocalUnixSocket,RemoteRealmTransport,ProviderRealmTransport}` | adapt | ZoneLink `spec.transportProviderRef` + transport settings variants | +| `SecurePeerSession` (Noise KK) | copy/adapt via main `a1cc0b2d` | ComponentSession Noise KK handshake (ADR046-zone-control-018) | +| `SessionLifecycle`, `SessionPhase` | adapt | ZoneLink session reconnect loop and connection detail fields (`status.connected`, `status.lastConnectedAt`); `Connecting`/`Established` current evidence phases do not become `ZoneLink.status.phase` values | +| `RouteRealmClass`/`RoutePlacementClass` label strings | reuse label values | ZoneLink OTEL telemetry labels (§13.2) | +| `workload_lists_and_advertises()`, `display_fails_closed_when_unsupported()` | adapt | Provider trust/conformance check (ADR046-zone-control-003) | +| `RuntimeCapabilitySet`, `WorkloadCapabilitySet`, `NodeCapabilitySet` | adapt | Provider component `supportedCapabilities` fields | +| `ProviderError`, `ErrorKind`, `RetryHint` | adapt | Provider `status.conditions[].message` + error bounds | +| `ProcessRole` variants | adapt (each → Process/EphemeralProcess resource `spec.role`) | Provider component type identifiers | +| `authorize_peer()` verb table, `PeerRole` two-role model | adapt | Bootstrap authorization constant policy (ADR046-zone-control-006) | +| `map_local_unix_daemon_access()` SO_PEERCRED derivation | adapt | Role/RoleBinding subject identity derivation | +| `providerType` submodule, label regex, `placement` option | adapt | `d2b.zones..providers.*` Nix option submodule | +| `WorkloadExecutionPosture` unsafe-local posture tuple | adapt | Host `status.observedPosture`; mandatory CLI/UI posture warnings; audit record body (no OTEL posture labels) | +| `UnsafeLocalShellPolicy.{defaultName, maxSessions}`, `MAX_UNSAFE_LOCAL_SHELL_SESSIONS=64`, `MAX_UNSAFE_LOCAL_WORKLOADS=256` | adapt | Host `spec.shellPolicy` bounds and per-Zone Host cardinality bounds | +| `HelperRegistry.dispatch_launch`, `bind_helper_socket`, `allowed_uids` | adapt | Zone runtime Host process launch broker; subject UID allowlist derived from `spec.defaultUserRef` | +| `d2b.realms..policy.allowUnsafeLocal` option | adapt | `d2b.zones..resources.*` Host ResourceType admission gate (separate opt-in required before `type = "Host"` with `defaultDomain=user` is permitted) | + +### 16.5 Replacement and deletion + +| Baseline artifact | Replacement trigger | Retention period | +| --- | --- | --- | +| `realm_stubs.rs` dead-code module | ADR046-zone-control-018 ComponentSession integration | Until gateway-mode work integrates live paths | +| `realm_access_resolver.rs` unwired module | ZoneLink resource + ComponentSession replaces entrypoint-table resolution | Same wave as ADR046-zone-control-002 | +| `PeerRole` coarse per-connection auth (`d2bd/admission.rs`) | Role/RoleBinding RBAC engine covers all verb gates | ADR046-zone-control-004/006 complete | +| `d2b-realm-provider` trait crate (all traits) | Provider resource + ComponentSession service definitions | ADR046-zone-control-017 integration complete | +| `realm-controllers.json` + `realm-identity.json` bundle artifacts | Zone self resource + ZoneLink resources in store | ADR046-zone-control-001/002 integration + Nix compiler (ADR046-zone-control-007) | +| `options-realms.nix`, `options-realms-workloads.nix`, `options-realms-network.nix` | `d2b.zones.*` Nix options | Purge wave (after all consumers migrate) | +| `realm-entrypoints.json` CLI config | ZoneLink access resolution via resource API | ADR046-zone-control-002 + CLI routing update | +| `unsafe-local-workloads.json` bundle artifact (`UnsafeLocalWorkloadsJson`) | Host resource store entries + Process Providers | ADR046-zone-control-008 complete | +| `d2bd/src/unsafe_local_helper.rs` `HelperRegistry` / `bind_helper_socket` | Zone runtime Host/Process broker | ADR046-zone-control-008 integration | +| `kind = "unsafe-local"` Nix enum value in `options-realms-workloads.nix` | `d2b.zones..resources. = { type = "Host"; spec = { defaultDomain = "user"; ... }; }` authoring | Purge wave (with full `options-realms*.nix` removal) | + +--- + +## 17. Implementation work items + +### ADR046-zone-control-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-001` | +| Dependency/owner | ADR046-object-001 (resource envelope); ADR046-store-001 (redb store); ADR046-identities-001 (types) | +| Current source | `packages/d2b-realm-core/src/ids.rs` (`RealmId`, `LABEL_PATTERN = "^[a-z][a-z0-9-]*$"`, `is_label()`, `MAX_ID_LEN = 128`, `IdError` — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-realm-core/src/realm.rs` (`RealmPath`, `MAX_REALM_LABELS = 16`, `RealmControllerPlacement`, `EntrypointMode` — `implemented-and-reachable`, baseline `b5ddbed6`); Zone resource schema: `ADR-only` | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/zone.rs`; `packages/d2b-core-controller/src/zone.rs` | +| Detailed design | Zone ResourceType schema with `spec = {}`; self-resource enforcement; store_meta binding checks; phase/conditions; cardinality-1 enforcement; `zone-drain` finalizer; canonical JSON schema. Nix Zone options include compiler-only `parentZone`: required for every non-root Zone, forbidden on `local-root`, and never emitted into `Zone.spec` Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`RealmId`→`ResourceName` validators, `RealmPath` depth bound→ZoneLink depth, `RealmControllerPlacement`→Provider component placement); new (Zone resource schema/store table/handler). | +| Integration | Zone runtime open/upgrade verifies `Zone/` self resource; configuration publication handler creates/reconciles Zone; resource API rejects cross-Zone refs | +| Data migration | Destructive d2b 3.0 reset; no v2/Realm Zone resource import | +| Validation | `zone-self-resource-enforced`, `zone-uid-mismatch-quarantine`, `zone-name-mismatch-rejected`, `zone-cardinality-one`, `zone-cross-zone-ref-rejected`, `zone-owner-rejected`, `zone-deletion-only-on-drain` | +| Removal proof | `d2b-realm-core` Realm struct removed only after Zone resource integration is live in Zone runtime | + +### ADR046-zone-control-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-002` | +| Dependency/owner | ADR046-zone-control-001; ADR046-zone-control-018 (ComponentSession) | +| Current source | `packages/d2b-realm-router/src/session.rs` (`PeerSession` — `implemented-but-unwired`, baseline `b5ddbed6`; no live call sites in d2bd or d2b — only reachable through `realm_stubs.rs` dead code in d2bd); `packages/d2b-realm-router/src/secure_session.rs` (`SecurePeerSession`, Noise-based — `implemented-but-unwired`); `packages/d2b-realm-router/src/mux_session.rs` (`MuxSession` — `implemented-but-unwired`); `packages/d2b-realm-router/src/session_lifecycle.rs` (`SessionLifecycle`, `SessionPhase` — `implemented-but-unwired`); `packages/d2b-realm-core/src/route_engine.rs` (`RouteTreeEngine`, `admit_advertisement()`, `decide_route()`, `RoutePruneReport` — `implemented-but-unwired`; exported from `d2b_realm_core::lib.rs:122` but zero call sites in live daemon or CLI code, tests only); `packages/d2b-realm-core/src/identity_store.rs` (`RealmIdentityStore`, `EnrollmentRecord`, `ChildKeyPin` — `implemented-and-reachable`); `packages/d2b-realm-core/src/realm.rs` (`RealmPath`, `MAX_REALM_LABELS = 16` — `implemented-and-reachable`); `packages/d2b-realm-core/src/access.rs` (`RealmTransportBinding::{LocalUnixSocket,RemoteRealmTransport,ProviderRealmTransport}` — `implemented-and-reachable` in CLI routing path); ZoneLink resource schema and cursor tables: `ADR-only` | +| Reuse source | main `a1cc0b2d`: `packages/d2b-session/src/lifecycle.rs`, `d2b-session-unix/src/adapter.rs` for reconnect/transport precedents | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/zone_link.rs`; `packages/d2b-core-controller/src/zone_link.rs` | +| Detailed design | Child-local ZoneLink schema with self-matching `spec.childZoneName` plus same-Zone `spec.transportProviderRef`, `spec.transportSettings`, and `spec.transportCredentials`; at most one uplink resource per non-root Zone and none in local root; child-store cursor/intent state; reconnect loop with exponential backoff; local intent queue (max 256 entries); local child UID change detection; drain finalizer; no reciprocal parent-store resource; compiler-only `parentZone` selects the one allocator owner, while that allocator owns privileged listener/placement/route allocation effects and exposes only a sealed bootstrap/allocation interface Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`SecurePeerSession` Noise model → ComponentSession Noise KK; `SessionLifecycle`/`SessionPhase` → ZoneLink session reconnect loop and connection detail fields; `Connecting`/`Established` current evidence phases drive `status.connected` and `status.phase` transitions to `Pending`/`Ready`, not direct phase values; `RouteTreeEngine.decide_route()` → cursor tracking; `RealmIdentityStore` enrollment → ZoneLink child key-pin). | +| Integration | child core-controller zone_link handler; child redb `zone_link_cursors` table; Nix-compiled `parentZone` bootstrap topology; selected parent allocator and route engine through sealed allocation authority; d2b-bus transport resolver; ComponentSession lifecycle | +| Data migration | Destructive reset; no v2 Realm peer migration | +| Validation | `zonelink-reconnect-child-uid-change`, `zonelink-disconnect-unknown-phase`, `zonelink-intent-queue-limit`, `zonelink-disabled-no-reconnect`, `zonelink-child-auth-denied-failed`, `zonelink-drain-closes-session`, `zonelink-child-name-matches-store`, `zonelink-one-child-local-uplink`, `zonelink-parent-bootstrap-binding`, `zonelink-parent-has-no-reciprocal-row` | +| Removal proof | `realm_stubs.rs` (`ApiFrontend`, `PeerOperationRouter`, `TargetResolver`) removed after ComponentSession integration (ADR046-zone-control-018); `realm_access_resolver.rs` module removed after ZoneLink replaces entrypoint-table resolution; gateway `PeerSession`/`SecurePeerSession` session types remain as dead code in d2b-realm-router until Provider session migration wave | + +### ADR046-zone-control-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-003` | +| Dependency/owner | ADR046-zone-control-001; ADR046-api-001; ADR046-zone-control-017 | +| Current source | `packages/d2b-realm-provider/src/provider.rs` (`HostSubstrateProvider`, `RuntimeProvider`, `WorkloadProvider`, `DurableExecutionProvider`, `InfrastructureProvider`, `NodeProvider` traits — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-realm-provider/src/capabilities.rs` (`RuntimeCapabilitySet`, `WorkloadCapabilitySet`, `NodeCapabilitySet` — `implemented-and-reachable`); `packages/d2b-realm-provider/src/conformance.rs` (`workload_lists_and_advertises`, `display_fails_closed_when_unsupported` — `implemented-and-reachable`); `packages/d2b-realm-provider/src/error.rs` (`ProviderError`, `ErrorKind`, `RetryHint`, `ProviderDiagnostic` — `implemented-and-reachable`); `packages/d2b-realm-core/src/ids.rs` (`ProviderId`, label-shaped — `implemented-and-reachable`); `packages/d2b-core/src/processes.rs` (`ProcessRole::{CloudHypervisorRunner, Virtiofsd, Swtpm, GpuRenderNode, Audio, Video, QemuMediaRunner, VsockRelay, OtelHostBridge, Usbip}`, `VmProcessDag`, `ProcessNode` — `implemented-and-reachable`); Provider resource schema and API catalog: `ADR-only` | +| Reuse source | main `a1cc0b2d`: any `d2b-provider-toolkit` registry/descriptor patterns named in ADR046-zone-control-017 sub-items | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/provider.rs`; `packages/d2b-core-controller/src/provider_lifecycle.rs`; `packages/d2b-core-controller/src/api_catalog.rs` | +| Detailed design | Provider resource schema with all spec fields from §4.3, including resolved component bounds (max 8 controllers, 8 services, 32 worker templates, 16 ResourceTypes per controller); trust/conformance/config validation; component descriptor validation; dependency alias resolution; API binding with permission intersection; lifecycle policies; Nix Provider installation options; Provider crate layout enforcement per §4.8 (see ADR046-pkg-001) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`workload_lists_and_advertises`/`display_fails_closed_when_unsupported` conformance behavior; `RuntimeCapabilitySet`/`WorkloadCapabilitySet`/`NodeCapabilitySet` → Provider component `supportedCapabilities`; `ProviderError`/`RetryHint` → Provider lifecycle error schema; `ProviderId` → Provider `metadata.name` validator; `ProcessRole` variants → Provider component type identifiers). | +| Integration | Zone config publication installs Provider resources; API catalog handler binds exported ResourceTypes; Provider/system-core and Provider/system-minijail are bootstrap exceptions with pre-created records | +| Data migration | Full reset; Provider packages recompiled and re-registered per new schema | +| Validation | All §15.3 Provider tests including the resolved bounds checks | +| Removal proof | `d2b-realm-provider` trait crate removed per ADR046-zone-control-017 after Provider resource integration | + +### ADR046-zone-control-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-004` | +| Dependency/owner | ADR046-zone-control-001; ADR046-api-002 | +| Current source | `packages/d2bd/src/admission.rs` (`PeerRole::{Admin, Launcher, HostShutdown}`, `PeerIdentity`, `authorize_peer()`, `verb_requires_admin()`, `verb_allowed_for_host_shutdown()` — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-daemon-access/src/lib.rs` (`LocalUnixAllowlistRole::{Admin, Launcher}`, `DaemonAccessPolicyRole::RealmAdmin`, `DaemonAccessDecision`, `MappedDaemonAccessPrincipal`, `map_local_unix_daemon_access()` — `implemented-and-reachable`); Role resource schema and RBAC index: `ADR-only` | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/role.rs`; `packages/d2b-core-controller/src/authz.rs` | +| Detailed design | Role resource schema with rule schema from §5.3 and resolved bounds (32 rules, 16 resourceTypes per rule, 64 resourceNames, 32 executionRefs); separate closed resource/session verb enums including `relay`; core-generated ZoneLink ownership, exact adjacent-Zone enrollment selector, exact target bounds, and explicit admin-policy exception admission; explicit wildcard enforcement; index builder; phase/conditions; `role-binding-drain` finalizer; generated Nix Role option help; audit/OTEL instrumentation Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`verb_requires_admin()` verb table → Role verb enum; `DaemonAccessDecision` error types → Role admission errors; `MappedDaemonAccessPrincipal` → subject identity model). | +| Integration | Authorization evaluator (ADR046-api-002) reads Role index entries; core `authz` handler owns reconcile loop | +| Data migration | Initial Roles generated from Nix config; no v2 Role resource import | +| Validation | All §15.4 Role tests including closed-enum, relay origin/scope/target-verb, and resolved-bounds checks | +| Removal proof | `daemon-access` capability enum removed after RBAC Role engine covers all access decisions | + +### ADR046-zone-control-005 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-005` | +| Dependency/owner | ADR046-zone-control-004 | +| Current source | `packages/d2b-daemon-access/src/lib.rs` (`DaemonAccessDecision::Authorized { role: DaemonAccessPolicyRole }`, `DaemonAccessAdmissionSource`, `map_remote_daemon_access()` — `implemented-and-reachable`, baseline `b5ddbed6`; these implement the current coarse binding decision per-connection); RoleBinding resource schema: `ADR-only` | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/role_binding.rs`; `packages/d2b-core-controller/src/authz.rs` (shared with Role handler) | +| Detailed design | RoleBinding resource schema with no expiry field and max 128 subjects; subject resolution and UID binding; external principal selector; scope narrowing intersection; revocation; immediate deletion with cache invalidation; Nix RoleBinding options Primary reuse disposition: `adapt`. Preserved source-plan detail: new (RoleBinding resource schema/store table/handler); adapt (`DaemonAccessAdmissionSource` identity fields → subject selector; `map_remote_daemon_access()` logic → subject UID-binding behavior). | +| Integration | Subject resolution uses store owner index; authorization evaluator reads combined Role+narrowing entry; revocation/update/delete flow uses the normal resource lifecycle | +| Data migration | Initial RoleBindings generated from Nix config | +| Validation | All §15.5 RoleBinding tests, including the 128-subject admission bound and no-expiry lifecycle model | +| Removal proof | Not applicable; new type | + +### ADR046-zone-control-006 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-006` | +| Dependency/owner | ADR046-zone-control-004, ADR046-zone-control-005; Zone runtime | +| Current source | `packages/d2bd/src/admission.rs` (`authorize_peer()`, `verb_requires_admin()`, `verb_allowed_for_host_shutdown()`, `PeerRole::{Admin, Launcher, HostShutdown}` — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-daemon-access/src/lib.rs` (`LocalUnixAllowlistRole`, `DaemonAccessDecision::Authorized/Denied`, `DaemonAccessAdmissionSource`, `MappedDaemonAccessPrincipal`, `map_local_unix_daemon_access()` — `implemented-and-reachable`); `packages/d2bd/src/lib.rs` (`LoadedRealmControllersConfig`, `LoadedRealmIdentityConfig` startup state — `implemented-and-reachable`); compiled bootstrap constant policy: `ADR-only` | +| Reuse action | adapt | +| Destination | `packages/d2b-resource-api/src/bootstrap_authz.rs`; Zone runtime startup path | +| Detailed design | Compiled bootstrap authorization as described in §9; exact subjects (system-core, system-minijail); closed verb table; non-configurable enforcement; atomic supersession after stored RBAC publishes; out-of-band reset path Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`authorize_peer()`/`verb_requires_admin()` verb table → bootstrap constant policy verb table; `PeerRole` two-variant model → system-core/system-minijail bootstrap subjects; `map_local_unix_daemon_access()` SO_PEERCRED mapping → bootstrap subject derivation; `LoadedRealmControllersConfig` startup path → Zone runtime startup bootstrap init sequence). | +| Integration | Resource API authorization layer checks bootstrap policy before stored RBAC; supersession is triggered by first `IndexBuilt=True` event from authorization handler | +| Data migration | Bootstrap is always freshly compiled; no migration | +| Validation | All §15.6 bootstrap tests | +| Removal proof | `daemon-access` bootstrap stub removed after Zone runtime bootstrap authz integrates | + +### ADR046-zone-control-007 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-007` | +| Dependency/owner | ADR046-identities-002; ADR046-zone-control-001; ADR046-zone-control-002; ADR046-zone-control-003; ADR046-zone-control-004; ADR046-zone-control-005; ADR046-zone-control-009; ADR046-zone-control-010 | +| Current source | `nixos-modules/options-realms.nix` (`providerKind = "^[a-z][a-z0-9-]*$"` label regex matching `LABEL_PATTERN` in `ids.rs`; `providerType` submodule with `enable`/`kind`/`placement`/`freeformType` fields; `d2b.realms..providers.*` attrset — `generated-or-eval-contract`, baseline `b5ddbed6`); `nixos-modules/options-realms-workloads.nix` (`d2b.realms..workloads.*` submodule shape — `generated-or-eval-contract`); `d2b.zones.*` Nix options: `ADR-only` | +| Reuse action | adapt | +| Destination | `nixos-modules/options-zones.nix`; `nixos-modules/resources-zone-control.nix`; extended `nixos-modules/index.nix` | +| Detailed design | `d2b.zones..*` Nix options for Zone/ZoneLink/Provider/Role/RoleBinding/Quota/EmergencyPolicy authoring; compiler-only scalar `parentZone` required on non-root Zones and forbidden on `local-root`; Nix eval-time validation of parent existence, one resolved parent, self/cycle rejection, 16-name ancestry depth, ResourceRefs, separate resource/session verb enums (including session-only `relay`), relay origin/scope restrictions, digest format, subject types, child-local ZoneLink self-name/one-uplink/local transport-ref constraints, and the no-expiry RoleBinding model; generated help carries the relay semantics; canonical JSON serialization; generation-bound resource bundle output; the compiler seals the validated parent map into allocator bootstrap topology without emitting `parentZone` into `Zone.spec` or reciprocal resources Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (`providerType` submodule → Provider option submodule; label regex `^[a-z][a-z0-9-]*$` retained unchanged; `placement` option → Provider component placement; `d2b.realms.*` option namespace → `d2b.zones.*` option namespace). | +| Integration | Nix compiler → sealed allocator topology for `parentZone`; resource compiler → configuration publication handler → Zone store; bootstrap Provider records auto-generated | +| Data migration | Full reset; Nix realm options (`d2b.realms.*`) remain until purge wave | +| Validation | nix-unit vectors for each Zone control type schema; closed resource/session verb and relay restriction vectors; cross-field constraint tests; rendered JSON contract tests (`make test-drift`) | +| Removal proof | `nixos-modules/options-realms.nix` and related realm options removed only after Zone resource Nix integration is live | + +### ADR046-zone-control-008 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-008` | +| Dependency/owner | ADR046-zone-control-001 (Zone resource); ADR046-zone-control-003 (Provider/system-core installed) | +| Current source | `d2b-realm-core/src/workload.rs:13,27,83` (`WorkloadProviderKind::UnsafeLocal`, `IsolationPosture::UnsafeLocal`, `WorkloadExecutionPosture` with canonical unsafe-local tuple at lines 206–211: `isolation=unsafe-local`, `environment=systemd-user-manager-ambient`, `executionIdentity=authenticated-requester-uid`, `sessionPersistence=user-manager-lifetime` — `implemented-and-reachable`, baseline `b5ddbed6`); `d2b-core/src/unsafe_local_workloads.rs:16,47,106,150` (`UnsafeLocalWorkloadsJson`, `UnsafeLocalWorkload`, `UnsafeLocalShellPolicy`, `MAX_UNSAFE_LOCAL_SHELL_SESSIONS=64`, `MAX_UNSAFE_LOCAL_WORKLOADS=256` — `implemented-and-reachable`); `d2bd/src/unsafe_local_helper.rs` (`HelperRegistry`, `bind_helper_socket`, `dispatch_launch` — `implemented-and-reachable`, live in `d2bd/src/lib.rs:1346–1468`); `nixos-modules/options-realms.nix:346` (`policy.allowUnsafeLocal` — `generated-or-eval-contract`); `nixos-modules/options-realms-workloads.nix:221,233` (`kind = "unsafe-local"`, doc "no isolation boundary" — `generated-or-eval-contract`) | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/host.rs` (Host resource schema, user-domain variant); `packages/d2b-core-controller/src/host_user.rs` (reconciler owned by Provider/system-core); Nix Host authoring via `d2b.zones..resources. = { type = "Host"; spec = { ... }; };` (validated per ResourceTypeSchema; no separate `options-zones-hosts.nix` submodule) | +| Detailed design | Host ResourceType schema for user-domain variant: `spec.defaultDomain=user`, `spec.allowedDomains=[user]`, `spec.defaultUserRef=User/`, `spec.shellPolicy` (adapted from `UnsafeLocalShellPolicy`), `spec.launcherItems` (adapted from `UnsafeLocalLauncherItem`). No-isolation posture recorded in `status.observedPosture`. A dedicated Host admission gate blocks unsafe-local Host creation without opt-in. Mandatory no-isolation warning in Host `status.conditions[0].message` and CLI/UI output for all Host commands. No-isolation posture included in audit record body (`isolation=no-isolation`); it is never emitted as an OTEL metric label value or span attribute. Child processes use standard Process Providers — no Provider resource with name or kind `unsafe-local` is created. Cardinality bounds: max 256 user-domain Hosts per Zone, max 64 launcher items per Host, max 64 shell sessions per Host. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (`WorkloadExecutionPosture` unsafe-local posture tuple → Host `status.observedPosture`; posture details in audit record body, not OTEL labels; `UnsafeLocalShellPolicy.{defaultName,maxSessions}` → Host `spec.shellPolicy`; `HelperRegistry.dispatch_launch` → Zone runtime Host process launch broker; `policy.allowUnsafeLocal` → dedicated Host admission gate); new (Host ResourceType user-domain schema with `defaultDomain`, `allowedDomains`, `defaultUserRef`, `shellPolicy`, cardinality bounds). | +| Integration | Provider/system-core bootstrap exception creates Host resources from Zone config publication; Host controller reconciles `defaultUserRef` via User resource lookup; `HelperRegistry` in Zone runtime becomes per-uid launch broker for Host Process launch; Host admission layer enforces the `allowUnsafeLocal` gate before Host creation | +| Data migration | Destructive reset; `unsafe-local-workloads.json` bundle artifact and `HelperRegistry` replaced by Host resource store entries + Process Provider launch | +| Validation | `host-user-domain-no-isolation-warning-required`, `host-user-only-disallows-system-domain`, `host-allowUnsafeLocal-gates-creation`, `host-defaultUserRef-user-type-required`, `host-shell-policy-max-sessions-bound`, `host-launcher-item-max-count-bound`, `host-audit-body-isolation-label-present`, `host-otel-no-posture-label`, `host-cli-no-isolation-warning-present` | +| Removal proof | `d2bd/src/unsafe_local_helper.rs` `HelperRegistry` removed after Zone runtime Host/Process broker integration; `unsafe-local-workloads.json` bundle artifact removed after Host resource store replaces it; `kind = "unsafe-local"` Nix enum value removed in purge wave | + + +### ADR046-zone-control-009 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-009` | +| Dependency/owner | ADR046-zone-control-001; Zone store (ADR046-store-001); Quota handler owner | +| Current source | `packages/d2b-realm-core/src/ids.rs` (`LABEL_PATTERN`, `MAX_ID_LEN` — ResourceName validation for quotaRef); `packages/d2b-core/src/unsafe_local_workloads.rs:16–164` (`MAX_UNSAFE_LOCAL_WORKLOADS=256`, etc. — bound evidence for quota ceiling defaults); no current quota ResourceType exists (`ADR-only`) | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-contracts/src/v3/quota.rs`; `packages/d2b-core-controller/src/quota.rs`; `packages/d2b-resource-api/src/quota_gate.rs` | +| Detailed design | Quota resource schema with all spec/status fields from §7; ceiling bounds enforcement at admission (hard policy: reject over-quota with `quota-exceeded`; soft policy: warn); usage index built from resource scan with `quotaRef` field; per-ResourceType ceiling in `perTypeCeilings`; `core.quota-drain` finalizer that blocks Quota deletion until all dependent resources with `spec.quotaRef` pointing to this Quota are reassigned or deleted by authorized owners/operators — the controller never issues spec-updates to clear `quotaRef` on other resources; `dependentCount` status field updated from dependency index; audit event `quota-check` per admission; Nix Quota options per §7.7 | +| Integration | Resource API admission gate (`packages/d2b-resource-api/src/quota_gate.rs`) called for every `create` verb; Zone controller triggers quota reconcile on resource-created/deleted/quotaRef-changed events; quota handler registered in core-controller process | +| Data migration | Full reset; no prior Quota resources exist | +| Validation | `quota-ceiling-hard-reject`, `quota-ceiling-soft-warn`, `quota-ceiling-pertype`, `quota-drain-blocks-on-dependents`, `quota-over-quota-status`, `quota-nix-eval-bounds`, `quota-nix-build-pertype-unknown-type` | +| Removal proof | Additive; no existing code removed | + +### ADR046-zone-control-010 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-010` | +| Dependency/owner | ADR046-zone-control-001; Zone store (ADR046-store-001); EmergencyPolicy handler owner | +| Current source | `packages/d2bd/src/lib.rs` admission gate (`authorize_peer()` — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-daemon-access/src/lib.rs` (`DaemonAccessDecision::Denied` — basis for admission rejection pattern); no EmergencyPolicy ResourceType exists (`ADR-only`) | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-contracts/src/v3/emergency_policy.rs`; `packages/d2b-core-controller/src/emergency_policy.rs`; `packages/d2b-resource-api/src/emergency_gate.rs` | +| Detailed design | EmergencyPolicy resource schema from §8; union semantics: multiple `enabled=true` policies allowed simultaneously; effective scope = OR of all enabled policies' scope flags; effective `drainDeadlineSeconds` = minimum across enabled policies; scope flag evaluation: `stopNewAdmissions` signal to API admission gate, `disconnectZoneLinks` graceful signal to ZoneLink handler, `stopProviderProcesses` suppresses Process launch and sends stop signal to running Provider component Processes (does NOT set `deletionRequestedAt` on Process resources; reconciliation resumes on deactivation), `drainOngoingOperations` deadline drain; `core.emergency-drain` finalizer for enabled policies; audit events `emergency-policy-activated` / `emergency-policy-deactivated`; `reason` field stored in spec and included in audit record body (never in OTEL metric label values, structured log labels, or status fields); Nix EmergencyPolicy options per §8.7 | +| Integration | API admission gate checks union of enabled EmergencyPolicies before every admitted request; ZoneLink handler subscribes to EmergencyPolicy watch triggers; Provider process lifecycle listens for effective `stopProviderProcesses` and resumes launch on deactivation | +| Data migration | Full reset; no prior EmergencyPolicy resources exist | +| Validation | `emergency-policy-activates-gate`, `emergency-policy-disconnects-zonelinks`, `emergency-policy-union-most-restrictive`, `emergency-policy-multi-enabled-combined-scope`, `emergency-policy-stop-processes-no-delete`, `emergency-policy-deactivation-restores-gate`, `emergency-policy-drain-finalizer`, `emergency-nix-eval-drain-deadline-bound-tightest` | +| Removal proof | Replaces the inline `emergencyDisable` field from the proposed Zone.spec option B; that option was not implemented | + +--- + +All items in this subsection copy or adapt from main commit `a1cc0b2da4a08ca3240a770a972fe4da6f912bef` +(the ADR 0045 in-progress implementation). Main is **not** current pre-ADR45 v3 behavior. +Citations use the prefix `main a1cc0b2d:` to distinguish from baseline `b5ddbed6` citations. +Evidence class for all: `main-reuse-source`. + +### ADR046-zone-control-011 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-011` | +| Dependency/owner | ADR046-identities-001; ADR046-store-001 | +| Current source | main `a1cc0b2d`: `packages/d2b-session/src/lifecycle.rs` (`SessionPhase::{Established,Disconnected,Reconnecting,Closing,Closed}`, `KeepaliveAction::{None,SendPing,Close}`, `SessionLifecycle::new/phase/on_activity/poll_keepalive/receive_pong/disconnect/begin_reconnect/reconnect_established/close` — lines 9–195); `packages/d2b-session/src/engine.rs` (`SessionEngine`, `SessionEvent`, `establish_initiator/responder`, `reconnect_initiator/responder`, `call/complete_call/cancel_call`, `send_ttrpc/receive`, `open/send/grant_credit/close/reset_named_stream`, `send_attachments`, `drive_keepalive`, `fail_closed`, encode/decode helpers for keepalive/cancel/close/stream-control/attachment — lines 33–995); `packages/d2b-session/src/driver.rs` (`ComponentSessionDriver` trait 18 methods, `SessionDriverHandle`, `DriverQueues` 4 event queues + named-send FIFO, `DRIVER_COMMAND_CAPACITY=128`, `DRIVER_EVENT_CAPACITY=128`, backpressure on enqueue and dequeue, test `cancelled_immediate_receiver_restores_queued_event` — lines 20–718); `packages/d2b-session/src/streams.rs` (`StreamId`, `StreamPhase::{Open,HalfClosedLocal,HalfClosedRemote,Closed,Reset}`, `StreamEvent::{Data,RemoteClosed,Reset}`, `NamedStreamMux::open/reserve_send/grant_send_credit/refund_send_credit/receive_data/close_local/receive_close/reset/remove_terminal/active` — lines 7–237); `packages/d2b-session/src/transport.rs` (`OwnedTransport` trait, `TransportDescriptor`, `TransportPacket`, `TransportError::{Disconnected,Truncated,LimitExceeded,InvalidAttachment,Other}` — lines 9–106); `packages/d2b-session/src/error.rs` (`SessionError`, all `From<>` mappings covering `ContractError`, `BinaryError`, `SequenceError`, `FragmentSequenceError`, `HandshakeRejectReason`, `TransportError` — lines 10–143); supporting modules: `attachment.rs`, `bootstrap.rs`, `cancellation.rs`, `deadline.rs`, `fragmentation.rs`, `metrics.rs`, `record.rs`, `scheduler.rs`, `server.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/{lifecycle,engine,driver,streams,transport,error}.rs` (new crate `d2b-bus`) | +| Detailed design | **Selected**: full `SessionLifecycle` FSM including keepalive ping-nonce/timeout close (`poll_keepalive` lines 81–124), nonce-exhaustion close (`NonceExhausted` remediation `ReplaceGeneration`), reconnect attempt counting + window expiry (`begin_reconnect` lines 147–174), generation increment on reconnect; `ComponentSessionDriver` 18-method trait as the d2b-bus driver contract; `SessionEngine` full frame encode/decode, keepalive, named-stream credit/half-close/reset; `NamedStreamMux` credit model with send/receive credit, phase transitions, terminal removal; `DriverQueues` backpressure (`QueueBackpressure`) on both deliver and receive paths; all `SessionError`/`From<>` mapping chains. **Excluded ADR45 assumptions**: `establish_initiator_with_generation_discovery()` (lines 102–123): ADR45 initiator-probes-server for current generation; in v3 the generation lives in the Zone resource store — use `establish_initiator()` directly. OTEL labels in `metrics.rs` reference ADR45 realm/workload dimensions — replace with Zone/ZoneLink labels per §13.2. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt. | +| Integration | `d2b-bus` crate is the sole session-transport dependency for ZoneLink (ADR046-zone-control-018), Provider component Processes (ADR046-zone-control-017), and the Zone resource API service layer (ADR046-api-001); `ComponentSessionDriver` is the interface ZoneLink uses to send/receive ttrpc frames and named streams | +| Data migration | Not applicable; new crate | +| Validation | `session-lifecycle-reconnect-attempts-exhausted`, `session-lifecycle-keepalive-timeout-closes`, `session-lifecycle-nonce-exhausted-close-with-replace-generation-remediation`, `session-driver-cancelled-receiver-restores-event` (ported from test at line 707), `named-stream-credit-half-close-then-remote-close-transitions-closed`, `named-stream-reset-cancels-pending-send` | +| Removal proof | `d2b-realm-router/src/{session.rs,secure_session.rs,mux_session.rs,session_lifecycle.rs}` removed after d2b-bus integration wave | + +### ADR046-zone-control-012 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-012` | +| Dependency/owner | ADR046-zone-control-011 | +| Current source | main `a1cc0b2d`: `packages/d2b-session-unix/src/adapter.rs` (`UnixSeqpacketTransport::new`, `UnixStreamTransport::new`, `PeerIdentityPolicy::{Pathname{verifier},InheritedSocketpair{expected_peer}}`, `OwnedUnixAttachment::file`, `UnixAttachmentPayload`, `consume_peer_credentials()` credential strip+verify, `map_validation_error`/`map_transport_error`, tests `inherited_first_packet_requires_credentials`/`inherited_first_packet_rejects_wrong_credentials` — lines 23–852); `packages/d2b-session-unix/src/socket.rs` (`AncillaryCapacity::from_policy`, `OutboundPacket::new`, `SeqpacketSocket`, `prearmed_seqpacket_pair()`, `SendBurst`/`PacketBurst`/`SentPacket`, `recv_one` with `DONTWAIT|CMSG_CLOEXEC`, `send_one` with `DONTWAIT|NOSIGNAL`, SCM_RIGHTS+SCM_CREDENTIALS, fairness-budget burst loops, tests `raw_control_scanner_rejects_unknown_and_partial_headers`/`raw_control_scanner_accepts_exact_rights_shape` — lines 35–698); `packages/d2b-session-unix/src/pidfd.rs` (`PidfdEvidence::new`, `ProcPidfdIdentityVerifier::verify` with double-read race guard, `ProcSelfFdInfoSource::read_pid_from_fdinfo`, `DigestEvidenceCallback` — lines 8–159); `packages/d2b-session-unix/src/credit.rs` (`CreditPool::new/reserve`, `CreditScope::{Packet,Request,Operation,Session,Process,Host}`, `CreditScopeSet::reserve/reserve_ingress`, `CreditBundle::acquire_dispatch`, `ProcessCreditLimit::derive` from `RLIMIT_NOFILE` — lines 13–286); `packages/d2b-session-unix/src/descriptor.rs` (`ReceivedPacket::verify/verify_first_packet_credentials`, `DescriptorPolicy`, `ObjectIdentity`, `PeerCredentials`, `FirstPacketCredentials`, sealed memfd `F_SEAL_WRITE|GROW|SHRINK|SEAL` — lines 21–617); `packages/d2b-session-unix/src/error.rs` (`UnixSessionError` 21 variants, Display strings — lines 4–92); `packages/d2b-session-unix/src/systemd.rs` (`ActivatedSeqpacketListener`, `SystemdActivationError`) | +| Reuse action | adapt | +| Destination | `packages/d2b-bus-unix/src/{adapter,socket,pidfd,credit,descriptor,error,systemd}.rs` (new crate `d2b-bus-unix`) | +| Detailed design | **Selected**: `UnixSeqpacketTransport` seqpacket adapter implementing `OwnedTransport`; `PeerIdentityPolicy` SO_PEERCRED verification (pathname + inherited socketpair); `prearmed_seqpacket_pair()` for bootstrap inherited-socketpair path; `CreditPool`/`CreditScopeSet` six-scope credit model with per-packet/request/operation/session/process/host reservation; `ProcessCreditLimit::derive()` from `RLIMIT_NOFILE` minus `RESERVED_CONTROL_FDS`; `ProcPidfdIdentityVerifier` double-read race guard on `/proc/self/fdinfo/` with executable + cgroup digest callbacks; sealed memfd four-seal enforcement; `AncillaryCapacity` derived from `AttachmentPolicy`; `UnixSessionError` 21-variant Display strings preserved verbatim for audit log compatibility; `consume_peer_credentials()` strips-and-verifies SCM_CREDENTIALS exactly once on first packet. **Excluded ADR45 assumptions**: `vsock.rs` (`FramedVsockTransport`, `NativeVsockListener`, `NativeVsockTransport`): ADR45 guest-control vsock transport; v3 ZoneLink uses Unix sockets for host-local; vsock for Guest connections is separate work. Specific socket paths (`PUBLIC_SOCKET_PATH`, `BROKER_SOCKET_PATH`) are replaced by Zone-resource-managed paths. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt. | +| Integration | `d2b-bus-unix` provides `OwnedTransport` impl consumed by `d2b-bus` `SessionEngine`; Zone runtime passes accepted seqpacket socket to `UnixSeqpacketTransport::new`; Process pidfd identity verifier integrates with Host/Process broker launch | +| Data migration | Not applicable; new crate | +| Validation | `seqpacket-inherited-missing-credentials-rejected` (ported from `inherited_first_packet_requires_credentials`), `seqpacket-inherited-wrong-credentials-rejected` (ported from `inherited_first_packet_rejects_wrong_credentials`), `pidfd-double-read-race-guard-detects-pid-reuse`, `sealed-memfd-partial-seal-rejected`, `credit-scope-six-levels-ordered`, `ancillary-capacity-from-disabled-policy-is-zero`, `raw-control-unknown-header-rejected` (ported from `raw_control_scanner_rejects_unknown_and_partial_headers`), `raw-control-valid-rights-parsed` (ported from `raw_control_scanner_accepts_exact_rights_shape`) | +| Removal proof | `d2b-realm-router/src/secure_session.rs` `SecurePeerSession` seqpacket layer removed after d2b-bus-unix provides the replacement | + +### ADR046-zone-control-013 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-013` | +| Dependency/owner | ADR046-identities-001 | +| Current source | main `a1cc0b2d`: `packages/d2b-contracts/src/v2_component_session.rs` (all protocol constants: `PREFACE_LEN=16`, `PREFACE_MAGIC=b"D2BCS2\r\n"`, `COMPONENT_SESSION_MAJOR=2`, `COMPONENT_SESSION_MINOR=0`, `MAX_HANDSHAKE_OFFER_BYTES=16384`, `HANDSHAKE_OFFER_CANONICAL_LEN=148`, `ENDPOINT_POLICY_IDENTITY_CANONICAL_LEN=140`, `MAX_PROTECTED_CIPHERTEXT_BYTES=65535`, `NOISE_TAG_BYTES=16`, `RECORD_LENGTH_BYTES=2`, `MAX_PROTECTED_PLAINTEXT_BYTES=65519`, `MAX_LOGICAL_MESSAGE_BYTES=1_048_576`, `MAX_ACTIVE_NAMED_STREAMS=128`, `MAX_PACKET_ATTACHMENTS=32`, `MAX_REQUEST_ATTACHMENTS=64`, `MAX_OPERATION_ATTACHMENTS=128`, `MAX_SESSION_ATTACHMENTS=256`, `MAX_PROCESS_ATTACHMENT_CREDITS=2048`, `MAX_HOST_ATTACHMENT_CREDITS=8192`, named-stream/control queue limits, credential size/flag constants; closed enums: `EndpointPurpose` 19 tags, `PurposeClass` 3 tags, `EndpointRole` 19 tags, `ServicePackage` 15 tags, `NoiseProfile` 3 tags, `IdentityEvidenceRequirement` 3 tags, `Locality` 4 tags, `TransportClass` 7 tags, `AttachmentPolicyKind`; structs: `ComponentSessionPreface::parse/encode`, `HandshakeOffer` 11 fields, `HandshakeAccept`, `HandshakeReject`, `HandshakeRejectReason` 21 tags, `LimitProfile` 15 fields with `local_default()`, `AttachmentPolicy::validate/disabled`, `BoundedVec` — throughout); `packages/d2b-session/src/handshake.rs` (`HandshakeRole::{Initiator,Responder}`, `HandshakeCredentials::{Nn,Kk,IkPsk2Initiator,IkPsk2Responder}`, `NegotiatedOffer`, `NoiseHandshake::new/write_next/read_next/finish`, `EstablishedHandshake::transcript_hash/generation`, `encode_offer/negotiate_offer`, generation-discovery magics `b"D2BGD2Q\n"`/`b"D2BGD2A\n"`, `encode_generation_discovery_request/accept/response/decode_generation_discovery_response` — lines 39–433) | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/component_session.rs` (new v3 namespace in existing contracts crate) | +| Detailed design | **Selected**: all numeric constants verbatim; all 21 `HandshakeRejectReason` tags; `ComponentSessionPreface::parse/encode` with exact magic/length layout; `HandshakeOffer` 11-field shape; `AttachmentPolicy::validate` transport constraints (packet-atomic requires seqpacket or inherited-socketpair); `LimitProfile` 15-field profile; `NoiseHandshake` for Nn/Kk/IkPsk2 profiles; `EstablishedHandshake::transcript_hash`; `BoundedVec`. **Excluded ADR45 assumptions**: `EndpointPurpose` 19 tags and `ServicePackage` 15 tags encode ADR45-specific service families — v3 will append new tags for Zone API endpoints without renumbering existing ones. `IdentityEvidenceRequirement` + `GuestSessionCredentialV1` / `GuestBootstrapPsk` / `GUEST_SESSION_CREDENTIAL_MAGIC`: ADR45 guest bootstrap credential formats, excluded until v3 Guest bootstrap work item. Generation-discovery protocol (`encode_generation_discovery_request` lines 138–149): ADR45 initiator probes server for current generation; v3 generation lives in Zone resource store — generation-discovery excluded from initial d2b-bus copy. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt. | +| Integration | All d2b-bus, d2b-bus-unix, Provider registry, and client packages import v3 protocol constants; v3 Zone API service layer uses `EndpointPurpose`/`ServicePackage` tags for Zone API endpoints | +| Data migration | Additive: new v3 tags appended to existing closed enums; ADR45 v2 tag values remain valid during coexistence | +| Validation | `preface-magic-exact-16-bytes`, `preface-offer-len-zero-rejected`, `preface-offer-len-over-max-rejected`, `preface-offer-len-canonical-accepted`, `handshake-21-reject-reasons-all-covered`, `attachment-policy-packet-atomic-requires-seqpacket-or-inherited`, `limit-profile-local-default-all-fields-positive` | +| Removal proof | `d2b-contracts/src/v2_component_session.rs` ADR45-specific tags retired after all v2 sessions decommissioned | + +### ADR046-zone-control-018 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-018` | +| Dependency/owner | ADR046-zone-control-011, ADR046-zone-control-012, ADR046-zone-control-013; ZoneLink session/admission foundation owner | +| Current source | main `a1cc0b2d`: `packages/d2b-session/src/lifecycle.rs` (`SessionPhase` 5-phase FSM, `begin_reconnect` window/attempt bounds — lines 9–195); `packages/d2b-realm-router/src/service_v2.rs` (`RealmSessionAuthority::local_controller/gateway_peer`, authority validation rejecting invalid combinations — lines 53–123; `RealmServiceServer::call` wire/request/generation/lifetime/attachment validation — lines 415–497; `RealmServiceLimits` 15 fields — lines 147–181; `RealmAuditEvent`/`RealmAuditOutcome` — lines 236–245); `packages/d2b-realm-router/src/session_lifecycle.rs` (`SessionPhase::{Allocating,TokenMinting,RelayConnecting,DisplayOpening,Running,Stopping,Stopped}`, `fail`/`stop`/`finish_stop`, tests `forward_sequence_reaches_running_then_refuses_to_advance`, `failure_mid_establishment_rolls_into_teardown_and_records_phase`, `stop_is_idempotent`, `finish_stop_without_stopping_is_a_no_op` — lines 31–220); `packages/d2b-client/src/session.rs` (`ComponentSessionConnector` trait — lines 79–86; `NamedStream` lifecycle/close/reset/send/receive — lines 426–576); `packages/d2b-daemon-access/src/component_session.rs` (`connect_component_session()` peer-UID verify + `HandshakeCredentials::Nn` + `TransportKind::LocalUnix` — lines 53–85) | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/zone_link.rs` (ZoneLink handler); `packages/d2b-resource-api/src/admission.rs` (request admission) | +| Detailed design | **Selected**: `SessionPhase` 5-phase FSM (from main `d2b-session`) drives ZoneLink session state; `Established` state → `status.connected=true` + ZoneLink `status.phase=Ready`; `Disconnected`/`Reconnecting` states → `status.connected=false` + ZoneLink `status.phase=Pending` (or `Degraded` if degraded capability); session-internal phases are not exposed as `ZoneLink.status.phase` values — only the common Resource phases (`Pending|Ready|Degraded|Failed|Unknown`) appear in `status.phase` (§3.5); `begin_reconnect` window/attempt logic → ZoneLink reconnect loop; `RealmSessionAuthority` local vs gateway pattern → ZoneLink authority types for host-local vs transport-bridge sessions; `RealmServiceServer::call` wire validation (generation, request lifetime, attachment) → Zone API request admission; `RealmServiceLimits` 15 fields → ZoneLink `spec.limits`; `connect_component_session()` Nn peer-UID path → Zone runtime bootstrap ComponentSession; `NamedStream` lifecycle → ZoneLink named-stream operations; session-lifecycle tests ported as ZoneLink phase regression tests. **Excluded ADR45 assumptions**: `RealmSessionAuthority::gateway_peer()` (lines 72–87): gateway custody and `Locality::Remote` + `CredentialCustody::GatewayGuest` are ADR45 realm-gateway patterns; v3 ZoneLink transport is bound by the resolved `spec.transportProviderRef`, `spec.transportSettings`, and `spec.transportCredentials` contract instead. Realm 7-phase `SessionLifecycle` (`Allocating→…→Running→Stopping→Stopped`) is the ADR45 realm-specific lifecycle; ZoneLink uses the 5-phase d2b-session model. `GuestBootstrapPsk`/`GuestSessionCredentialV1`: ADR45 guest bootstrap, excluded. `realm_stubs.rs` `ApiService`/`ApiFrontend` dead code excluded (§16.2). | +| Integration | Zone runtime startup creates bootstrap ComponentSession using `HandshakeCredentials::Nn` + local domain socket + peer-UID verification (adapted from `connect_component_session`); the child-local ZoneLink handler opens its allocator-bound uplink session using d2b-bus `SessionEngine`, and the parent routes child calls over that session; resource API admission validates requests using the `RealmServiceServer::call` pattern | +| Data migration | Not applicable; new implementation | +| Validation | `zonelink-reconnect-child-uid-change`, `zonelink-disconnect-unknown-phase`, `zonelink-intent-queue-limit`, `zone-session-phase-forward-sequence-refuses-repeat` (ported from `forward_sequence_reaches_running_then_refuses_to_advance`), `zone-session-failure-records-phase` (ported from `failure_mid_establishment_rolls_into_teardown_and_records_phase`), `zone-session-stop-is-idempotent` (ported), `zone-bootstrap-session-nn-peer-uid-verified` | +| Removal proof | `d2b-realm-router/src/service_v2.rs` `RealmServiceServer` removed after Zone API service layer replaces realm v2 service; `d2b-realm-router/src/session_lifecycle.rs` removed after d2b-bus lifecycle replaces realm-specific one | + +### ADR046-zone-control-017 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-017` | +| Dependency/owner | ADR046-zone-control-011, ADR046-zone-control-013; Provider registry/toolkit foundation owner | +| Current source | main `a1cc0b2d`: `packages/d2b-provider/src/registry.rs` (`RegistryLimits`, `AdmissionOptions`, `ProviderRegistryBuilder::new/limits/register_factory/register_instance/register_constructed/finish`, `ProviderRegistry::lifecycle/snapshot/instance/admit/shutdown`, `InFlightPermit`, `AdmittedProvider`, `ProviderRegistryManager::new/current/publish` — lines 33–568; tests `shutdown_closes_final_permit_notify_race`, `finish_drain_closes_final_permit_notify_race` — lines 683–691); `packages/d2b-provider/src/rpc.rs` (`SessionIdentity`, `ProviderClock`/`SystemProviderClock`, `RpcCall`, `RpcPayload`, `RpcResponse`, `AuthenticatedProviderRpc`, `RpcProviderProxy::new/preflight/call*`, `session_identity_matches_placement` — lines 27–895; test `provider_and_user_agent_session_identities_are_placement_exact` — lines 923–959); `packages/d2b-provider-toolkit/src/adapter.rs` (`ProviderAgentAdapter::new/invoke_session/invoke` — lines 18–194); `packages/d2b-provider-toolkit/src/conformance.rs` (`ConformanceError`, `check_descriptor_conformance`, `check_provider_conformance` — lines 8–126); `packages/d2b-provider-toolkit/src/fixture.rs` (`DeterministicClock`, `Fixture::new/from_descriptor/operation/request/call_context/session_identity`, `FakeProvider`, `sample_lease_request` — lines 39–262); `packages/d2b-provider-toolkit/src/registration.rs` (`ToolkitError`, `register_exact_instances` — lines 12–107); `packages/d2b-provider-toolkit/src/server.rs` (`GeneratedProviderServiceServer::from_session_handle/new/shutdown/generated_services` — lines 59–176; test `rpc_statuses_retain_closed_actionable_reasons`); `packages/d2b-provider-toolkit/src/values.rs` (`ProviderValues::new/descriptor/health/plan/handle_from_request/handle_from_plan/observation` — lines 18–192); `packages/d2b-provider-toolkit/src/redaction.rs` (`Redacted`, `Secret` — lines 3–37) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider/src/{registry,rpc}.rs` (new v3 Provider package); `packages/d2b-provider-toolkit/src/{adapter,conformance,fixture,registration,server,values,redaction}.rs` (new v3 toolkit) | +| Detailed design | **Selected**: `ProviderRegistry` lifecycle `Accepting→Draining→Retired` with drain-waiter notify-race safety (ported from `shutdown_closes_final_permit_notify_race` / `finish_drain_closes_final_permit_notify_race` tests); `InFlightPermit` + global + per-provider in-flight quota; `ProviderRegistryManager::publish` validates snapshot before swap; `RpcProviderProxy::preflight` cancellation-check, deadline-check, method/capability match, session-identity/placement exactness; `ProviderAgentAdapter` rejects attachments with missing descriptors or non-increasing indexes (lines 79–99); `GeneratedProviderServiceServer::new` single-service-per-agent requirement + shutdown via atomic accept-flag + idle notify + timeout; `check_provider_conformance` health/inspection/observability check sequence; `Fixture`/`FakeProvider`/`DeterministicClock` as conformance harness; `Redacted`/`Secret` retained unchanged. **Excluded ADR45 assumptions**: `TrustedFirstPartyInProcess` as the only accepted placement (in `session_identity_matches_placement` lines 577–598): v3 Provider resources support multiple placements per `RealmControllerPlacement` mapping (§16.2); in-process placement is retained but not exclusive. Generated ttrpc stubs in `d2b-contracts/src/generated_v2_services/` are v2-service-specific; v3 Provider processes compile their own stubs from v3 proto files. ACA workload adapter (`d2b-gateway-runtime/src/aca_workload.rs`) excluded. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt. | +| Integration | Provider/system-core and Provider/system-minijail use `ProviderRegistryBuilder::register_constructed` as bootstrap exceptions; other Providers register instances from Process-spawned agents via `ProviderAgentAdapter`; Zone runtime hosts one `ProviderRegistry` per installed Provider component; `ProviderRegistryManager::publish` swaps registry on Provider update | +| Data migration | Provider descriptors re-registered on Zone store bootstrap; no v2 provider state migration | +| Validation | `provider-registry-drain-waiter-race-safe` (ported from both notify-race tests), `provider-registry-publish-validates-snapshot-before-swap`, `provider-rpc-proxy-placement-exact` (ported from `provider_and_user_agent_session_identities_are_placement_exact`), `provider-agent-adapter-rejects-non-monotone-attachment-indexes`, `provider-server-shutdown-drains-in-flight-requests`, `provider-conformance-health-inspection-observability-sequence` | +| Removal proof | `d2b-realm-provider` trait crate removed after v3 Provider resource + registry integration (§16.5) | + +### ADR046-provider-agent-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-provider-agent-001` | +| Dependency/owner | ADR046-zone-control-011, ADR046-zone-control-017, ADR046-zone-control-018 | +| Current source | main `a1cc0b2d`: `packages/d2b-gateway-runtime/src/provider_agent.rs` (`ProviderAgentError::{UnregisteredAdapter,RegistryNotAccepting,RegistrationRejected,InvalidAuditCapacity,SessionClosed,ProtocolViolation}`, `ProviderAgentAuditOutcome`, `ProviderAgentAuditEvent`, `ProviderAgentProcess::from_registry/from_registry_with/provider_type/service_names/audit_snapshot/serve`, `run_registered`, bounded in-memory audit ring, frame dispatch loop: semaphore in-flight limit, service/method routing, negative-timeout guard, `SessionClosed` termination, `ProtocolViolation` audit + terminate — lines 31–452; tests `standalone_entrypoint_fails_without_registration`, `audit_capacity_is_bounded` — lines 454–486); `packages/d2b-contracts/src/provider_registry_v2.rs` (`ProviderRegistryV2` wire contract, `ProviderRegistryEntryV2::validate` with provider-id derivation rule, schema fingerprint, scope-digest, generation exactness, `TrustedFirstPartyInProcess` placement requirement, `ProviderIntentId` label rules `max 128 bytes`, `MAX_PROVIDER_REGISTRY_ENTRIES`, `MAX_PROVIDER_MAPPING_IDS=64`, `ProviderBindingV2` non-exhaustive + `UnsupportedProviderBindingV2` fallback, `ProviderRegistryV2::validate` sort/unique/count checks — lines 23–566; tests `validates_closed_local_runtime_mapping`, `validates_closed_local_observability_mapping`, `serializes_declared_mapping_axes_as_closed_variants`, `rejects_duplicate_or_unbounded_mapping_ids`, `local_storage_binding_realm_must_match_descriptor_placement`, `rejects_generation_and_exact_identity_mismatches`, `contradictory_binding_realm_json_is_unrepresentable`, `unknown_binding_axis_remains_rejected_on_the_wire`, `identity_mismatch_messages_name_the_failed_contract`, `accepts_explicit_empty_registry` — lines 722–1044) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider/src/agent.rs` (v3 provider agent dispatch); `packages/d2b-contracts/src/v3/provider_registry.rs` (v3 provider registry wire contract) | +| Detailed design | **Selected**: `ProviderAgentProcess::serve` dispatch loop with semaphore in-flight limit; unsupported-service/method → ttrpc error; negative-timeout rejection; `SessionClosed` clean termination; `ProtocolViolation` audit-and-terminate path; bounded audit ring; `GeneratedProviderServiceServer` single-service-per-agent requirement; `ProviderRegistryV2` entry validation: provider-id derivation, schema fingerprint, scope digest, generation exactness; `MAX_PROVIDER_MAPPING_IDS=64` → Provider component mapping bound; `ProviderBindingV2` non-exhaustive + explicit `UnsupportedProviderBindingV2` fallback (never panics on unknown axis); `ProviderIntentId` `max 128 bytes` label rules → Provider component `spec.intentRef`; all 10 `provider_registry_v2.rs` tests ported. **Excluded ADR45 assumptions**: `aca_workload.rs` (`AcaGatewayWorkload`): ADR45 ACA external provider adapter, excluded entirely. `waypipe_display.rs` (`WaypipeDisplayProvider`): ADR45 display provider, excluded. `ProviderRegistryV2.registry_generation` / `configuration_fingerprint` bind to ADR45 bundle generation; v3 Provider resource version is tracked in redb store, not a JSON bundle. `run()` binary entrypoint uses the fixed `d2b-provider-agent` command; v3 provider processes use normal Zone runtime Process launch. `TrustedFirstPartyInProcess` is the only placement in v2; v3 Provider resources extend to `HostLocal`/`GatewayVm` etc. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt. | +| Integration | Zone runtime spawns each Provider component Process via normal Process launch; Process binary calls `ProviderAgentProcess::from_registry` then `serve()` on established ComponentSession; on `SessionClosed` the process exits and Zone runtime observes `status.phase` transition | +| Data migration | Not applicable; new implementation | +| Validation | `provider-agent-dispatch-unsupported-service-returns-ttrpc-error`, `provider-agent-negative-timeout-rejected`, `provider-agent-session-closed-terminates-serve-loop`, `provider-agent-audit-ring-capacity-bounded` (ported from `audit_capacity_is_bounded`), `provider-registry-entry-fingerprint-generation-exact` (ported from `rejects_generation_and_exact_identity_mismatches`), `provider-registry-unknown-axis-fallback-non-exhaustive` (ported from `unknown_binding_axis_remains_rejected_on_the_wire`), `provider-registry-duplicate-ids-rejected` (ported from `rejects_duplicate_or_unbounded_mapping_ids`) | +| Removal proof | Not applicable; new implementation | + +### ADR046-client-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-client-001` | +| Dependency/owner | ADR046-zone-control-011, ADR046-zone-control-012, ADR046-zone-control-013, ADR046-zone-control-018 | +| Current source | main `a1cc0b2d`: `packages/d2b-client/src/client.rs` (`WallClock`, `MetadataInput`, `RetryPolicy` 1..8 attempts, `CallOptions`, `CancellationToken`, `Client::new/with_clock/connect`, `ConnectedClient` methods incl. `session_generation/session_limits/service/invoke/invoke_with_attachments/named_stream/open_server_stream`, `prepare_typed_request/prepare_operation_context`, `can_retry/retryable_failure/validate_outbound_attachments/validate_reply/service_package/map_remote_kind/map_retry` — lines 35–921); `packages/d2b-client/src/session.rs` (`ComponentSessionConnector` trait, `NamedStream` lifecycle, `StreamDispatcher`, `SharedDriver`, aggregate-queue-bound test — lines 24–626); `packages/d2b-client/src/target.rs` (`ServiceOwner`, `TargetInput`, `TransportKind`, `RouteRecord`, `ResolvedTarget`, `TargetResolver`, `RouteTable` — lines 7–228); `packages/d2b-client/src/service.rs` (`ServiceKind`, `GeneratedClient`, `MethodHandle`, `ServiceHandle::new/kind/generated/proxy/method/invoke` — lines 21–184); `packages/d2b-client/src/daemon_service.rs` (`DaemonClient::new/session_generation/connected/resolve/inspect/lifecycle/open_terminal`, `DaemonTerminal`, `daemon_call_options`, `ensure_daemon_outcome`, `map_ttrpc_error`, test `redacted_terminal_debug_payload` — lines 29–689); `packages/d2b-client/src/host_socket.rs` (`HostSocketConnector::new/from_seqpacket_fd`, `local_daemon_endpoint_identity`, `ComponentSessionConnector::connect` — lines 252–383); `packages/d2b-client/src/error.rs` (`RemoteErrorKind`, `RetryClass`, `ClientError` — lines 5–128) | +| Reuse action | adapt | +| Destination | `packages/d2b-client/src/` (updated for v3 Zone API, replacing ADR45 daemon verbs with Zone resource operations) | +| Detailed design | **Selected**: `Client::connect()` target-resolve → ComponentSession-open → `ConnectedClient` lifecycle; `RetryPolicy` 1..8 bound + `retryable_failure()` safe-only retry detection; `NamedStream::send/receive/close_local/reset` lifecycle; `ComponentSessionConnector` trait as connector abstraction; `HostSocketConnector::from_seqpacket_fd` + `local_daemon_endpoint_identity` for Zone runtime socket connector; `RouteTable` ambiguous-route rejection; `ServiceHandle`/`GeneratedClient`/`MethodHandle` typed service client pattern; `map_ttrpc_error`/`validate_reply`/`map_retry` error-handling chain; `ClientError`/`RemoteErrorKind`/`RetryClass` error taxonomy; `DaemonClient` call-options and outcome helpers (infrastructure only). **Excluded ADR45 assumptions**: `DaemonMethod` enum (lines 29–56 of daemon_service.rs): ADR45 daemon verbs (`vm_start`, `vm_stop`, `list_realms`, etc.) — replaced with Zone API verbs. `GuestClient`/`guest_service.rs`: ADR45 guest operations; excluded until v3 Guest transport work item. Hardcoded socket path `PUBLIC_SOCKET_PATH = "/run/d2b/public.sock"` in `host_socket.rs`: replaced by Zone-resource-managed path. `TransportKind::LocalUnix` restriction in daemon-access: v3 allows multiple transport kinds per ZoneLink binding. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt. | +| Integration | `d2b` CLI uses `d2b-client` to connect to Zone runtime via `HostSocketConnector`; a child Zone runtime uses `d2b-client` for its allocator-bound uplink while the parent route engine uses the established session for child calls; Provider toolkit conformance tests use `Fixture`/`FakeProvider` with `d2b-client` service handles | +| Data migration | Not applicable; updated in place | +| Validation | `client-retry-policy-max-8-attempts-enforced`, `client-named-stream-close-local-then-remote-close-transitions-closed`, `client-route-table-ambiguous-route-rejected`, `client-host-socket-peer-uid-verified-on-connect`, `client-retryable-failure-only-safe-mutations` | +| Removal proof | `DaemonMethod` v2 verb enum retired after all v2 daemon operations migrated to Zone API | + +### ADR046-wire-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-wire-001` | +| Dependency/owner | ADR046-zone-control-013 | +| Current source | main `a1cc0b2d`: `packages/d2b-contracts/src/v2_services.rs` (`MethodSpec{mutating,stream_kind,...}`, `ServiceSpec`, `SERVICE_INVENTORY` covering 20+ services and all provider services, `service_schema_fingerprint`, `public_daemon_schema_fingerprint`, `direct_guest_schema_fingerprint`, `StrictWireMessage`, `decode_strict`, `encode_strict`, `admit_metadata`, `TerminalStreamValidator`, `ServerStreamLease`, `RedactedTerminalFrame`, stream-name validators — lines 204–1004; tests `public_endpoint_fingerprint_binds_both_services_dependencies_and_order`, `public_endpoint_fingerprint_binds_daemon_and_guest_method_descriptors`, `direct_guest_fingerprint_binds_activation_and_remains_separate_from_public_endpoint` — lines 872–1030); `packages/d2b-contracts/src/v2_state.rs` (constants: `STATE_SCHEMA_VERSION=2`, `STATE_SCHEMA_GENERATION=1`, `MAX_JSON_DOCUMENT_BYTES=1_048_576`, `MAX_INVENTORY_ROWS=4096`, `MAX_LOCKS=1024`, `MAX_LOCK_DEPENDENCIES=32`, `MAX_DISCOVERY_OBSERVATIONS=4096`, `MAX_AUDIT_RECORD_BYTES=8192`, `MAX_AUDIT_RECORDS_PER_SEGMENT=16384`, `MAX_AUDIT_SEGMENT_BYTES=64*1024*1024`, `MAX_AUDIT_RETENTION_DAYS=14`, `MAX_LOCK_DEADLINE_MS=300_000`; types: `Digest`, `Generation`, `OwnershipEpoch`, `SafeJsonInteger`, `StorageCategory`, `StateEnvelope`, `CanonicalPayloadVerifier`, `AtomicWritePhase`, `RunnerEvidence`, audit types incl. `AuditRecord/Segment/Checkpoint/Gap`, `detect_audit_gap`, `AuditRetentionPolicy`, lock types: `LockClass`, `LockSpec`, `SyncInventory`, `LeaseRecord`); `packages/d2b-contracts/src/v2_identity.rs` (`IdentityError` 13 variants, canonical name rules `^[a-z][a-z0-9-]*$` max 63 bytes start-lowercase-letter, `RealmPath` label/separator rules, `ProviderType::ALL` 11 types + `as_str()` — lines 11–250); `packages/d2b-contracts/src/v2_provider.rs` (bounded opaque IDs `[a-z][a-z0-9-]{0,63}`: `ImplementationId`, `OperationId`, `IdempotencyKey`, `PlanId`, `HandleId`, `LeaseId`, `TransferId`, `PROVIDER_CONTRACT_FINGERPRINT`, `ProviderContractError` 34 variants, `Fingerprint` 64 lowercase-hex chars — lines 18–219); `packages/d2b-contracts/src/generated_v2_services/` (all 40+ generated ttrpc client/server stubs for v2 services) | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/{services,state,identity,provider}.rs` (new v3 namespace); generated stubs regenerated from v3 proto files in `packages/d2b-contracts/proto/v3/` | +| Detailed design | **Selected**: `MethodSpec`/`ServiceSpec`/fingerprinting infrastructure → v3 Zone API service schema fingerprinting; `StrictWireMessage`/`decode_strict`/`encode_strict`/`admit_metadata` → v3 wire decode/admit for all resource API requests; `CanonicalPayloadVerifier` payload-digest binding → v3 resource store integrity checks; audit chain types `AuditRecord`/`AuditSegment`/`AuditCheckpoint`/`detect_audit_gap` + `MAX_AUDIT_RETENTION_DAYS=14`/`MAX_AUDIT_RECORD_BYTES=8192`/`MAX_AUDIT_SEGMENT_BYTES=64MiB` → v3 Zone audit (§13.3); lock types `LockSpec`/`SyncInventory`/`LeaseRecord` + `MAX_LOCKS=1024`/`MAX_LOCK_DEADLINE_MS=300_000` → v3 resource store lock layer; bounded opaque ID pattern `[a-z][a-z0-9-]{0,63}` → v3 `OperationId`/`HandleId`/`PlanId`/`LeaseId`; `Fingerprint` 64-hex-char → v3 Provider `spec.configFingerprint`; `ProviderContractError` 34 variants → v3 Provider operation error taxonomy; canonical name constraint `^[a-z][a-z0-9-]*$` max 63 bytes → same as `ids.rs::is_label()` shared validator (§16.2). **Excluded ADR45 assumptions**: `RealmLabel`/`WorkloadName`/`RealmPath` identity types: ADR45 workload/realm address format; replaced by `metadata.name` + Zone `ResourceRef`. `ProviderType::ALL` fixed 11-type closed enum: v3 Provider type is an open string field in the Provider resource spec. `STATE_SCHEMA_VERSION=2`/`STATE_SCHEMA_GENERATION=1`: v3 store schema uses redb table versioning, not a JSON schema version field. v2 service fingerprint tests reference ADR45-specific proto files; v3 fingerprints use different proto inputs but the same `service_schema_fingerprint` seeding mechanism. Generated stubs in `generated_v2_services/` are v2-specific and excluded; v3 uses regenerated stubs from `proto/v3/`. | +| Integration | v3 Zone API service layer uses `MethodSpec.mutating` + `StrictWireMessage` for admission and fingerprinting; `v3/state.rs` `Digest`/`StateEnvelope` integrate with redb store writes; `CanonicalPayloadVerifier` validates resource payloads loaded from store; audit types feed §13.3 Zone audit segment; lock types wire into ADR 0034 lock lifecycle | +| Data migration | v2 and v3 wire type namespaces coexist in the same contracts crate; no migration | +| Validation | `v3-canonical-name-matches-ids-is-label-regex`, `v3-service-fingerprint-changes-on-method-mutation` (behavior ported from `public_endpoint_fingerprint_binds_daemon_and_guest_method_descriptors`), `v3-audit-gap-detection-covers-missing-segment`, `v3-strict-wire-rejects-unknown-fields`, `v3-state-envelope-digest-mismatch-rejected`, `v3-canonical-payload-verifier-binding-holds-under-mutation` | +| Removal proof | `d2b-contracts/src/v2_{services,state,identity,provider}.rs` and `v2_component_session.rs` deprecated in contracts crate after all v2 clients decommissioned | + +--- + +### ADR046-zone-control-014 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-014` | +| Dependency/owner | ADR046-zone-control-001, ADR046-routing-011; Nix module owner | +| Current source | `nixos-modules/options-realms.nix` (option schema/assertion conventions); `nixos-modules/bundle-artifacts.nix` (bundle emit pattern); `nixos-modules/provider-registry-v2-json.nix` (provider-registry JSON emit); `nixos-modules/assertions.nix:730` (assertion style for unsafe-local); `packages/d2b-realm-core/src/ids.rs` (`LABEL_PATTERN`, `MAX_ID_LEN`, `is_label()` — eval-time name validation target) | +| Reuse source | None | +| Reuse action | create | +| Destination | `nixos-modules/options-zones.nix`, `nixos-modules/generated/resource-types.nix`, `nixos-modules/generated/options-zones-.nix`, `nixos-modules/resource-type-validators.nix` | +| Detailed design | Consume the generator and registry established by ADR046-routing-011 to declare the unified `d2b.zones..resources.` option tree plus the Zone-level compiler-only `parentZone` scalar. The generated standard registry is exactly the canonical 19 types (`Zone`, `ZoneLink`, `Provider`, `Host`, `Guest`, `Process`, `EphemeralProcess`, `Network`, `Volume`, `Credential`, `Device`, `Endpoint`, `User`, `Role`, `RoleBinding`, `Quota`, `EmergencyPolicy`, `ResourceExport`, `ResourceImport`), and every standard type has a strict generated `spec` submodule carrying the schema's Nix types, defaults, bounds, and documentation. Installed Provider artifacts may append only signed qualified ResourceTypes whose strict schema has been verified and generated into the evaluated option set. `type` is selected from that closed combined registry: an unknown standard string, an unqualified extension, or a qualified type without an installed verified schema fails evaluation; there is no unrestricted string or free-form `spec` fallback. Require `parentZone` on every non-root Zone and forbid it on `local-root`; resolve it against declared Zone keys, reject self-parent/conflicting module definitions/cycles, and cap each ancestry path at 16 Zone names. Compile the validated map into sealed allocator bootstrap topology; never emit it into the resource bundle or `Zone.spec`. Declare the global `d2b.artifacts.` catalog with `package` (types.package, required) and `type` (closed enum, required). Provider `spec.artifactId` is a plain catalog ID; the derivation is not a `spec` field. Implement the Phase 1 cross-resource assertions (§14.10 Phase 1 table); retain `credentialRef`, `resourceRef`, and `closedEnum` helpers; reject operator-authored `Zone`; and enforce child-local ZoneLink topology. The runtime creates `Zone/`, `Provider/system-core`, and `Provider/system-minijail` with `managedBy=controller`; none is emitted or inferred from the configuration bundle. `allowUnsafeLocal` maps to the dedicated Host admission gate. Provider manifest-derived fields (`spec.exports`, `spec.components`, `spec.dependencies`, `spec.permissionClaims`, `spec.upgradePolicy`, `spec.restartPolicy`) are read-only and setting one is an eval error. | +| Integration | ADR046-routing-011 supplies the one canonical 19-type registry and generated option family; validated `parentZone` feeds the allocator bootstrap sealer; the closed `d2b.zones..resources.*` tree is consumed by ADR046-zone-control-015; the Zone controller (ADR046-zone-control-001) reads the resulting bundle; Provider package conventions come from ADR046-zone-control-003 | +| Data migration | Replace `nixos-modules/options-realms.nix`-derived option trees once Zone controller is live and has reached parity | +| Validation | All Phase 1 eval tests in §15.8 (`nix-eval-name-regex-enforced`, `nix-eval-verb-closed-enum`, `nix-eval-session-verb-closed-enum`, `nix-eval-relay-session-verb-known`, `nix-eval-roleref-format`, `nix-eval-subject-type-restricted`, `nix-eval-no-duplicate-subjects`, `nix-eval-bootstrap-provider-rejected`, `nix-eval-provider-missing-artifact-id`, `nix-eval-artifact-id-not-in-catalog`, `nix-eval-artifact-wrong-type`, `nix-eval-artifact-id-format`, `nix-eval-credentialref-declared`, `nix-eval-dollar-key-rejected`, all five `nix-eval-parent-zone-*` vectors, `nix-eval-zonelink-child-name-mismatch-rejected`, `nix-eval-zonelink-second-uplink-rejected`, `nix-eval-zonelink-limits-maxpendingintents-bound`); Phase 2 runs `nix-build-relay-scope-restricted`; drift test asserts the standard registry and generated option modules cover exactly all 19 canonical types; negative evals reject unknown strings, unqualified extensions, unsigned or uninstalled qualified types, and unknown `spec` fields; a positive fixture admits an installed signed qualified type and validates its strict generated schema | +| Removal proof | `nixos-modules/options-realms.nix`, `nixos-modules/realm-controller-config-json.nix`, `nixos-modules/realm-identity-config-json.nix` deleted after Zone controller and resource compiler reach full parity; `nixos-modules/assertions.nix` lines referencing `allowUnsafeLocal`/realm names removed after Host admission validation replaces them | + +### ADR046-zone-control-015 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-015` | +| Dependency/owner | ADR046-zone-control-014; resource compiler owner | +| Current source | `nixos-modules/bundle-artifacts.nix` (artifact emit pattern); `nixos-modules/processes-json.nix` (canonical JSON serialization conventions); `packages/xtask/src/main.rs` `gen-schemas` subcommand (schema-from-derivation pattern); `packages/d2b-core/src/bundle.rs` (current bundle DTO shape for adaptation reference) | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-resource-compiler/src/{main,bundle,schema,validator,digest,sort,secret_lint,generation}.rs`; exposed as `pkgs.d2b-resource-compiler`; called from `nixos-modules/resource-compiler.nix` | +| Detailed design | Implement all Phase 2 build-time checks (§14.10 Phase 2 table): dispatch on `type` to look up ResourceTypeSchema; validate each resource's `spec` canonical JSON against the committed schema (build validation compares canonical rendered JSON against ResourceTypeSchema for each core type); compile the `d2b.artifacts.*` catalog: for each entry, build/include the derivation, verify `type` is a recognized value, compute `digest` over the derivation output, extract and hash manifest and config schema files, validate signature chain and conformance attestation; detect duplicate artifact IDs; for each Provider resource, look up `spec.artifactId` in the compiled catalog (build failure if absent or wrong type), verify `configSchemaDigest` matches schema SHA-256, validate operator `spec.config` against loaded JSON Schema using a pure-Rust validator bundled in the derivation, verify `manifestDigest` and signature chain, load manifest-derived fields (`exports`, `components`, `dependencies`, `permissionClaims`, `upgradePolicy`, `restartPolicy`) into the bundle envelope; emit private integrity-pinned artifact catalog (ID → type/digest/closure metadata) as a separate private file (never merged into the public resource bundle); check `spec.rules[*].resourceTypes` against installed Provider catalogs in the bundle (Role); verify `spec.roleRef` names an existing Role in the bundle (RoleBinding); verify `spec.subjects[*]` names resolve in bundle (RoleBinding); check ResourceType short-name collision across all Zone Providers; RFC 8785 canonical JSON serialization; per-resource `digest` computation; `bundleDigest` computation over sorted `resources` array; inline-secret heuristic lint (`--strict-secrets` flag); `generation` counter persistence in Nix module state; emit `zone-resources.json` bundle with all fields per §14.9 | +| Integration | Reads from `d2b.zones..resources.*` (ADR046-zone-control-014); emits bundle consumed by ADR046-zone-control-001 configuration publication handler; generation counter stored as Nix module derivation input hash (hermetic) or in a NixOS state file (impure) — exact mechanism is implementation decision | +| Data migration | Full reset; no prior bundle state exists to carry forward | +| Validation | All Phase 2 build tests in §15.8 (`nix-build-artifact-id-missing-from-catalog`, `nix-build-artifact-wrong-type-rejected`, `nix-build-duplicate-artifact-id`, `nix-build-artifact-store-path-absent-from-bundle`, `nix-build-artifact-store-path-absent-from-config`, `nix-build-config-schema-failure`, `nix-build-schema-digest-mismatch`, `nix-build-manifest-digest-mismatch`, `nix-build-resourcetype-collision`, `nix-build-bundle-sorted`, `nix-build-bundle-digest-stable`, `nix-build-per-resource-digest-correct`, `nix-build-credential-ref-survives-build`, `nix-build-inline-secret-lint-warning`, `nix-build-inline-secret-strict-failure`) | +| Removal proof | No current equivalent; additive only; no prior code removed | + +### ADR046-zone-control-016 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-016` | +| Dependency/owner | ADR046-zone-control-015; ADR046-zone-control-001; configuration publication handler owner | +| Current source | `packages/d2bd/src/lib.rs` lines 1408 and 16741 (`RealmControllersJson` load — live active generation load pattern); `nixos-modules/realm-controller-config-json.nix` (current config bundle emit to `/etc/d2b/`); `packages/d2b-realm-core/src/allocator_engine.rs` (generation/activation pattern); `packages/d2b-core/src/unsafe_local_workloads.rs:16–164` (`MAX_UNSAFE_LOCAL_WORKLOADS=256`, etc. — bounds reference for Credential/Host cleanup) | +| Reuse source | main `a1cc0b2d`: `packages/d2b-session/src/lifecycle.rs` `begin_reconnect` exponential backoff logic (cleanup retry); `packages/d2b-state/` lock/lease types (ADR046-store-001 dependency for bundle file locking) | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/configuration.rs` (Phase 3 activation, diff, delete dispatch); `packages/d2b-core-controller/src/cleanup.rs` (pending tracking, status, stuck detection, rollback verb handler) | +| Detailed design | Implement all Phase 3 runtime activation checks (§14.10 Phase 3 table); `bundleDigest` integrity verify; `zoneUid` consistency check; `generation` monotone check; per-resource `digest` re-verify; atomic generation advance in `store_meta`; diff computation (resources with `managedBy=configuration` absent from new bundle → async Delete; `managedBy=controller`/`managedBy=api` resources untouched); `managedBy` and `configurationGeneration` field maintenance on resources in redb store; `cleanupPendingCount` and `generationCleanupPending` maintenance; Zone.status.phase → Degraded while cleanup pending, reverts on completion; `GenerationCleanupPending`/`GenerationCleanupFailed` condition management; stuck-cleanup `GenerationCleanupFailed=True` at `cleanupStuckThreshold` (default 5 min) with exponential backoff retry; prior generation bundle retention and pruning up to configured `retainedPriorGenerationCount` (default 3, range 1..16); audit emission for all four cleanup audit kinds (§14.11); `zone.config-rollback` verb handler Primary reuse disposition: `adapt`. Preserved source-plan detail: extract exponential backoff from `begin_reconnect`. | +| Integration | Zone store / redb (ADR046-store-001); core-controller watch/trigger bus (ADR046-zone-control-011); Zone status writer (ADR046-zone-control-001); audit emitter (§13.2); Credential revocation hook (triggered when `deletionRequestedAt` is set on a Credential and `core.credential-revoke` finalizer is present; revocation completes before finalizer clearance) | +| Data migration | Full reset; no prior bundle activation state exists to carry forward | +| Validation | All Phase 3 runtime and cleanup tests in §15.8 (`nix-runtime-bundledigest-integrity`, `nix-runtime-generation-monotone`, `nix-runtime-zoneuid-mismatch-rejected`, `nix-runtime-zonename-mismatch-rejected`, `nix-runtime-activation-nonblocking`, `nix-runtime-provider-config-invalid-continues`, all `cleanup-*` and `rollback-*` tests) | +| Removal proof | `d2bd/src/lib.rs` config-load at lines 1408 and 16741 removed after Zone configuration publication handler reaches parity; `realm-controller-config-json.nix` and `realm-identity-config-json.nix` Nix bundle-emit removed after resource compiler reaches parity | + +### ADR046-pkg-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-pkg-001` | +| Dependency/owner | ADR046-zone-control-003; workspace policy owner | +| Current source | `packages/d2b-contract-tests/tests/policy_contracts.rs` lines 5–6 (D2B_FIXTURES gate / workspace-checks integration pattern — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-contract-tests/tests/static_invariants.rs` (hermetic policy test structure — `implemented-and-reachable`); `tests/tools/rust-workspace-checks.sh` (D2B_FIXTURES step shell harness — `implemented-and-reachable`); AGENTS.md "Naming conventions" section (`-` workspace sort rules — `implemented-and-reachable`); `packages/d2b-realm-core/src/ids.rs` `LABEL_PATTERN` / `MAX_ID_LEN` (name regex reused for crate name token validation — `implemented-and-reachable`) | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-contract-tests/tests/policy_provider_crate_layout.rs` (new file; gated under D2B_FIXTURES in existing `tests/tools/rust-workspace-checks.sh`) | +| Detailed design | Implement `policy_provider_crate_layout.rs` with the following test functions: (1) `every_provider_crate_has_src` — walk `packages/d2b-provider-*/` directories in the workspace, assert each contains `src/`; failure names crate and missing path; (2) `every_provider_crate_has_tests` — assert `tests/` present; (3) `every_provider_crate_has_integration` — assert `integration/` present; (4) `every_provider_crate_has_readme` — assert `README.md` present; (5) `every_provider_readme_has_required_sections` — read `README.md`, check for all nine section headings from §4.8.3 (case-insensitive, after stripping `#` and whitespace); failure names the missing heading(s); (6) `every_integration_file_has_target_declaration` — for each `integration/*.rs` file, scan first 20 lines for exactly one `//! integration-target: (container|host-integration)` declaration; failure names the file and the violation (missing/multiple/invalid value); (7) `non_provider_crates_exempt` — verify the check does not run on non-`d2b-provider-*` crates. All checks are filesystem-only (no compilation). Workspace member list is discovered by parsing `packages/Cargo.toml` `[workspace].members`. Gate: add the new test file to `tests/tools/rust-workspace-checks.sh` D2B_FIXTURES list alongside existing policy tests | +| Integration | `make test-policy` and `make check` both fail if any provider crate violates §4.8; consistent with existing `no-bash-ast-walker` and workspace-sort gates; ADR046-zone-control-003 references §4.8 for Provider package conventions | +| Data migration | Additive; no existing `d2b-provider-*` crates in the pre-ADR45 baseline; first Provider crate created must comply from inception | +| Validation | §15.3 layout conformance tests: `provider-crate-layout-src-required`, `provider-crate-layout-tests-required`, `provider-crate-layout-integration-required`, `provider-crate-layout-readme-required`, `provider-readme-sections-all-present`, `provider-readme-sections-partial-missing`, `provider-integration-target-declared`, `provider-integration-target-unique`, `provider-integration-target-valid-values`, `provider-crate-naming-convention`, `provider-crate-layout-non-provider-exempt` | +| Removal proof | No existing code removed; additive policy test only | + +### ADR046-zone-control-019 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-019` | +| Dependency/owner | ADR046-zone-control-001, ADR046-provider-004; ADR046-zonelink owner; `d2b-core-controller` + `d2b-contracts` owners | +| Current source | None — net-new ADR 0046 cross-Zone sharing model (D096); no pre-ADR45 baseline equivalent | +| Reuse source | ZoneLink reconcile/handler scaffolding (§3); `packages/d2b-session/src/streams.rs` `NamedStream` credit/backpressure (bounded encrypted stream carriage) | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/{resource_export,resource_import}.rs` (base schemas); `packages/d2b-core-controller/src/export_import.rs` (core ZoneLink export/import routing controller); shared adapter trait in `packages/d2b-provider/src/share_adapter.rs` (`ExportAdapter`/`ImportAdapter` signed-capability traits) | +| Detailed design | Implement the `ResourceExport` and `ResourceImport` standard ResourceTypes per §8A plus signed Provider `ProjectionFactory` metadata binding qualified Service type, qualified Binding type, allowed owner-Service backing refs, allowed Binding target refs, projection schema/fingerprint, and aggregate factory fingerprint. Admission accepts only an owner Service as `ResourceExport.resourceRef`; matches export/import/local-factory type and fingerprints; and creates exactly one same-qualified-type projection Service (`ownerRef: ResourceImport/`). It never projects Device/Endpoint/Binding and never creates Binding. Binding spec is desired consumer intent only; observations belong only in status. No cross-Zone Ref, FD, secret, path, locator, or resource grant crosses a Zone; payload bytes use bounded encrypted named streams and high-churn sessions/streams remain internal. Export removal/ZoneLink loss revokes leases and degrades the projection Service; reconnect revalidates generation and both fingerprints. D091 currency propagates Service → export → import → projection Service → authored Binding → children. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (extend ZoneLink controller). | +| Integration | Zone store/redb (ADR046-store-001); shared D098 semantic base catalog (ADR046-provider-004); ZoneLink reconcile (§3); ComponentSession bounded encrypted named streams; signed projection factories/adapters for audio-pipewire, device-security-key, observability-otel, and policy-gated device-usbip; CLI graph rendering | +| Data migration | None — full d2b 3.0 reset; no prior cross-Zone sharing state | +| Validation | §8A.7: fast hermetic factory absent/mismatch/tamper, Service-only export target, exactly-one same-type projection Service, no Device/Endpoint/Binding projection, no auto-Binding, intent-only spec/status-only observations, backing/target allowlists, finalizer/update propagation, Provider classification, canonical Nix stability including compiler-only `d2b.zones.work.parentZone = "local-root"`, child-local `ZoneLink/work-uplink` in `d2b.zones.work.resources`, local `zoneLinkRef` resolution, quotas/reconnect/revoke, and no FD/secret/path tests; slower real encrypted-stream integration for audio/security-key/observability/policy-gated USBIP | +| Removal proof | Not applicable (new surface) | + +### ADR046-zone-control-020 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-020` | +| Dependency/owner | ADR046-zone-control-019; `d2b-core-controller` owner | +| Current source | None — net-new ADR 0046 cross-Zone sharing model (D096); the core owner/child reconcile machinery (§11) is prior-art design context only | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-core-controller/src/export_import_projection.rs` (local qualified Service projection lifecycle owned by `ResourceImport`) | +| Detailed design | Core creates exactly one same-qualified-type projection Service per `ResourceImport` and keeps it synchronized with the remote Service lease. Operators/Nix separately author same-Zone matching Binding resources with `serviceRef` plus an allowed Guest/User/Zone target; Binding specs hold desired intent only and Binding controllers write observations only to status while owning Process/Endpoint children. On revoke, mark the projection draining/revoked and let Binding controllers stop children. On delete, wait for Bindings to be deleted/retargeted (`BindingReferencesRemain`), release the lease, delete only the projection Service/provider-owned children, then clear the import finalizer. Never create/delete Binding or project Device/Endpoint. | +| Integration | ADR046-zone-control-019 controller; owner/dependency reconcile (§11, ADR046-reconcile-*); local semantic Provider import adapter | +| Data migration | None — full d2b 3.0 reset | +| Validation | Exactly one same-type Service projection owned by import; no Device/Endpoint/Binding projection; Binding never auto-created/deleted; Binding target allowlist; intent-only spec/status-only observations; owned Process/Endpoint child cleanup; pending finalizer while Binding refs remain; reconnect only after generation/factory/schema revalidation; hermetic fake-adapter + real-stream integration tiers | +| Removal proof | Not applicable (new surface) | + +### ADR046-zone-control-021 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-021` | +| Dependency/owner | ADR046-zone-control-001, ADR046-zone-control-016; `d2b-core-controller` owner | +| Current source | `packages/d2bd/src/` process-global statics `USBIP_BACKGROUND_RECONCILE_ACTIVE`, `FORCE_SHUTDOWN_GENERATIONS`, and `activation_locks()`; current per-VM configuration staging symbols; ZoneLink cursor persistence in `zone_link_cursors` | +| Reuse source | ZoneLink handler/cursor scaffolding (§3); core coordinator patterns | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/{coordinator,configuration,zonelink}.rs` | +| Detailed design | Per D097 core-audit findings (§8B.2): move the process-global `USBIP_BACKGROUND_RECONCILE_ACTIVE`, `FORCE_SHUTDOWN_GENERATIONS`, and `activation_locks()` state into **per-Zone** provider/resource status or a per-Zone coordinator keyed by the authority index (no process-global singletons that ignore Zone boundaries). Migrate the configuration publisher's per-VM staging symbols to **per-Zone** staging under the single configuration-publisher authority. Make ZoneLink cursor persistence and restart adoption an authority owned by the ZoneLink handler (`ownerProof`; ambiguity quarantines). All coordinated through the core authority index; no direct broker path, no process-global lock. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (move process-global state to per-Zone status/coordinator). | +| Integration | Core authority index (ADR046-zone-control-019); ZoneLink reconcile (§3); configuration publication handler (ADR046-zone-control-016) | +| Data migration | Full d2b 3.0 reset; no process-global state persisted across the cutover | +| Validation | Two Zones on one host do not share `USBIP_BACKGROUND_RECONCILE_ACTIVE`/`FORCE_SHUTDOWN_GENERATIONS`/activation-lock state; per-Zone configuration staging isolation; ZoneLink cursor adoption by `ownerProof` and quarantine on ambiguity; hermetic with fakes | +| Removal proof | The process-global statics and per-VM staging symbols are deleted after the per-Zone coordinator reaches parity; confirmed by `cargo check` and a no-process-global lint | + +### ADR046-zone-control-022 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-022` | +| Dependency/owner | ADR046-zone-control-019, ADR046-api-001; `d2b-core-controller` + resource API owners | +| Current source | None — net-new D097 admission (Provider cardinality) | +| Reuse source | Core authority index (ADR046-zone-control-019); resource API admission (`ADR-046-resource-api-and-authorization`) | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/authority.rs`; resource API admission hook | +| Detailed design | Admission enforces **Provider controller cardinality** via the core authority index: most Providers are `exactly-one` per Zone; the observability Provider is `at-most-one` (zero-or-one). A `Create`/activation that would install a second controller for an `exactly-one` Provider (or a second observability Provider) is rejected with `duplicateConflict` naming the incumbent owner digest before any effect; config activation goes `Degraded`. | +| Integration | Core authority index; resource API admission; configuration activation | +| Data migration | None — full d2b 3.0 reset | +| Validation | Second Provider controller for an `exactly-one` Provider rejected with `duplicateConflict`; second observability Provider rejected; single controller admitted; `Degraded` config activation names the incumbent digest; hermetic | +| Removal proof | Not applicable (new surface) | + +### ADR046-zone-control-023 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-023` | +| Dependency/owner | ADR046-zone-control-019; `d2b-core-controller` + `d2b-contracts` owners | +| Current source | None — `Quota` and `EmergencyPolicy` scope-uniqueness are specified (§7, §8) but not implemented/tested at baseline | +| Reuse source | `Quota`/`EmergencyPolicy` schemas (§7, §8); core authority index | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/{quota,emergency_policy}.rs`; `packages/d2b-contracts/src/v3/{quota,emergency_policy}.rs` | +| Detailed design | Implement `Quota` and `EmergencyPolicy` scope-uniqueness as `exactly-one`-per-scope authorities in the core authority index: a second `Quota`/`EmergencyPolicy` claiming the same scope is a `duplicateConflict`. Add the scope-uniqueness admission, status, and the test matrix (per D094 fast hermetic tests). Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (implementation + tests). | +| Integration | Core authority index (ADR046-zone-control-019); resource API admission | +| Data migration | None — full d2b 3.0 reset | +| Validation | Duplicate-scope `Quota`/`EmergencyPolicy` rejected with `duplicateConflict`; single-scope admitted; union/individual scope flags honored; fast hermetic tests | +| Removal proof | Not applicable (new implementation) | + +### ADR046-zone-control-024 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-zone-control-024` | +| Dependency/owner | ADR046-zone-control-019, ADR046-zone-control-022; `d2b-core-controller` owner | +| Current source | None — net-new D097 hardware-audit contract; today physical/kernel backings are guarded per-Zone or per-process, not Host-global | +| Reuse source | Core authority index (ADR046-zone-control-019) | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/authority.rs` (Host-global index scope + hardware admission) | +| Detailed design | Extend the core authority index so `host`, `physical-device`, `seat`, and `external-service` authorities are keyed **Host-global** (`(Host, authorityClass, opaqueKeyDigest)`), admitting exactly one owner across all Zones on the host, while `zone`-scoped authorities stay Zone-local. Enforce the §8B.3 hardware rows: GPU full-device exclusive vs render-node shared; per-Guest swtpm and physical TPM exclusive (state never wiped); one Core-derived `physical-usb-backing/v1` identity digest claimed through the exact `(Host, physical-usb-backing, opaqueKeyDigest)` tuple by every USB or security-key implementation before effects, plus the separate host-global `usbip-host` module and the Host-global `Provider/device-usbip` relay `Endpoint`, exactly one per Core-derived Network UID/signed-policy-port digest with multiplexed arbitration and `usbip-network-relay-authority-conflict` on a second owner; macvtap/NIC `parentInterface` `passthru` globally exclusive across all Zones; host-shared `/dev/kvm` and `/dev/vhost-vsock` as `Provider/system-core` grants (no 28th Provider, no `kvm` busClass); globally-unique vsock CID; other fixed listener ports as `Endpoint`s; host store + per-Guest store-view writer; Network TAP/bridge. A second Zone claiming the same physical backing receives `physical-usb-backing-conflict` before any open, bind, withhold, module, relay, or attachment effect; restart adopts by `ownerProof`; Guest-stop drains dependent leases. GPU-owned `udmabuf`/video and per-session `vhost-vsock` tokens stay authority subresources/DeviceGrants (not resources/Providers). Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (Host-global index scope for host/physical-device authorities). | +| Integration | Core authority index (ADR046-zone-control-019); Provider cardinality admission (ADR046-zone-control-022); `Provider/system-core` KVM/vhost-vsock grant; `Provider/device-*` and `Network` authorities | +| Data migration | None — full d2b 3.0 reset | +| Validation | Two Zones on one host cannot both claim one GPU/TPM/USB/`/dev/kvm`/passthru NIC/vsock CID/fixed port — second is `duplicateConflict`; security-key and USB implementations resolving the same physical token submit byte-identical `physical-usb-backing` tuple keys and the loser receives `physical-usb-backing-conflict` before any effect; a second USBIP relay `Endpoint` for one Core-derived Network UID/signed-policy-port digest receives `usbip-network-relay-authority-conflict`, while multiple admitted Services share the multiplexed owner and no `Network` authority owns the listener/firewall; Provider-private authority classes/digests cannot bypass the collision; render-node shared admits bounded holders; per-Guest swtpm exclusive and marker never wiped; host-global adoption by `ownerProof`; hardware D096 exportability (GPU/KVM/TPM/store/macvtap non-exportable; semantic USB policy-gated); fast hermetic with fakes | +| Removal proof | Not applicable (new surface) | + +--- + +## 18. All decisions resolved + +All design decisions for Zone, ZoneLink, Provider, Role, RoleBinding, Quota, +and EmergencyPolicy are resolved as of this revision. No unresolved design +blocks remain. New design decisions are tracked in +`docs/specs/ADR-046-decision-register.md`. diff --git a/docs/specs/ADR-046-security-and-threat-model.md b/docs/specs/ADR-046-security-and-threat-model.md new file mode 100644 index 000000000..301be5d1e --- /dev/null +++ b/docs/specs/ADR-046-security-and-threat-model.md @@ -0,0 +1,2264 @@ +# ADR 0046 security and threat model + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-security-and-threat-model` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-session`, `d2b-bus`, Zone runtime/core-controller, `d2b-priv-broker`, every Provider dossier owner | +| Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-resource-store-redb`, `ADR-046-componentsession-and-bus`, `ADR-046-zone-routing`, `ADR-046-resources-zone-control`, `ADR-046-core-controllers`, `ADR-046-provider-model-and-packaging`, `ADR-046-provider-state`, `ADR-046-components-processes-and-sandbox`, `ADR-046-primitive-resource-composition`, `ADR-046-resources-host-guest-process-user`, `ADR-046-resources-volume`, `ADR-046-resources-network`, `ADR-046-resources-device`, `ADR-046-resources-credential`, `ADR-046-nix-configuration`, `ADR-046-cli-and-operations`, `ADR-046-telemetry-audit-and-support`, `ADR-046-current-code-migration-map`, `ADR-046-decision-register`, and all 27 `docs/specs/providers/ADR-046-provider-*.md` dossiers | +| Supersedes | None. This spec is a new cross-cutting synthesis. `SECURITY.md` and `docs/explanation/design.md` remain the disclosure policy and pre-v3 (v1/v2) threat model for the protected `v3` baseline and are not superseded; historical main-branch ADR 0045 (`a1cc0b2d^:docs/adr/0045-provider-and-transport-framework.md`, "d2b 2.0 provider and transport framework") and ADR 0032/0034 remain non-ancestry reuse sources whose useful invariants this spec adapts under D001/D041 | + +This spec is the normative closing security and threat-model contract for the +ADR 0046 d2b 3.0 Provider control plane. It does not introduce new +ResourceTypes, Providers, or wire contracts. It collects, cross-references, and +makes independently testable every security invariant that the other +ADR-046 specs and the 27 Provider dossiers already state, and it resolves the +remaining cross-cutting threat-model questions (asset inventory, attacker +classes, per-ResourceType/per-Provider-family threat matrix, reset boundary, +incident response) that no single resource or Provider spec owns end to end. +Every normative statement below cites the owning spec/dossier and, where the +owning spec has not yet stated a control explicitly, this spec states it and +assigns it to the owning spec's future implementation surface through a +`ADR046-security-*` validation work item defined in +[Implementation work items](#implementation-work-items). + +## Table of contents + +1. [Purpose, scope, and normative precedence](#purpose-scope-and-normative-precedence) +2. [Assets and trust boundaries](#assets-and-trust-boundaries) +3. [Attacker classes](#attacker-classes) +4. [Minimal core vs. semantic Provider authority](#minimal-core-vs-semantic-provider-authority) +5. [Signed package/manifest/schema/config trust and publisher roots](#signed-packagemanifestschemaconfig-trust-and-publisher-roots) +6. [Component identities and separate Processes](#component-identities-and-separate-processes) +7. [ComponentSession Noise boundaries and binding](#componentsession-noise-boundaries-and-binding) +8. [d2b-bus routing, RBAC, and authorization](#d2b-bus-routing-rbac-and-authorization) +9. [Exact recipient confidentiality](#exact-recipient-confidentiality) +10. [ZoneLink: no FD/resource grants, transport carriage-only](#zonelink-no-fdresource-grants-transport-carriage-only) +- [Cross-Zone Service projection security](#cross-zone-service-projection-security) +11. [Gateway Guest custody](#gateway-guest-custody) +12. [Injected EffectPort boundary and the privileged broker](#injected-effectport-boundary-and-the-privileged-broker) +13. [Prohibited Provider discovery/ambient authority](#prohibited-provider-discoveryambient-authority) +14. [Process least privilege: controller/service/worker](#process-least-privilege-controllerserviceworker) +15. [LaunchTicket integrity](#launchticket-integrity) +16. [User-domain Host: no isolation](#user-domain-host-no-isolation) +17. [Volume security](#volume-security) +18. [Guest-local vs. host-backed-guest custody](#guest-local-vs-host-backed-guest-custody) +19. [Credential security: end-to-end delivery and zeroization](#credential-security-end-to-end-delivery-and-zeroization) +20. [Content secrecy: clipboard, terminal, CTAP, notification](#content-secrecy-clipboard-terminal-ctap-notification) +21. [Audit vs. OTEL: redaction, cardinality, durability](#audit-vs-otel-redaction-cardinality-durability) +22. [Lifecycle security: update, revocation, finalizer, restart, adoption, quarantine](#lifecycle-security-update-revocation-finalizer-restart-adoption-quarantine) +23. [Availability and DoS controls](#availability-and-dos-controls) +24. [Incident response and support bundle](#incident-response-and-support-bundle) +25. [Reset boundary](#reset-boundary) +26. [Per-ResourceType threat matrix](#per-resourcetype-threat-matrix) +27. [Per-Provider-family threat matrix](#per-provider-family-threat-matrix) +28. [Forbidden designs](#forbidden-designs) +29. [Residual risks and explicit non-goals](#residual-risks-and-explicit-non-goals) +30. [Current-code fit](#current-code-fit) +31. [Implementation work items](#implementation-work-items) + +## Purpose, scope, and normative precedence + +This spec is normative wherever it states a control not already stated by an +owning spec, and it is a cross-reference (never an override) wherever an +owning spec has already stated the control. Per `docs/specs/README.md`, "no +spec may silently override another spec." Where this spec and an owning spec +disagree, the owning spec's ResourceType/Provider-specific text is +authoritative for that ResourceType/Provider, and this spec's cross-cutting +statement is corrected to match in the same revision that discovers the +conflict; no such conflict exists as of this baseline (see +[Current-code fit](#current-code-fit)). + +Scope: the v3 Zone resource/control plane described by the parent ADR and its +spec set — Zone runtime, redb resource store, ComponentSession/d2b-bus, +core-controller, every standard ResourceType (D035), the frozen initial +Provider catalog (D043-D049), Nix authoring/activation, and the CLI. Out of +scope: pre-v3 (v1/v2) daemon/broker/bash surfaces already covered by +`SECURITY.md` and `docs/explanation/design.md` (retained, not superseded); +upstream `nixpkgs`/`cloud-hypervisor`/`crosvm`/`swtpm` vulnerabilities; physical +attacks defeating disk encryption/TPM-bound unlock; CPU side channels; Nix +store supply-chain attacks upstream of the pinned artifact catalog (all four +remain explicitly out of scope per `SECURITY.md` and are not reintroduced +here). + +## Assets and trust boundaries + +### Assets + +| Asset class | Concrete instances | Owning spec | +| --- | --- | --- | +| Zone resource store | Per-Zone embedded redb database, revision log, RBAC index, reverse-owner index | `ADR-046-resource-store-redb` | +| ComponentSession key material | Zone/ZoneLink Noise static keys, ephemeral session keys, bootstrap PSKs, session nonces/replay caches | `ADR-046-componentsession-and-bus`, `ADR-046-zone-routing` | +| Credential bytes | Secret-service secrets, Entra tokens, managed-identity tokens, relay SAS/Entra credentials, TPM-sealed systemd credentials | `ADR-046-resources-credential`, credential/transport Provider dossiers | +| Volume state | Layout entries, ACLs, identity markers, sealed state (TPM NVRAM, provider component state, store-view hardlink farm) | `ADR-046-resources-volume`, `ADR-046-provider-state` | +| Process identity | pidfd, cgroup leaf, LaunchTicket, InvocationID | `ADR-046-resources-host-guest-process-user`, `ADR-046-components-processes-and-sandbox` | +| Provider signed packages | Artifact catalog entries (digest/manifestDigest/configSchemaDigest/signatureId/trustEpoch/conformanceAttestationDigest) | `ADR-046-provider-model-and-packaging`, `ADR-046-resources-zone-control` | +| Audit segments | Append-only hash-chained JSONL, privileged/standard/best-effort records | `ADR-046-telemetry-audit-and-support` | +| Gateway Guest custody | Cloud/relay credentials, remote registries, per-realm audit held only inside a Guest execution context | `ADR-046-resources-host-guest-process-user`, runtime/transport/credential Provider dossiers | +| Host substrate | Physical/local execution context, `/dev/kvm`, TPM/USBIP/GPU/security-key hardware, host network namespace | `ADR-046-resources-host-guest-process-user`, `ADR-046-resources-device`, `ADR-046-resources-network` | +| Interactive content | Clipboard bytes, terminal/PTY bytes, CTAP payloads, notification bodies | interaction Provider dossiers (`display-wayland`, `clipboard-wayland`, `shell-terminal`, `device-security-key`, `notification-desktop`) | +| Zone configuration | Nix-emitted resource bundle, catalog, schema fingerprints | `ADR-046-nix-configuration` | + +### Trust boundaries + +```text +Nix build/eval (offline, hermetic) + -> signed artifact catalog + Zone resource bundle (integrity-pinned) + -> Zone runtime activation (verifies bundle/catalog digests) + -> Zone resource store (redb; one writer; RBAC-gated) + -> core-controller (system-core, system-minijail: fixed bootstrap) + -> Provider processes (controller/service/worker; EffectPort-only) + -> privileged broker (sole privileged executor; allocator leases) + -> Host kernel / Guest VMM / cloud control plane + -> ComponentSession/d2b-bus (Noise NN/KK/IKpsk2; RBAC per hop) + -> child-local ZoneLink (parent keeps sealed topology/route state only; + carriage-only, no FD/path/credential) + -> Gateway Guest (relay/cloud credential custody; never the Host) +``` + +Six named boundaries, each independently enforced and independently +testable: + +1. **Nix build to runtime.** Everything the Zone runtime trusts at activation + (resource bundle, artifact catalog, ResourceTypeSchema/Provider settings + schema fingerprints) is produced by a hermetic, deterministic, offline Nix + build and is integrity-pinned by digest before activation. See + [Signed package/manifest/schema/config trust](#signed-packagemanifestschemaconfig-trust-and-publisher-roots). +2. **Zone resource store boundary.** Only the Zone runtime's redb coordinator + reads/writes the store. Providers/controllers reach it only through + ComponentSession/d2b-bus and the native RBAC engine — never a direct + handle, path, or ambient socket (D011, D006). +3. **Component/process boundary.** Every controller, service, and worker is a + distinct process/UID pair with least-privilege sandboxing; privileged + effects cross this boundary only through an injected `EffectPort` trait + (D077). See [Injected EffectPort boundary](#injected-effectport-boundary-and-the-privileged-broker). +4. **d2b-bus/ComponentSession boundary.** All control-plane traffic — + local, user, guest, remote, and nested — is authenticated Noise traffic + authorized by the native Role/RoleBinding engine; a payload cannot + self-assert a role (D054, D040). See + [ComponentSession Noise boundaries](#componentsession-noise-boundaries-and-binding) + and [d2b-bus routing, RBAC, and authorization](#d2b-bus-routing-rbac-and-authorization). +5. **ZoneLink boundary.** Cross-Zone traffic is capability-ceilinged, + RBAC-relayed, and structurally excludes FDs, credential bytes, and host + paths (D016, D081). See + [ZoneLink: no FD/resource grants, transport carriage-only](#zonelink-no-fdresource-grants-transport-carriage-only). +6. **Gateway Guest boundary.** Realm/cloud/relay credentials, remote node + registries, and realm audit for any non-host-resident Zone live inside a + Guest execution context, never the Host (ADR 0032, carried into D043 Guest + runtime Providers). See [Gateway Guest custody](#gateway-guest-custody). + +## Attacker classes + +Every subsequent section maps its controls back to one or more of these eight +classes by name. Prevention, detection, and recovery controls are cited per +class; "N/A" means the class cannot reach that asset by construction. + +### AC1: Malicious or compromised Provider package/process + +A Provider crate, once installed, is untrusted code running with +declared-but-bounded authority. AC1 covers a malicious publisher, a +compromised build pipeline, or a Provider process compromised at runtime +(e.g. via a parsing bug in its semantic logic). + +- **Prevention:** signed `PackageIdentity` (digest/manifestDigest/ + configSchemaDigest/signatureId/trustEpoch/conformanceAttestationDigest, + RZC lines 479-527, 671-683); component/RBAC bounds (D073); `EffectPort`-only + privileged access (D077); no broker client import (RZC lines 1621-1628 per + Provider dossier); canonical Provider spec is exactly `{ artifactId; config + }` with all other properties resolved from the signed manifest (D075). +- **Detection:** build-time signature chain verification (RZC lines + 2731-2735); runtime trust/conformance re-validation at activation; + `PackageTrusted` condition (`trusted|revoked|expired-epoch| + attestation-failed|conformance-failed`, RZC lines 671-683, 1753-1778). +- **Recovery:** quarantine (not deletion) on trust/conformance failure — all + component Processes stopped, exported ResourceTypes withdrawn, state + Volumes preserved for incident investigation (RZC lines 1752-1897, + `provider-quarantine-on-trust-failure`, RZC line 2983). + +### AC2: Compromised Guest + +A compromised VM/sandbox/cloud Guest is assumed to have full control of its +own execution context but no ambient authority beyond what its Guest +resource, attached Volumes/Networks/Devices, and ComponentSession +authorization grant. + +- **Prevention:** guest-agent network capabilities (CAP_NET_ADMIN/ + BIND_SERVICE/RAW) confined to the guest network namespace and verified + absent from the host netns effective set at broker spawn time (INV-NET-009); + east-west isolation default off (INV-NET-003); Volume mount views scoped per + attachment; ComponentSession KK enrollment required for any resource-plane + access from inside the Guest. +- **Detection:** capability presence in host netns → immediate Process + quarantine (INV-NET-009); pidfd/InvocationID re-verification on controller + restart catches an impersonating re-attach. +- **Recovery:** Guest quarantine (`Degraded`, no broad kill); Volume/Network/ + Device detach through normal finalizer teardown; parent allocator route + revocation plus child-local ZoneLink session teardown if the Guest was itself + hosting a child Zone gateway. The parent never deletes a child-store resource. + +### AC3: Compromised Host or local same-UID malicious process + +A compromised Host process, or any process sharing a UID with a d2b +component, is explicitly **not** contained by d2b — this matches +`SECURITY.md`'s existing "does NOT defend against ... multi-user trust on a +single host" and the historical "unlocked Secret Service is ambient to +processes with the same uid" invariant. AC3 is scoped to what remains true +even under this assumption. + +- **Prevention:** every non-host-resident credential/registry/audit lives in + a Guest (gateway custody, §11), so a compromised Host does not + automatically yield cloud/relay credentials; per-component sandboxing + (namespaces, zero ambient capabilities) limits a compromised same-UID + Process to its own declared Volume views and device attachments, not the + whole Host. +- **Detection:** `no_isolation: true` audit records make a user-domain + same-UID Host's reduced guarantee explicit and non-suppressible in status/ + CLI/audit (D067; see [User-domain Host: no isolation](#user-domain-host-no-isolation)). +- **Recovery:** none beyond the explicit warning — this is a documented + residual risk (see [Residual risks](#residual-risks-and-explicit-non-goals)), + matching `SECURITY.md`'s unsafe-local threat-model carve-out. + +### AC4: Remote relay / cross-Zone peer + +A relay (Azure Relay or equivalent) or a remote Zone reachable through a +ZoneLink is treated as untrusted transport carriage, not an authorization +input. + +- **Prevention:** relay/transport auth material never maps to a local Role + (`transport-azure-relay` invariant, lines 389-404: "a relay token proving + the Listen SAS claim does not grant Admin"); ZoneLink structurally excludes + FDs/credentials/paths (§10); capability ceilings propagate monotonically + downward and a child cannot advertise beyond its parent's grant (ZR lines + 2081-2098). +- **Detection:** `childStaticKeyFingerprint` re-verified on every reconnect + (ZR lines 554-580); route advertisement replay window and signature check + (ZR lines 1835-1895); malformed/exhausted hop count rejected at the + source bus (ZR lines 1880-1895). +- **Recovery:** parent allocator revocation sets the admitted capability ceiling + to empty and withdraws its private route projection; the child-local ZoneLink + handler then closes existing streams with `zone-link-revoked`. + Already-committed child-Zone operations are not rolled back (ZR lines + 1835-1895). + +### AC5: Forged or tampered Nix artifact / configuration bundle + +A forged or downgraded Zone resource bundle, artifact catalog, or Provider +package attempts to make the Zone runtime activate untrusted state. + +- **Prevention:** bundle SHA-256 digest computed at eval time and verified + before any Create/UpdateSpec (`ADR-046-resources-credential` lines + 1069-1082); `catalogSha256` binds the artifact catalog to the bundle + (`ADR-046-nix-configuration` lines 192-202); schema fingerprints + (`resourceTypeSchemaDigests`, `providerSchemaFingerprints`) verified before + any resource of that type/Provider activates (`ADR-046-nix-configuration` + lines 126-137). +- **Detection:** any mismatch aborts the **entire** bundle + (`config-bundle-integrity-failed`, `bundle-schema-mismatch`, + `config-catalog-mismatch`) — never a partial activation. +- **Recovery:** the prior activated generation remains live; generation + counter rejects replay/downgrade (bundle `generation` must be strictly + greater than `store_meta.active_configuration_revision`, RZC lines + 2753-2754). + +### AC6: Local same-UID peer abusing ComponentSession/d2b-bus + +A local process with a legitimate Unix-socket connection attempts to gain +authority beyond what its authenticated peer credentials grant (role +self-assertion, wildcard permission claims, replayed handshake). + +- **Prevention:** `AuthenticatedSubjectContext.subjectRef` is derived only + from kernel `SO_PEERCRED`/enrolled static-key identity, never from the + request payload (D054; ZR lines 1526-1555); explicit wildcards + (`resourceNames: ["*"]`) permitted only for core-controller-generated + Roles (RZC lines 868-997); RoleBinding `spec.roleRef` immutable after + creation. +- **Detection:** `provider-wildcard-permission-restricted` rejects any + non-bootstrap wildcard claim (RZC line 2985); denial decisions are never + cached as allow (RZC lines 868-997). +- **Recovery:** RoleBinding deletion is one atomic redb transaction (index + removal + `Deleted` revision + row removal) — no observable intermediate + state a racing caller could exploit (RZC lines 1062-1260). + +### AC7: Privileged broker abuse (compromised Provider requesting broker action) + +A compromised or buggy Provider process attempts to obtain privileged host +mutation beyond its declared authority by abusing the injected `EffectPort`. + +- **Prevention:** the broker is the sole privileged executor; it re-derives + every privileged parameter from allocator-approved leases and the trusted + bundle, never from caller-supplied raw parameters (`d2b-priv-broker` + invariant, `SECURITY.md` "Host-prepare trust-boundary delta"; D077); a + Provider crate imports no broker service/client/DTO (compile-time-audited + per Provider dossier, e.g. `ADR-046-provider-system-minijail.md` lines + 1621-1628). +- **Detection:** `MinijailProcessEffectPort`/`VolumeEffectPort`/ + `NetworkEffectPort`/`AzureEffectPort` calls are all typed, opaque-ID, + bounded operations; any operation outside the closed enum is a compile + error, not a runtime check to bypass. +- **Recovery:** compromise of a Provider process cannot escalate to + arbitrary host mutation beyond the declared broker-op enum (carried + forward from `SECURITY.md`'s existing daemon-compromise invariant). + +### AC8: State tamper and resource/API DoS + +An attacker with some authorized access attempts to corrupt persistent state +(Volume identity markers, redb rows) or exhaust shared resources (connection +slots, attachment credits, audit writes) to deny service to other Zone +tenants. + +- **Prevention:** Volume identity markers are tamper-evident + (`st_dev`/`st_ino` + schema digest, `ADR-046-provider-state` lines + 301-321); redb single-writer with expected-revision preconditions rejects + stale/conflicting writes (D005); quota/backpressure ceilings bound every + session/attachment/queue resource (§23). +- **Detection:** marker `missing`/`replaced` status transitions the Volume + to `Failed` rather than silently re-provisioning; `d2b_api_admission_rejected_total` + counter tracks rejection reasons (`auth|quota|conflict|invalid|schema`). +- **Recovery:** quarantine-not-kill for ambiguous process/Volume identity + (§22); privileged audit records are never dropped under load, even during + a DoS (`ADR-046-telemetry-audit-and-support` durability classes, §21). + +## Minimal core vs. semantic Provider authority + +The Zone core (redb store, RBAC engine, ComponentSession/d2b-bus, and the two +fixed bootstrap controllers `Provider/system-core` and +`Provider/system-minijail`) is the only code trusted with ambient authority. +Every other Provider — including all four Guest runtimes, both Volume +Providers, and every interaction/credential/transport Provider — is a +semantic Provider: it decides *what* should happen and calls an injected, +narrowly typed `EffectPort` trait to make it happen; it never decides *how* +to reach the kernel/network/filesystem/cloud API directly (D077). + +| Property | Minimal core | Semantic Provider | +| --- | --- | --- | +| Broker/allocator access | Yes (core-controller + broker are the sole privileged executors) | No — never imports a broker client/DTO (D077) | +| Bootstrap exception | `Provider/system-core` and `Provider/system-minijail` are the two fixed, non-configurable bootstrap Providers (D036, D051); embedded in the Zone runtime binary, pre-created before any other resource, and still subject to full trust/package/conformance validation (RZC lines 754-777, "not exempt from trust checks") | None — every other Provider is installed as an ordinary `Provider/` resource before use (D016) | +| Own Process bootstrap | The fixed core-controller process launches every other Provider's static controller/service Processes and any *declared* optional per-component state Volumes (D078; a component declares a state Volume only under the storage-need test, D087) | A Provider controller never bootstraps its own Process; it may create authorized dynamic child Process/EphemeralProcess/primitive resources after it exists | +| Status authority | Computes aggregate Provider status from component/dependency/process health (D085) | Writes status only for the ResourceTypes/fields it is explicitly authorized to own; never self-declares its own aggregate `Provider` resource status | +| RBAC authority | Native Role/RoleBinding engine, wildcard Roles restricted to core-controller-generated Roles only | Cannot claim a wildcard permission (`resourceNames: ["*"]`/empty `executionRefs`) without explicit review (RZC lines 868-997, `provider-wildcard-permission-restricted`) | + +Authoring `system-core` or `system-minijail` as an operator-declared Provider +in Nix fails at eval time with `"system-core and system-minijail are +bootstrap-only providers and cannot be hand-authored"` (RZC lines 754-777, +2350-2354). This closes the obvious "shadow the bootstrap Provider" attack: +an attacker cannot register a second, attacker-controlled +`Provider/system-core`. + +## Signed package/manifest/schema/config trust and publisher roots + +Every `PackageIdentity` field is resolved at Nix build time from the artifact +catalog, never operator-authored inline in `spec` (`ADR-046-resources-zone-control` +lines 479-527, 671-683): + +| Field | Requirement | +| --- | --- | +| `digest` | Required, non-empty SHA-256 of the artifact | +| `manifestDigest` | Required, non-empty SHA-256 of the manifest file | +| `configSchemaDigest` | Required, non-empty SHA-256 of the config schema file | +| `signatureId` | Required, non-empty; links to the publisher's trust root | +| `trustEpoch` | Required; must not be revoked | +| `conformanceAttestationDigest` | Required SHA-256; must be present in the known attestation store | +| `revocationRef` | `null` or a stable revocation-check token | + +Verification happens twice, at two independent points, so a compromised +build host cannot alone forge a trusted artifact: + +1. **Build time** (resource compiler / `xtask gen-schemas`-class tooling): + artifact manifest signature chain verified against the installed trust + store; `conformanceAttestationDigest` verified present in the known + attestation store; `configSchemaDigest` and `manifestDigest` verified + against the actual derivation output — any mismatch is a build failure, + not a runtime warning (RZC lines 2731-2735). +2. **Runtime** (Zone activation): the activation controller re-verifies + `catalogSha256` and all schema fingerprints before applying any bundle + (`ADR-046-nix-configuration` lines 126-137, 192-202). + +`spec.config` for every Provider/ResourceType is restricted at both layers: +no credential bytes, no raw host paths, no PIDs/process arguments, no ambient +authority values (RZC lines 479-527). A field annotated `credentialRef: true` +in the signed Provider settings schema accepts **only** a +`Credential/[a-z][a-z0-9-]*` ResourceRef string; a raw inline value is a +build-time `inline-secret-in-settings`/`credential-value-must-be-ref` error +(`ADR-046-resources-credential` lines 929-930, 954-957; RZC lines 479-527). +`$credentialRef` is the only permitted `$`-prefixed key in config JSON — any +other `$`-prefixed key is rejected at build time (RZC line 2396). A +heuristic `contains_sensitive_shape` lint additionally scans every string +value for PEM headers, bearer prefixes, AWS-key shapes, and long base64/hex +runs at Nix eval time, failing the build under `--strict-secrets` +(`ADR-046-resources-credential` lines 929-930; RZC lines 2408-2411). + +Trust failure and conformance failure are handled identically: the Provider +is **quarantined**, not deleted. `quarantined = true`; all component +Processes are stopped; exported ResourceTypes are withdrawn; state Volumes +are preserved for incident investigation (RZC lines 1752-1897). See +[Lifecycle security](#lifecycle-security-update-revocation-finalizer-restart-adoption-quarantine). + +Store paths and raw Nix closure metadata are catalog-private implementation +data. They never appear in a public ResourceSpec, status, audit record, log +line, metric label, or span attribute (`ADR-046-nix-configuration` lines +178-181; RZC lines 2566-2569, 3075). Build tests +`nix-build-artifact-store-path-absent-from-bundle` and +`nix-build-artifact-store-path-absent-from-config` enforce this at every +build (RZC line 3057). + +## Component identities and separate Processes + +A Provider is one independently buildable, signed crate/package (D012), but +it may declare up to 8 controller components, 8 service components, and 32 +worker process templates (D073); each is compiled to its own `binaryRef` — +a key into the package's `executableDigests` table binding that component to +one exact, digest-identified binary (`ADR-046-resources-zone-control` lines +754-777). `allowedDomains` (`system`/`user`) on each component prevents a +user-domain component from claiming system-domain execution privileges. + +`Provider/system-core` and `Provider/system-minijail` share the same +core-controller process boundary at bootstrap, but they use **distinct +authenticated subjects and closed RBAC grants**; after bootstrap, both are +ordinary RBAC subjects like any other component (RZC lines 1564-1627). +Bootstrap authorization is a compiled, non-extensible narrow policy: only +these two exact subjects receive the closed initial recovery/config +publication verbs, it cannot be widened by operator config, and +`bootstrap-supersession-atomic` guarantees no window where both bootstrap +policy and stored RBAC are simultaneously active once +`IndexBuilt=True` (D052; RZC lines 1564-1627, line 3030). + +Controllers own their watch/coalescing queues and retry decisions +independently; workers have **no** `ResourceClient`, no d2b-bus/dependency +portal, no Credential access, no CLI, no broker, and no child-spawn +authority — every resource, FD, and config value they need is inherited +through their `LaunchTicket` (D078). This is the concrete mechanism behind +[Process least privilege](#process-least-privilege-controllerserviceworker). + +## ComponentSession Noise boundaries and binding + +ComponentSession (`ADR-046-componentsession-and-bus`) is the sole +control-plane transport authentication layer. Three Noise profiles, all +`25519·ChaChaPoly·SHA256`, are the closed set: + +| Profile | Usage | Forbidden fallback | +| --- | --- | --- | +| `Nn` (ephemeral/ephemeral) | Bootstrap and intra-Zone local sessions where static keys are unavailable | Never used for sensitive credential delivery (D056) | +| `Kk` (static/static) | Enrolled peer sessions — ZoneLink, credential delivery, guest control | Both sides must already have enrolled static keys; no downgrade to `Nn` | +| `IKpsk2` (initiator-known/responder-known + PSK) | One-time bootstrap using an allocator-issued single-use PSK | PSK is `Secret32` (zeroizing), checked single-use at handshake time | + +There is no `none`, local plaintext, HMAC, long-lived guest PSK, or weaker +retry. Noise ephemeral keys, cipher state, nonces, sockets, and session keys +are never persisted or adopted across reconnect (adapted from historical +main ADR 0045 lines 1389-1391; re-affirmed structurally in +`ADR-046-zone-routing` lines 96-124). + +**Prologue and offer binding.** `prologue = preface‖canonical-offer`: the +16-byte network-order preface (`PREFACE_MAGIC = b"D2BCS2\r\n"`, +`COMPONENT_SESSION_MAJOR = 2`, `COMPONENT_SESSION_MINOR = 0`) and the +canonical-serialized `HandshakeOffer` (bounded +`MAX_HANDSHAKE_OFFER_BYTES = 16384`, canonical length 148 bytes) are +concatenated and bound into the Noise prologue. Any tampering with the offer +diverges the transcript and fails the handshake (ZR lines 96-124). The offer +carries exactly one endpoint-policy allowed purpose, initiator role, +responder role, service package, schema fingerprint, Noise profile, limit +profile, and attachment policy — there is no semver range, ignored feature +flag, codec fallback, or lower limit selected after failure. + +**Generation/revision binding.** The handshake `INIT` payload contains a +SHA-256 commitment to the current generation/revision; the responder +verifies this before completing `ACCEPT` and fails closed on any mismatch +(ZR lines 96-124). For ZoneLink specifically, `spec.childStaticKeyFingerprint` +(sha256-hex) pins the child's expected static public key and is re-verified +on **every** reconnect; a child presenting a different key is refused before +any resource exchange. `childZoneUid` is recorded on first successful +connection and checked on every reconnect; a UID change resets the cursor to +revision 0 rather than silently continuing (ZR lines 554-580). Reconnect +always performs a new handshake and increments the session generation; +calls retry only under the provider/operation idempotency contract, and +streams resume only through an explicit generation and cursor contract +(historical main ADR 0045 lines 1574-1576, carried forward unchanged). + +**Record protection.** AEAD encryption with a directional sequence number; a +1024-entry replay cache per direction; `MAX_PROTECTED_CIPHERTEXT_BYTES = +65535`; `MAX_LOGICAL_MESSAGE_BYTES = 1_048_576` (1 MiB). Handshake deadlines +are `LOCAL_HANDSHAKE_DEADLINE_MS = 5000`, `REMOTE_HANDSHAKE_DEADLINE_MS = +15000` (ZR lines 37-55, 96-124; RZC line 3568). + +**Directional local peer credentials.** The acceptor reads kernel +`SO_PEERCRED` from the accepted socket before the preface. The connecting +initiator authenticates the responder endpoint from an expected fixed or +broker-generated path under the integrity-checked endpoint contract, anchored +parent ownership/mode, matching pre-/post-connect path device/inode/type +observations, and the expected system/user socket unit and activation +owner. An established ComponentSession endpoint is never transferable by +`SCM_RIGHTS`, pidfd duplication, inheritance, or broker handoff (historical +main ADR 0045 lines 1394-1449, carried forward as `d2b-bus` invariants). + +`GUEST_SESSION_CREDENTIAL_*` constants and `GuestBootstrapPsk`/ +`GuestSessionCredentialV1` from the historical realm-era ADR 0045 model are +explicitly excluded from v3 (ZR lines 37-55): v3 has no long-lived shared +guest-control HMAC token; every guest session is an enrolled `Kk` (or +one-time `IKpsk2`) ComponentSession like any other Zone peer. + +## d2b-bus routing, RBAC, and authorization + +ComponentSession authentication and native resource authorization share one +identity: `AuthenticatedSubjectContext.subjectRef` is set from the KK-enrolled +parent static key identity (or the local `SO_PEERCRED` peer for `Nn` +sessions), **never** from any field in the forwarded request payload (D054; +RZC lines 868-997, 1062-1260). This is the concrete mechanism behind "no +self-asserted role": a component cannot claim `Admin`, a different +principal, or a wider capability merely by writing it into a request. + +**Closed verb sets.** Resource verbs: `get, list, watch, create, update-spec, +update-status, update-metadata, update-finalizers, delete, use-credential, +admin-credential`. Session verbs: `connect, invoke, open-stream, relay, attach, +cancel, observe, audit-export, support-bundle`. Credential use/admin grants +require their exact canonical subresources. The two diagnostic session verbs +bind only `d2b.audit.v3.AuditService/Export` and +`d2b.support.v3.SupportService/GenerateBundle` and imply no resource +authority. All verb tokens outside these closed sets are rejected with +`role-unknown-verb-rejected` +(RZC lines 3422-3430, adapted from `verb_requires_admin()` in the baseline +`d2bd/src/admission.rs`). + +**Relay is narrow transport authority.** `relay` permits the exact +already-authenticated ZoneLink/transport subject to forward one admitted +invocation or stream to the route-selected next hop. It grants no resource +CRUD, identity mapping, capability widening, FD/credential authority, or local +lifecycle permission. It is core-generated and ZoneLink-scoped by default. +Wildcard, self-asserted, Provider-authored, or ordinary operator-authored relay +grants fail admission unless an already-authorized local administrator +explicitly permits the exact bounded grant through durable admin policy. + +**Per-hop RBAC.** For every intermediate Zone hop in a ZoneLink chain, the +forwarding d2b-bus performs two independent local checks before forwarding: +`relay` for the authenticated adjacent-Zone transport subject and the immutable +target verb for the invocation/stream. Named methods retain one immutable +resource name. Nameless `List`/`Watch` retain the exact non-empty authorized +name set and bounded filters; every hop rejects a selector that could widen the +set and forwards the filters byte-for-byte. The final target checks the same +target verb and selector again. A Zone never grants authority beyond its own +Role/RoleBinding evaluation; missing relay, missing target authority, or +unavailable policy state fails closed. This closes both the historical "one +relay hop is authorized, therefore every downstream hop is trusted" gap and +the converse "relay implies target authority" gap. + +**Wildcard restriction.** Explicit wildcard (`resourceNames: ["*"]`) is +permitted only for core-controller-generated Roles. Operator-authored or +Provider Roles with a wildcard are rejected at admission with +`resource-schema-invalid` (RZC lines 868-997); `provider-wildcard-permission-restricted` +is the corresponding conformance test (RZC line 2985). An empty +`resourceNames: []` for a non-core Role means "all names of this +ResourceType" and is a distinct, narrower grant than `["*"]`. +Relay is stricter: neither `["*"]` nor the empty/all-name form is accepted, +including for core-generated relay Roles. + +**RoleBinding immutability and atomic deletion.** `spec.roleRef` is immutable +after creation. Deletion is one atomic redb write transaction: RBAC index +entry removal, `Deleted` revision event, and resource row removal all commit +together — there is no observable intermediate state a racing authorization +check could exploit (RZC lines 1062-1260). A subject UID change invalidates +existing sessions via `SubjectIdentityChanged`, and existing positive +authorization-decision caches are invalidated; **denial decisions are never +cached as allow** (RZC lines 868-997). + +**Scope narrowing.** `scopeNarrowing` may only restrict, never grant beyond +the referenced Role; a `scopeNarrowing` verb absent from the Role is rejected +with `rolebinding-scope-exceeds-role-rejected` (RZC lines 1062-1260). + +**Status write authority.** Only the core-controller handler may +`update-status` for any Zone control resource (`zone-control-status-owner-only`, +RZC line 3039) — a Provider cannot forge its own aggregate health. + +## Exact recipient confidentiality + +Every dedicated-recipient authorization check in the resource/session model +enforces exact-match recipient identity, never a class or wildcard match: + +- **Credential `consumerRef`.** The bus admission check verifies that the + requesting session's authenticated subject matches the Credential's + `consumerRef` exactly; a mismatch closes the session with + `authorization-denied` (`ADR-046-resources-credential` lines 378-383, + 480-491). Effective permission is the intersection of the exact + `Credential.spec.allowedOperations` value and the exact Role `subresources` + value under `use-credential`, further narrowed by consumer/scope/structural + checks; a request outside that intersection is rejected the same way. +- **externalPrincipalSelector.** Bounded at 512 bytes canonical JSON and + restricted to opaque enrollment digests — it may not contain credential + bytes, so an external-principal RoleBinding cannot be used to smuggle + secret material into the RBAC index (RZC lines 1062-1260). +- **Sensitive KK delivery.** Credential Providers may deliver raw token + bytes or `SignChallenge` signatures only over a dedicated end-to-end Noise + `Kk` ComponentSession to the exact enrolled consumer Provider/component + named by the Credential's signed descriptor/RBAC grant (D055, D056, D068); + see [Credential security](#credential-security-end-to-end-delivery-and-zeroization). + +The common failure mode this prevents is a "close enough" recipient match — +for example a Credential intended for one Provider component being readable +by a sibling component of the same Provider, or by any subject holding a +generic "read credentials" Role. Exact `consumerRef`/session-subject matching +closes both. + +## ZoneLink: no FD/resource grants, transport carriage-only + +Every resource belongs to exactly one Zone (D016). Each non-root child stores +and reconciles its own self-matching `ZoneLink/` uplink. The compiler-only +`{ childZone, parentZone }` topology selects the parent allocator; that parent +keeps only sealed allocation/topology state and authenticated route/projection +status. It has no reciprocal ZoneLink row or ZoneLink handler. A parent accesses +child resources only through the child's own Zone API over the authenticated +route — it never mounts or mirrors the child store, and ordinary resource +references never cross Zones (D017). + +**Structural, not policy, enforcement.** No FD, credential, or host path is +forwarded across a ZoneLink. This is enforced as a **structural +serialization-boundary failure**, not a runtime policy decision that could be +misconfigured away (ZR lines 1779-1801): + +- **File descriptors:** `SCM_RIGHTS` is local-Unix-socket only. Every + ZoneLink transport (vsock, Azure Relay, etc.) carries no FDs. Attempting to + attach an FD to a ZoneLink frame fails with + `attachment-not-permitted-over-zone-link` (ZR line 2065). + `transport-vsock`'s `TransportDescriptor.attachment_support = false` + (INV-VSOCK-003) and `transport-unix`'s `attachments_enabled=false` for + `route_class=zone-link` (`attachment-policy-conflict`) both structurally + enforce the same invariant at the transport layer. +- **Credential bytes:** no token, PSK, private key, bearer token, enrollment + secret, or credential-lease byte may appear in any forwarded frame. +- **Filesystem paths:** no filesystem path, socket path, device path, or Nix + store path in routing metadata. `ZoneRoutePath` carries only Zone tree + edges and a session generation — no transport socket information or + credential. +- **Process identities:** no PIDs, pidfds, or broker operations propagate + across a Zone boundary. + +**Transport carriage-only.** Transport Providers (`transport-unix`, +`transport-vsock`, `transport-azure-relay`) never own ZoneLink state (D081). +The child Zone's core ZoneLink handler alone reads/writes/finalizes its local +`ZoneLink` and owns Noise/session/reconnect/route/idempotency/intent state; the +parent has only its allocator and route-engine state. The child handler calls +typed `OpenTransport`/`CloseTransport`/`ObserveTransport` on the installed +Transport Provider, which returns only opaque `OwnedTransport`/byte-stream +handles and observations. A Transport Provider crate does not call the raw +transport syscalls itself (e.g. `transport-vsock` does not depend on +`tokio-vsock`; `AF_VSOCK` `socket`/`connect`/`bind`/`accept` live exclusively +in the core `LiveVsockEffectPort` adapter, INV-VSOCK-004). + +**Additional structural rules:** + +- Cross-Zone `ownerRef` fails admission with `resource-ref-invalid`; + `ResolveRef` cannot cross Zone boundaries (RZC lines 479-527). +- A `CommitBatch` spanning multiple Zones is rejected before forwarding. +- Capability ceilings propagate monotonically downward; a child cannot + advertise capabilities beyond what its parent allocated + (`zone-advertisement-namespace-violation`, ZR lines 2081-2098, line 2070). +- A parent Zone never receives a database handle, credential, token, or + cross-Zone `ResourceRef` from a child; the parent only learns child state + from authenticated responses (ZR lines 2081-2098). +- Route advertisements are signed and expiring (max 7200 s, max 64 routes per + advertisement); replay is rejected on a + `(zone, principal, node, operation_kind, idempotency_key)` 6-tuple within a + 15-minute dedup / 60-minute tombstone window (ZR lines 1835-1895). +- Hop count is enforced at the **source** bus, not the destination + (`maxHops` default 8, max 16); a malformed hop count that claims more hops + than physically arrived is `malformed-hop-count`, and counter exhaustion is + `hop-limit-exceeded` (ZR lines 1880-1895). + +## Cross-Zone Service projection security + +D096 shares capability, not backing authority. Every approved exportable +Provider declares a signed projection factory that binds one qualified +semantic/provider-neutral `*Service` type, its qualified semantic `*Binding` +type, allowed same-Zone owner-Service backing ref types, allowed Binding target +ref types, and a strict projection schema/fingerprint plus aggregate semantic +factory fingerprint. Missing, unsigned, downgraded, or mismatched metadata +fails closed before advertisement, lease, or projection creation. + +The security boundary is structural: + +- `ResourceExport.resourceRef` targets only the owner Service. Exporting a + Device, Endpoint, Binding, Credential, or backend directly is rejected. +- An import creates exactly one same-qualified-type, non-authoritative local + projection Service. It cannot open the remote backing or claim remote + authority and never becomes a Device/Endpoint projection. +- Binding is same-Zone operator/Nix consumer intent. Its spec references + `serviceRef` and one factory-allowed consuming Guest/User/Zone and contains no + observed state; all observations belong only in status. The import controller + cannot create, export, mutate as authority, or delete Binding. +- A forged Binding cannot spoof remote authority: Binding status is written only + by the registered Provider controller; service availability comes only from + the authenticated import lease and verified factory/generation; RoleBinding + checks independently authorize Binding and target use. +- Projection specs/status/advertisements/CLI graphs contain no backing refs, + FDs, credentials, secrets, raw paths/locators, payload bytes, or remote refs. + High-churn session, stream, ceremony, transfer, and lease handles remain + internal and are never resources or authority evidence. +- The semantic base is not an implementation side channel. `providerRef` + selects a local implementation. Strict `spec.provider` is permitted only on + authored owner Services and Bindings; canonical minimal base admission works + without that extension. A Core-generated projection has only ResourceImport + ownership, `providerRef`, and semantic base/import fields, never + `spec.provider`. Its route derives from the signed local Provider descriptor, + `providerRef`, and ResourceImport record; implementation observation appears + only in `status.provider`. Export/import never copies the remote extension or + changes semantic type. + PipeWire, CTAPHID, OTEL, USBIP, package, binary, adapter, and protocol details + are rejected from base spec/status, conditions, errors, fingerprints, + advertisements, and graph identity. The base `providerRef` is the sole opaque + implementation selector and is not advertised across the ZoneLink. +- Revocation first prevents new sessions and degrades the projection Service. + Binding controllers stop owned Process/Endpoint children. Import finalization + waits for operator-owned Bindings to be deleted/retargeted and never cascades + them, preventing a controller from erasing policy intent. + +The initial allowlist is `audio.d2bus.org.AudioService`, +`security-key.d2bus.org.SecurityKeyService`, and +`telemetry.d2bus.org.TelemetryService`; +`usb.d2bus.org.UsbService` requires Provider, Zone, export, and physical-device +policy opt-in. Their matching `AudioBinding`, `SecurityKeyBinding`, +`TelemetryBinding`, and `UsbBinding` types and all backing resources remain +non-exportable. The initial implementations are respectively +`Provider/audio-pipewire`, `Provider/device-security-key`, +`Provider/observability-otel`, and `Provider/device-usbip`; those Provider names +do not confer authority and are not semantic type namespaces. All other +Providers remain non-exportable. + +## Gateway Guest custody + +A Zone that is not host-resident reaches its remote/cloud/relay surface only +through a Guest execution context; the Host never holds that Zone's relay, +provider, or remote-node credentials, registries, or audit (adapted from +ADR 0032's gateway-VM invariant, carried into the v3 Guest runtime Provider +family via D043 and D050's Host/Guest split). + +- **Admission-enforced placement.** Every cloud/relay-facing component's + `executionRef`/`gatewayExecutionRef` must resolve to a `Guest/`, never + a `Host/`; the admission controller rejects a Process with + `executionRef: Host/*` for any ACA, Azure VM, or Azure Relay component at + Nix eval time and at runtime (`ADR-046-provider-runtime-azure-container-apps.md` + lines 1100, 1186-1201, 1288; `ADR-046-provider-transport-azure-relay.md` + lines 428-431). + `Provider.spec.config.executionRef`/`gatewayExecutionRef` absent or + Host-pointing is a hard rejection, not a warning. +- **No Host-domain subject has any Role/RoleBinding for the cloud/relay + Provider** — the RBAC grant itself does not exist for Host subjects, so + there is no privilege to escalate even if a Host process were compromised + (ACA dossier, "No Host-level Azure transport; ZoneLink from gateway Guest + only"). +- **Credential acquisition stays inside the Guest.** Token bytes are + acquired per reconcile tick inside the gateway Guest's credential-consuming + component, held only in zeroizing memory, and zeroized immediately after + the cloud/relay API call; they never cross to the Host Zone controller, + never appear in any resource spec/status/audit/OTEL/log/environment + variable (`ADR-046-provider-transport-azure-relay.md` lines 416-424, + 461-478; `ADR-046-provider-runtime-azure-virtual-machine.md` lines + 1363-1367). +- **No cross-Zone credential minting.** No parent-minted token is delivered + to a child Zone; each Zone's gateway component independently acquires its + own credentials (`ADR-046-provider-transport-azure-relay.md` lines + 454-459). +- **Relay/managed-identity/Entra auth material never maps to a local Role.** + A relay token proving a `Listen` SAS claim does not grant `Admin`; a + managed-identity token proving a `Send` Entra claim does not grant Zone + resource API access beyond what the KK-enrolled child static key + authorizes (ADR 0032; `ADR-046-provider-transport-azure-relay.md` lines + 389-404). This is the same invariant as AC4's "relay identity is not local + auth." +- **Bootstrap PSK single-use, sealed at rest.** Where a Guest bootstraps a + cloud VM via `IKpsk2` (Azure VM `bootstrap-svc`), the PSK is sealed + ciphertext in a Volume — plaintext is held only in the controller process + address space during the delivery window and zeroized immediately after + (`ADR-046-provider-runtime-azure-virtual-machine.md` lines 412, 432, 447, + 450-454). Replay of a consumed PSK is rejected. +- **Degraded, not silently local, on Guest unavailability.** If the gateway + Guest is unreachable, the Provider transitions to `Degraded` with a typed + reason (`gateway-guest-unavailable`); there is no fallback to a Host + process (ACA dossier line 1218). + +## Injected EffectPort boundary and the privileged broker + +Every semantic Provider composes behavior by creating owned primitive +resources and by calling one injected, narrowly typed, async `EffectPort` +trait per domain (D077). No Provider process imports the broker, receives a +broker socket/DTO, or directly opens a host path/device/systemd socket, or +performs a privileged mutation — including the primitive Process/Volume/ +Network/Device Providers themselves. + +| Domain | EffectPort | Sole privileged executor | Enforcement | +| --- | --- | --- | --- | +| Process | `MinijailProcessEffectPort` | `Provider/system-minijail` controller + `d2b-priv-broker` `SpawnRunner` | Compile-time dependency audit: the Provider crate imports no `d2b.broker.v3` service/client/DTO (`ADR-046-provider-system-minijail.md` lines 1621-1628) | +| Process (systemd) | `SystemdProcessEffectPort` | `Provider/system-systemd` controller via D-Bus transient unit API | Controller never connects to the systemd D-Bus socket directly and never calls `systemctl` as a subprocess | +| Volume | `VolumeEffectPort` | `Provider/volume-local` controller + broker `ProvisionLayoutEntry`/`RepairLayoutEntry`/`CleanupLayoutEntry`/`RotateSealingKey`/`PrepareSwtpmDir`; key rotation is requested only through `VolumeEffectPort::rotate_sealing_key` | "The controller process holds no claim that grants access to raw host paths" (`ADR-046-provider-volume-local.md` lines 1739-1776) | +| Network | `NetworkEffectPort` | `Provider/network-local` controller + broker `CreatePersistentTap`/`DeletePersistentTap`/`SetBridgePortFlags`/`ApplyNftables`/`ApplySysctl` | "The controller holds no broker role and no `network-admin` capability" (`ADR-046-provider-network-local.md` lines 1680-1682) | +| Device (USBIP) | `UsbipEffectPort` | Core adapter + broker `UsbipBindFirewallRule { action: Ensure \| Remove }` for both acquisition and release | Network-local never owns USBIP TCP/3240 exposure; there is no separate release operation | +| Device (vsock) | `VsockEffectPort` | Zone runtime `LiveVsockEffectPort` | `tokio-vsock` is not a dependency of `transport-vsock` (INV-VSOCK-004) | +| Cloud (ARM/ACA) | `AzureEffectPort` | The cloud runtime Provider's own controller, confined to the gateway Guest | All calls non-blocking; `AzureOperationHandle` is opaque, max 256 bytes | + +These names are the planned closed ADR 0046 broker-operation contract even +where the baseline has no corresponding variant yet. In particular, +`RotateSealingKey` is the sole broker effect behind +`VolumeEffectPort::rotate_sealing_key`; `DeletePersistentTap` is a new +idempotent peer to `CreatePersistentTap`; and USBIP release dispatches the +existing `UsbipBindFirewallRule` with closed `action: Remove`, not a renamed or +second release variant. `action: Ensure` is the only acquisition form. + +The broker remains the sole privileged executor and independent audit owner +(`ADR-046-provider-model-and-packaging`, D077). It re-derives every +privileged parameter from allocator-approved leases and the trusted bundle, +never from a caller-supplied raw parameter — carrying forward +`SECURITY.md`'s existing "compromise of `d2bd` cannot escalate to arbitrary +host mutation beyond the declared broker enum variants" invariant into the +Zone/Provider model. Opaque `LaunchTicket`/lease IDs are passed across the +`EffectPort`; the Provider never receives raw broker parameters or a broker +wire message. + +## Prohibited Provider discovery/ambient authority + +No Provider process discovers or opens a broker client, host path, device, +systemd unit, or compositor socket by ambient lookup. Every dossier states +this as an explicit "MUST NOT"/"never" invariant for its domain: + +| Surface | Explicit prohibition | Source | +| --- | --- | --- | +| Broker client | Provider crate imports no broker service/client/DTO; compile-time dependency audit | `ADR-046-provider-system-minijail.md` lines 1621-1628; every EffectPort-consuming dossier | +| Host path | `sourcePolicyId` is opaque; "the controller itself never opens the host path directly and never sees the raw path" | `ADR-046-resources-volume.md` lines 794-799 | +| Device node | "No Device Provider receives a blanket device-path grant, a raw socket address, or an ambient host capability" | `ADR-046-resources-device.md` lines 320-322 | +| Security-key hidraw | "The broker opens the hidraw node exclusively from the trusted bundle device table; it never accepts a caller-supplied path"; relay process "MUST NOT open any device node itself" | `ADR-046-resources-device.md` lines 328-329, 776-779 | +| systemd D-Bus | "The controller MUST NOT connect to the systemd D-Bus socket directly ... MUST NOT call `systemctl` CLI as a subprocess" | `ADR-046-resources-host-guest-process-user.md` §6.3 (system-systemd) | +| Compositor socket | "The `Provider/display-wayland` status resource never publishes a D-Bus address or socket path. No ambient environment lookup or `/run/user//bus` default path is used as a fallback" | `ADR-046-provider-notification-desktop.md` lines 1292-1293; `ADR-046-provider-display-wayland.md` (compositor session) | +| D-Bus session bus | No `$DBUS_SESSION_BUS_ADDRESS` lookup, no `/run/user//bus` fallback; D-Bus FD delivered only via ComponentSession attachment at a declared in-jail number | `ADR-046-provider-notification-desktop.md` lines 1290-1295; `ADR-046-provider-audio-pipewire.md` | +| vsock AF_VSOCK syscalls | `tokio-vsock` is not a dependency; `socket`/`connect`/`bind`/`accept` live only in the core effect adapter | `ADR-046-provider-transport-vsock.md` INV-VSOCK-004, lines 212-220 | +| Nix daemon socket | "No ambient Nix daemon socket access; activation helper uses sealed closure from artifact catalog exclusively" | `ADR-046-provider-activation-nixos.md` (technical_details) | +| ARM/cloud credential | "No ambient credential fallback (no `AZURE_CLIENT_ID`, `MSI_ENDPOINT`, SDK env chain)" | `ADR-046-provider-runtime-azure-virtual-machine.md` line 428 | + +Every FD a Provider process needs is delivered at a declared in-jail number +by its `LaunchTicket` before the process's first instruction — never +discovered by path, environment variable, or ambient socket lookup. This is +the concrete mechanism the table above enforces; see +[LaunchTicket integrity](#launchticket-integrity). + +## Process least privilege: controller/service/worker + +Every component is one of three narrowly bounded classes (D078, D073): + +| Class | Authority | Bound | +| --- | --- | --- | +| Controller | Owns its watch/coalescing queue and retry decisions; may create authorized dynamic child Process/EphemeralProcess/primitive/vendor resources | Max 8 controllers per Provider | +| Service | Exports ttrpc methods; may send typed internal requests to its controller for worker help | Max 8 services per Provider | +| Worker | Ephemeral execution; **no** `ResourceClient`, no d2b-bus/dependency portal, no Credential access, no CLI, no broker, no child-spawn authority — everything is inherited via `LaunchTicket` | Max 32 worker templates per Provider | + +A narrowly declared worker may own its exact workload child only when that +child is the worker's manifest-fixed data-plane purpose under explicit +descriptor policy (e.g. a shell-supervisor worker owning its shell) — this is +the one, explicitly scoped exception to "workers have no child-spawn +authority" (D078). + +Sandbox defaults (`ADR-046-resources-host-guest-process-user`, D079): + +- `noNewPrivileges: true` by default. +- `startRoot: false` for user-domain Processes; any user-domain Process + requesting `startRoot: true` is rejected at `validateSpec`. +- `readOnlyRoot: true` by default. +- `capabilityClasses: [sys-admin]` requires an explicit carve-out in the + Provider descriptor's `allowedCapabilityClasses`; without it, the request + is rejected. +- `seccompClass: allow-all` requires the same explicit carve-out. +- `UserNamespaceSpec.mappingClass: process-principal-root` is the sole + frozen value (D079): in-namespace UID/GID 0 maps to the host UID/GID of the + Process's resolved `User/` principal; the numeric host UID/GID is + **never** in the public ResourceSpec, status, audit payload, or API + surface — resolution happens exclusively inside the private + effect-adapter state at launch time. + +**Eleven pidfd lifecycle/non-exportability invariants** +(`ADR-046-resources-host-guest-process-user`), verbatim: + +1. Pidfd is never stored in the resource store. +2. Pidfd is never sent over d2b-bus, ComponentSession, or any ttrpc call. +3. Pidfd is never written to any log, metric, audit record, or status field. +4. Pidfd is never inferred from status by a caller. +5. No API method accepts a pidfd from a caller. +6. Every ProviderSupervisor restart reacquires its duplicate through + re-verification. A Provider-controller restart transfers no raw pidfd. +7. An ambiguous pidfd identity (suspected PID reuse) results in quarantine, + never a broad kill of all processes in a cgroup. +8. For system-minijail, the broker that called `clone3` remains the child + parent and alone calls `waitid(P_PIDFD)`, collects exit status, and reaps. +9. A non-parent ProviderSupervisor may poll pidfd readability, but readability + is a liveness hint, not wait/reap or exit status; the controller consumes + only the broker-relayed typed terminal result. +10. A verified broker/ProviderSupervisor pidfd holder may use + `pidfd_send_signal` for the exact main process without being its parent; + the controller holds only an opaque handle. +11. ProviderSupervisor owns no PID/PGID kill record. An unambiguous + system-minijail subtree uses anchored cgroup v2 `cgroup.kill` after the + graceful main-process signal/deadline; an ambiguous candidate is + quarantined without any signal or cgroup kill. + +The system-minijail placement gate therefore requires Linux ≥5.14 and a +writable `cgroup.kill` on a delegated cgroup v2 leaf. This floor is mandatory +even when the Host's optional `kernelVersionMin` is unset; unsupported hosts +fail before launch with `kernel-too-old` or `cgroup-kill-unavailable`. There is +no older-kernel PID/PGID fallback. + +`Provider/system-systemd` binds process identity to +`InvocationID`+cgroup+`MainPID`/start-time rather than the pidfd primitive +(D022); `Type=forking` is forbidden (daemonizing prevents pidfd-based +identity verification), and the unit name alone must never be treated as +process identity — identity requires verification against the unit's main +PID (`ADR-046-resources-host-guest-process-user` lines 803, 818). + +A Process ResourceSpec must not contain: raw numeric UID/GID/supplementary +groups, a raw cgroup path, a raw socket address/file path, raw seccomp BPF +bytecode, a raw capability bitmask, a raw minijail argument string, a raw +systemd unit property fragment, a raw broker operation name/parameter, or an +environment variable containing a credential/token/secret byte +(`ADR-046-resources-host-guest-process-user` lines 1291-1303). Violating this +is a synchronous `spec-security-violation` rejection at admission, not a +later runtime check. + +## LaunchTicket integrity + +The `LaunchTicket` is the cryptographically bound, single-use launch +authorization every Process/EphemeralProcess Provider issues to its +privileged executor (`ADR-046-provider-system-minijail.md` §7.4, +`ADR-046-components-processes-and-sandbox`). It is bound to: + +- the Process/EphemeralProcess `ResourceRef`; +- the resolved principal UID (never the numeric value itself — an opaque + resolved identity); +- the spec generation; +- the `CompiledSandboxPlan` digest (namespace/capability/seccomp + compilation output); +- the cgroup placement digest; +- the mount table digest; +- the inherited FD table. + +**Descriptor identity and validation.** + +- FDs are delivered at a declared in-jail number, installed before the + process's first instruction; `CLOEXEC` is cleared only at installation, + never held open across an unrelated exec. +- The compiled sandbox plan digest bound into the `LaunchTicket` is + re-verified by the broker at exec time; any change between ticket issue + and exec fails the spawn closed — this prevents a TOCTOU window between + the Provider deciding to launch and the broker actually launching + (`ADR-046-provider-system-minijail.md` §22 Invariant 6, lines 1593-1596). +- The pidfd is obtained atomically from `clone3(CLONE_PIDFD | + CLONE_INTO_CGROUP)`: no window exists between clone and pidfd acquisition + where a PID could be reused, and the process is placed in its cgroup leaf + before any instruction executes — no window exists for cgroup escape + (§22 Invariants 2-3, lines 1567-1575). +- Duplicate kernel objects (same `st_dev`/`st_ino`) presented as two FDs in + one packet are rejected and cleaned up (`transport-unix` + `duplicate-kernel-object`). +- Pidfd identity for adoption additionally requires live launch evidence via + `/proc//fdinfo`, a same-kernel-object check, and a double-read race + guard on `/proc/self/fdinfo/` (`pidfd-double-read-race-guard-detects-pid-reuse`, + [ADR046-zone-control-012](ADR-046-resources-zone-control.md#adr046-zone-control-012)). + +**Credit/quota.** `LaunchTicket` issuance is bounded per Zone (max 64 +concurrent in-flight `LaunchTicket`s for `system-minijail`, +`ADR-046-provider-system-minijail.md` §17 line 1290); `startDeadline` is +range-checked 1 s-3600 s and TTL-enforced by a controller ticker so an +expired ticket cannot be exec'd late. + +**Sealed memfd / attachment credit.** All memfd attachments require all four +seals (`F_SEAL_WRITE | F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL`); partial +seals are rejected with `sealed-memfd-partial-seal-rejected`. Attachment +credit is scoped and bounded (`Packet` 32, `Request` 64, `Operation` 128, +`Session` 256, `Process` 2048, `Host` 8192), each scope reserving emergency +headroom so a saturated data path cannot starve control-plane FDs +(`ADR-046-zone-routing.md` lines 617-641; `ADR-046-resources-zone-control.md` +lines 3553-3559). + +## User-domain Host: no isolation + +Current `unsafe-local` becomes a user-only `Host` under `Provider/system-core` +in v3, not a separate Provider (D042). It is `defaultDomain=user`, +`allowedDomains=[user]`, `defaultUserRef` set, and +`spec.isolationPosture = "none"` (a promoted Host base field). This is a genuine reduction in +guarantee — a Process running there executes as the operator's own host UID +with **no VM or Provider-managed isolation boundary** — and the spec +requires the reduction to be impossible to hide. + +`isolationPosture = "none"` is mandatory and non-suppressible across three +independent surfaces simultaneously (`ADR-046-resources-host-guest-process-user` +lines 1270-1278; `ADR-046-nix-configuration` lines 719-764; +`ADR-046-cli-and-operations` lines 685-703): + +1. **Status.** `status.isolationPosture = "none"` is always present in + `--json`; a `[no isolation]` annotation appears in `--human` table rows. +2. **CLI/UI.** `d2b shell open` and `d2b exec run` emit + `warning: no isolation boundary — this process runs as your host user` to + stderr before attaching, unconditionally. **This warning has no + suppression flag** — there is no `--quiet`/`--no-warn` escape. +3. **Audit.** Every `ProcessEffect` record (launch, stop, adopt, quarantine) + for a Process under this Host carries `no_isolation=true` as a fixed + closed audit label. + +`no_isolation` is audit-only. It **must not** appear as an OTEL metric label, +span attribute, or structured log field, in either direction (D067; +`ADR-046-telemetry-audit-and-support`) — this prevents the isolation +weakness itself from becoming a high-cardinality or externally-queryable +telemetry dimension while keeping it fully auditable. + +"A missing or silent posture field is a correctness violation. Reviewers +MUST reject any diff that adds a code path serving a Host/unsafe-local +resource without propagating the isolation posture through all three +surfaces" (`ADR-046-resources-host-guest-process-user` lines 1270-1278). + +**Bidirectional rejection of `null` posture.** A Host with +`defaultDomain=user`, `allowedDomains=[user]`, `defaultUserRef` set, and +`isolationPosture=null` is rejected at both `validateSpec` and Nix eval time +— the null/absent value may not be used to evade the explicit declaration +(`ADR-046-resources-host-guest-process-user` line ~391; +`ADR-046-nix-configuration` line ~1806). Symmetrically, a system-domain +Process (`domain: system`) targeting a user-only Host +(`executionRef: Host/host-unsafe-local`) is a Nix eval error +(`ADR-046-nix-configuration` lines 748-764). + +User reconcile itself must not hold OS-user credentials or perform +authentication/login — it is purely observational (NSS lookup, home-stat, +group query); numeric UID/GID appearing in status is diagnostic-only and is +never an authorization input (`ADR-046-resources-host-guest-process-user` +lines 1184-1186, 1232-1233). Authorization always uses the canonical Zone +`User/` subject reference. + +## Volume security + +Volume (`ADR-046-resources-volume`) is the one ResourceType covering files, +directories, ACLs, views, and mount lifecycle (D032). Every security +property below is enforced by `volume-local`/`volume-virtiofs` through the +`VolumeEffectPort`, never by the calling controller directly. + +**`sourcePolicyId`: opaque, never a raw path.** `source.settings.sourcePolicyId` +is an opaque bounded string referencing an entry in `volume-local`'s private +`config.allowedHostPaths` policy catalog (D082). The raw path is resolved +exclusively inside the private effect-adapter/broker path and handed to the +caller only as an opaque FD; it never appears in public status, audit +records, CLI output, or telemetry (`ADR-046-resources-volume` lines 143, +390-406). Resolving `sourcePolicyId` requires the +`volume-local/source-policy-resolve` permission, granted only to +`ProviderSupervisor` on behalf of `Provider/volume-local` — never the +controller process itself. + +**ACL: typed `User/` refs only.** ACL principals are always typed +`User/` ResourceRefs in the same Zone — no numeric UID/GID form is +accepted (`ADR-046-resources-volume` lines 204-214; +`ADR-046-nix-configuration` lines 991-996). `foreignChildPolicy: fail` sets a +`ForeignAclViolation` condition when the broker finds directory children not +covered by a declared ACL entry, providing tamper detection for controlled +subtrees. + +**View and `no-follow`.** `noFollow: true` is the default on every +`LayoutEntry`; symlink traversal is disabled by default and only explicit +`symlink`-type entries with `noFollow: false` may traverse (line 179, +802-805). Anchored relative paths are enforced: no absolute path, no `..`, +no drive letter, no null byte, no Unicode path-separator homoglyph. A worker +Process with a narrow view fails with `volume-view-rights-exceeded` if it +requests a right absent from its view declaration (`ADR-046-provider-state` +lines 437-461). + +**Identity marker.** Every Volume has an identity marker — a regular file +under the broker-maintained root recording `(st_dev, st_ino)`, `schemaId`, +`schemaVersion`, and a tamper-evident HMAC digest (`ADR-046-provider-state` +lines 301-321). Fail-closed rules: a missing marker after provisioning sets +`markerStatus: missing` and the Volume transitions to `Failed` — the broker +**never** silently re-provisions; an `st_ino` mismatch sets +`markerStatus: replaced` and also fails closed. Neither condition +auto-recovers; only an operator can remediate. + +**Quota.** `enforcement: hard` fails Volume creation if the backing +filesystem cannot enforce byte/inode limits; for `tmpfs` sources, +`quota.maxBytes`/`quota.maxInodes` are required and kernel-enforced as mount +options (`ADR-046-resources-volume` lines 333-352). `quotaBytes: 0` is +rejected with `component-quota-zero` (D035 catalog invariant, +`ADR-046-resources-zone-control` lines 1319-1326). + +**Sensitivity classes and single-writer enforcement.** + +| Class | Access | +| --- | --- | +| `private` | Single-process only — no sharing between processes | +| `internal` | Same-Provider multi-component sharing permitted | +| `shared-read` | Cross-Provider read-only sharing permitted | + +At most one attachment may use `access: read-write` at any time; the +controller rejects a second read-write attachment while one is active +(`ADR-046-resources-volume` lines 730-735). + +**Destruction sequence.** Volume destruction is an ordered, resumable-once +sequence: (1) shred sealing key material if sealed, (2) fd-relative +`unlinkat` to remove layout, (3) `fsync` the parent directory, (4) remove the +identity marker, (5) remove the root directory, (6) commit finalizer removal +(D084; `ADR-046-provider-state` lines 513-529). Any abort between steps 2-5 +leaves a partially removed Volume with a valid marker, which is +**quarantined, not silently re-provisioned**. + +**ProviderStateSet per-component isolation (D076, D087).** `ProviderStateSet` +is the optional, logical, query-time grouping of the *declared* Volume +resources owned by a Provider (`ProviderStateSet(zone, provider-name)`); it is +never a ResourceType or a stored row, and it is empty for a Provider that +declares no state Volume. Bounded non-secret operational state belongs in the +owning resource's `status` subresource and the core Operation ledger by default +(D087). A component declares a state Volume only when a payload passes the +storage-need test (secret/sensitive private recovery data; large/binary/file +content; private data unsafe for status readers; or bounded-but-revision- +unsuitable data with a demonstrated recovery need). Core `ProviderDeployment` +creates one private, framework-created Volume per *declared* `stateNamespaces` +entry from the signed state declarations, **before** launching that component's +Process; a stateless component declares no namespace and receives no Volume, +and there is no empty identity-only Volume. These declared state Volumes use the +canonical full Volume schema (extended with +`stateSchema`/`persistenceClass`/`sensitivityClass`/quota/sealing fields) and +`User/` layout principals drawn from bounded, Nix-preprovisioned pools. +Each component mounts only its own declared view; there is **no cross-component +or cross-Provider sharing** and no separate non-Volume "compartment" concept. +For declared component state Volumes, `persistenceClass: persistent` is +required — `ephemeral`, `cache`, and `config` are rejected with +`component-persistence-class-forbidden`. + +**Status confidentiality is RBAC, not secret storage (D087).** Resource +`status` is a redacted, RBAC-readable observation surface. It MUST NOT contain +secrets, raw tokens/keys/PSKs, authority-conferring credential handles, private +endpoint/path/argv/environment/PID/unit data, terminal/clipboard/CTAP bytes, +raw cloud error bodies, large binary blobs, or unbounded/churn-heavy content +(`ADR-046-resource-object-model` § Status prohibitions). Confidentiality of the +bounded non-secret observations status does carry is provided by the status +subresource's RBAC read authorization; status is never used to store a secret +whose exposure would depend on redaction alone. + +**Three-layer status shape (D088).** All three status layers — the universal +`ResourceStatus` base, the ResourceType-common `status.resource`, and the +optional Provider-specific `status.provider` — are redacted and non-secret. The +`status.provider.details` extension schema is signed into and registered with +the Provider package and is versioned; the resource store validates every +`status.provider` write against that registered schema with strict unknown-field +denial and per-layer size/cardinality bounds, rejecting an unregistered or +version-mismatched extension (`status-provider-schema-invalid`) or one that +duplicates a universal/`status.resource` field (`status-provider-overlap`). The +signed, versioned, strict, bounded extension surface prevents an implementation +from smuggling secret, unbounded, or authority-conferring data into status under +a private field, and keeps cross-provider consumers on the provider-neutral +base-only projection (universal base + `status.resource`) so no consumer parses +another Provider's opaque details. + +**Three-layer spec shape (D089).** Desired `spec` is symmetric: the universal +envelope, the ResourceType base spec at `spec.*` (including `spec.providerRef`), +and the optional canonical `spec.provider = { schemaId, schemaVersion, settings }` +extension. The `spec.provider.settings` schema is signed into and registered with +the Provider package, versioned/digested, and validated against `spec.providerRef` +at Nix build and API admission with strict unknown-field denial and spec bounds; +it may not shadow a base field (`spec-provider-shadow`) or use an +unregistered/version-mismatched schema (`spec-provider-schema-invalid`). This +prevents an implementation from smuggling unbounded, secret-shaped, or +authority-shaped desired data into spec under a private field, or from silently +reinterpreting, renaming, or weakening a base field; a Provider that cannot honor +an optional base capability must say so through its signed capability matrix and +the provider-neutral `unsupported-capability` result rather than degrading a base +field. Generic tooling authors only the base spec, so no operator or generic +controller depends on a Provider's opaque `settings`. + +**Expedited reconcile (D090).** Only authorized UX mutations and core (and the +admin `resource reconcile` action) may set `waitForReconcile`; an unauthorized +request is rejected with `expedited-not-authorized`. The expedited priority lane +is quota-bounded and fair so it cannot starve ordinary reconciles or serve as a +DoS amplifier. A controller performs no external effect, finalizer release, or +status mutation until Core's typed `CommittedRevisionProof` arrives, so a +never-committed mutation causes no effect; and a durable commit is authoritative +regardless of whether the expedited pass later fails, so the API cannot be +tricked into reporting an uncommitted mutation as durable or an uncommitted +status as persisted (`statusPersistence: pending`). + +**Currency and disruptive upgrade (D091).** The `status.update` currency object +carries only bounded, non-secret observed/target generation/digest IDs and +bounded/truncated owned/dependency refs — never secret material, raw artifact +paths, or unbounded collections. Disruptive changes require an explicit, +authorized `resource upgrade ... --apply`; a controller must report +`UpgradeRequired` rather than silently disrupt a running workload, and the +dependency-aware planner drains dependents before recycling so there is no +surprise disruption. Upgrades preserve TPM identity and durable/state/secret +Volumes (`preserveState`); `Replace` of a resource-row identity is used only +when explicitly required and planned with ownership/state transfer, and full +destructive factory reset remains a separate authorized path. + +**Endpoint resource (D092).** A stable endpoint is the `Endpoint` ResourceType, +never a raw locator in spec/status/CLI: `visibility` is exactly +`owner|provider|zone`, with finer exact access in `consumerPolicy`; the base +carries only closed +class/transport/locality/purpose values and bounded fingerprints, and status +carries no path/address/CID/port/fd/credential. Core/ProviderSupervisor resolves +an `Endpoint` to a live transport/FD only through the EffectPort/LaunchTicket +path under authorization; an unauthorized resolve is denied with a typed error +and returns no locator. Promoting endpoints to resources brings independent +RBAC, audit, ownership, and dependency edges to what were opaque IDs; the frozen +permitted-opaque set (pidfd, fd index, per-session named stream/`OwnedTransport` +handle, `operationId`, digests) stays internal and non-locator by the +`ADR-046-resource-object-model` promotion test. + +**Entra identity Guest grounding (D093).** `credential-entra` login/token +acquisition is grounded in an Entrablau-enabled identity `Guest`; the controller +is secret-free and there is no Host login/token chain, `DefaultAzureCredential`, +env, DBus, path, or browser fallback. The Entrablau Guest owns and stores the +machine credential, TPM binding, enrollment, and refresh-token/private login +state (guest-local, secret/large/private, outside resource status); refresh +tokens and private login state never leave the Guest. A raw access token is +delivered end-to-end (Noise_KK) only to the exact authenticated consumer process +allowed by the Credential `consumerRef`/scope/RBAC; the Host, bus, and +intermediate controllers see ciphertext only, so the Host never holds realm +tokens/registries/audit (consistent with ADR 0032). d2b Credential status/audit/ +OTEL carry only bounded non-secret lease/login observations — never a token, +login URL, cookie, authority-conferring device code, or user PII; the +interactive UI stream is a named ComponentSession stream direct to the Guest. +Entrablau owns all Entra network/TLS egress; the credential-entra +controller/agent performs none. There is no cross-Zone ResourceRef. + +Two invariants close the specific attacks Volume state is most exposed to: + +- **TPM Volume never re-provisioned.** After the swtpm provisioning marker + exists, a missing or replaced swtpm directory is a hard failure + (`previously-provisioned-swtpm-state-missing`); the controller never + silently creates a new empty TPM directory, which would look like a clean + TPM to any IdP relying on attestation continuity (`ADR-046-resources-volume` + lines 817-819, 628-665). +- **Store-view isolation.** virtiofsd serving `access: read-only` for the + per-Guest Nix store ALWAYS uses `store-view/live` as `--shared-dir`, never + the host's `/nix/store`; the `share.source == "/nix/store"` string in + `processes-json.nix`-equivalent config is an eval-time sentinel only — + runtime virtiofsd always serves the isolated hardlink-farm subtree + (`ADR-046-resources-volume` lines 820-822). The virtiofsd `--shared-dir` + argv is delivered as a `/proc/self/fd/` inherited-FD path, never a + literal host path string. + +## Guest-local vs. host-backed-guest custody + +By default, a Volume is **guest-local**: the Host never holds bytes, +dirfds, or identity markers for it (`ADR-046-provider-state` lines +159-272). `host-backed-guest` placement requires `hostCustodyPermitted: +true` in the Volume's **signed descriptor**; without it, the placement fails +with `placement-host-custody-violation`. Credential, audit, remote-node, and +cloud-control schemas *require* `guest-local`; attempting `host-backed-guest` +for one of these classes fails with `guest-local-required`. This is the +Volume-layer enforcement of the same custody boundary described in +[Gateway Guest custody](#gateway-guest-custody). + +**No bootstrap state Volume (D086, superseded by D087).** The fixed bootstrap +components — the first `volume-local` controller instance on each execution +target, and (where present) `system-core` and `system-minijail` — keep their +bounded non-secret operational state in resource `status` and the core +Operation ledger and declare **no** state Volume. Because no component requires +a state Volume before a `volume-local` instance is Ready, there is no bootstrap +state-Volume cycle, no per-execution-target local bootstrap storage mechanism, +and no bootstrap-storage exception. There is no hidden bootstrap store: a fixed +bootstrap component reaches Ready by adopting running processes and re-deriving +its observed state from `status`, the core Operation ledger, and independent +external observation (cgroup-leaf scanning, fresh pidfds, marker reverification +against external reality). + +A Guest still bootstraps its own primitive controllers (including a Guest-local +`volume-local` instance) independently of the Host: the Guest-local instance +uses only Guest-local primitives, never a leaked parent-Host dirfd or other +Host-local resource handle, and reaches Ready from Guest-local primitives and +its own status alone. + +`volume-local` remains the single owner of the Volume ResourceType's layout/ +spec/ownership fields; `volume-virtiofs` is a separate attachment- +implementation Provider that never writes Volume layout/spec/ownership +fields — it writes only its own owned virtiofsd Process children and the +per-attachment status entries it is authorized for (D083). Two controllers +never write the same Volume resource row. + +## Credential security: end-to-end delivery and zeroization + +**Zero-secret invariant.** Credential bytes MUST NOT appear in any resource +spec, resource status, resource store row, WAL entry, audit record, OTEL +span attribute, OTEL metric label, log line, or inter-process bus DTO +(`ADR-046-resources-credential` lines 26-55). This is unconditional: every +DTO used on d2b-bus, every audit record, every OTEL export, and every log +line is excluded from carrying secret bytes by construction, not by +best-effort filtering. Violating it is a correctness defect, not a +configuration issue. + +**Noise KK is required; NN is forbidden for delivery.** d2b-bus authorizes +the route and forwards opaque Noise records for credential delivery; it +never terminates the delivery channel, decrypts message content, inspects +ciphertext, or stores delivery channel records (lines 398-401). +`Noise_KK_25519_ChaChaPoly_SHA256` is the required profile; `Noise_NN_*` is +explicitly forbidden for this purpose because NN does not authenticate the +responder (lines 405-407; D056). Every delivery session enforces seven +requirements (lines 440-476): + +1. Only keys enrolled at credential registration are accepted. +2. Replay protection: a monotonically increasing sequence number per + consumer; replay closes the session immediately. +3. Output size is bounded by `maxTokenBytes`; exceeding it closes the + session. +4. Zeroizing buffers (`zeroize::Zeroize`) are mandatory; allocating a + non-zeroizing type for token bytes is a defect. +5. `Debug` is hand-written and redacted; `#[derive(Debug)]` is forbidden on + any type touching key material or token bytes. +6. No auto-retry on an ambiguous outcome — a delivery-channel failure or + missing acknowledgment leaves state unchanged rather than assuming + success and retrying. +7. Immediate close and zeroize after delivery — no state is retained. + +`SignChallenge` signatures use the same sensitive KK delivery channel as +tokens (D068): the outer response carries only outcome metadata, and the +signature bytes themselves remain end-to-end protected and opaque to every +intermediary. + +**Interactive vs. unattended credential distinction.** `credential-secret-service` +is user-domain only (interactive desktop secret-service backend); +`credential-managed-identity` is system-domain only (unattended/machine +IMDS-backed managed identity); a Credential resource of the wrong domain for +either type is rejected at spec validation (`ADR-046-resources-credential` +lines 1440-1443, 1563-1567). `credential-entra` may be user- or +system-domain depending on the requesting Process's domain (lines +1671-1674). + +**No plaintext-at-rest guarantee is claimed by d2b.** The zero-secret +invariant, zeroizing delivery buffers, and immediate post-delivery zeroize +are the guarantees d2b makes. The backing secret store (OS keyring, IMDS +endpoint, Entra token cache) is managed by the underlying OS/cloud provider +and its at-rest security is out of scope, matching the existing +`SECURITY.md` scoping for upstream components. + +**Bytes-in-motion redaction.** Excluded from audit records: token bytes, key +material, password hashes, bearer strings, provider diagnostics, host paths, +connection strings, audience literals, tenant/subscription/client IDs, +endpoint URIs, and Noise/session key material (lines 699-702). Excluded from +OTEL: token bytes, audience literals, provider diagnostics, host paths, +resource IDs, tenant/subscription IDs, endpoint URIs, and any correlation ID +embedding a secret shape (lines 730-732). Error messages are bounded to 240 +UTF-8 characters, stripped of control characters, and must not contain +token bytes, URLs, UUIDs, provider diagnostics, host paths, or connection +strings (lines 681-683). + +**RBAC.** `use-credential` is required and checked by d2b-bus before +forwarding delivery. Each service method maps to exactly one canonical +allowed-operation value and requires that same exact value in both +`Credential.spec.allowedOperations` and Role `subresources`. +`admin-credential` is supplemental to ordinary Credential CRUD and accepts +only matching `create`, `update-spec`, or `delete` subresources; there is no +coarse `admin`, `identity-reset`, `observe`, or `revoke` alias. + +## Content secrecy: clipboard, terminal, CTAP, notification + +A closed set of asset classes is explicitly, per-dossier, "never in any +surface" — never audit, OTEL, Debug output, error messages, or structured +logs — regardless of general redaction rules elsewhere. This table collects +every such class with its exact source quote: + +| Sensitive asset | Provider | Explicit "never in any surface" statement | Source | +| --- | --- | --- | --- | +| CTAP payloads, PINs, CBOR assertions, FIDO credential material | `device-security-key` | "No CTAP payloads, PINs, CBOR assertions, or any FIDO credential material appear in any log, audit record, OTEL span attribute, metric label, or error message" | `ADR-046-provider-device-security-key.md` (Invariant I-7) | +| Clipboard content bytes | `clipboard-wayland` | "clipboard bytes only as SCM_RIGHTS attachment FDs — NEVER in method arguments, stream frames, status, audit, or traces" | `ADR-046-provider-clipboard-wayland.md` (Invariant 1) | +| Terminal/PTY bytes, argv, cwd, environment | `shell-terminal` | "No terminal bytes, argv, cwd, environment, paths, PIDs, unit names, usernames, session names, socket paths, or opaque handles may appear in Debug, audit, metrics, or span attributes" | `ADR-046-provider-shell-terminal.md` (SR-8) | +| Notification summary, body, action text, icon ref | `notification-desktop` | "Notification summary, body, action text, and icon ref never appear in any audit field" | `ADR-046-provider-notification-desktop.md` §10.1 | +| TPM NVRAM content | `device-tpm` | "No path, PID, NVRAM content in any audit payload (sensitivity: private)" | `ADR-046-provider-device-tpm.md` | +| IMDS token bytes, IMDS URLs | `credential-managed-identity` | "token bytes, IMDS URLs, and IMDS response fragments excluded from all audit records and OTEL" | `ADR-046-provider-credential-managed-identity.md` | +| Entra token bytes, key material, tenant IDs | `credential-entra` | "No token bytes, key material, tenant IDs, authority URLs, or MSAL cache entries in any audit record" | `ADR-046-provider-credential-entra.md` | +| Secret-service secret bytes | `credential-secret-service` | "Zero-secret-bytes: no secret bytes cross the `Oo7SecretServicePort` boundary" | `ADR-046-provider-credential-secret-service.md` (Invariant 1) | +| PipeWire node IDs, socket paths, gain/volume levels | `audio-pipewire` | "AudioMediator does not export PipeWire node IDs (never in spec/status/d2b-bus/audit/OTEL)"; no level/gain labels | `ADR-046-provider-audio-pipewire.md` (Invariant 8) | +| Compositor socket paths, window titles, app-ids, DnD content | `display-wayland` | "must not contain: compositor socket paths; user or session identities beyond subject_digest; window titles or app-id values; clipboard payloads or DnD content; raw argv; process PIDs, pidfds, or unit names; Wayland protocol message bodies" | `ADR-046-provider-display-wayland.md` | +| Raw busid, USB vendor/product/serial | `device-usbip` | "Raw busid MUST NOT appear in spec, status, audit, OTEL, or `ListBusIds` response" | `ADR-046-provider-device-usbip.md` | +| GPU device node paths | `device-gpu` | "No device path on public wire"; audit excludes raw GPU device paths | `ADR-046-provider-device-gpu.md` | +| hidraw / UHID device path | `device-security-key` | "No path on any public surface (spec, status, audit, OTEL, error messages)" (I-2) | `ADR-046-provider-device-security-key.md` | +| Credential bytes forwarded through OTEL | `observability-otel` | "Credential bytes are never held, routed, or processed by this Provider; all auth material stays inside the transport Provider's scope" | `ADR-046-provider-observability-otel.md` | + +**Replay/nonce protections for interactive content actions.** Where content +delivery has an associated user-facing "act on this" callback (notification +action buttons), replay is bounded by a single-use nonce, not by relying on +content secrecy alone: `notification-desktop`'s `ActionNonce` is 256-bit +random, single-use (cleared from its store on first consumption), TTL 120 s, +`MAX_STORE_SIZE = 256`; `InvokeAction` is accepted only on a +`desktop-observer` session (`ADR-046-provider-notification-desktop.md`). + +**FD-only delivery, never method arguments.** The universal pattern for +transferring sensitive bytes between a compositor/host process and a +consuming component is an `SCM_RIGHTS`-delivered FD (clipboard, D-Bus +session, CTAP relay) — never a serialized byte payload inside a ttrpc +method argument, resource spec, or stream frame. This both keeps the bytes +out of any place a redaction filter would need to catch them and keeps the +delivery bounded by the same attachment-credit/CLOEXEC machinery as every +other FD (see [LaunchTicket integrity](#launchticket-integrity)). + +## Audit vs. OTEL: redaction, cardinality, durability + +Telemetry and authoritative audit are two distinct subsystems with **no +shared writer path** (`ADR-046-telemetry-audit-and-support` lines 85-105). +OTEL is best-effort, buffered, lossy, and is never an authorization input; +audit must be committed **before** the operation it describes completes. + +**Durability classes.** + +| Class | Records | Durability | Rate limiting | +| --- | --- | --- | --- | +| Privileged | `ResourceMutation` (RBAC verbs), `RBACChange`, `SessionConnect` (auth failure), `StateReset` | Durable fsync before the operation completes | Never rate-limited under any load condition | +| Standard | Most resource lifecycle events | Durable within a bounded window | Rate-limited, default `DEFAULT_AUDIT_WRITES_PER_SECOND = 4096` | +| Best-effort | Informational | Async | Dropped under rate limit | + +Audit unavailability for a privileged record **fails the operation closed** +(`audit-unavailable`) — this is the concrete recovery control for AC8 that +makes "attack under load, then corrupt state while audit is dropped" +impossible for privileged operations (lines 1141-1165). + +**`subject_digest`.** SHA-256 of the normalized canonical subject string +(never the raw name) is what identifies the acting principal in audit +records (lines 1141-1165, 1861). + +**Forbidden audit fields** (lines 914-1170): resource name, spec bytes, +status bytes, raw filesystem paths, device identifiers, broker operation +arguments, credential bytes/key material, PIDs/process identities, `argv`, +environment variables. + +**`no_isolation` is audit-only** (line 1966; see +[User-domain Host: no isolation](#user-domain-host-no-isolation)): present +in `ProcessEffect` records, forbidden in any metric label, span attribute, or +log field. + +**OTEL forbidden metric dimensions** (telemetry spec §Cardinality rules): +the exact keys `vm`, `zone`, `zone_id`, and `zone_uid`; every +resource-name-derived key; VM/Zone/Provider-resource/resource names and UIDs; +filesystem paths, `argv`, environment variables; status detail messages; +subject names; PIDs, pidfds, cgroup paths; operation IDs; and endpoint +addresses. `d2b.zone` is allowed in resource attributes but never in a metric +label. Structural closed-label policy tests parse every descriptor and enforce +both key and value-domain rules in `policy_telemetry_redaction.rs` under +`ADR046-telem-008`. + +**Audit record types** (lines 914-1170): `ResourceMutation`, `RBACChange`, +`SessionConnect`, `RouteAdmission`, `BrokerEffect`, `ProcessEffect`, +`StateReset`. `ResourceMutation`/`RBACChange` are emitted by the store actor +inside the write transaction before `commit` returns; the sink must durably +fsync before the commit is reported successful (privileged durability class, +line 1861-1862). Zone tree paths in audit records are replaced by opaque +digests (ZR line 2098). + +**Segment lifecycle.** Rotation at 64 MiB or UTC midnight, whichever comes +first; 30-day default retention; export requires the admin-only +`audit-export` session verb bound exactly to +`d2b.audit.v3.AuditService/Export`, with no implied Zone resource read; +hash-chain breaks are reported inline in the export stream, never silently +skipped (line 1846). + +**`observability-otel` is never a bootstrap dependency and never reads +audit.** Zone startup does not wait for `observability-otel`; unavailability +causes bounded frame drops (`d2b_telemetry_drop_total`), never blocks Zone +startup or a resource mutation (`ADR-046-provider-observability-otel.md`; +D065). The Provider never reads from the authoritative audit sink — strict +separation between OTEL telemetry and authoritative audit is preserved even +though both may describe the same underlying event. + +## Lifecycle security: update, revocation, finalizer, restart, adoption, quarantine + +**Finalizer/deletion ordering is fixed and never force-clears an undrained +finalizer** (D084). The owning Provider controller drains/deletes its +effects/children and clears its own finalizer; the core resource-store +transaction then writes an event-only `Deleted` revision and removes the +row/index atomically; the audit subsystem appends the deletion record +afterward using a dedup/exactly-once recovery key. Generation-retention +pruning removes only the historical bundle record, never a live resource's +finalizer state; a resource whose finalizers have not drained remains +`Degraded`/blocked until it finishes normally — it is never silently +force-deleted to unblock a generation sweep. + +**Generation cleanup never touches controller/API-managed resources.** +Config-owned (`managedBy: configuration`) resources absent from a new bundle +generation receive an async `Delete`; `managedBy: controller` and +`managedBy: api` resources are **never** touched by generation cleanup +(D069). The Zone transitions to `Degraded` while cleanup is pending; if a +cleanup candidate exceeds `cleanupStuckThreshold` (default 5 minutes), a +`GenerationCleanupFailed` condition is set — the runtime never force-removes +finalizers to clear it (`ADR-046-resources-zone-control` lines 2808-2912). +The generation counter itself rejects replay/downgrade: a bundle +`generation` must be strictly greater than +`store_meta.active_configuration_revision` (lines 2753-2754). + +**Provider upgrade policies.** `drain-then-replace` (drain the old component +before launching new), `rolling` (phased replacement), and `immediate` (stop +all and replace) are the closed set (RZC lines 645-683); test +`provider-upgrade-drain-then-replace` confirms old-component drain completes +before new-component launch is attempted. + +**Trust/conformance failure quarantines, never deletes** (see +[Signed package/manifest/schema/config trust](#signed-packagemanifestschemaconfig-trust-and-publisher-roots)). +Provider deletion itself uses the `core.provider-api-binding` finalizer: all +exported ResourceTypes must be withdrawn, and the controller verifies no +resource of those types remains before the finalizer clears. + +**Credential revocation precedes finalizer clearing.** When a Credential +resource has `deletionRequestedAt` set, the Zone runtime revokes the +resolved secret binding **before** clearing the `core.credential-revoke` +finalizer (RZC lines 2808-2821, 3108) — this closes the "delete the +Credential resource but leave the underlying secret-service/IMDS/Entra +binding live" gap. + +**Quarantine-not-kill is the universal ambiguous-identity response.** On any +adoption ambiguity — a restarted controller finding a process/Volume/Guest it +cannot unambiguously re-identify — the response is quarantine +(`Degraded`/`Quarantined`, cgroup leaf isolated, no signal sent to the +candidate process), never a broad kill: + +- `Provider/system-minijail`: ambiguous adoption → `AdoptionState::Quarantined` + + `runtime-security-violation` audit; the cgroup leaf is blocked from + reuse until externally established process-absence proof + (`ADR-046-provider-system-minijail.md` §22 Invariant 4). +- `Provider/system-systemd`: multiple candidate units matching the adoption + heuristic → `AdoptionState::Quarantined`; no signal sent; operator must + confirm absence before reuse. +- `Provider/runtime-cloud-hypervisor`: ambiguous VMM candidate on controller + restart → `Degraded`; operator must resolve, never an automatic kill. +- `Provider/runtime-azure-container-apps`: ambiguous ACA sandbox state on + restart → `Degraded`, never re-provisioned without an explicit + `RuntimeAdopt` confirmation. +- `Provider/volume-local`: a partially removed Volume with a valid marker is + quarantined rather than silently re-provisioned. + +This rule does not weaken intentional teardown of an **unambiguously owned** +system-minijail process subtree. After exact-main `SIGTERM` and the bounded +graceful phase, the original broker parent writes `1` to the reverified cgroup +v2 leaf's `cgroup.kill`, waits for `cgroup.events` `populated 0`, performs and +relays its parent-only `waitid(P_PIDFD)` result, and only then permits leaf rmdir +and finalizer clearing. This is the required Linux ≥5.14 path: it terminates +descendants despite `setsid(2)` and avoids PGID-reuse attacks. A non-parent's +pidfd readability remains only a hint, and no PID/PGID SIGKILL fallback exists. +If ownership is ambiguous, `cgroup.kill` is not written and the finalizer +remains. + +This is the direct v3 continuation of ADR 0034's restart-adoption contract +("D2b re-adopts live runner processes when it can prove identity, +quarantines ... otherwise") and the pidfd non-exportability invariants in +[Process least privilege](#process-least-privilege-controllerserviceworker). + +## Availability and DoS controls + +Every shared control-plane resource has an explicit, tested ceiling; none is +"reasonably bounded by the client's good behavior." + +**ComponentSession/d2b-bus ceilings** (adapted unchanged from historical main +ADR 0045, re-affirmed by `ADR-046-zone-routing`/`ADR-046-cli-and-operations`): + +| Resource | Ceiling | +| --- | --- | +| Canonical handshake offer | 16 KiB | +| Logical ttrpc request/response or named-stream message | 1 MiB | +| Active named streams per session | 128 | +| Attachments in one packet / one request / one operation / one session | 32 / 64 / 128 / 256 | +| Process-global / Host-wide transferable attachment credits | 2,048 / 8,192 | +| Reserved non-attachment control FD headroom | 64 | +| Queued plaintext per named stream, each direction | 256 KiB | +| Aggregate queued named-stream plaintext, each direction | 4 MiB | +| Request lifetime (`MAX_REQUEST_LIFETIME_MS`) | 900,000 ms (15 min) | +| Max wall-clock skew accepted on a request | 30 s (never added to remaining duration) | +| Reconnect attempts / window | 10 / 300,000 ms | +| Provider agent in-flight dispatch | 64 (semaphore-guarded) | + +**Zone-level ceilings** (`ADR-046-resources-zone-control`): `Quota` +(`enforcementPolicy: hard` rejects with `quota-exceeded`; `soft` sets +`overQuota: true` and warns; `quotaBytes: 0` is rejected outright); +`EmergencyPolicy` (union semantics — the most restrictive enabled flag wins +across `stopNewAdmissions`, `disconnectZoneLinks`, `stopProviderProcesses`, +`drainOngoingOperations`; `drainDeadlineSeconds` bounded 1-300 s; +`stopProviderProcesses` stops Processes **without** setting +`deletionRequestedAt`, so they resume automatically on deactivation rather +than requiring re-creation). + +**ZoneLink-specific ceilings**: `maxPendingIntents` (max 1024, default 256), +`maxActiveStreams` (max 128), `localIntentPolicy` (`queue|drop|fail`), route +tree bounds (max 32 hops, max 4096 parent/route entries, max 16 labels per +path), named-stream credit backpressure (source blocks rather than buffers +unboundedly when credit is exhausted). + +**Checked arithmetic, never a panic path.** Every admission, fragmentation, +queue-credit, and pre-allocation calculation uses checked arithmetic +including all wire overhead; underflow, overflow, or a value above the +selected profile limit is a typed pre-allocation rejection — it never +reaches indexing, allocation, or a panic (historical main ADR 0045 lines +1514-1523, carried forward unchanged as a `d2b-bus` invariant). + +**Priority scheduling under load.** No lock is held across `await`; a +stalled data stream cannot consume reserved control credit. Priority order: +(1) fatal close/revocation/session control, (2) ttrpc control and +cancellation, (3) attachment acknowledgement, (4) named-stream data with +bounded round-robin fairness. This is the mechanism that keeps a +backpressured data path from starving the cancellation path an operator +needs to recover from it. + +**Admission observability.** `d2b_api_admission_rejected_total` carries a +closed `reason` label (`auth|quota|conflict|invalid|schema`) so a DoS +attempt is visible in aggregate without leaking which specific resource was +targeted. + +## Incident response and support bundle + +**`d2b zone doctor`.** Resource status reads only; no resource names, paths, +`argv`, or PIDs in output; includes an audit hash-chain integrity check +(`ADR-046-telemetry-audit-and-support`, `ADR046-doctor-001`). + +**`d2b zone support-bundle`.** The command requires the admin-only +`support-bundle` session verb bound exactly to +`d2b.support.v3.SupportService/GenerateBundle`; that grant supplies no +resource `get` or `list` authority. No spec bytes and no `metadata.name` appear +in the bundle; metadata and status only. When a Provider is quarantined, the +bundle reports `bundle_completeness: "partial"` rather than silently omitting +the gap or blocking entirely (`ADR046-doctor-002`). This is the concrete tool +an operator or a coordinated-disclosure responder uses to gather evidence +without themselves becoming a redaction bypass — it is bound by exactly the +same audit/status redaction rules as every other read path in +[Audit vs. OTEL](#audit-vs-otel-redaction-cardinality-durability). + +**`d2b zone audit export`.** Admin-only (`audit-export` session verb, exact +`d2b.audit.v3.AuditService/Export` binding, no resource-authority widening); +hash-chain breaks are reported inline in the export stream rather than +silently truncating history; output carries no old field names +(`realm`/`node`/`workload_id`) and no path/`argv` content +(`ADR046-audit-004`). + +**Coordination with disclosure policy.** `SECURITY.md`'s GitHub Security +Advisory channel, response-time targets (7-day acknowledgment, 30-day +assessment), and scope table are unchanged by this spec and remain the +authoritative disclosure process for the v3 Zone control plane; this section +only adds the operator-facing evidence-gathering commands a responder or +reporter is expected to use while a v3 advisory is investigated. + +## Reset boundary + +Reset in v3 is Zone/Provider/Host/Guest-scoped, not the whole-host factory +reset the historical main ADR 0045 defined for the v1→v2 cutover. The +`StateReset` audit record is the normative v3 contract +(`ADR-046-telemetry-audit-and-support` §"StateReset", lines 1115-1128): + +```json +{ + "record_class": "state-reset", + "state_reset_fields": { + "scope": "zone|provider|host|guest", + "trigger": "operator|upgrade|corruption|emergency", + "generation": 5, + "prior_digest": "sha256:", + "outcome": "ok|error" + } +} +``` + +`StateReset` is a `Privileged` durability-class record: it is durably +fsynced before the reset operation completes, and it is never +rate-limited (§21). A reset that cannot durably record its `StateReset` +event fails closed with `audit-unavailable`, exactly like any other +privileged mutation. + +**Why v3's reset boundary is structurally simpler than v2's.** The +historical v2 factory-reset design (main ADR 0045) needed a mandatory +live-session ceremony to delete Secret Service items and revoke TPM exports +*because v2 allowed persisted, host-readable secret material to exist at +all*. v3's [zero-secret invariant](#credential-security-end-to-end-delivery-and-zeroization) +means Credential Providers never persist secret bytes in the resource +store, audit, or telemetry in the first place — the backing secret (OS +keyring entry, IMDS binding, Entra token cache) lives entirely inside the +owning OS/cloud provider, not inside d2b state. A v3 reset therefore does not +need a bespoke pre-reset "delete d2b-owned Secret Service items" ceremony; +it needs the two operations d2b's own resource model already performs on +every deletion: + +1. **Credential lease revocation before removal**, exactly as on ordinary + Credential deletion (see [Lifecycle security](#lifecycle-security-update-revocation-finalizer-restart-adoption-quarantine)): + any open lease for a Credential in the reset scope is revoked before its + finalizer clears, so no stale lease survives the reset. +2. **Volume destruction with key-shredding**, exactly as on ordinary Volume + deletion (see [Volume security](#volume-security)): sealed Volumes in the + reset scope shred their sealing key material as the first destruction + step, so no orphaned ciphertext-with-key survives a scope-bounded reset. + +**Quiesce before destructive reset.** A `scope: host` or `scope: guest` +reset first drives the affected scope through `EmergencyPolicy` +(`stopNewAdmissions` + `drainOngoingOperations`, §23) so in-flight mutations +finish or are cleanly cancelled rather than being torn out from under a +live write transaction; only after the scope reports no in-flight mutation +does destructive cleanup (finalizer-driven resource deletion, Volume +destruction) proceed. + +**No partial reset.** Exactly as with bundle activation (AC5) and RoleBinding +deletion (§8), a reset is atomic at its declared scope: every resource in +scope is either fully torn down (finalizers drained, `Deleted` revision +committed, `StateReset` record fsynced) or the reset fails closed and the +scope remains in its pre-reset state — there is no state where a reset has +"partially happened" and is silently retried later. A resumed reset after a +crash resumes only from the durable `StateReset` record already committed +for that generation; it never treats an in-progress reset as complete +without that record. Volume identity-marker fail-closed semantics (§17) mean +an aborted mid-destruction Volume is quarantined, not silently +re-provisioned or silently treated as already gone, on the next reconcile +after the interrupted reset. + +**No host-wide "factory reset boot generation" concept in v3.** Because +reset is Zone/Provider/Host/Guest-scoped rather than whole-host, v3 has no +equivalent of the v2 dedicated reset boot generation, no-startable-daemon +generation, or `d2b host reset --factory` command; the closest analogue for +a full Host wipe is a `scope: host` `StateReset` covering every Zone +resource that resolves to that Host, which is bound by the same finalizer/ +quiesce/atomicity rules as any other scope. + +## Per-ResourceType threat matrix + +The nineteen standard ResourceTypes (D035) each face a distinct primary +threat and are covered by controls already defined above. This table is the +single index a reviewer uses to confirm every ResourceType has at least one +documented threat/control pair; the "Detail" column points to the exact +section with the full control description. + +| ResourceType | Primary threat | Prevention/detection/recovery | Detail | +| --- | --- | --- | --- | +| `Zone` | A second `Zone/` masquerading as the Zone's own self resource | Cardinality-1 admission (`resource-already-exists`); `Zone.spec` must be exactly `{}` (`zone-spec-invalid` otherwise) | §26/§8 | +| `ZoneLink` | Child static key substitution / stale child identity reuse | `childStaticKeyFingerprint` re-verified every reconnect; `childZoneUid` mismatch resets cursor to revision 0 | §7/§10 | +| `Provider` | Forged/downgraded/malicious package | Signed `PackageIdentity`, build+runtime signature/conformance verification, quarantine-not-delete | §5 | +| `Role` | Wildcard privilege escalation via operator/Provider-authored Role | Wildcard restricted to core-controller-generated Roles; `provider-wildcard-permission-restricted` | §8 | +| `RoleBinding` | TOCTOU during deletion/re-creation; scope widening via narrowing | Atomic one-transaction deletion; `scopeNarrowing` restriction-only; immutable `roleRef` | §8 | +| `Quota` | Zero-quota bypass; soft-quota silently treated as enforcement | `component-quota-zero` rejection; `enforcementPolicy: hard` rejects over-quota, `soft` only warns | §23 | +| `EmergencyPolicy` | Reason field leaking into telemetry; policy silently deleting Processes instead of pausing | `reason` confined to spec/audit body, never OTEL/status; `stopProviderProcesses` never sets `deletionRequestedAt` | §23 | +| `Host` | Silent isolation-guarantee downgrade for user-domain execution | Non-suppressible three-surface `isolationPosture: "none"` propagation | §16 | +| `Guest` | Compromised Guest escalating via network/device/credential ambient authority | Guest-agent capability confinement to guest netns; ComponentSession KK enrollment; per-attachment Volume/Network/Device scoping | §3 (AC2) | +| `Process` | pidfd/identity confusion enabling signal-based attack on the wrong process; setsid/PGID escape or reuse during teardown | Eleven pidfd lifecycle/non-exportability invariants; broker-parent-only wait/reap; anchored cgroup.kill for unambiguous teardown; quarantine-not-kill on ambiguity | §15/§22 | +| `EphemeralProcess` | Terminal-result retention becoming an unbounded secret/output store | Bounded `successfulTtl`/`failedTtl`; forbidden-field list identical to Process | §15 | +| `Volume` | Identity-marker tamper / silent re-provisioning after tamper or deletion race | HMAC identity marker, fail-closed `missing`/`replaced` status, quarantine on partial destruction | §17 | +| `Network` | Firewall/IPv6/east-west drift silently reopening isolation | Dual-point IPv6 suppression enforcement; `hostBlocklist` additive-only; `firewallDigest` drift detection | Cross-cutting network invariants (`ADR-046-resources-network`) | +| `Device` | Blanket device-path grant / cross-consumer device sharing (e.g. security-key + USBIP on one physical device) | No blanket grant; broker-derived node only; eval preflight plus mandatory Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` claim shared by all USB/security-key Providers before effects | §13 | +| `User` | Numeric UID/GID leaking into authorization decisions or public surface | `User/` typed refs only; `mappingClass: process-principal-root` never exposes numeric UID/GID publicly | §15 | +| `Credential` | Secret bytes reaching resource store/audit/telemetry, or a non-enrolled consumer reading a token | Zero-secret invariant; Noise KK-only sensitive delivery; exact `consumerRef` match | §9/§19 | +| `Endpoint` | A stable endpoint leaking a raw locator (path/address/CID/port/fd/credential) into spec/status/CLI, or an unauthorized consumer resolving it to a live transport/FD | No raw locator in spec/status (closed transport/locality classes only); Core/ProviderSupervisor resolves via EffectPort/LaunchTicket under authorization; unauthorized resolve denied with a typed error (D092) | §D092 | +| `ResourceExport` | Direct Device/Endpoint/Binding export, backing/secret/path leakage, unsigned factory substitution, or unauthorized Zone admission | `resourceRef` must be the factory-bound owner Service; signed Service/Binding/ref/schema/factory binding; only bounded key/type/fingerprints/capability ceiling advertised; per-hop RBAC; revoke on delete/link loss | Cross-Zone Service projection security | +| `ResourceImport` | Stale/wrong-type projection, a local Binding spoofing remote authority, auto-created policy intent, observed state smuggled into spec, or cross-Zone Ref/FD leakage | Exact factory/type/schema/generation match; exactly one non-authoritative same-type projection Service; Binding separately authored/status-owner checked; Binding spec is intent-only and observations are status-only; no auto-Binding; encrypted bounded streams; projection degrades on revoke | Cross-Zone Service projection security | + +## Per-Provider-family threat matrix + +The 27 frozen initial Providers (D043-D049) each have a full `Security` +section in their own dossier under `docs/specs/providers/`; this table is +the cross-Provider index, grouped by family, so a reviewer can confirm no +family is missing a documented primary threat/mitigation pair. `EffectPort` +names and forbidden-design citations are given in full in +[Injected EffectPort boundary](#injected-effectport-boundary-and-the-privileged-broker) +and [Forbidden designs](#forbidden-designs). + +| Family | Providers | Primary threat class | Key mitigation | Dossier | +| --- | --- | --- | --- | --- | +| System/bootstrap | `system-core`, `system-systemd`, `system-minijail` | Bootstrap-authority widening; unit/PID identity confusion | Compiled non-extensible bootstrap policy (D052); pidfd/InvocationID identity verification; quarantine-not-kill | `provider-system-{core,systemd,minijail}.md` | +| Guest runtime | `runtime-cloud-hypervisor`, `runtime-qemu-media`, `runtime-azure-container-apps`, `runtime-azure-virtual-machine` | Host holding cloud/relay credentials; adoption ambiguity re-provisioning a live workload | Mandatory `executionRef: Guest/*` for cloud-facing components (§11); `Degraded`-not-recreate on ambiguity | `provider-runtime-*.md` | +| Activation | `activation-nixos` | Store-path leakage revealing exact system closure; privilege escalation via `startRoot: true` | Sealed store path never in any public surface; `startRoot: true` paired with `noNewPrivileges: true` + zero host capabilities | `provider-activation-nixos.md` | +| Volume | `volume-local`, `volume-virtiofs` | Host path leakage; cross-VM store leakage; ADR 0021 capability violation | Opaque `sourcePolicyId`; `store-view/live` never real `/nix/store`; zero host capabilities for virtiofsd (conformance-kit tested) | `provider-volume-{local,virtiofs}.md` | +| Network | `network-local` | Internal bridge/tap topology (IfName) leakage; firewall/isolation drift | FNV-1a-hashed IfNames never exposed; dual-point IPv6 suppression; `firewallDigest` drift detection | `provider-network-local.md` | +| Device | `device-tpm`, `device-usbip`, `device-security-key`, `device-gpu` | Device-path leakage; cross-consumer device conflict; TPM re-provisioning as clean device | Broker-derived node only; trusted USB identity resolution plus byte-identical `physical-usb-backing` tuple collision before security-key/USB effects; TPM fail-closed marker | `provider-device-*.md` | +| Credential | `credential-secret-service`, `credential-entra`, `credential-managed-identity` | Secret bytes crossing a process/session/audit boundary | Zero-secret-bytes port boundary; domain-locked Credential type (user vs. system) | `provider-credential-*.md` | +| Interaction | `display-wayland`, `audio-pipewire`, `clipboard-wayland`, `shell-terminal`, `notification-desktop` | Interactive content (clipboard/terminal/notification) exfiltration via observability surfaces | FD-only content delivery (SCM_RIGHTS); closed content-secrecy table (§20); nonce-bound action replay protection | `provider-{display-wayland,audio-pipewire,clipboard-wayland,shell-terminal,notification-desktop}.md` | +| Transport | `transport-unix`, `transport-vsock`, `transport-azure-relay` | Raw CID/port/socket-path leakage; FD/credential smuggling over a remote transport | Opaque endpoint IDs with no public accessor; structural `attachment_support=false` over ZoneLink transports | `provider-transport-*.md` | +| Observability | `observability-otel` | Credential/isolation-posture leakage into the optional telemetry pipeline; telemetry outage blocking the Zone | Redaction filter drops forbidden attributes before OTLP batching; Provider is never a bootstrap dependency | `provider-observability-otel.md` | + +## Forbidden designs + +Consolidated from every owning spec/dossier; each is a structural/admission- +time rejection, not an operational recommendation. + +**Trust and packaging.** + +- Authoring `system-core`/`system-minijail` as an operator-declared Provider + in Nix (`ADR-046-resources-zone-control` lines 754-777, 2350-2354). +- Any Provider claiming a wildcard permission + (`resourceNames: ["*"]`/empty `executionRefs`) without being a + core-controller-generated Role (RZC lines 868-997, line 2985). +- Manifest-derived Provider spec fields set by the operator — + `spec.exports`, `spec.components`, `spec.dependencies`, + `spec.permissionClaims`, `spec.upgradePolicy`, `spec.restartPolicy` are + resolved only from the signed manifest (RZC lines 2722-2723). +- Any Provider process importing a broker service/client/DTO, or receiving a + broker socket, host path, device node, systemd D-Bus connection, or + compositor socket by ambient discovery (§13, D077). + +**Identity and authorization.** + +- A payload supplying or replacing `uid`, `gid`, `pid`, a realm/Zone role, or + broker authority (§7-§8). +- An established ComponentSession endpoint transferred by `SCM_RIGHTS`, + pidfd duplication, inheritance, or broker handoff (§7). +- A cross-Zone `ownerRef`, a multi-Zone `CommitBatch`, or a `ResolveRef` + crossing a Zone boundary (§10). +- A relay/managed-identity/Entra credential mapped to a local Role/Admin + (§11). +- Cross-Zone credential minting — no parent-minted token delivered to a + child Zone (§11). +- Exporting a Device, Endpoint, Binding, Credential, or backend directly; creating + any import projection other than the factory-bound same-qualified-type + Service; auto-creating/deleting Binding; placing observed state in Binding + spec; or accepting a missing/mismatched signed + projection factory. +- Treating an authored Binding, projection status, session/stream handle, or + imported Service Ref as proof of remote authority. + +**Process and sandbox.** + +- `Type=forking` for any `Provider/system-systemd`-managed Process + (§15). +- Treating a systemd unit name alone as process identity (§15). +- A Process ResourceSpec containing raw UID/GID, a raw cgroup path, a raw + socket address/host path, raw seccomp BPF, a raw capability bitmask, raw + minijail arguments, raw systemd unit properties, a raw broker operation + name/parameter, or a credential/secret/token in an environment variable + (§15). +- Pidfd serialized, stored, sent over d2b-bus, exposed in status, or used to + infer identity across a restart (§15). +- A non-parent ProviderSupervisor/controller calling `waitid`/`waitpid` for a + system-minijail child or treating pidfd readability as exit status; only the + `clone3` broker parent waits/reaps and relays status (§15). +- PID/PGID SIGKILL ownership for a system-minijail subtree, or an + older-kernel fallback that omits the Linux ≥5.14 anchored `cgroup.kill` + sequence (§15/§22). +- A `LaunchTicket` executed after its compiled sandbox plan digest has + changed since issue (§15). +- virtiofsd spawned with any non-empty host capability set, `startRoot: + true`, `--sandbox=namespace`, or an `extra_args` field (§17, + `ADR-046-provider-volume-virtiofs.md`). +- A broad kill of an entire cgroup/process group on ambiguous adoption, + instead of quarantine (§22). + +**Volume and state.** + +- A raw host path in any Volume spec field; only `sourcePolicyId` is + permitted (§17). +- `ephemeral`, `cache`, or `config` `persistenceClass` for a component state + Volume (§17, `component-persistence-class-forbidden`). +- `host-backed-guest` placement for a Credential/audit/remote-node/ + cloud-control schema Volume (§18, `guest-local-required`). +- `quotaBytes: 0` on any persistent Volume (§17, + `component-quota-zero`). +- Silent re-provisioning of a Volume after an identity-marker + `missing`/`replaced` failure, or of a partially destroyed Volume (§17). +- virtiofsd serving the host's actual `/nix/store` instead of + `store-view/live` (§17). + +**Credential.** + +- `Noise_NN_*` for sensitive credential/`SignChallenge` delivery — `Kk` is + required (§19, D056). +- `#[derive(Debug)]` on any type touching key material or token bytes (§19). +- Auto-retry on an ambiguous credential-delivery outcome (§19). +- An inline secret byte value anywhere a `credentialRef: true` field is + declared (§5, `inline-secret-in-settings`/`credential-value-must-be-ref`). +- Any `$`-prefixed config key other than `$credentialRef` (§5). + +**Networking.** + +- Removing or conditioning `lib.mkForce` on the net-VM's `10-eth-dhcp` + neutralizer (carried forward unchanged from the pre-v3 baseline, + `AGENTS.md` "Don'ts (security-relevant)"). +- Removing IPv6 suppression from either the bridge-creation path or the + periodic reconcile path — both are required together + (`ADR-046-resources-network` INV-NET-002). +- Reducing or emptying `hostBlocklist` — additive-only (INV-NET-004). +- A workload VM name, user identifier, DHCP hostname, or workload label in a + host nftables rule (INV-NET-006). +- `CAP_NET_ADMIN`/`BIND_SERVICE`/`RAW` present in the host network namespace + effective capability set for a guest-agent Process (INV-NET-009). + +**ZoneLink and transport.** + +- An FD, credential byte, or host/socket/device/store path in any + ZoneLink-forwarded frame (§10). +- `attachmentsEnabled: true` for any ZoneLink transport, or + `socketKind: stream` with attachments enabled (§10, transport-unix/vsock + dossiers). +- A Transport Provider calling raw transport syscalls itself (`AF_VSOCK`, + raw `socket(2)`, etc.) instead of going through the core effect adapter + (§10). +- A Transport Provider or parent Zone owning child-local ZoneLink state + directly instead of the child core handler owning the resource and receiving + opaque transport handles (§10, D081). + +**Telemetry and audit.** + +- `no_isolation`, a raw path, `argv`, a PID/pidfd/cgroup path, a resource + name, or a credential/token appearing as an OTEL metric label, span + attribute, or structured log field (§21). +- `observability-otel` reading the authoritative audit sink, or blocking + Zone startup (§21). +- Any interactive content class from the [content-secrecy table](#content-secrecy-clipboard-terminal-ctap-notification) + appearing in audit, OTEL, Debug output, or an error message (§20). + +**Lifecycle.** + +- Force-removing an undrained finalizer to unblock a generation sweep or a + reset (§22, §25). +- Generation cleanup deleting a `managedBy: controller` or `managedBy: api` + resource merely because it is absent from a new Nix bundle (§22, D069). +- A reset proceeding without a durably committed `StateReset` audit record, + or resuming an interrupted reset without re-checking that record (§25). + +## Residual risks and explicit non-goals + +These are documented, accepted limitations — not gaps this spec claims to +close. Each maps to the attacker class it is scoped against. + +1. **Compromised Host root, or any process sharing a UID with a d2b + component, is not contained (AC3).** This matches `SECURITY.md`'s + existing "does NOT defend against ... multi-user trust on a single host." + A user-domain `Host` with `isolationPosture: "none"` makes this residual + risk explicit and non-suppressible (§16) rather than eliminating it — + elimination would require a VM/sandbox boundary, which is precisely what + `isolationPosture: "none"` declares is absent. +2. **A fully compromised gateway Guest can still exhaust or misuse its own + Zone's relay/cloud reachability.** Gateway custody (§11) bounds the blast + radius to that Guest and its Zone — it does not claim the Guest itself is + unbreachable. A compromised gateway Guest cannot escalate to Host root, + the privileged broker, sibling Zones, or unrelated Host-resident + workloads, matching ADR 0032's "protection direction" framing. +3. **TPM sealing binds TPM, host, user, and credential name — it does not + cryptographically bind a specific systemd unit or Process identity.** + PID1/broker-assigned identity (DAC, mount namespace, sandbox + configuration), not TPM cryptography, is what isolates the receiving + unit. This is an accepted limitation of TPM2 sealed-credential semantics, + not a d2b defect. +4. **Physical attacks, CPU side channels, and upstream supply-chain + compromise remain out of scope**, unchanged from `SECURITY.md`: disk + encryption + TPM-bound unlock is a Lanzaboote concern; SMT/cache + side-channels are a hardware-level concern; `nixpkgs`/Nix-store + supply-chain attacks upstream of the pinned artifact catalog are deferred + to upstream Nix/nixpkgs. +5. **A relay provider (Azure Relay or equivalent) sees connection metadata + and traffic shape even though it never sees plaintext operations.** + ZoneLink/ComponentSession confidentiality (§7, §10) protects payload + content and identity, not the existence/timing/volume of cross-Zone + traffic. Traffic analysis by the relay operator is an accepted residual + risk, matching the existing ADR 0032 relay-trust framing. +6. **`observability-otel` telemetry loss under exporter outage is accepted, + not prevented.** The design explicitly chooses availability of the Zone + over completeness of telemetry (§21); a determined attacker able to + induce sustained exporter outage can degrade observability (never + authorization or audit) during that window. +7. **Non-goal: this spec does not define a new cryptographic primitive, + key-exchange protocol, or audit storage engine.** It normatively + references the Noise profiles, redb transaction model, and JSONL + hash-chain format already defined by the owning specs; any future change + to those primitives is scoped to the owning spec, not this one. +8. **Non-goal: whole-host physical/hardware attestation.** `guest-controller-host`-class + attestation for confidential computing or hardware-backed vTPM is + explicitly future work requiring its own ADR/spec and conformance + contract before any Provider may advertise it (historical main ADR 0045 + invariant, carried forward as a residual non-goal since no v3 Guest + runtime Provider claims this capability at this baseline). + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| v3 current anchor | `packages/d2b-priv-broker/src/{sys.rs,ops/*}` (broker sole-executor, cgroup/namespace pre-establishment); `packages/d2b-core/src/{storage,processes,privileges,minijail_profile}.rs` (typed process DAG/minijail profiles/broker effects); `packages/d2b-realm-core/src/workload.rs` (`IsolationPosture::UnsafeLocal`); `packages/d2bd/src/{exec_session.rs,realm_access_resolver.rs}`; `nixos-modules/{assertions.nix,net.nix,manifest.nix,bundle-artifacts.nix}`; `SECURITY.md` (disclosure policy, v1/v2 trust-boundary deltas) | +| v3 evidence class | Mixed. Broker sole-executor, cgroup/namespace pre-establishment, TPM/USBIP device hardening, and Nix eval-time assertions are `implemented-and-reachable`. ComponentSession, native RBAC, Zone resource store, every standard ResourceType, and every frozen Provider are `ADR-only` (no `Provider/*` crate exists in the protected v3 baseline; see `ADR-046-current-code-migration-map.md` §8.3 disposition table) | +| Main reuse source | main `a1cc0b2d`: `d2b-session`/`d2b-session-unix` (ComponentSession Noise/record/attachment machinery), `d2b-contracts/src/{public_wire.rs,provider_registry_v2.rs}` (typed RPC/registry shape), `d2b-priv-broker/src/ops/{swtpm_dir.rs,storage_contract.rs}` (fail-closed marker/quarantine pattern) | +| Behavior retained | Broker-as-sole-privileged-executor; fail-closed typed errors; pidfd/InvocationID adoption identity; positive-capability provider traits; argv/secret/path redaction discipline; OTEL/audit architectural separation; quarantine-not-kill on ambiguous adoption (ADR 0034 continuation) | +| Required delta | Native Zone resource plane/redb store; ComponentSession production wiring with native RBAC; Provider packaging/signing/trust/quarantine; primitive ResourceSpecs (Host/Guest/Process/Volume/Network/Device/User/Credential); ZoneLink/d2b-bus routing; per-Provider EffectPort boundary; StateReset audit contract | +| Excluded assumptions | Historical main ADR 0045's Realm/gateway-VM-as-ResourceKind model, per-realm PID1 broker sockets, long-lived guest-control HMAC token, and whole-host factory-reset generation are not v3 architecture — D050 (Guest replaces gateway-VM-as-special-realm), D016/D017 (Zone/ZoneLink replace Realm), and this spec's own [reset boundary](#reset-boundary) supersede them for v3 | +| Feasibility proof | Per-spec hermetic/Nix-eval/container/host-integration/fuzz suites named in [Implementation work items](#implementation-work-items) below and in every owning spec's own work-item validation column | +| Future owner | `ADR046-security-*` work items below own cross-cutting security validation; each ResourceType/Provider spec's own `ADR046-*` work items own the implementation itself | + +## Implementation work items + +### ADR046-security-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-001` | +| Dependency/owner | All ResourceType/Provider specs; owned by the security/telemetry integrator alongside `ADR046-telem-008` | +| Current source | `packages/d2b-contract-tests/tests/policy_observability.rs` (existing v3 cardinality/label policy gate) | +| Reuse source | None (new cross-cutting gate; no equivalent exists in main) | +| Reuse action | adapt | +| Destination | `packages/d2b-contract-tests/tests/policy_telemetry_redaction.rs` | +| Detailed design | One policy test enumerating every forbidden metric-label/audit-field value from §21 and the content-secrecy table in §20 (store paths, `no_isolation`, credential bytes, raw paths/argv/PID/cgroup, CTAP/clipboard/terminal/notification content) and asserting, by static scan of instrumentation call sites plus a redaction-guard runtime test, that no `ADR046-*` Provider crate emits any of them Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Runs as part of `make test-lint`/`make test-rust`; every Provider crate's own redaction test (e.g. `tests/stream_redaction.rs`) is a per-Provider instance of the same closed list | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic (`cargo test -p d2b-contract-tests policy_telemetry_redaction`); fails the build if a new Provider crate is added without a corresponding redaction test file under its `tests/` | +| Removal proof | Not applicable — this is a permanent gate, not a migration | + +### ADR046-security-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-002` | +| Dependency/owner | `ADR046-session-001`/`ADR046-bus-001` (ComponentSession/d2b-bus implementation) | +| Current source | main `a1cc0b2d`: `d2b-session/tests/noise_vectors.rs`, `d2b-session/tests/component_session.rs`, `d2b-session-unix/tests/unix_session.rs` | +| Reuse source | Same main commit/paths | +| Reuse action | adapt | +| Destination | `packages/d2b-session/tests/noise_conformance.rs`, `packages/d2b-session/fuzz/fuzz_targets/{handshake_offer,record_frame}.rs` | +| Detailed design | Property/fuzz test suite over the three Noise profiles (§7): exact NN/KK/IKpsk2 vectors and rejection mutations (copied), plus new `cargo-fuzz` targets mutating the canonical handshake offer, preface, and encrypted record frame to assert no panic/UB and that every malformed input is a typed rejection (never a partial accept) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | Wired into `make test-rust` (vectors) and a separate `make test-fuzz` target (new; time-boxed nightly run, not part of the PR-blocking gate) | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic vector tests plus fuzz corpus with a minimum 4-hour nightly run and zero crashes/hangs as acceptance | +| Removal proof | Not applicable | + +### ADR046-security-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-003` | +| Dependency/owner | `ADR046-zone-control-004`/`ADR046-zone-control-005` (Role/RoleBinding implementation) | +| Current source | `packages/d2bd/src/admission.rs` (`verb_requires_admin()` baseline verb table) | +| Reuse source | None beyond the verb-table adaptation already tracked by `ADR046-zone-control-004` | +| Reuse action | adapt | +| Destination | `packages/d2b-resource-store/tests/rbac_property.rs` | +| Detailed design | Property test asserting, for a randomly generated Role/RoleBinding/request corpus: (1) no request whose payload sets a subject/role field ever changes the resolved `AuthenticatedSubjectContext.subjectRef`; (2) no non-core Role with a wildcard grant is ever admitted; (3) `scopeNarrowing` never widens beyond the referenced Role; (4) RoleBinding deletion never leaves an observable intermediate state under concurrent readers; (5) `relay` is accepted only as the canonical ZoneLink-scoped session verb with core-generated or explicit admin-policy provenance and exact target bounds; and (6) every forwarding hop independently requires relay plus the target verb Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Runs against the real redb-backed resource store test harness, not a mock | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic property test (`proptest`/`quickcheck`-style, minimum 10,000 cases per property) | +| Removal proof | Not applicable | + +### ADR046-security-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-004` | +| Dependency/owner | `ADR046-routing-005`/`ADR046-bus-001` (ZoneLink/d2b-bus relay implementation) | +| Current source | None (v3 ZoneLink relay is `ADR-only`) | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/fuzz/fuzz_targets/zonelink_frame.rs`, `packages/d2b-bus/tests/zonelink_structural_rejection.rs` | +| Detailed design | Fuzz + property suite asserting that no mutation of a ZoneLink-bound frame (attachment count, credential-shaped byte runs, path-shaped strings, PID-shaped integers) is ever forwarded — every such mutation is rejected at serialization with `attachment-not-permitted-over-zone-link` or the transport-specific equivalent, never silently dropped or partially forwarded Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (design copied from `ADR-046-zone-routing.md` structural-rejection sections). | +| Integration | `make test-fuzz`; a companion container test (`tests/integration/containers/zonelink-cross-zone.rs`) runs two real Zone runtime containers connected by a real ZoneLink and asserts the same property end to end over the wire, not just in the frame-serialization unit | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Fuzz corpus (`cargo fuzz run zonelink_frame -- -runs=1000000`, zero crashes); container test passes in `make test-integration` | +| Removal proof | Not applicable | + +### ADR046-security-005 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-005` | +| Dependency/owner | Every Provider dossier's own crate; owned by the Provider packaging integrator (`ADR-046-provider-model-and-packaging`) | +| Current source | None (compile-time dependency audit does not exist yet; per-Provider "imports no broker DTO" claims are currently prose-only in each dossier) | +| Reuse source | `cargo-deny`/`cargo tree`-style dependency graph tooling already used by `nix flake check`'s `rust-deny`/`rust-audit` derivations (`AGENTS.md` "Disk hygiene contract") | +| Reuse action | adapt | +| Destination | `packages/xtask/src/effectport_boundary_check.rs`, wired into `make test-policy` | +| Detailed design | For every crate under `packages/d2b-provider-*`, walk its `Cargo.toml` dependency graph and fail the build if it transitively depends on `d2b-priv-broker` or any crate exposing a raw broker client/DTO type; separately, grep-scan for direct syscalls forbidden per dossier (e.g. `socket(AF_VSOCK` in `transport-vsock`, `Command::new("systemctl"` in `system-systemd`) | +| Integration | `make test-policy`; blocks any PR adding a forbidden dependency edge or forbidden syscall string to a Provider crate | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic (`cargo xtask effectport-boundary-check`); a negative test intentionally adds a forbidden dependency to a scratch crate and asserts the check fails | +| Removal proof | Not applicable — permanent gate | + +### ADR046-security-006 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-006` | +| Dependency/owner | `ADR046-minijail-002`/`ADR046-minijail-003` (LaunchTicket/EffectPort implementation) | +| Current source | `packages/d2b-priv-broker/src/sys.rs` (`clone3_spawn_runner` user-namespace path); `packages/d2b-host/src/virtiofsd_argv.rs` | +| Reuse source | Same v3 paths (already `implemented-and-reachable` per the migration map) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-minijail/tests/launchticket_toctou.rs` | +| Detailed design | Fault-injection test that issues a `LaunchTicket`, then mutates the referenced `CompiledSandboxPlan` digest (simulating a race between issue and exec) before the broker execs, and asserts the spawn fails closed rather than launching with the old plan; a companion test kills the broker mid-`clone3` and asserts no half-initialized process (missing cgroup placement, non-zero host capabilities) is ever observable by a concurrent reader | +| Integration | `make test-rust` (unit-level fault injection via a fake clock/fault-injecting `EffectPort` test double); a host/KVM integration test (`tests/host-integration/launchticket-toctou.nix`) repeats the same scenario against the real broker and real `clone3(2)` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic fault-injection test plus `make test-host-integration` NixOS/KVM test; acceptance is zero observable non-zero-capability or missing-cgroup-placement windows across 10,000 injected-fault iterations | +| Removal proof | Not applicable | + +### ADR046-security-007 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-007` | +| Dependency/owner | `ADR046-minijail-005`, `ADR046-systemd-002`, `ADR046-aca-001`, `ADR046-volume-001` (every quarantine-on-ambiguity implementation) | +| Current source | `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` (existing fail-closed marker/quarantine pattern, `implemented-and-reachable`) | +| Reuse source | Same v3 path, generalized | +| Reuse action | adapt | +| Destination | `packages/d2b-contract-tests/tests/quarantine_not_kill_matrix.rs` | +| Detailed design | One parameterized fault-injection matrix test, run once per adoption-capable Provider (`system-minijail`, `system-systemd`, `runtime-cloud-hypervisor`, `runtime-azure-container-apps`, `volume-local`), that restarts the controller with a deliberately ambiguous adoption candidate (duplicate InvocationID, mismatched marker inode, stale ACA operation handle) and asserts: (a) the resource transitions to `Degraded`/`Quarantined`, never `Deleted` or silently re-adopted; (b) no signal is sent to the ambiguous candidate process; (c) a `runtime-security-violation`-class audit record is emitted Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | `make test-rust` for the in-process cases; `make test-host-integration` for the real-pidfd/real-cgroup cases (`tests/host-integration/quarantine-not-kill.nix`) | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Matrix covers all five Providers listed; acceptance is 100% pass across all five with no signal sent to the ambiguous candidate in any case | +| Removal proof | Not applicable | + +### ADR046-security-008 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-008` | +| Dependency/owner | `ADR046-exec-002` (Host `isolationPosture` propagation) | +| Current source | `packages/d2b-contracts/src/public_wire.rs:267` (`WorkloadPublicSummary.execution_posture`, `implemented-and-reachable`); `packages/d2bd/src/unsafe_local_helper.rs` (`HelperRegistry::dispatch_launch`, current gap: does not emit a `ProcessEffect`-class event) | +| Reuse source | Same v3 paths | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-core/tests/no_isolation_propagation.rs` | +| Detailed design | Integration test that creates a user-only `Host`, launches a Process on it, and asserts all three non-suppressible surfaces simultaneously: `status.isolationPosture == "none"` in JSON output; the unconditional stderr warning string is present with `--json` and non-JSON CLI invocation and cannot be suppressed by any combination of flags; the corresponding `ProcessEffect` audit record carries `no_isolation: true`; and a companion negative assertion that `no_isolation` never appears in any OTEL metric/span emitted during the same test run | +| Integration | `make test-rust` (CLI/status/audit assertions) plus a Nix eval test (`tests/unit/nix/cases/no-isolation-null-posture-rejected.nix`) asserting the D042/D067 bidirectional-rejection eval assertions fire | +| Data migration | None (closes the current `HelperRegistry::dispatch_launch` audit gap noted in `ADR-046-telemetry-audit-and-support.md`) | +| Validation | Hermetic CLI/audit integration test; Nix eval test; acceptance is zero code paths reaching a live user-only-Host Process without all three surfaces firing | +| Removal proof | Legacy `d2b-unsafe-local-helper` v2-protocol warning path removed only after this test passes against the v3 replacement and the legacy crate has no remaining callers | + +### ADR046-security-009 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-009` | +| Dependency/owner | `ADR046-pstate-003` (Volume identity marker) | +| Current source | `tests/unit/nix/cases/per-vm-state-ownership.nix` (existing v3 ownership test, adapted target) | +| Reuse source | None new | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-volume-local/tests/marker_tamper_fault_injection.rs` | +| Detailed design | Fault-injection test that provisions a Volume, then out-of-band (as a simulated attacker with filesystem access) replaces the marker file, swaps the backing directory for a different inode on the same `st_dev`, and deletes the marker entirely — three separate scenarios — and asserts each transitions the Volume to `Failed` with `markerStatus: missing`/`replaced` respectively, never a silent re-provision, and that operator-only remediation is the only recovery path exercised | +| Integration | `make test-rust`; a host-integration variant (`tests/host-integration/volume-marker-tamper.nix`) repeats the inode-swap scenario against the real broker-maintained marker root on a real filesystem | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic + host/KVM fault-injection test; acceptance is 100% fail-closed across all three tamper scenarios | +| Removal proof | Not applicable | + +### ADR046-security-010 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-010` | +| Dependency/owner | `ADR046-credential-001` through `ADR046-credential-007` | +| Current source | None (zero-secret invariant has no existing automated gate) | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `packages/d2b-contract-tests/tests/zero_secret_invariant.rs` | +| Detailed design | Static + dynamic gate: (1) static — every DTO type reachable from a `Credential`-adjacent module must implement a hand-written redacted `Debug` and must not derive `Debug`, enforced by a `#[forbid(clippy::derive_debug_ambient)]`-style custom lint or an `xtask` AST scan; (2) dynamic — a property test that generates random `Credential` delivery sessions and asserts the delivered token/`SignChallenge` byte sequence never appears, byte-for-byte, in any captured audit record, OTEL span, log line, or resource-store row taken during the same test run Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (design from `ADR-046-resources-credential.md` §1.1). | +| Integration | `make test-lint` (static scan) and `make test-rust` (dynamic property test) | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic; the dynamic test additionally runs as a canary-byte test (a unique random marker is embedded in the token and searched for across every observability surface) | +| Removal proof | Not applicable | + +### ADR046-security-011 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-011` | +| Dependency/owner | `ADR046-notify-004`, clipboard/terminal/security-key Provider work items | +| Current source | `packages/d2b-unsafe-local-helper/src/{shell_runtime,shell_supervisor}.rs` (existing terminal-byte redaction discipline, `implemented-and-reachable`, adapted target) | +| Reuse source | Same v3 paths | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-{clipboard-wayland,shell-terminal,device-security-key,notification-desktop}/tests/stream_redaction.rs` (one per Provider, same shared test helper crate) | +| Detailed design | Shared canary-byte test helper: each Provider's test injects a unique random marker into its sensitive content path (clipboard bytes, terminal output, CTAP payload, notification body) and asserts the marker never appears in audit, OTEL, Debug output, or CLI error text captured during the test | +| Integration | `make test-rust`; a container integration test (`tests/integration/containers/content-secrecy.rs`) runs a real Wayland/D-Bus mock session end to end for the clipboard/notification cases | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic canary-byte test per Provider (4 Providers, shared helper crate); container test for the two D-Bus/Wayland-mediated cases | +| Removal proof | Not applicable | + +### ADR046-security-012 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-012` | +| Dependency/owner | `ADR046-audit-002` (privileged audit durability) | +| Current source | `packages/d2bd/src/daemon_audit.rs` (existing audit-write path, adapted target) | +| Reuse source | Same v3 path | +| Reuse action | adapt | +| Destination | `packages/d2b-audit/tests/privileged_fail_closed.rs` | +| Detailed design | Fault-injection test that makes the audit sink's fsync fail (simulated ENOSPC/EIO) during a privileged `ResourceMutation`/`RBACChange`/`StateReset` write, and asserts the originating operation itself fails with `audit-unavailable` rather than completing with a lost audit record; a companion test floods `Standard`/`Best-effort` records past `DEFAULT_AUDIT_WRITES_PER_SECOND` and asserts privileged records are never dropped or delayed by the resulting backpressure | +| Integration | `make test-rust` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic fault-injection test; acceptance is zero privileged-class operations that complete despite a failed durable audit write | +| Removal proof | Not applicable | + +### ADR046-security-013 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-013` | +| Dependency/owner | `ADR046-zone-control-009`/`ADR046-zone-control-010` (Quota/EmergencyPolicy), `ADR046-session-001` (session limits) | +| Current source | None new; ceiling values are already enumerated in `ADR-046-zone-routing.md`/`ADR-046-cli-and-operations.md` | +| Reuse source | main `a1cc0b2d`: `d2b-session` credit-accounting/priority-scheduling tests, copied for the checked-arithmetic/priority-ordering assertions | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/tests/dos_ceiling_fault_injection.rs` | +| Detailed design | Fault-injection/load test suite: (1) attachment-credit exhaustion at each of the six scopes (Packet/Request/Operation/Session/Process/Host), asserting typed rejection never a panic; (2) reconnect-storm exceeding `MAX_RECONNECT_ATTEMPTS`/`MAX_RECONNECT_WINDOW_MS`, asserting the session fails closed rather than looping; (3) ZoneLink hop-count/route-advertisement replay flood, asserting `hop-limit-exceeded`/`zone-advertisement-replay` rather than unbounded forwarding; (4) a stalled data stream under load, asserting control/cancellation traffic is never starved (priority-scheduling property) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | `make test-rust`; item (4) additionally runs as a container load test (`tests/integration/containers/backpressure-priority.rs`) with a real slow consumer | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic fault-injection suite; container load test; acceptance is zero panics/unbounded-growth across all four scenarios | +| Removal proof | Not applicable | + +### ADR046-security-014 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-014` | +| Dependency/owner | `ADR046-doctor-001`/`ADR046-doctor-002` (doctor/support-bundle commands) | +| Current source | None (both commands are `ADR-only`) | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `packages/d2b/src/commands/{doctor,support_bundle}.rs` | +| Detailed design | `d2b zone doctor` performs read-only status/audit-hash-chain checks with the redaction rules from §21 enforced on every field it prints; `d2b zone support-bundle` assembles a bounded archive of metadata+status (never spec bytes or `metadata.name`) and sets `bundle_completeness: "partial"` when any Provider in scope is quarantined, rather than omitting the gap silently Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (design from `ADR-046-telemetry-audit-and-support.md`). | +| Integration | `make test-rust` (CLI integration tests); a container test (`tests/integration/containers/support-bundle-quarantined.rs`) runs a real Zone with one quarantined Provider and asserts the bundle correctly reports `partial` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic CLI test asserting no spec byte or `metadata.name` appears in a generated bundle; container test for the quarantined-Provider case | +| Removal proof | Not applicable | + +### ADR046-security-015 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-015` | +| Dependency/owner | `ADR046-audit-001` (`StateReset` record), Volume/Credential lifecycle work items | +| Current source | Historical main ADR 0045 factory-reset design (`a1cc0b2d^:docs/adr/0045-provider-and-transport-framework.md`, reset process overview and apply-command verification steps) — reused only as a design precedent for atomicity/fail-closed sequencing, not as v3 architecture (see [Reset boundary](#reset-boundary) for the explicitly excluded assumptions) | +| Reuse source | Same historical commit, sequencing pattern only (no code reuse; historical implementation was bash/systemd-generation-based and does not exist in any Rust crate) | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/reset.rs`, `packages/d2b-core-controller/tests/reset_atomicity.rs` | +| Detailed design | Implements the `scope` (`zone`, `provider`, `host`, or `guest`) `StateReset` flow from §25: quiesce via `EmergencyPolicy`, revoke open Credential leases in scope, destroy Volumes in scope (key-shred first), commit the `StateReset` audit record durably, and only then report the reset complete. A crash-recovery path re-derives "was this reset already committed?" solely from the durable `StateReset` record, never from partial filesystem state Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (pattern only). | +| Integration | `make test-rust` (unit-level state machine); a host/KVM integration test (`tests/host-integration/reset-atomicity.nix`) kills the process mid-reset at each of the four phases (quiesce, credential revoke, Volume destroy, audit commit) and asserts recovery never double-destroys, never silently completes without the audit record, and never leaves an orphaned sealed-Volume-without-key state | +| Data migration | None (v3-native; no v1/v2 reset-generation state to migrate) | +| Validation | Hermetic state-machine test; host/KVM crash-injection test at all four phases; acceptance is zero non-atomic outcomes across all injected crash points | +| Removal proof | Not applicable | + +### ADR046-security-016 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-016` | +| Dependency/owner | Documentation/CI integrator; depends on every Provider dossier's own `Security` section existing | +| Current source | None (cross-reference check does not exist) | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `tests/unit/gates/security-matrix-coverage.sh` | +| Detailed design | A drift-style gate that parses [Per-ResourceType threat matrix](#per-resourcetype-threat-matrix) and [Per-Provider-family threat matrix](#per-provider-family-threat-matrix), confirms every one of the 19 standard ResourceTypes and all 27 Provider dossiers under `docs/specs/providers/` has a row, and confirms every referenced dossier file actually contains a `## Security`-class section (by heading grep) — failing the gate if a new ResourceType/Provider is added without a corresponding row and dossier section Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (pattern from `tests/unit/gates/drift-check.sh`). | +| Integration | `make test-drift` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Hermetic shell-script gate; a negative test adds a scratch Provider dossier missing a Security section and asserts the gate fails | +| Removal proof | Not applicable — permanent gate | + +### ADR046-security-017 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-017` | +| Dependency/owner | `ADR046-routing-004`, gateway-custody Provider work items (`ADR046-aca-*`, `ADR046-azure-vm-*`, `ADR046-transport-relay-*`) | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `tests/integration/containers/malicious-child-zone.rs` | +| Detailed design | Container-based penetration test running a real parent Zone and a deliberately malicious child Zone container. Before attack injection, assert the one ZoneLink row/handler exists only in the child store/runtime and the parent has only sealed `{ childZone, parentZone }` topology plus authenticated allocator/route projection state. The child then attempts, over that link: FD smuggling, credential-shaped byte injection, cross-Zone `ownerRef` forgery, capability-ceiling widening claims, and route-advertisement replay. Every attempt must be rejected by the parent with the specific typed error named in §10, and none may create a parent ZoneLink row or reach the parent's resource store, Credential state, or Host substrate Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | `make test-integration` (requires podman, per `AGENTS.md` "Local Layer 1 + container integration") | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Container integration test; acceptance is zero successful attacks across all five attempted vectors | +| Removal proof | Not applicable | + +### ADR046-security-018 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-018` | +| Dependency/owner | Manual validation owner; depends on `ADR046-azure-vm-*`, `ADR046-transport-relay-*`, `ADR046-aca-*`, device Provider work items reaching a testable state | +| Current source | None (manual cloud/hardware validation has no automated equivalent by design) | +| Reuse source | None | +| Reuse action | adapt | +| Destination | `docs/reference/security-manual-validation-checklist.md` (new reference doc, out of scope for this spec's own file but named here as the required destination for the future implementation PR) | +| Detailed design | A checklist covering the scenarios that cannot be hermetically or even container-tested: (1) real Azure Container Apps/Azure VM credential rotation and revocation under `AzureEffectPort`, confirming zeroization on a real managed-identity/Entra token; (2) real TPM 2.0 hardware NVRAM persistence/tamper-marker behavior across a real host reboot; (3) real USBIP/security-key hardware mutual-exclusion enforcement with a physical FIDO2 device; (4) real Azure Relay listener/sender credential acquisition and relay-identity-not-local-auth verification against a live relay namespace Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (checklist pattern from `SECURITY.md`'s existing portability-roadmap manual milestones and `tests/README.md`'s manual hardware tier). | +| Integration | Run manually before each tagged release touching a cloud/hardware Provider, per the existing `tests/README.md` manual-tier convention | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Checklist sign-off recorded in the release's validation evidence, not a CI gate (matches `D2b_LIVE=1` manual-tier precedent in `AGENTS.md`) | +| Removal proof | Not applicable | + +### ADR046-security-019 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-security-019` | +| Dependency/owner | `ADR046-exec-003`/`ADR046-exec-007` and `ADR046-minijail-003` through `ADR046-minijail-005`; system-minijail/broker integration owner | +| Current source | `packages/d2bd/src/supervisor/pidfd_table.rs` (`BrokerReapLog`) and `packages/d2b-priv-broker/src/sys.rs` (`clone3_spawn_runner`) | +| Reuse source | Same v3 paths, adapted to the corrected ownership contract | +| Reuse action | adapt | +| Destination | `packages/d2b-contract-tests/tests/minijail_process_ownership.rs`; `tests/host-integration/minijail-cgroup-kill.nix` | +| Detailed design | Hermetic contract test proves only the broker that called `clone3` can produce the identity-bound `BrokerTerminalResult`; a non-parent poll-readable pidfd cannot be converted to status, while a verified duplicate holder can still request exact-main `pidfd_send_signal`. Host integration launches an owned descendant that calls `setsid(2)` plus an unrelated recycled-PGID decoy, performs graceful exact-main stop followed by anchored leaf `cgroup.kill`, and proves the owned leaf reaches `populated 0`, the broker reaps exactly once, the decoy survives, and rmdir/finalizer clearing wait for both proofs. Negative cases prove ambiguous adoption emits no signal/`cgroup.kill`, and Linux <5.14 or missing/unwritable `cgroup.kill` fails before spawn. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Hermetic contract test in `make test-rust`; real pidfd/cgroup scenario in `make test-host-integration` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Parent-only wait/reap, poll-readability-not-status, duplicate-holder signaling, setsid/PGID-reuse resistance, quarantine no-kill, exact-once reap, and Linux ≥5.14 platform-gate assertions all pass | +| Removal proof | Old ProviderSupervisor/controller wait/reap and PGID-kill paths are removed only after this gate passes against the replacement | diff --git a/docs/specs/ADR-046-spec-set.json b/docs/specs/ADR-046-spec-set.json new file mode 100644 index 000000000..b99b934f0 --- /dev/null +++ b/docs/specs/ADR-046-spec-set.json @@ -0,0 +1,1241 @@ +{ + "adr" : "0046", + "artifactKind" : "d2b-adr-spec-set", + "baseline" : "b5ddbed67867d9244bf33390868101bd9b053e49", + "members" : [ + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-cli-and-operations.md", + "sha256" : "9cb69967240869f2a902d87da25d706bfed580b7a4afefadfec94218c034e07f", + "specId" : "ADR-046-cli-and-operations", + "status" : "Proposed", + "supersedes" : "Current v3 `d2b` CLI contract (`packages/d2b/src/lib.rs` at baseline)", + "version" : 1, + "workItemPrefixes" : [ + "cli" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging" + ], + "path" : "docs/specs/ADR-046-components-processes-and-sandbox.md", + "sha256" : "a52ce5403399596d942f0c65f8f28b22de6b0e57c9623416c7756a5a7ac5be7c", + "specId" : "ADR-046-components-processes-and-sandbox", + "status" : "Proposed", + "supersedes" : "Current ProcessRole/VmProcessDag as the public process model", + "version" : 1, + "workItemPrefixes" : [ + "process" + ] + }, + { + "dependsOn" : [ + "ADR-046-resource-api-and-authorization", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-componentsession-and-bus.md", + "sha256" : "1e52660d95859e10fc82decf7757fc54b0c4ea6021dd9967de66c4748b8d9f81", + "specId" : "ADR-046-componentsession-and-bus", + "status" : "Proposed", + "supersedes" : "Current v3 Realm PeerSession and ad hoc guest/user/public IPC", + "version" : 1, + "workItemPrefixes" : [ + "bus", + "session" + ] + }, + { + "dependsOn" : [ + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb" + ], + "path" : "docs/specs/ADR-046-core-controllers.md", + "sha256" : "f118cb0b4f1852ab54cbee3a85ebf257acc6d38bf69161a259428a772078ca62", + "specId" : "ADR-046-core-controllers", + "status" : "Proposed", + "supersedes" : "Current daemon-global hard-coded orchestration ownership", + "version" : 1, + "workItemPrefixes" : [ + "core" + ] + }, + { + "dependsOn" : [], + "path" : "docs/specs/ADR-046-current-code-migration-map.md", + "sha256" : "efcbbabba718ca595ea58785ff9ee229d34df61856fd9d334c1ec9b9fec4b84f", + "specId" : "ADR-046-current-code-migration-map", + "status" : "Proposed", + "supersedes" : null, + "version" : 1, + "workItemPrefixes" : [] + }, + { + "dependsOn" : [], + "path" : "docs/specs/ADR-046-decision-register.md", + "sha256" : "0c1105cf9dafd70cc77526818bb997f64d15a68df02fe96d03a35d4722c95a72", + "specId" : "ADR-046-decision-register", + "status" : "Proposed", + "supersedes" : null, + "version" : 1, + "workItemPrefixes" : [ + "decisions" + ] + }, + { + "dependsOn" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "path" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "sha256" : "7dba615dc1c3a7173d1344a16b3ae0247b4b1158f63c7b217592ad60fd50100e", + "specId" : "ADR-046-feasibility-and-spikes", + "status" : "Proposed", + "supersedes" : null, + "version" : 1, + "workItemPrefixes" : [ + "feasibility" + ] + }, + { + "dependsOn" : [ + "ADR-046-core-controllers", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-nix-configuration.md", + "sha256" : "9ff388089c225d43f6bea705ae4fd095ba203f234226aeeb0a91e869c8400160", + "specId" : "ADR-046-nix-configuration", + "status" : "Proposed", + "supersedes" : "Current `nixos-modules/options-realms*.nix`, `options-envs.nix`, `options-vms.nix`, `index.nix`, `bundle*.nix`, `*-json.nix`, and generated `/etc/d2b/*.json`", + "version" : 2, + "workItemPrefixes" : [ + "nix" + ] + }, + { + "dependsOn" : [ + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "path" : "docs/specs/ADR-046-primitive-resource-composition.md", + "sha256" : "917d208ed95aa39b3e4c5c62fa8a8d4a0ee048a11e536a82c71b87875f4f4c50", + "specId" : "ADR-046-primitive-resource-composition", + "status" : "Proposed", + "supersedes" : "Current implementation-shaped ProcessRole composition", + "version" : 1, + "workItemPrefixes" : [ + "primitives" + ] + }, + { + "dependsOn" : [ + "ADR-046-cli-and-operations", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-activation-nixos.md", + "sha256" : "1389b1542c8dd9cf5a8a1744e56d9040bba5645e170652bf3653d56328c4b1d7", + "specId" : "ADR-046-provider-activation-nixos", + "status" : "Proposed", + "supersedes" : "Current `d2b switch`/`boot`/`test`/`rollback`/`build`/`generations`/`gc`/`migrate` top-level verbs in `packages/d2b/src/lib.rs` and hardlink-farm activation in `packages/d2b-host/src/hardlink_farm.rs`", + "version" : 3, + "workItemPrefixes" : [ + "activation" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "sha256" : "ca4f477bb5711ee1e74b3f1261e81b5a17cd15744e619ab7e100cf25a7c4f6e8", + "specId" : "ADR-046-provider-audio-pipewire", + "status" : "Proposed", + "supersedes" : "`nixos-modules/components/audio/host.nix`, `nixos-modules/components/audio/guest.nix`, `packages/d2b-core/src/audio_policy.rs`, `packages/d2bd/src/audio_dispatch.rs`, `packages/d2bd/src/audio_host_controller.rs`, `packages/d2b-host/src/audio_argv.rs`", + "version" : 10, + "workItemPrefixes" : [ + "audio" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "sha256" : "8904a31c92495698b240f55a88e3038e4fbb20ab1035ec3abe58d13191afde06", + "specId" : "ADR-046-provider-clipboard-wayland", + "status" : "Proposed", + "supersedes" : "`nixos-modules/clipboard.nix` (v3 migration), ADR-046-provider-clipboard-wayland v1", + "version" : 2, + "workItemPrefixes" : [ + "clipboard" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-credential-entra.md", + "sha256" : "0d83de1ef4e63c361abe5ce0a3bfde365e019efdfcf8d18b1b1d9b5ab9842fda", + "specId" : "ADR-046-provider-credential-entra", + "status" : "Proposed", + "supersedes" : "No Host login/token chains; no direct `EntraCredentialClient` production egress; no `DefaultAzureCredential`, environment, DBus, browser, or path discovery", + "version" : 2, + "workItemPrefixes" : [ + "cred-entra" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-credential-managed-identity.md", + "sha256" : "a0d61d8fab742709682250d7fb7beb925f79e1fc2344fa6794cc66d3abe77d78", + "specId" : "ADR-046-provider-credential-managed-identity", + "status" : "Proposed", + "supersedes" : "Current v3 `ManagedIdentityRef`, `managed_identity_client_id` ACA config field, `CredentialProvider` trait (status/enrollment-only) in `d2b-realm-provider/src/credential.rs` and `provider.rs`", + "version" : 1, + "workItemPrefixes" : [ + "cred-mi", + "mi-topology" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-credential-secret-service.md", + "sha256" : "a5e2783bc02e74ce629c5922c8b08192f81b0e1c3bdcfe82a004879d56dd8b30", + "specId" : "ADR-046-provider-credential-secret-service", + "status" : "Proposed", + "supersedes" : "v2 `SecretServiceCredentialProvider` / `SecretServiceCredentialProviderFactory` in `d2b-realm-provider`; v2 `CredentialProvider` trait", + "version" : 1, + "workItemPrefixes" : [ + "cred-ss" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-device-gpu.md", + "sha256" : "f6ba2521edb78e7f573fa3c144f257b74f30796bb9ca31badc53ef51a003690d", + "specId" : "ADR-046-provider-device-gpu", + "status" : "Proposed", + "supersedes" : "`ProcessRole::Gpu`, `ProcessRole::GpuRenderNode`, `ProcessRole::Video` in `packages/d2b-core/src/processes.rs`; Nix `nixos-modules/components/graphics.nix`; Nix `nixos-modules/components/video/guest.nix`; `d2b.vms..graphics.*` options", + "version" : 8, + "workItemPrefixes" : [ + "gpu" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "sha256" : "3cf5553f3eb4320ee14d67985be5e629265687328d72f584929e5a0f9869c5c5", + "specId" : "ADR-046-provider-device-security-key", + "status" : "Proposed", + "supersedes" : "`ProcessRole::SecurityKeyFrontend` daemon-internal accept loop (`packages/d2bd/src/security_key.rs`), `nixos-modules/components/security-key-guest.nix` untracked guest `d2b-sk-frontend.service` unit", + "version" : 6, + "workItemPrefixes" : [ + "security-key" + ] + }, + { + "dependsOn" : [ + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume" + ], + "path" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "sha256" : "a9fb0308cae40f091623cb7f8e247510bba17cb4dd528756bd061ad951d725c0", + "specId" : "ADR-046-provider-device-tpm", + "status" : "Proposed", + "supersedes" : "Current `nixos-modules/components/tpm.nix` swtpm provisioning and `d2b-priv-broker` swtpm-dir hardening path", + "version" : 3, + "workItemPrefixes" : [ + "device-tpm" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-network", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support", + "ADR-046-zone-routing" + ], + "path" : "docs/specs/providers/ADR-046-provider-device-usbip.md", + "sha256" : "d4600cee4c450d9ed857155003a61af4c299133eef3a49fbce2940a7315519ee", + "specId" : "ADR-046-provider-device-usbip", + "status" : "Proposed", + "supersedes" : "`nixos-modules/components/usbip.nix` (host-side), per-env usbipd systemd units in `nixos-modules/network.nix`, `ProcessRole::Usbip` / `RunnerRole::Usbip` in current v3 baseline", + "version" : 7, + "workItemPrefixes" : [ + "usbip" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-display-wayland.md", + "sha256" : "fd57e50f657980881cb2e0f3117cdfc317fe9fbc489f7311f5308653a82f3c89", + "specId" : "ADR-046-provider-display-wayland", + "status" : "Proposed", + "supersedes" : "`ProcessRole::WaylandProxy` in `packages/d2b-core/src/processes.rs`; `LocalCrossDomainWaylandProvider` in `packages/d2b-host-providers/src/lib.rs`; `generate_wayland_proxy_argv` in `packages/d2b-host/src/wayland_proxy_argv.rs`; `nixos-modules/components/graphics.nix` `graphics.waylandProxy.*` options; `nixos-modules/ui-colors.nix` VM border color resolution; current `pkgs/wl-cross-domain-proxy` guest binary", + "version" : 2, + "workItemPrefixes" : [ + "display" + ] + }, + { + "dependsOn" : [ + "ADR-046-primitive-resource-composition", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "path" : "docs/specs/ADR-046-provider-model-and-packaging.md", + "sha256" : "2d002c97ddb6237cd2ea0d5d8b07f10678d6a27ee61910bcba6dd47d5743f41b", + "specId" : "ADR-046-provider-model-and-packaging", + "status" : "Proposed", + "supersedes" : "Current direct Provider construction/composition", + "version" : 1, + "workItemPrefixes" : [ + "provider" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-network-local.md", + "sha256" : "e7e351bd27d7ebfb37d4c47917a0e7f17b4c673b3b7e9fc4ad59170a0ca5cfaf", + "specId" : "ADR-046-provider-network-local", + "status" : "Proposed", + "supersedes" : "`nixos-modules/network.nix`, `nixos-modules/net.nix`", + "version" : 3, + "workItemPrefixes" : [ + "nl" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-reconciliation", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-notification-desktop.md", + "sha256" : "3746c29d036e27ea4a7086f6e1f47c5a83b0a54d60973f5e5d3c9e2d4f791bd7", + "specId" : "ADR-046-provider-notification-desktop", + "status" : "Proposed", + "supersedes" : null, + "version" : 2, + "workItemPrefixes" : [ + "notify" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/providers/ADR-046-provider-observability-otel.md", + "sha256" : "ed4262392dc6f775c0d85faa270e4480b47136ff2614485496c5681c859a28ab", + "specId" : "ADR-046-provider-observability-otel", + "status" : "Proposed", + "supersedes" : "`ProcessRole::OtelHostBridge` / `RunnerRole::OtelHostBridge`; socat-based vsock forwarder in `packages/d2b-host/src/otel_host_bridge_argv.rs`; `packages/d2bd/src/otel_host_bridge_readiness.rs`; hand-rolled per-VM `nixos-modules/components/observability/` pipeline (adapted to per-Zone)", + "version" : 3, + "workItemPrefixes" : [ + "otel" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-runtime-azure-container-apps.md", + "sha256" : "6511457a36f7049008eb1a996f0028d5926f6daffcf04e41d782cfb16624c2a4", + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "status" : "Proposed", + "supersedes" : "`packages/d2b-provider-aca/` (`AcaWorkloadProvider`, `GuestControlEndpointProvider`), `AcaRelayTransportConfig`, direct vsock guest-control path", + "version" : 1, + "workItemPrefixes" : [ + "aca" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "path" : "docs/specs/providers/ADR-046-provider-runtime-azure-virtual-machine.md", + "sha256" : "303386a96301cc0b3b3904b8f233263063f7f6b5f3f6c501da277bd51d5ca0c6", + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "status" : "Proposed", + "supersedes" : "Current `InfrastructureProvider` trait (`d2b-realm-provider/src/provider.rs`); `AzureVmForbidden` explicit rejection in `provider_registry.rs`; `AZURE_VM_IMPLEMENTATION_ID` constant; `WorkloadProviderKind::ProviderManaged` paths for Azure VM workloads", + "version" : 3, + "workItemPrefixes" : [ + "azure-vm" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-runtime-cloud-hypervisor.md", + "sha256" : "62f6af217aa398e2f801e30157e3d7f124963893d3120c1ca622cd47776cf92a", + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "status" : "Proposed", + "supersedes" : "`packages/d2b-host/src/runtime_provider.rs` `CloudHypervisorRuntimeProvider`; `packages/d2bd/src/` VM lifecycle paths; `d2b-host-providers` adapter; `ProcessRole::CloudHypervisor`, `ProcessRole::Swtpm`, `ProcessRole::NetVm`; systemd unit `d2b--vm.service`; `SwtpmDir` broker op", + "version" : 1, + "workItemPrefixes" : [ + "ch" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-reconciliation", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "sha256" : "438f37a0baeb88e31f5e432c6e153ee1d086d7edbacbabdda354316a2eb2e8f8", + "specId" : "ADR-046-provider-runtime-qemu-media", + "status" : "Proposed", + "supersedes" : "`docs/adr/0036-qemu-media-runtime.md`", + "version" : 2, + "workItemPrefixes" : [ + "qemu-media" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-credential", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "path" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "sha256" : "2fdfc20da92d0ff065d3c0e15c4d15811b6f045ac1b787045a904b580112d2c9", + "specId" : "ADR-046-provider-shell-terminal", + "status" : "Proposed", + "supersedes" : "v1 of this spec; `guestd/src/shell.rs` guest persistent-shell runtime (ADR 0038); `d2b-unsafe-local-helper` shell supervisor and wire protocol v2 (ADR 0044); `ShellOp`/`ShellOpResponse` seqpacket protocol (`d2b-contracts/src/public_wire.rs:1319,1394,1527`)", + "version" : 2, + "workItemPrefixes" : [ + "sterm" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-provider-state.md", + "sha256" : "dcf1087dfeaff6ad8cb1d5acbc33960a5c1bc692fa1109a95a58e4f4ab7e060f", + "specId" : "ADR-046-provider-state", + "status" : "Proposed", + "supersedes" : "`d2b-core/src/storage.rs` generated contract, ADR 0034 storage/sync/adoption model for v3", + "version" : 1, + "workItemPrefixes" : [ + "pstate" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-system-core.md", + "sha256" : "cf73df14354c8e1d9a709091578a5e1bc90598f2bf8462faf257f7f9f6248ace", + "specId" : "ADR-046-provider-system-core", + "status" : "Proposed", + "supersedes" : "Current `WorkloadProviderKind::UnsafeLocal`, host/User grouping helpers, `HostJson`/`VmRuntimeRow` host-management paths in `d2bd` and `d2b-realm-core`", + "version" : 1, + "workItemPrefixes" : [ + "system-core" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/providers/ADR-046-provider-system-minijail.md", + "sha256" : "2553d0727a0b9e48abf39b7cd2cfb7da33b299e658e49ab84dce4eab36ebf3f3", + "specId" : "ADR-046-provider-system-minijail", + "status" : "Proposed", + "supersedes" : "Current `d2b-priv-broker` SpawnRunner, `d2b-core` minijail profile, and `d2bd` supervisor pidfd/wait paths for minijail-spawned processes", + "version" : 1, + "workItemPrefixes" : [ + "minijail" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-current-code-migration-map", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resources-host-guest-process-user", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-system-systemd.md", + "sha256" : "fb726ed676381c6505247d28540ad131634ef8460f7747c6663ae6b2e8d475d7", + "specId" : "ADR-046-provider-system-systemd", + "status" : "Proposed", + "supersedes" : "Current `d2b-unsafe-local-helper` systemd scope runtime; current `d2bd` `supervisor/` VM process management; broker `SpawnRunner` (systemd-owned roles)", + "version" : 1, + "workItemPrefixes" : [ + "systemd" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-resources-credential", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "path" : "docs/specs/providers/ADR-046-provider-transport-azure-relay.md", + "sha256" : "a4c9e2ac8c1298adaf23baee6092d8f5e8d912d1076479dd73ade0561a6b72ec", + "specId" : "ADR-046-provider-transport-azure-relay", + "status" : "Proposed", + "supersedes" : "`d2b-provider-relay` gateway-display relay path (`AcaWorkloadProvider` + `RelayProvider` traits in `d2b-realm-provider`); `d2b-gateway-runtime/src/bin/d2b-gateway-relay.rs`; `packages/d2b-provider-relay/src/lib.rs` as a first-party transport surface", + "version" : 1, + "workItemPrefixes" : [ + "transport-relay" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "path" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "sha256" : "cff79bd84e16247446d87af97c97218e4bab4e56af6478c4760015b2d447f048", + "specId" : "ADR-046-provider-transport-unix", + "status" : "Proposed", + "supersedes" : "v1 of this dossier (ADR-046-provider-transport-unix v1; incorrect ownership model); `InheritedSocketTransport` SD_LISTEN_FDS path (`packages/d2b-session-unix/src/systemd.rs`)", + "version" : 3, + "workItemPrefixes" : [ + "transport-unix" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-zone-control", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "path" : "docs/specs/providers/ADR-046-provider-transport-vsock.md", + "sha256" : "056326ac85dc5b1077c2a6e9cf66822dafda8249c4523db021a57b4000e2ff13", + "specId" : "ADR-046-provider-transport-vsock", + "status" : "Proposed", + "supersedes" : "`packages/d2bd/src/guest_control_vsock.rs` transport probe (→ allocator-issued endpoint via VsockEffectPort); `packages/d2b-host/src/vsock_relay_argv.rs` socat relay (→ native FramedVsockTransport in Provider service); `NativeVsock`/`CloudHypervisorVsock` in `d2b-session-unix/src/vsock.rs` (→ FramedVsockTransport implementing OwnedTransport adapted as service-Provider transport bridge)", + "version" : 2, + "workItemPrefixes" : [ + "vsock" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "sha256" : "e8bd00b95e141dcf00c1808b04acd42d6f2de80d14cd176593d8eec7bdc8097f", + "specId" : "ADR-046-provider-volume-local", + "status" : "Proposed", + "supersedes" : "`d2b-priv-broker/src/ops/{state_dir,store_sync,store_sync_audit,store_sync_export,store_view_farm,store_view_posture,swtpm_dir}.rs`; `d2b-core/src/{storage,sync,storage_lifecycle}.rs` StorageJson/SyncJson contract; `nixos-modules/store.nix` per-VM hardlink farm activation", + "version" : 2, + "workItemPrefixes" : [ + "vl" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-nix-configuration", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-reconciliation", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-volume", + "ADR-046-telemetry-audit-and-support" + ], + "path" : "docs/specs/providers/ADR-046-provider-volume-virtiofs.md", + "sha256" : "4bbd2516c4c401cb1d8e781e61009b554bf478125458e040d44489fd70744b09", + "specId" : "ADR-046-provider-volume-virtiofs", + "status" : "Proposed", + "supersedes" : "`nixos-modules/processes-json.nix` virtiofsdRunner block; `nixos-modules/minijail-profiles.nix` virtiofsdProfiles; `packages/d2b-host/src/virtiofsd_argv.rs`; `ProcessRole::Virtiofsd` dag nodes in `packages/d2bd/src/supervisor/dag.rs`", + "version" : 2, + "workItemPrefixes" : [ + "vvfs", + "vvfs-export" + ] + }, + { + "dependsOn" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "path" : "docs/specs/ADR-046-reset-and-cutover.md", + "sha256" : "5df1a458e45ca3ac9b91fadddbadbc4bc2fbac371afb7946a2363c7be82674f5", + "specId" : "ADR-046-reset-and-cutover", + "status" : "Proposed", + "supersedes" : "[ADR 0034](../adr/0034-storage-lifecycle-restart-and-synchronization.md) \"Migration decision\" section for the d2b 3.0 cutover; the current `d2b host migrate-storage` verb (retired per `ADR-046-current-code-migration-map` §6 and `ADR-046-cli-and-operations` \"v2 command surface removed at 3.0 clean break\": it served the one-time v1→v2 storage layout cutover only and has no v3 successor)", + "version" : 1, + "workItemPrefixes" : [ + "reset" + ] + }, + { + "dependsOn" : [ + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-resource-api-and-authorization.md", + "sha256" : "2b6a0c11a8c41c9dbd2725b2b6c8586cc0c7c6de65c43a37c11c36cb31e5f6b1", + "specId" : "ADR-046-resource-api-and-authorization", + "status" : "Proposed", + "supersedes" : null, + "version" : 1, + "workItemPrefixes" : [ + "api" + ] + }, + { + "dependsOn" : [ + "ADR-046-decision-register", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-resource-object-model.md", + "sha256" : "a3b2361edee0a19279a81ada3a7ede3ecbc730890df40ea882b787b320e79883", + "specId" : "ADR-046-resource-object-model", + "status" : "Proposed", + "supersedes" : null, + "version" : 1, + "workItemPrefixes" : [ + "object" + ] + }, + { + "dependsOn" : [ + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb" + ], + "path" : "docs/specs/ADR-046-resource-reconciliation.md", + "sha256" : "77b5561822464f15e3f623090026908b4954f5b031f17e5d35dfa054639b5733", + "specId" : "ADR-046-resource-reconciliation", + "status" : "Proposed", + "supersedes" : null, + "version" : 1, + "workItemPrefixes" : [ + "reconcile" + ] + }, + { + "dependsOn" : [ + "ADR-046-resource-object-model", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-resource-store-redb.md", + "sha256" : "9e8d68afc50aafeb6717b498da2790a88e37d9de0715c9a8a187caed620d7380", + "specId" : "ADR-046-resource-store-redb", + "status" : "Proposed", + "supersedes" : null, + "version" : 1, + "workItemPrefixes" : [ + "store" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-resources-credential.md", + "sha256" : "1ebfcf9219269bbdff9478e83e2d0c83055da0f426a3b90c0af145bfaca2cf3a", + "specId" : "ADR-046-resources-credential", + "status" : "Proposed", + "supersedes" : "Current v3 `CredentialProvider` trait, `CredentialStatus`, `CredentialPlane` model in `d2b-realm-provider/src/credential.rs` and `provider.rs`", + "version" : 1, + "workItemPrefixes" : [ + "credential" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "path" : "docs/specs/ADR-046-resources-device.md", + "sha256" : "15f8bd34429076a12dffb897ddb5e3cbf00ae74b90ee597581e343ddcc529181", + "specId" : "ADR-046-resources-device", + "status" : "Proposed", + "supersedes" : "Current ProcessRole device sidecars (Swtpm, SwtpmPreStartFlush, Usbip, SecurityKeyFrontend, Gpu, GpuRenderNode, Video) and their Nix components", + "version" : 1, + "workItemPrefixes" : [ + "device" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-core-controllers", + "ADR-046-decision-register", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "sha256" : "82d27ef45b6b832c9edabd69c0b187374a30de711e0cec15395b4f7595b63273", + "specId" : "ADR-046-resources-host-guest-process-user", + "status" : "Proposed", + "supersedes" : "Current `ProcessRole`/`VmProcessDag` as the public process model; current Realm workload/unsafe-local/execution DTOs; `WorkloadProviderKind`/`IsolationPosture` posture enums", + "version" : 1, + "workItemPrefixes" : [ + "exec", + "user-session" + ] + }, + { + "dependsOn" : [ + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-resources-network.md", + "sha256" : "541c0dbfe2fe00fd35b8dfaac663160f49c4c69662f6c989cbd6977374fec1f1", + "specId" : "ADR-046-resources-network", + "status" : "Proposed", + "supersedes" : "`d2b.envs.` and `d2b.realms..network` Nix surfaces (v3 reset)", + "version" : 2, + "workItemPrefixes" : [ + "network" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation" + ], + "path" : "docs/specs/ADR-046-resources-volume.md", + "sha256" : "57e2534fea5ed3a9a636b772e4f2f206f530dadb8a13e57f00d9304107a882e3", + "specId" : "ADR-046-resources-volume", + "status" : "Proposed", + "supersedes" : "`storage.json` v2, `sync.json` v1, per-VM hardlink-farm and store-view emitters in `nixos-modules/storage-json.nix`, `nixos-modules/store.nix`", + "version" : 1, + "workItemPrefixes" : [ + "volume" + ] + }, + { + "dependsOn" : [ + "ADR-046-core-controllers", + "ADR-046-decision-register", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-resources-zone-control.md", + "sha256" : "6772bd85ab368501826d468b76633d15474175c7b30350c607451b76ac164d8b", + "specId" : "ADR-046-resources-zone-control", + "status" : "Proposed", + "supersedes" : null, + "version" : 1, + "workItemPrefixes" : [ + "client", + "pkg", + "provider-agent", + "wire", + "zone-control" + ] + }, + { + "dependsOn" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-activation-nixos", + "ADR-046-provider-audio-pipewire", + "ADR-046-provider-clipboard-wayland", + "ADR-046-provider-credential-entra", + "ADR-046-provider-credential-managed-identity", + "ADR-046-provider-credential-secret-service", + "ADR-046-provider-device-gpu", + "ADR-046-provider-device-security-key", + "ADR-046-provider-device-tpm", + "ADR-046-provider-device-usbip", + "ADR-046-provider-display-wayland", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-network-local", + "ADR-046-provider-notification-desktop", + "ADR-046-provider-observability-otel", + "ADR-046-provider-runtime-azure-container-apps", + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR-046-provider-runtime-qemu-media", + "ADR-046-provider-shell-terminal", + "ADR-046-provider-state", + "ADR-046-provider-system-core", + "ADR-046-provider-system-minijail", + "ADR-046-provider-system-systemd", + "ADR-046-provider-transport-azure-relay", + "ADR-046-provider-transport-unix", + "ADR-046-provider-transport-vsock", + "ADR-046-provider-volume-local", + "ADR-046-provider-volume-virtiofs", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-resources-zone-control", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "path" : "docs/specs/ADR-046-security-and-threat-model.md", + "sha256" : "58ea74bbda2154e05f5127fdedf0b5ae8f481cc6bc29c4841b3dee77d645c3e2", + "specId" : "ADR-046-security-and-threat-model", + "status" : "Proposed", + "supersedes" : null, + "version" : 1, + "workItemPrefixes" : [ + "security" + ] + }, + { + "dependsOn" : [ + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-state", + "ADR-046-reset-and-cutover", + "ADR-046-resources-volume", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "path" : "docs/specs/ADR-046-streamline.md", + "sha256" : "e8dffc8d7f35457d502cf0836e7956b7f8279e36788e4d391b23accbfdb1c063", + "specId" : "ADR-046-streamline", + "status" : "Proposed", + "supersedes" : null, + "version" : 1, + "workItemPrefixes" : [ + "streamline" + ] + }, + { + "dependsOn" : [ + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-provider-model-and-packaging", + "ADR-046-resource-object-model", + "ADR-046-resource-store-redb", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "sha256" : "6e680e6d87a9370d749c5e1c3d3bf62954039deca2494b45c6ce5b62c75061ba", + "specId" : "ADR-046-telemetry-audit-and-support", + "status" : "Proposed", + "supersedes" : "Current `d2bd` hand-rolled Prometheus registry; current daemon/broker/gateway JSONL audit paths", + "version" : 2, + "workItemPrefixes" : [ + "audit", + "doctor", + "host-posture", + "reuse", + "telem" + ] + }, + { + "dependsOn" : [ + "ADR-046-decision-register" + ], + "path" : "docs/specs/ADR-046-terminology-and-identities.md", + "sha256" : "1039934d58fa0777d8cf8216a0f8742eb46b09786715e1c3eb62ad53789e8531", + "specId" : "ADR-046-terminology-and-identities", + "status" : "Proposed", + "supersedes" : "Public Realm terminology selected by ADR 0043 for d2b v3", + "version" : 1, + "workItemPrefixes" : [ + "identities" + ] + }, + { + "dependsOn" : [ + "ADR-046-cli-and-operations", + "ADR-046-components-processes-and-sandbox", + "ADR-046-componentsession-and-bus", + "ADR-046-core-controllers", + "ADR-046-current-code-migration-map", + "ADR-046-decision-register", + "ADR-046-feasibility-and-spikes", + "ADR-046-nix-configuration", + "ADR-046-primitive-resource-composition", + "ADR-046-provider-activation-nixos", + "ADR-046-provider-audio-pipewire", + "ADR-046-provider-clipboard-wayland", + "ADR-046-provider-credential-entra", + "ADR-046-provider-credential-managed-identity", + "ADR-046-provider-credential-secret-service", + "ADR-046-provider-device-gpu", + "ADR-046-provider-device-security-key", + "ADR-046-provider-device-tpm", + "ADR-046-provider-device-usbip", + "ADR-046-provider-display-wayland", + "ADR-046-provider-model-and-packaging", + "ADR-046-provider-network-local", + "ADR-046-provider-notification-desktop", + "ADR-046-provider-observability-otel", + "ADR-046-provider-runtime-azure-container-apps", + "ADR-046-provider-runtime-azure-virtual-machine", + "ADR-046-provider-runtime-cloud-hypervisor", + "ADR-046-provider-runtime-qemu-media", + "ADR-046-provider-shell-terminal", + "ADR-046-provider-state", + "ADR-046-provider-system-core", + "ADR-046-provider-system-minijail", + "ADR-046-provider-system-systemd", + "ADR-046-provider-transport-azure-relay", + "ADR-046-provider-transport-unix", + "ADR-046-provider-transport-vsock", + "ADR-046-provider-volume-local", + "ADR-046-provider-volume-virtiofs", + "ADR-046-reset-and-cutover", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-resource-store-redb", + "ADR-046-resources-credential", + "ADR-046-resources-device", + "ADR-046-resources-host-guest-process-user", + "ADR-046-resources-network", + "ADR-046-resources-volume", + "ADR-046-resources-zone-control", + "ADR-046-security-and-threat-model", + "ADR-046-streamline", + "ADR-046-telemetry-audit-and-support", + "ADR-046-terminology-and-identities", + "ADR-046-zone-routing" + ], + "path" : "docs/specs/ADR-046-validation-and-delivery.md", + "sha256" : "4f60fd968f973bb029ca5189a110c0d64fb6fe88a2cbf932e4af8cc06d152091", + "specId" : "ADR-046-validation-and-delivery", + "status" : "Proposed", + "supersedes" : "This repository's current `AGENTS.md` \"Panel review\" phase-gate as the *sole* review mechanism for ADR 0046 work (extended, not replaced, per §12); ad hoc per-agent validation ordering for ADR 0046 implementation", + "version" : 1, + "workItemPrefixes" : [ + "delivery" + ] + }, + { + "dependsOn" : [ + "ADR-046-componentsession-and-bus", + "ADR-046-resource-api-and-authorization", + "ADR-046-resource-object-model", + "ADR-046-resource-reconciliation", + "ADR-046-terminology-and-identities" + ], + "path" : "docs/specs/ADR-046-zone-routing.md", + "sha256" : "c2434f0a8f9b7ecadeb2d7b700096bc40f71da6061547b19d0c59818af7ab13d", + "specId" : "ADR-046-zone-routing", + "status" : "Proposed", + "supersedes" : "`RealmEntrypointTable`/`EntrypointMode`/`RealmControllerPlacement` enum (→ compiler-only `parentZone` topology + ZoneLink transport + private Zone runtime bootstrap placement); `RouteTreeEngine`/`routing.rs` route contracts (adapted, not deleted); `RemoteNodeRegistry` (→ ZoneLink controller); `OperationRouter` (→ ZoneLinkIdempotencyKey in d2b-bus); `realm_access_resolver` module (→ ZoneEntrypointResolver); `realm-controllers.json`/`realm-identity.json` bundle artifacts (→ Nix-authored Zone settings/ZoneLink resources); `d2b.realms.*` Nix option namespace (→ `d2b.zones.*`); `WorkloadId` routing targets (→ typed Guest/Host resource refs); `CapabilitySet`-only routing authz (→ per-hop RBAC narrowing + allocator-issued capability scope)", + "version" : 1, + "workItemPrefixes" : [ + "routing" + ] + } + ], + "parent" : "docs/adr/0046-d2b-3-provider-control-plane.md", + "schemaVersion" : 3, + "status" : "Proposed" +} diff --git a/docs/specs/ADR-046-streamline.md b/docs/specs/ADR-046-streamline.md new file mode 100644 index 000000000..9f2d65228 --- /dev/null +++ b/docs/specs/ADR-046-streamline.md @@ -0,0 +1,1019 @@ +# ADR 0046 streamline contract + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-streamline` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | ADR 0046 integrator; `packages/xtask` owner; `packages/d2b-contract-tests` policy-lint owner | +| Depends on | `ADR-046-decision-register`, `ADR-046-provider-model-and-packaging`, `ADR-046-provider-state`, `ADR-046-primitive-resource-composition`, `ADR-046-resources-volume`, `ADR-046-zone-routing`, `ADR-046-current-code-migration-map`, `ADR-046-terminology-and-identities`, `ADR-046-reset-and-cutover` | +| Supersedes | None | + +## Purpose + +This spec is the normative contract for the tooling and process changes that +close the authoring friction actually observed while producing the +`docs/specs/ADR-046-*` set. It defines no new Zone/Provider/resource runtime +behavior. Every improvement below traces to a specific, citable repository +artifact: a commit, a duplicated diff, a decision-register entry that exists +because an invented field had to be walked back, or a measured repetition +count across the committed dossiers. No item in this spec is speculative or +grounded in general best practice; an item that cannot be tied to one of the +evidence citations in [Observed friction evidence](#observed-friction-evidence) +does not belong here and was deliberately left out. + +This is a documentation-only contract. It defines future +`packages/xtask`, `packages/d2b-contract-tests`, `tests/tools/`, and +`AGENTS.md`/`tests/AGENTS.md` changes as implementation work items per +[Implementation work items](#implementation-work-items). It does not itself +add a build, script, or test — that requires the separate implementation +request every other ADR 0046 work item requires per D024. + +**Scope boundary.** This spec's tooling is a meta-process artifact for +authoring and delivering the ADR 0046 spec set and its future W0-W10 +implementation work items. It is unrelated to, and does not reintroduce, any +v3 Zone/Provider runtime architecture excluded elsewhere in the set — in +particular `ADR-046-zone-routing`'s explicit exclusion of "ADR45 delivery +seals (xtask delivery wave / panel / seal process)" from *runtime* reuse is +about Zone socket/spawn/credential architecture, not about this repository's +own authoring workflow. Only the external delivery-tooling *shape* (generated +command indexes, panel-record schemas, disk/worktree hygiene, anti-serialization +reporting) is evaluated for reuse here, and only as process tooling that ships +outside the Zone runtime. + +## Ordering: final wave, with explicit shared prep + +Per `docs/specs/README.md` "Parallel authoring" and the parent ADR's +"Foundation specs are authored first... all ready file-disjoint... specs are +authored in parallel", tooling that changes how specs are *written* must not +sit downstream of the specs it is meant to fix. Per this repository's own +`AGENTS.md` "Integrator-prep-first pattern (W3 onwards)", the same rule that +governs code waves applies here: prep that a wave's downstream work depends on +lands before that wave opens, not after. + +This spec therefore splits its work items into two tranches, not one flat +"implementation wave": + +- **Tier A — shared prep.** Items that operate purely on already-committed + Markdown (schemas, extractors, lints, generators, scaffolds, process/ + reporting tooling) carry no risk to in-flight spec content: they are + additive, read-only over the tree, and require no unresolved decision. These + land now, before the ADR 0046 documentation set's own pre-panel gate (D014), + because every Tier A item's underlying friction already occurred during + documentation authoring — waiting for a future implementation wave would let + the same friction recur in the remaining spec-set iteration and in the + Provider dossiers that still receive corrections. +- **Tier B — the streamline wave.** Items that can only run meaningfully + against real Rust crates, real controller processes, or real generated + code — none of which exist yet, since D024 confirms ADR 0046 has delivered + documentation only — are deferred. This tier is the actual "implementation + wave after all product waves": it lands only after every ResourceType, + core-controller, and Provider dossier work item across the W0-W10 + implementation range reaches its own Validation-complete state (per each + spec's own "Implementation work items" acceptance rows) and the + `ADR-046-reset-and-cutover` cutover engine is integrated, immediately before + that implementation delivery's own pre-panel gate. + +No Tier A item blocks, gates, or gains veto power over any dossier or +foundation spec's own authoring; Tier A tooling only reports, generates, and +validates. No Tier B item is scheduled ahead of the product code it depends +on. This is the same anti-serialization posture this repository's Panel +review / stacked-PR sections already apply to code waves, extended to the +documentation-authoring tooling that governs this ADR's own delivery. + +## Observed friction evidence + +Every citation below is a commit SHA, file path, or line count already in this +repository's history (`git log --all`) or the committed `docs/specs/` tree, no +session transcript or tool/agent attribution. Counts were reproduced with +`git log --all --oneline --grep=... -i | wc -l`, `git rev-list --count`, and +`grep -c` against the exact tree state. + +### F1 — Dossier branches authored against stale foundation commits + +Every per-dossier Provider branch that exists in this repository diverges from +the shared foundation at the exact same commit, +`3ca4daf598ee3e33084bccd4cc5497ee78d87bac` ("docs: define d2b 3.0 Network +resources") — confirmed by `git merge-base +adr0046-provider-resource-control-plane` returning that SHA for +`adr0046-provider-device-gpu`, `adr0046-provider-volume-virtiofs`, +`adr0046-provider-volume-local`, `adr0046-provider-transport-vsock`, and +`adr0046-provider-runtime-azure-container-apps`. The integrator branch kept +moving after that point through `f76787b5` ("system-core and virtiofs Provider +dossiers"), `c8f47656` ("normalize ADR 0046 cleanup contracts"), `038b4169`, +`b63ce675`, `3b21a7cf`, and the two foundation-harmonize commits +`38ab1d4cd3697df19765d9a238764d98ff562b0d` / +`dfea59933251fbfb13eb70613642f5215bbc1f74` — an identical-diff pair (same +parent `b4e0d6c7`, same tree, same author timestamp, two different commit +objects on two different branches) that reconciles Provider `ResourceSpec`, +`ProviderStateSet`, and bootstrap-exception wording the stale dossier branches +had already diverged from. Every dossier branch therefore had to receive a +second, independent correction pass instead of restacking cleanly onto one +prep commit. This is the exact failure mode this repository's own +`AGENTS.md` "Integrator-prep-first pattern (W3 onwards)" section exists to +prevent for code waves; it was not applied to this documentation wave. + +### F2 — Invented Process/Volume fields later rejected + +The decision register records the rejection of specific invented fields as +normative corrections, not hypothetical risk: + +- `ADR-046-decision-register` D075: "Canonical Provider ResourceSpec is + exactly `{ artifactId; config }`" — the corresponding foundation-harmonize + commits (`38ab1d4c`/`dfea5993`) exist because earlier dossier text (per + `git log --all -S"rootConfig" -- docs/specs`, 15+ matching commits) carried + an authored `rootConfig`/manifest/package/status field set that had to be + collapsed back to `artifactId`/`config` across every dossier. +- `2af904efdee589454d7b6ee1353acf46db06549b` ("fix readiness fields, + sourcePolicyId enum, Volume quota structure, mount required ( W1 )") and + `367eb867ccc85eb9f5e5946cb84e1c479428313e` ("fix environmentClass enum, + readiness.probe, Volume schema/quota regressions ( W1 )") each retract an + invented custom `readiness`/`restart`/budget shape back to the common + status/ExecutionPolicy fields the foundation already defined. +- `200aaa2d5ea7e9854d4c41ca396f6b92f001e618` ("empty/read-only state Volumes, + sourcePolicyId, required mounts ( W0 )") retracts an invented optional-mount + shape back to `mounts` being `required: true` for state Volumes. +- D079 exists because dossier text had authored numeric `hostUid`/`hostGid` + fields directly on the public `SandboxSpec` (`git log --all -S"hostUid" -- + docs/specs` and `-S"hostGid"` both match `3b21a7cf`, `38ab1d4c`, `dfea5993`, + `158a019c`, `42383089`, `e112a9a2`, `b4da8460`); D079 replaces them with the + frozen semantic `mappingClass: process-principal-root` field precisely + because the numeric fields leaked host identity into a public spec. + +### F3 — ProviderStateSet representation and bootstrap-cycle confusion + +`ProviderStateSet` churned through at least four distinct representations +before D076/D086 froze it: a `ResourceType`, a stored row, a non-Volume +"compartment" concept, and finally "the logical, query-time grouping of +ordinary Volume resources owned by a Provider... never a ResourceType or a +stored row." Evidence: `52a43339` ("correct ProviderStateSet to +framework-created Volume with provider-state extension schema"), +`f00696d1` ("correct ProviderStateSet to ordinary Volume resources with +provider-state extension schema ( W1 )"), `de303739` ("correct +ProviderStateSet representation in ADR-046 virtiofs dossier ( W3 )"), +`3ff0fa9c` ("fix ProviderStateSet representation in +ADR-046-provider-shell-terminal ( W0 )"), and `bd0e9545`/`2755448d`/`bac0c9d7` +("align Provider state contracts" / "tighten Provider state quota policy"). +`git log --all --oneline -S"ProviderStateSet" -- docs/specs | wc -l` returns +20+ distinct commits correcting the same concept across separate dossiers. +The bootstrap-cycle confusion (which component may create its own state +Volume before any `volume-local` controller exists to create it) produced a +parallel churn: `5c287f51` ("add bootstrap-state exception to volume-local +dossier"), `7632ebec` ("add bootstrap-state exception to +Provider/system-minijail dossier"), and `24598e5c` ("scope bootstrap exception +to volume-local controller per execution domain") before D086 froze the +single per-execution-target, non-resource, closed exception. D087 later +removed that exception entirely by making resource `status`/the core Operation +ledger the default state surface and Provider state Volumes optional, so no +bootstrap state Volume and no bootstrap-storage mechanism remain. + +### F4 — One-resource/one-controller Volume-vs-virtiofs ownership conflict + +`a7c0ceebbddca8e98482bba3e2231a5ed3cb19ed` ("docs(specs): redesign +volume-virtiofs dossier around Export ResourceType") is titled +"Controller-ownership bug fix and full architectural realignment: One +resource instance has one controller (Volume.providerRef=volume-local). +volume-virtiofs cannot reconcile or write Volume resources." This followed an +earlier design where `volume-virtiofs` wrote Volume rows directly, violating +the single-owner-controller invariant the resource object model already +required. `bf8c7c04` ("correct Volume ownership — Core ProviderDeployment +creates/deletes, not controller"), `dc3f3349` ("core ProviderDeployment owns +state Volume lifecycle, not transport controller"), and `d4677af5` ("Volume +lifecycle owned by core ProviderDeployment, not controller") show the same +one-owner violation recurring across three different Provider families before +D083 froze `volume-local` as sole Volume-row owner and `volume-virtiofs` as an +attachment-status-only Provider. + +### F5 — Transport Providers incorrectly owning ZoneLink + +D081 ("Transport Providers are carriage services only; they never own +ZoneLink") exists because earlier transport dossier text had the Provider +itself reading/writing/finalizing `ZoneLink`. Evidence: +`f2ed0c93` ("ZoneLink FD-attachment prohibition; add route_class to +OpenTransport ( W1 )"), `14590cc5` ("rewrite transport-unix Provider dossier; +correct ownership model ( W1 )"), `94746f20` ("rewrite Provider +transport-vsock dossier as service carriage Provider ( W0 )"), and +`d5944e67`/`6799e193` ("final alignment corrections to transport-azure-relay +dossier ( W1 )" / "apply contract corrections to transport-azure-relay +dossier"). Four of the five frozen transport/observability/activation +Providers (D049) each required a dedicated ownership-correction commit before +converging on the core-owns-ZoneLink / Provider-returns-opaque-handle split. + +### F6 — Direct-broker boundary drift + +D077 ("No Provider process imports/calls the broker... Providers validate/ +decide semantics and call injected async typed EffectPort traits") exists +because dossier text repeatedly drifted toward direct broker/syscall/ +filesystem access before landing on the EffectPort adapter boundary. The two +foundation-harmonize commits (`38ab1d4c`/`dfea5993`) and the `ACA EffectPort +traits in d2b-contracts provider-effects` commit (`54ee8df4`) exist because +individual Provider dossiers (ACA, GPU, TPM, minijail) had each independently +proposed a Provider-owned broker call site before the shared EffectPort +pattern was frozen once and propagated. + +### F7 — Qualified ResourceType grammar drift + +D080 froze the single qualification grammar `.d2bus.org.` +for Provider-specific ResourceTypes. Before that freeze, dossiers used +inconsistent forms; `git log --all --oneline -S"qualified" -- docs/specs` +shows the grammar question surfacing independently in +`caa74ca0` ("display-wayland dossier v6 cross-dossier corrections"), +`36592596` ("major scope/schema rewrite of activation-nixos dossier"), and +`83e623d7` ("add credential-managed-identity Provider dossier") before the +foundation-harmonize commits fixed the grammar as one frozen decision (D080) +referenced by every later dossier instead of re-derived per dossier. + +### F8 — Open-question leakage before the shared decision register + +Individual specs invented their own per-spec decision-tracking ID prefixes +instead of using one canonical register from the start: +`b1db144a32894ed7f3edbfd7dcb864d9d89fc6e1` ("resolve all 30 DR-NC items in +ADR-046-nix-configuration ( W0 )") used prefix `DR-NC-###`; +`d20e229fbf323eee6de7173095b53d0b317b94f1` ("resolve D-NETWORK-001/002/003 in +ADR-046-resources-network ( W0 )") used prefix `D-NETWORK-###`; +`b38cd984b6d42dacd6a951941b829081b6296cde` ("resolve DR-CLI-001..005 ( W1 )") +used prefix `DR-CLI-###`. Three different naming schemes (`DR-NC-`, +`D-NETWORK-`, `DR-CLI-`) for the same "decision-required" concept existed +simultaneously across three specs before consolidation into the single +`ADR-046-decision-register` D0xx numbering the README's "Decision-required +protocol" now mandates. `ADR-046-decision-register.md`'s own "Open decisions" +section ("No unresolved foundation decision is currently recorded") is the +end state; the 30+3+5 per-spec IDs that had to be resolved and folded in are +the friction this closes. + +### F9 — `sourcePolicy`/host-path drift + +D082 froze `sourcePolicyId` as the only way a Volume `source.settings` +references a Provider-declared host-path allowlist entry, never a raw path. +`git log --all --oneline -S"sourcePolicyId" -- docs/specs | wc -l` returns 33 +matching commits; `200aaa2d`, `2af904ef`, `715ef273` ("AttachmentSpec +executionRef, read-only state views/mounts, sourcePolicyId, per-component +Volumes"), `dbc92230` ("remove SSH path; add Volume base quota fields"), and +`7ea8970b` ("activation-nixos: source.settings sourcePolicyId; dual quota; +reset destroys Volume") each independently reintroduce and then retract a +raw-path or SSH-path field on Volume `source.settings` before D082 froze the +opaque-ID indirection as the one normative form. + +### F10 — Empty/no-SHA handoffs forcing duplicate reconciliation + +`38ab1d4cd3697df19765d9a238764d98ff562b0d` and +`dfea59933251fbfb13eb70613642f5215bbc1f74` are byte-identical patches (same +parent `b4e0d6c7d01ef43338229a9e19e8cd98c6f057f1`, same tree, same author +timestamp) landed independently on two different branches +(`adr0046-foundation-harmonize` and the integrator branch). Likewise +`05fefffe3e434f0fa00699ebbc36f90479e0eb1c` and +`0150475f211367a74de359157b2f42960b114e53` ("harmonize Provider dossiers") are +an identical 24-file, 588-insertion/402-deletion diff landed independently on +two branches. Neither pair carries a cross-reference to the other commit's +SHA. A handoff record binding an assigned file set, a base SHA, and a test/ +validation result to each corrective pass would have let the second landing +reuse the first's result instead of independently re-deriving and +re-committing the same 24-file reconciliation. + +### F11 — Correction commits after branches diverged, creating avoidable conflicts + +Of the 2,478 commits reachable from any `adr0046-*` branch, +`git log --all --oneline --grep="fix\|correct\|realign\|harmonize" -i | wc -l` +returns 768 (≈31%). Restricted to one representative dossier branch, +`adr0046-provider-device-gpu` has 12 commits since its divergence point, of +which `8be36a63`, `e112a9a2`, `1ac3b1d3`, `d884c272`, and `4e82fefe` (5 of 12, +≈42%) are corrective. Because the correcting commits landed on the same +already-diverged branch rather than through a rebase onto a reconciled +foundation commit, later Nix-authoring-alignment sweeps (`827e874f`/`6bcb60b3` +"align Nix authoring contracts") had to re-touch files the corrective commits +had already changed, which is exactly the shape of an avoidable cherry-pick/ +rebase conflict this spec's [ADR046-streamline-010](#adr046-streamline-010--stale-basecurrent-parent-reconcile-helper) +targets. + +### F12 — Manual global `rg`-shaped scans instead of generated lints + +`05fefffe`/`0150475f` ("harmonize Provider dossiers") each touch 24 files (588 +insertions, 402 deletions) in one commit — a full-tree sweep, not a targeted +per-dossier fix. `a7c0ceeb`'s "full architectural realignment" is the same +shape. No `packages/d2b-contract-tests/tests/policy_*.rs`-equivalent lint +exists over `docs/specs/` today (`packages/d2b-contract-tests/tests/` +contains `policy_docs.rs` and 20+ other `policy_*.rs` files, none of which +scan `docs/specs/providers/*.md` for the vocabulary/ownership/finalizer/ +phase/source-policy invariants this ADR's own decision register freezes). +Every one of these sweeps had to be re-derived by reading every dossier file +rather than run as one generated check. + +### F13 — Repetitive per-dossier state-Volume boilerplate + +`grep -l "ProviderStateSet" docs/specs/providers/*.md | wc -l` returns 27 (all +committed Provider dossiers). `grep -c +"persistenceClass\|stateSchema\|sensitivityClass" docs/specs/providers/*.md` +sums to 314 matching lines across those 27 files (`docs/specs/providers/` +totals 44,006 lines). The same canonical state-Volume field block — +`kind: state`, `persistenceClass: persistent`, nonzero `quotaBytes`/ +`maxBytes`/`maxInodes`, `identityMarker`, `sourcePolicyId`, `mounts` with +`required: true` — is hand-authored in each dossier rather than emitted from +one canonical snippet, which is also why F2/F3/F9's per-dossier drift was +possible: each hand-copy was an independent chance to diverge. + +### F14 — Inability to validate Markdown-embedded YAML against canonical schemas + +Every ResourceType/Provider spec's "Nix authoring and configuration cleanup" +section embeds fenced YAML/JSON `ResourceSpec` examples (README.md +"Required metadata" + "Nix authoring" sections). No tool extracts and +schema-validates those fenced blocks today; every field-name/shape drift in +F2/F3/F9 was caught by a human reviewer reading prose, not by a validator +comparing the fenced example against `ADR-046-decision-register` D057/D058's +canonical rendering rules. + +### F15 — Current-source old Realm/Workload terminology confusing target authors + +`ADR-046-terminology-and-identities.md` and `ADR-046-current-code-migration-map.md` +exist specifically to map old `Realm`/`Workload` vocabulary (still the live +vocabulary in this repository's own root `AGENTS.md`, e.g. "daemon-only +control plane", `d2b-realm-core`, `d2b-realm-router`) onto new `Zone`/`Guest` +vocabulary. D050 ("Rename the non-host execution parent from Workload to +Guest and add Host as a separate ResourceType") is itself evidence that an +early dossier round used `Workload` as the target-side name before the +rename; `9eba2987` ("add guest-local and host-backed-guest placement modes +for Guest state") and `af7a33fe` ("specify Zone, ZoneLink, Provider, Role, +and RoleBinding ResourceTypes") both post-date the D050 rename and had to +re-terminology-check every Guest-state reference against it. + +### F16 — Tracking database not automatically bound to the git tree + +This ADR's authoring session state (todo/task tracking) is not itself part of +the committed tree and carries no enforced binding to a base SHA, branch, or +commit range; the only cross-check available today is manual comparison +between a plan and `git log`. `ADR-046-decision-register`'s own +"Implementation work items" section (`ADR046-decisions-001`) has no such +binding either, and no other spec's work-item table declares one. + +### F17 — Docs-only tier0 catches syntax but not schema/cross-link/work-item consistency + +`tests/tools/tier0-first-pass.sh` runs `bash -n` and `shellcheck` over +`tests/`, `scripts/`, and `harness/ubuntu/*.sh` only (confirmed by reading the +script: "Pure host-local checks only: bash -n on tracked shell scripts... +shellcheck... Intentionally excludes nix eval, cargo fmt/clippy/test"). None +of the churn in F1-F14 is shell-script content; `make check-tier0` passes +unconditionally for any `docs/specs/*.md` change, including every one of the +768 correction commits counted in F11, because there is no fast documentation- +schema/cross-link/work-item-ID gate in the tier0 tranche. + +## Metrics + +Every metric below is derived from artifacts this spec's Tier A tooling +produces (registry, lints, handoff manifests, task-DB import) so it can be +computed mechanically, not estimated. + +| Metric | Definition | Source | Current baseline (this evidence ledger) | +| --- | --- | --- | --- | +| Correction rounds | Count of commits matching `fix\|correct\|realign\|harmonize` (case-insensitive) reachable from any `adr0046-*` branch, divided by total commits in the same set | `git log --all --oneline --grep=... -i \| wc -l` over `git rev-list --all --count` restricted to `adr0046-*` refs | 768 / 2,478 ≈ 31% (see F11) | +| Schema violations | Count of fenced-YAML/JSON blocks under `docs/specs/**` failing [ADR046-streamline-003](#adr046-streamline-003--markdown-fenced-yamljson-extractor-and-schema-validator) validation, per commit | `xtask spec-schema-check --format json` violation count | Not measured before this tool exists (F14); target 0 at every commit once adopted | +| Conflict count | Count of `git rebase`/`git town sync` conflict hunks reported by [ADR046-streamline-010](#adr046-streamline-010--stale-basecurrent-parent-reconcile-helper) per dossier restack | Reconcile-helper JSON report `conflicts[]` length | Not measured before this tool exists; F1/F11 show at least 5 dossier branches diverged from a foundation point superseded by 6+ later foundation commits | +| Handoff completeness | Fraction of integrator-merged commits whose commit trailer references a [ADR046-streamline-011](#adr046-streamline-011--agent-handoff-manifest) manifest with non-empty `assigned_files`, `commit_sha`, `test_result`, and `base_sha` | Task-DB↔git import report | 0% before adoption (F10: the two identical-diff commit pairs carry no such trailer) | +| Ready/launched ratio | Count of todo-tracked scopes with all dependencies `done` ("ready"), divided by count of scopes with an open worktree/branch ("launched"), per [ADR046-streamline-013](#adr046-streamline-013--anti-serialization-readylaunchedblocker-report) | Anti-serialization report | Not measured before this tool exists | +| Time-to-green | Wall-clock time from a dossier branch's first commit to its `xtask spec-schema-check` + cross-spec lint passing with zero violations | Task-DB timestamps + lint pass timestamp | Not measured before this tool exists; F1/F11 imply multiple hours-to-days of re-derivation per affected dossier | +| Disk usage | Aggregate size of `packages/target/` and stale worktrees reported by [ADR046-streamline-018](#adr046-streamline-018--worktreedisktarget-cleanup-reporting), never auto-deleted | `du -sh` per reported path, summed | Not measured before this tool exists | + +## Permanent methodology after Accepted + +Once ADR 0046 reaches Accepted, the following graduate from this spec into +permanent, repository-wide methodology rather than remaining a one-time +cleanup: + +| Item | Destination after Accepted | Rationale | +| --- | --- | --- | +| ADR046-streamline-001 (spec registry/graph) | New `AGENTS.md` "ADR spec-set tooling" subsection referencing `xtask spec-registry` | Every future ADR-scale spec set (not only ADR 0046) benefits from a generated dependency graph instead of manifest hand-maintenance | +| ADR046-streamline-003 (Markdown YAML/JSON extractor+validator) | `tests/AGENTS.md` Layer-1 type 5 (policy lint) entry `policy_spec_schema.rs`; permanent CI gate | Directly closes F14; becomes as permanent as any other drift gate | +| ADR046-streamline-004 (dossier scaffold/template) | `docs/specs/providers/TEMPLATE.md` plus `xtask new-provider-dossier`; referenced from `docs/specs/README.md` "Required metadata" | Every future Provider dossier (not only the 27 already committed) starts from the corrected shape, closing F13 permanently | +| ADR046-streamline-005 (cross-spec vocabulary/ownership lint) | `packages/d2b-contract-tests/tests/policy_spec_vocabulary.rs`, permanent | Directly closes F2/F5/F6/F7/F9; must never regress once frozen | +| ADR046-streamline-008 (work-item schema/ID/dependency validator) | `packages/d2b-contract-tests/tests/policy_work_items.rs`, permanent | Every future spec's work-item table is validated the same way, closing F16's cross-check gap | +| ADR046-streamline-009 (Provider catalog/index generator) | `docs/specs/ADR-046-provider-catalog.md` (generated, committed), regenerated by drift gate | Same status as any other generated-artifact drift gate in `tests/unit/gates/` | +| ADR046-streamline-010 (stale-base reconcile helper) | `AGENTS.md` "Integrator-prep-first pattern" section, extended with a `git town`-based dossier-restack subsection | F1/F11 show this is not ADR-0046-specific; any future stacked documentation wave needs it | +| ADR046-streamline-011 (agent handoff manifest) | `AGENTS.md` "Worktrees for parallel agents" section, extended with a mandatory handoff-manifest requirement | F10 is a general parallel-authoring risk, not unique to this ADR | +| ADR046-streamline-012 (task DB↔git import) | `AGENTS.md`, new short subsection cross-referencing the session DB schema | Closes F16 permanently for any future plan-driven wave | +| ADR046-streamline-013 (anti-serialization report) | `AGENTS.md`, folded into "Stacked PR workflow for large waves" as a required per-round report | Generalizes the existing informal rule into a checked artifact | +| ADR046-streamline-016 (pre-panel zero-open-decision gate) | `AGENTS.md` "Panel review" phase-gate preconditions | Closes F8 permanently: no future ADR-scale spec set reaches its panel with leaked per-spec decision-ID prefixes | +| ADR046-streamline-018 (worktree/disk/target cleanup reporting) | `AGENTS.md` "Disk hygiene contract"-equivalent section for this repository | Same operational need this repository's sibling documents already recognize for code waves | +| ADR046-streamline-019 (terminology helper) | `docs/reference/` how-to entry plus `xtask terminology-check` | F15 recurs whenever new dossiers reference current-source Realm/Workload evidence | + +Tier B items (ADR046-streamline-002, -006, -007, -014, -015, -017) become +permanent only once their owning implementation crates exist and their tests +are wired into the taxonomy `tests/AGENTS.md` defines (types 2-5 as +applicable); they are listed under [Implementation work items](#implementation-work-items) +with their eventual test-type destination, not promoted early. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `tests/tools/tier0-first-pass.sh` (shell-only tier0 gate); `packages/xtask/src/main.rs` `gen-schemas`/`gen-cli-schemas`/`gen-error-codes`/`gen-daemon-api` generator pattern; `packages/d2b-contract-tests/tests/policy_*.rs` (20+ existing policy lints, none over `docs/specs/`); `tests/tools/gen-migration-ledger.sh` (existing ledger-generation precedent); this repository's own `AGENTS.md` "Stacked PR workflow for large waves" and "Integrator-prep-first pattern (W3 onwards)" sections (precedent for prep-before-wave, not yet applied to this documentation wave, per F1) | +| Evidence class | The `xtask gen-*`/`policy_*.rs`/ledger-generation patterns are `implemented-and-reachable` for code artifacts today; extending the same pattern to `docs/specs/**` Markdown is `ADR-only` until the work items below land | +| Behavior retained | Generated-artifact-plus-drift-gate shape (`xtask gen-* + git diff --exit-code`); policy-lint-as-Rust-test shape; ledger-file-as-source-of-truth shape; explicit non-destructive cleanup reporting posture already required for code (`packages/d2bd` storage lifecycle "never a broad `/run/d2b` sweep") is extended here to worktree/disk cleanup | +| Required delta | No current tool reads `docs/specs/**` Markdown as structured data; no generated spec registry/graph exists; no Provider-dossier scaffold exists (every one of the 27 committed dossiers was hand-authored from scratch, which is why F13's boilerplate exists) | +| Reuse path | Extend `packages/xtask/src/main.rs`'s existing `gen-*` dispatch table with new `spec-registry`, `gen-provider-catalog`, and `spec-schema-check` subcommands using the same `run_task`/drift-gate wiring already proven for `gen-schemas`; extend `packages/d2b-contract-tests/tests/policy_*.rs` with new `policy_spec_*.rs` files using the same test-harness pattern the existing 20+ policy lints use | +| Replacement/deletion | Nothing is replaced; `tests/tools/tier0-first-pass.sh` gains an additional fast Markdown-schema pre-check alongside its existing `bash -n`/shellcheck checks rather than being rewritten (closes F17 additively) | +| Feasibility proof | Every Tier A item operates on data that already exists in the committed tree (the 27 dossiers, the decision register, the work-item tables); a spike is unnecessary because the extraction/validation inputs are fixed Markdown tables and fenced blocks with a documented shape (README.md "Required metadata", "Nix authoring", "Implementation work items") | +| Future owner | Work items below | + +## Tests + +Every test type below follows the taxonomy `tests/AGENTS.md` defines; this +spec introduces no new top-level `tests/*.sh` gate. + +### Type 1 — eval cases (`tests/unit/nix/cases/`) + +Not applicable: this spec's tooling operates on `docs/specs/**` Markdown and +Rust, not NixOS module evaluation. + +### Type 2 — unit tests (`packages//src/**`) + +| Test | Asserts | +| --- | --- | +| `spec_registry::parse_metadata_table` | Every `docs/specs/ADR-046-*.md` and `docs/specs/providers/ADR-046-*.md` file's leading metadata table parses to the exact `Spec ID`/`Parent`/`Status`/`Version`/`Baseline`/`Owners`/`Depends on`/`Supersedes` fields README.md requires; a malformed table is a parse error naming the offending file/row | +| `spec_registry::acyclic_dependency_graph` | The `Depends on` edges across every parsed spec form a DAG; an introduced cycle fails with the exact cycle path | +| `spec_schema_check::extract_fenced_blocks` | Every fenced ` ```yaml `/` ```json ` block under a "Nix authoring and configuration cleanup" heading is extracted with its owning spec/section location preserved for error messages | +| `spec_schema_check::canonical_field_set` | A fenced ResourceSpec example whose top-level `spec` field name is not in the canonical `ResourceTypeSchema` for its declared `type` fails with the exact unknown field name (regression test seeded from the F2 `rootConfig`/`hostUid`/`hostGid` examples) | +| `work_item_validator::unique_ids` | Every `ADR046--` work item ID across the whole tree is unique; a duplicate fails naming both locations | +| `work_item_validator::required_fields` | Every work item table has all eleven README.md-required fields (or this spec's nine bespoke tooling fields) non-empty and non-placeholder (`TBD`, `TODO`, `N/A` without justification text fail) | +| `decision_id_validator::single_prefix` | Every "decision-required" marker anywhere under `docs/specs/**` uses the `D0xx`/`DR0xx`-equivalent single canonical prefix this spec fixes, not a per-spec prefix (regression test seeded from the F8 `DR-NC-`/`D-NETWORK-`/`DR-CLI-` examples) | +| `provider_catalog::frozen_family_membership` | Every dossier under `docs/specs/providers/` maps to exactly one D043-D049 frozen Provider family; an unmapped or duplicate-mapped dossier fails | + +### Type 3 — integration tests (`packages//tests/*.rs`) + +| Test | Asserts | +| --- | --- | +| `xtask_spec_registry_regenerates_clean` | Running `cargo run -p xtask -- spec-registry` twice in a row produces byte-identical output (determinism) | +| `xtask_new_provider_dossier_scaffold` | `cargo run -p xtask -- new-provider-dossier --name ` emits a dossier skeleton whose metadata table, "Nix authoring" section, `src`/`tests`/`integration`/`README.md` work-item rows, and "Implementation work items" heading are all present and pass `spec_schema_check`/`work_item_validator` on first generation | +| `reconcile_helper_reports_stale_base` | Given two fixture branches sharing a synthetic foundation commit where one branch's tip is behind the foundation's current tip, the reconcile helper reports the exact commit range the stale branch is missing, without performing any git mutation | +| `handoff_manifest_rejects_incomplete_record` | A handoff manifest JSON missing any of `assigned_files`/`commit_sha`/`test_result`/`base_sha` is rejected with the exact missing field named | + +### Type 4 — contract tests (`packages/d2b-contract-tests/tests/*.rs`) + +| Test | Asserts | +| --- | --- | +| `spec_registry_json_schema_matches_doc` | The generated `docs/specs/ADR-046-spec-set.json`/`ADR-046-work-items.json` shape matches this and the parent `docs/specs/README.md`'s documented fields exactly (drift gate) | +| `provider_catalog_matches_frozen_families` | The generated `docs/specs/ADR-046-provider-catalog.md` table matches `ADR-046-decision-register` D043-D049 membership exactly | + +### Type 5 — policy lints (`packages/d2b-contract-tests/tests/policy_*.rs`) + +| Test | Asserts | +| --- | --- | +| `policy_spec_vocabulary` | No `docs/specs/**` file uses a Provider-specific ResourceType name outside the D080 `.d2bus.org.` grammar; no file authors a numeric `hostUid`/`hostGid` field on a public spec (F2/F7 regression guard) | +| `policy_spec_ownership` | No dossier other than `volume-local` writes Volume layout/spec/ownership fields (D083); no transport dossier reads/writes/finalizes `ZoneLink` (D081) (F4/F5 regression guard) | +| `policy_spec_effectport_boundary` | No Provider dossier's detailed-design section names a direct broker/syscall/filesystem/systemd-socket call; every privileged effect is expressed as an injected typed `EffectPort` call (D077) (F6 regression guard) | +| `policy_spec_source_policy` | No Volume `source.settings` example anywhere under `docs/specs/**` contains a raw absolute host path; every `local-path`/`block-image` source example uses `sourcePolicyId` (D082) (F9 regression guard) | +| `policy_spec_finalizer_phase` | Every ResourceType/Provider spec's status section uses only the common phase enum `Pending|Ready|Succeeded|Degraded|Failed|Deleted|Unknown` (D037) and the fixed finalizer/deletion ordering (D084); no spec invents a parallel phase or deletion-ordering scheme | +| `policy_no_leaked_decision_prefix` | No file under `docs/specs/**` contains a "decision-required" marker using any ID prefix other than the canonical `ADR-046-decision-register` `D0xx` numbering (F8 regression guard) | +| `policy_test_placement` | Every `src/` `#[cfg(test)]` module and crate `tests/*.rs` file is hermetic (D094): no process spawn, container, network, DBus, systemd, broker daemon, Nix eval/build, KVM, hardware, live cloud, or non-tiny filesystem tree, and no `#[ignore]`; such needs must move to `integration/` | +| `policy_test_determinism` | No hermetic-tier test uses wall-clock sleep/retry (D094); a deterministic fake clock/RNG is used instead, except for explicitly allow-listed crypto/property tests with a declared per-test budget and capped case count | + +### Type 6 — flake checks (`tests/unit/smoke/`) + +Not applicable: no NixOS module surface is introduced. + +### Type 9 — container (`tests/integration/containers/`, `make test-integration`) + +| Test | Asserts | +| --- | --- | +| `spec-tooling-fresh-clone.sh` | A rootless-podman fixture clones a synthetic fixture repo shaped like `docs/specs/`, runs `spec-registry`/`spec-schema-check`/`policy_spec_*` against it, and confirms all four Tier A generators/lints run without any dependency beyond the pinned toolchain | + +### Type 10-12 + +Not applicable: Tier A tooling requires no VM, live host, or hardware; Tier B +items requiring these are specified per-item in +[Implementation work items](#implementation-work-items). + +## Implementation work items + +Each item below uses the bespoke field set this meta-spec requires instead of +the standard ResourceType/Provider work-item table, because none of these +items reuse or replace v3 runtime code — they are new tooling with no current +v3 source to extract from. + +### ADR046-streamline-001 — Generated spec registry, dependency graph, and implementation DAG + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-001` | +| Tier | A (shared prep) | +| Observed friction evidence | F1, F8, F11: dossier branches diverged from a superseded foundation commit with no generated way to detect it; three different per-spec decision-ID prefixes existed with no generated cross-reference; launch order and parallelism were re-derived from prose per round | +| Desired behavior | A generated, deterministic JSON registry enumerating every `docs/specs/ADR-046-*.md` and `docs/specs/providers/ADR-046-*.md` file's metadata-table fields and `Depends on` edges, rendered as an acyclic dependency graph; and, per D095, a generated machine-readable implementation DAG (`ADR-046-implementation-graph.json` + human view `ADR-046-implementation-graph.md`) mapping every member spec and every work item to a `W0`–`W7` launch wave, a file-disjoint parallel group, typed edges, and a topological rank | +| Destination | `docs/specs/ADR-046-spec-set.json`, `docs/specs/ADR-046-work-items.json`, `docs/specs/ADR-046-implementation-graph.json`, `docs/specs/ADR-046-implementation-graph.md` (generated, committed non-member artifacts; named in `docs/specs/README.md`) | +| Owner/dependencies | `packages/xtask` owner; no dependency on any other streamline item | +| Dependency/owner | `packages/xtask` owner; no dependency on any other streamline item | +| Current source | None — net-new ADR 0046 spec tooling; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Implementation shape | New `cargo run -p xtask -- spec-registry` subcommand parsing every spec's metadata table + work-item tables with a Markdown-table parser, emitting `ADR-046-spec-set.json` and `ADR-046-work-items.json`; a companion `cargo run -p xtask -- implementation-graph` reads those two manifests plus the `ADR-046-validation-and-delivery.md` §3 wave topology and emits `ADR-046-implementation-graph.json` and its rendered `.md`; both wired into the existing `gen-*`/`run_task` dispatch pattern in `packages/xtask/src/main.rs` | +| Detailed design | New `cargo run -p xtask -- spec-registry` subcommand parsing every spec's metadata table + work-item tables with a Markdown-table parser, emitting the two manifest JSON files; the `implementation-graph` generator then maps every member spec and every work item exactly once to a wave and file-disjoint parallel group, emits typed edges (`spec-depends-on`, `work-item-depends-on`, `implements-spec`, `shared-contract`, `file-overlap-order`), computes topological rank, and renders the Mermaid/table human view; output is deterministic with sorted keys and no timestamps or host paths | +| Integration | `tests/unit/gates/` drift gate (`xtask spec-registry`/`xtask implementation-graph` + `git diff --exit-code`) added to the existing drift-gate set; the graph is consumed by ADR046-streamline-005/006/008/013 and the ready-wave query in `ADR-046-validation-and-delivery` instead of each re-parsing Markdown independently | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `spec_registry::parse_metadata_table`, `spec_registry::acyclic_dependency_graph`, `xtask_spec_registry_regenerates_clean`, `spec_registry_json_schema_matches_doc`, `implementation_graph::every_spec_and_work_item_mapped_once`, `implementation_graph::acyclic_and_wave_monotonic`, `implementation_graph::parallel_groups_are_file_disjoint`, `implementation_graph_regenerates_clean` | +| Adoption timing | Immediately; first Tier A item, since every other generator/lint below consumes its output | +| Removal/supersession | None; this is the foundational generator for the remaining items | +| Removal proof | None — net-new; no prior owner to remove; this is the foundational generator for the remaining items | + +### ADR046-streamline-002 — Canonical schema/snippet generator + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-002` | +| Tier | A for the doc-level canonical field-set source; B for generating from real Rust `ResourceTypeSchema` types once they exist | +| Observed friction evidence | F2, F13: `rootConfig`/`hostUid`/`hostGid`/custom readiness-restart fields were independently invented per dossier; 314 lines of near-identical state-Volume boilerplate exist across 27 dossiers | +| Desired behavior | One canonical, versioned field-set definition per primitive (`Provider`, `Process`, `EphemeralProcess`, `Volume`, `ProviderStateSet` extension fields, qualified-ref grammar) that every dossier's fenced example is generated from or checked against, instead of hand-copied | +| Destination | `docs/specs/schemas/*.schema.json` (Tier A: hand-authored-once canonical source checked into the tree, matching the frozen D010/D032/D075/D076/D080 decisions exactly); `packages/d2b-core/src/resource_schema/*.rs` (Tier B: the eventual Rust source of truth once ResourceType implementation exists, at which point the Tier A JSON becomes generated from Rust instead of hand-authored) | +| Owner/dependencies | ADR046-streamline-001; `d2b-core` owner (Tier B only) | +| Dependency/owner | ADR046-streamline-001; `d2b-core` owner (Tier B only) | +| Current source | Decision-register D010/D032/D075/D076/D080 prose is the Tier A source; no existing generated ResourceType schema source until Tier B | +| Reuse action | create | +| Implementation shape | Tier A: author the schema JSON once per primitive directly from the already-frozen decision-register entries (D010, D032, D075, D076, D080); Tier B: `xtask gen-spec-schemas` derives the same JSON from real Rust `#[derive(JsonSchema)]`-equivalent types once they land, replacing the hand-authored Tier A source without changing its consumers | +| Detailed design | Tier A: author the schema JSON once per primitive directly from the already-frozen decision-register entries (D010, D032, D075, D076, D080); Tier B: `xtask gen-spec-schemas` derives the same JSON from real Rust `#[derive(JsonSchema)]`-equivalent types once they land, replacing the hand-authored Tier A source without changing its consumers Primary reuse disposition: `create`. Preserved source-plan detail: net-new Tier A schema source; later replace source with generated output from real d2b-core ResourceType types without changing consumers. | +| Integration | Consumed by ADR046-streamline-003 as the validation target and by ADR046-streamline-004's scaffold as the snippet source | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `spec_schema_check::canonical_field_set` | +| Adoption timing | Tier A ships now; Tier B graduation happens only after the owning implementation work item for `d2b-core` resource schemas reaches Validation-complete | +| Removal/supersession | Tier A hand-authored JSON is superseded (not deleted) by Tier B generated JSON once real Rust types exist; consumers are unaffected since the file shape is identical | +| Removal proof | Tier A hand-authored JSON is superseded (not deleted) by Tier B generated JSON once real Rust types exist; consumers are unaffected since the file shape is identical | + +### ADR046-streamline-003 — Markdown fenced-YAML/JSON extractor and schema validator + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-003` | +| Tier | A | +| Observed friction evidence | F14: no tool extracts and validates fenced ResourceSpec examples against a canonical schema; every field-name drift in F2/F3/F9 was caught by manual review | +| Desired behavior | Every fenced ` ```yaml `/` ```json ` block under a spec's "Nix authoring and configuration cleanup" heading is extracted, matched to its declared `type`, and schema-validated against ADR046-streamline-002's canonical field set, with documented exclusions for blocks explicitly marked as illustrating a rejected/historical shape (current-evidence exclusions, e.g. the "current-code fit" tables' own citation blocks are not ResourceSpec examples and are excluded by heading context, not by an ad hoc allowlist) | +| Destination | `packages/xtask/src/bin/spec_schema_check.rs`; wired as a Layer-1 policy lint at `packages/d2b-contract-tests/tests/policy_spec_schema.rs` | +| Owner/dependencies | ADR046-streamline-001, ADR046-streamline-002 | +| Dependency/owner | ADR046-streamline-001, ADR046-streamline-002 | +| Current source | None — net-new spec Markdown/schema lint; no existing fenced ResourceSpec extractor | +| Reuse action | create | +| Implementation shape | Reuse the CommonMark parser already available to the Rust toolchain (the same class of dependency `xtask`'s existing `gen-*` commands already use for structured generation) to walk fenced blocks; classify each block by its enclosing heading (`## Nix authoring and configuration cleanup` vs. any other heading) to apply the current-evidence exclusion without a manual per-block marker | +| Detailed design | Reuse the CommonMark parser already available to the Rust toolchain (the same class of dependency `xtask`'s existing `gen-*` commands already use for structured generation) to walk fenced blocks; classify each block by its enclosing heading (`## Nix authoring and configuration cleanup` vs. any other heading) to apply the current-evidence exclusion without a manual per-block marker Primary reuse disposition: `create`. Preserved source-plan detail: net-new; reuse only the established xtask dispatch pattern and Rust Markdown-parser dependency class. | +| Integration | Added to `tests/tools/tier0-first-pass.sh` as an additional fast pre-check (closes F17) and to the standing `packages/d2b-contract-tests` policy-lint suite | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `spec_schema_check::extract_fenced_blocks`, `spec_schema_check::canonical_field_set`, `policy_spec_schema` (Type 4/5) | +| Adoption timing | Immediately, before the ADR 0046 documentation set's own pre-panel gate, so any remaining spec iteration is caught before, not after, panel | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-004 — Provider dossier scaffold/template + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-004` | +| Tier | A | +| Observed friction evidence | F13: 27 dossiers each hand-authored the same ProviderStateSet/Volume boilerplate (314 duplicated lines) and D059's `src`/`tests`/`integration`/`README.md` requirement from scratch | +| Desired behavior | A committed template enumerating every required section (metadata table, "Nix authoring and configuration cleanup", `src`/`tests`/`integration`/`README.md` work-item rows per D059, "Current-code fit", "Tests", "Implementation work items") and the canonical state-Volume snippet from ADR046-streamline-002, so a new or corrected dossier starts compliant instead of converging over several correction commits | +| Destination | `docs/specs/providers/TEMPLATE.md` (committed, non-normative reference); `packages/xtask/src/bin/new_provider_dossier.rs` (`cargo run -p xtask -- new-provider-dossier --name `) | +| Owner/dependencies | ADR046-streamline-001, ADR046-streamline-002 | +| Dependency/owner | ADR046-streamline-001, ADR046-streamline-002 | +| Current source | None — net-new provider-dossier scaffold; canonical snippets come from ADR046-streamline-002 | +| Reuse action | create | +| Implementation shape | Scaffold generator emits the template pre-filled with the requesting Provider's name, D080 qualification-grammar examples, and the canonical state-Volume snippet; does not attempt to author Provider-specific semantic sections (those remain the dossier author's normative content) | +| Detailed design | Scaffold generator emits the template pre-filled with the requesting Provider's name, D080 qualification-grammar examples, and the canonical state-Volume snippet; does not attempt to author Provider-specific semantic sections (those remain the dossier author's normative content) Primary reuse disposition: `create`. Preserved source-plan detail: net-new scaffold; reuse canonical schema/snippet source from ADR046-streamline-002. | +| Integration | Referenced from `docs/specs/README.md` "Required metadata" section (a follow-up doc edit outside this task's scope, tracked as a required cross-reference) | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `xtask_new_provider_dossier_scaffold`; the emitted scaffold must independently pass `spec_schema_check`/`work_item_validator` on first generation | +| Adoption timing | Immediately; used for any future new Provider dossier and for re-basing an existing dossier onto the corrected shape during its next revision | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-005 — Cross-spec vocabulary/ownership/finalizer/phase/source-policy lint + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-005` | +| Tier | A | +| Observed friction evidence | F2, F4, F5, F7, F9: qualified-ResourceType grammar drift, Volume-vs-virtiofs ownership conflicts, ZoneLink ownership drift, sourcePolicy/path drift, all caught by manual full-tree sweeps (F12) rather than a generated check | +| Desired behavior | One Rust policy lint enumerating and enforcing every frozen cross-cutting invariant already in the decision register (D080 grammar, D081 ZoneLink ownership, D082 sourcePolicyId, D083 Volume/virtiofs ownership, D084 finalizer ordering, D037 phase enum) across all committed specs in one pass | +| Destination | `packages/d2b-contract-tests/tests/policy_spec_vocabulary.rs`, `policy_spec_ownership.rs`, `policy_spec_finalizer_phase.rs`, `policy_spec_source_policy.rs` | +| Owner/dependencies | ADR046-streamline-001 | +| Dependency/owner | ADR046-streamline-001 | +| Current source | Decision-register D080/D081/D082/D083/D084/D037 invariants and ADR046-streamline-001 registry output; no existing cross-spec lint | +| Reuse action | create | +| Implementation shape | Four focused lint files (one invariant family per file, matching the existing `packages/d2b-contract-tests/tests/policy_*.rs` one-concern-per-file convention) each scanning the registry output plus raw Markdown text for the specific violation patterns named in F2/F4/F5/F7/F9 | +| Detailed design | Four focused lint files (one invariant family per file, matching the existing `packages/d2b-contract-tests/tests/policy_*.rs` one-concern-per-file convention) each scanning the registry output plus raw Markdown text for the specific violation patterns named in F2/F4/F5/F7/F9 Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lints over generated registry and raw Markdown. | +| Integration | Standing Layer-1 policy-lint suite; runs on every PR touching `docs/specs/**` | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `policy_spec_vocabulary`, `policy_spec_ownership`, `policy_spec_finalizer_phase`, `policy_spec_source_policy` (all Type 5) | +| Adoption timing | Immediately, before the ADR 0046 documentation set's own pre-panel gate | +| Removal/supersession | None; graduates to permanent per [Permanent methodology](#permanent-methodology-after-accepted) | +| Removal proof | None — net-new; no prior owner to remove; graduates to permanent per [Permanent methodology](#permanent-methodology-after-accepted) | + +### ADR046-streamline-006 — ProviderStateSet/status-first/one-controller graph checker + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-006` | +| Tier | B | +| Observed friction evidence | F3: ProviderStateSet churned through multiple representations and the bootstrap-cycle exception was independently re-scoped three times (`5c287f51`, `7632ebec`, `24598e5c`) before D076/D086 froze it; D087 later removed the mandatory-state model in favor of a status-first, optional state Volume | +| Desired behavior | A runtime-level checker that, given a Zone's actual resource/controller registration graph, verifies every Provider component's state Volumes are *declared* (a component with no declared namespace owns no state Volume and no empty identity-only Volume exists), no ResourceType or stored row named `ProviderStateSet` exists, and no bootstrap state Volume or bootstrap-storage mechanism exists (the fixed `volume-local`/`system-core`/`system-minijail` bootstrap set declares no state Volume and reaches Ready from `status`/the core Operation ledger per D087) | +| Destination | `packages/d2b-resource-store-redb/tests/provider_state_graph.rs` (or the eventual crate implementing Zone resource storage) | +| Owner/dependencies | The Zone resource-store implementation work item (not yet filed; blocked on W0-W10 implementation request per D024); ADR046-streamline-001 for the doc-level invariant source | +| Dependency/owner | The Zone resource-store implementation work item (not yet filed; blocked on W0-W10 implementation request per D024); ADR046-streamline-001 for the doc-level invariant source | +| Current source | None — real Zone resource-store/controller-registration graph not implemented yet; doc-level invariant source is ADR046-streamline-005 | +| Reuse action | create | +| Implementation shape | A graph-walk over the real controller-registration/resource-ownership index (not Markdown) asserting the D076/D086/D087 invariants; the doc-level half of this check (dossier text describing the invariant correctly) is covered now by ADR046-streamline-005's `policy_spec_ownership` | +| Detailed design | A graph-walk over the real controller-registration/resource-ownership index (not Markdown) asserting the D076/D086/D087 invariants; the doc-level half of this check (dossier text describing the invariant correctly) is covered now by ADR046-streamline-005's `policy_spec_ownership` Primary reuse disposition: `create`. Preserved source-plan detail: net-new future runtime graph checker. | +| Integration | Runs as a Type 3 integration test against the real resource-store crate once it exists | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | New test asserting: zero `ProviderStateSet` rows in the store; every state Volume corresponds to a declared component namespace; no empty identity-only Volume; no bootstrap state Volume or bootstrap-storage mechanism exists | +| Adoption timing | Streamline wave (Tier B); lands after the Zone resource-store and core-controller implementation work items reach Validation-complete | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-007 — EffectPort/broker and worker-bus boundary lint + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-007` | +| Tier | B | +| Observed friction evidence | F6: dossier text repeatedly drifted toward direct broker/syscall access before D077's EffectPort pattern was frozen and propagated | +| Desired behavior | A source-level lint (the same shape as the existing `packages/d2b-contract-tests/tests/policy_broker_dispositions.rs`/`policy_broker_schema.rs`) scanning real Provider crate source for any direct broker import, raw socket/DTO use, or unmediated host path/device/systemd-socket open, and for any Worker binary reaching for a `ResourceClient`/d2b-bus/Credential/CLI/child-spawn capability D078 reserves to controllers/services | +| Destination | `packages/d2b-contract-tests/tests/policy_effectport_boundary.rs`, `policy_worker_bus_boundary.rs` | +| Owner/dependencies | The Provider-toolkit/EffectPort implementation work item (not yet filed; blocked on the W0-W10 implementation request); ADR046-streamline-001 for the doc-level invariant source | +| Dependency/owner | The Provider-toolkit/EffectPort implementation work item (not yet filed; blocked on the W0-W10 implementation request); ADR046-streamline-001 for the doc-level invariant source | +| Current source | Existing policy-broker lint pattern in packages/d2b-contract-tests/tests/policy_broker_dispositions.rs and policy_broker_schema.rs; no Provider crate source exists yet | +| Reuse action | adapt | +| Implementation shape | Static source scan (import-graph/symbol-use analysis) over compiled Provider crates, mirroring the existing `policy_broker_dispositions.rs` pattern already proven against `packages/d2bd`/`packages/d2b-priv-broker` | +| Detailed design | Static source scan (import-graph/symbol-use analysis) over compiled Provider crates, mirroring the existing `policy_broker_dispositions.rs` pattern already proven against `packages/d2bd`/`packages/d2b-priv-broker` Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing policy-broker lint pattern to Provider/Worker boundary checks. | +| Integration | Standing Layer-1 policy lint once Provider crates exist | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | New tests asserting zero direct-broker-import matches in any Provider crate; zero disallowed-capability matches in any Worker binary | +| Adoption timing | Streamline wave (Tier B); lands after the first Provider-toolkit implementation work item reaches Validation-complete, so there is real Provider crate source to lint | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-008 — Work-item schema and unique-ID/dependency validator + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-008` | +| Tier | A | +| Observed friction evidence | F8, F16: three separate per-spec decision-ID prefixes existed simultaneously with no generated cross-check; no work-item table is validated for required-field completeness or ID uniqueness today | +| Desired behavior | Every `ADR046--` work item ID is unique across the whole tree; every table has all required fields (the eleven README.md fields, or this spec's nine bespoke fields) non-empty and non-placeholder; every `Dependency/owner` reference to another work item ID resolves | +| Destination | `packages/d2b-contract-tests/tests/policy_work_items.rs` | +| Owner/dependencies | ADR046-streamline-001 | +| Dependency/owner | ADR046-streamline-001 | +| Current source | None — no existing work-item required-field or unique-ID validator | +| Reuse action | create | +| Implementation shape | Consumes `docs/specs/ADR-046-work-items.json` (generated by ADR046-streamline-001); flags duplicate IDs, missing/placeholder fields, and dangling cross-references | +| Detailed design | Consumes `docs/specs/ADR-046-work-items.json` (generated by ADR046-streamline-001); flags duplicate IDs, missing/placeholder fields, and dangling cross-references Primary reuse disposition: `create`. Preserved source-plan detail: net-new validator consuming ADR046-streamline-001 output. | +| Integration | Standing Layer-1 policy lint | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `work_item_validator::unique_ids`, `work_item_validator::required_fields` | +| Adoption timing | Immediately, before the ADR 0046 documentation set's own pre-panel gate (D014's "set cannot become Accepted while it contains... a work item without exact v3 source and future destination paths" precondition) | +| Removal/supersession | None; graduates to permanent | +| Removal proof | None — net-new; no prior owner to remove; graduates to permanent | + +### ADR046-streamline-009 — Provider catalog/index generator + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-009` | +| Tier | A | +| Observed friction evidence | F7, F15: no single generated index of the D043-D049 frozen Provider families exists; cross-dossier consistency checks (e.g. "is this the fourth or fifth interaction Provider") are done by manual counting today | +| Desired behavior | A generated, committed table listing every frozen Provider family (D043-D049), its dossier file, its ResourceTypes owned, and its qualification-grammar examples, kept in sync with the dossier tree by a drift gate | +| Destination | `docs/specs/ADR-046-provider-catalog.md` (generated, committed) | +| Owner/dependencies | ADR046-streamline-001 | +| Dependency/owner | ADR046-streamline-001 | +| Current source | None — no existing generated Provider catalog/index | +| Reuse action | create | +| Implementation shape | `cargo run -p xtask -- gen-provider-catalog` reads the registry and decision-register D043-D049 rows and renders the table; drift-gated like the existing `gen-schemas`/`gen-migration-ledger` pattern | +| Detailed design | `cargo run -p xtask -- gen-provider-catalog` reads the registry and decision-register D043-D049 rows and renders the table; drift-gated like the existing `gen-schemas`/`gen-migration-ledger` pattern Primary reuse disposition: `create`. Preserved source-plan detail: net-new generator consuming registry and decision-register rows. | +| Integration | `tests/unit/gates/` drift gate | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `provider_catalog::frozen_family_membership`, `provider_catalog_matches_frozen_families` | +| Adoption timing | Immediately | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-010 — Stale-base/current-parent reconcile helper + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-010` | +| Tier | A | +| Observed friction evidence | F1, F11: every checked dossier branch diverged at the same superseded foundation commit; corrective commits landed on already-diverged branches instead of through a clean restack, producing avoidable Nix cherry-pick conflicts in later Nix-authoring-alignment sweeps | +| Desired behavior | A read-only helper reporting, for a given dossier branch, the exact commit range between its divergence point and the current foundation-spec tip, and whether any of those commits touch a file the branch also touches (a likely-conflict signal), without performing any git mutation itself | +| Destination | `tests/tools/reconcile-stale-base.sh` (reporting only) plus a documented `git town sync`/`git town` restack procedure this report feeds into, since this repository does not yet use Git Town and F1/F11 show plain rebase/cherry-pick was insufficient to prevent duplicate reconciliation | +| Owner/dependencies | ADR046-streamline-001 | +| Dependency/owner | ADR046-streamline-001 | +| Current source | None — no existing stale-base/current-parent reconcile helper | +| Reuse action | adapt | +| Implementation shape | `git merge-base ` plus `git diff --name-only` intersection reporting; emits a JSON report (branch, divergence SHA, commits-behind count, file-overlap list) consumed by ADR046-streamline-013's ready/launched/blocker report | +| Detailed design | `git merge-base ` plus `git diff --name-only` intersection reporting; emits a JSON report (branch, divergence SHA, commits-behind count, file-overlap list) consumed by ADR046-streamline-013's ready/launched/blocker report Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new read-only git report. | +| Integration | Referenced from `AGENTS.md` once graduated (see [Permanent methodology](#permanent-methodology-after-accepted)); used manually before opening or restacking any future dossier branch | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `reconcile_helper_reports_stale_base` | +| Adoption timing | Immediately; this is the single highest-value item given F1 shows 100% of checked dossier branches were affected | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-011 — Agent handoff manifest + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-011` | +| Tier | A | +| Observed friction evidence | F10: two independent byte-identical 1-commit and 24-file reconciliation passes landed on separate branches with no cross-reference to each other's SHA or test result | +| Desired behavior | Every parallel-authoring handoff (a sub-agent or contributor returning a completed spec/dossier slice to the integrator) is accompanied by a manifest naming the exact assigned file set, the commit SHA it produced, the validation/test result obtained, and the base SHA it started from, so a second corrective pass can detect it is duplicating already-reconciled work instead of re-deriving it | +| Destination | `packages/xtask/src/bin/handoff_manifest.rs` (schema/validator only); manifest instances are per-round artifacts referenced from PR bodies, not committed to the tree (consistent with this repository's "Screenshot and visual artifact hygiene"-style external-evidence posture) | +| Owner/dependencies | None — no prerequisite work item; owned by this spec | +| Dependency/owner | No prerequisite; `packages/xtask` schema/validator owner | +| Current source | None — no existing agent handoff manifest schema or validator | +| Reuse action | create | +| Implementation shape | A small JSON schema (`assigned_files: [string]`, `commit_sha: string`, `test_result: {command, exit_code}`, `base_sha: string`) plus a validator rejecting incomplete records; no attribution field for any AI/tool/model, consistent with this repository's commit/PR-body attribution rule | +| Detailed design | A small JSON schema (`assigned_files: [string]`, `commit_sha: string`, `test_result: {command, exit_code}`, `base_sha: string`) plus a validator rejecting incomplete records; no attribution field for any AI/tool/model, consistent with this repository's commit/PR-body attribution rule Primary reuse disposition: `create`. Preserved source-plan detail: net-new schema and validator. | +| Integration | Referenced from `AGENTS.md` "Worktrees for parallel agents" once graduated | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `handoff_manifest_rejects_incomplete_record` | +| Adoption timing | Immediately, for any remaining ADR 0046 spec-set round and for the future W0-W10 implementation phase's parallel scopes | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-012 — Task DB↔git consistency import + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-012` | +| Tier | A | +| Observed friction evidence | F16: authoring session task tracking carries no enforced binding to the git tree; the only available cross-check today is manual comparison | +| Desired behavior | An importer that reads the session task-tracking database and cross-checks every "done" task against a resolvable commit SHA reachable from the current branch, flagging any "done" task with no corresponding commit and any commit range with no corresponding tracked task | +| Destination | `tests/tools/import-task-db-consistency.sh` | +| Owner/dependencies | ADR046-streamline-011 (shares the manifest schema for commit-SHA binding) | +| Dependency/owner | ADR046-streamline-011 (shares the manifest schema for commit-SHA binding) | +| Current source | None — no existing task DB to git consistency importer | +| Reuse action | create | +| Implementation shape | Reads the session database's todo table, resolves each `done` row's expected file set against `git log --name-only` for the current branch, and reports mismatches; read-only, no database or git mutation | +| Detailed design | Reads the session database's todo table, resolves each `done` row's expected file set against `git log --name-only` for the current branch, and reports mismatches; read-only, no database or git mutation Primary reuse disposition: `create`. Preserved source-plan detail: net-new read-only importer/checker. | +| Integration | Run manually at the end of each authoring round and before any pre-panel gate | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | New test seeding a synthetic task DB with one orphaned "done" row and confirming the importer flags it | +| Adoption timing | Immediately | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-013 — Anti-serialization ready/launched/blocker report + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-013` | +| Tier | A | +| Observed friction evidence | F1, F11: dossier branches were opened against a stale foundation and left to accumulate independent correction commits rather than being promptly restacked once the foundation moved, which is exactly the serialization failure mode this repository's own "Stacked PR workflow for large waves" section warns against for code waves | +| Desired behavior | A report enumerating every dossier/spec scope's readiness (dependencies satisfied), launch state (open worktree/branch), and any recorded blocker, so a scope that is ready but not launched is visible immediately rather than discovered after correction commits have already accumulated | +| Destination | `tests/tools/anti-serialization-report.sh` | +| Owner/dependencies | ADR046-streamline-001, ADR046-streamline-010 | +| Dependency/owner | ADR046-streamline-001, ADR046-streamline-010 | +| Current source | None — no existing anti-serialization ready/launched/blocker report | +| Reuse action | adapt | +| Implementation shape | Cross-references the spec registry's dependency graph (ADR046-streamline-001) against the set of currently open `adr0046-*` branches and the reconcile helper's (ADR046-streamline-010) staleness report; emits a per-scope ready/launched/blocked classification | +| Detailed design | Cross-references the spec registry's dependency graph (ADR046-streamline-001) against the set of currently open `adr0046-*` branches and the reconcile helper's (ADR046-streamline-010) staleness report; emits a per-scope ready/launched/blocked classification Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new report combining registry, branch, and staleness data. | +| Integration | Run at the start of each authoring round and after any foundation-spec change | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | New test seeding a synthetic registry + branch list with one ready-but-unlaunched scope and confirming it is reported | +| Adoption timing | Immediately | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-014 — Bounded parallel test/fake-dependency harness + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-014` | +| Tier | B | +| Observed friction evidence | F11: corrective commits on already-diverged branches had no fast way to verify only their own scope was affected without running the full doc/Rust suite; this generalizes to the future implementation phase where per-Provider-crate test runs must not require every other Provider crate to build first | +| Desired behavior | A bounded-parallelism test harness that can run one Provider crate's (or one dossier's) tests/lints against fake/stub dependencies for every other declared Provider, so a single scope's correction round does not require building or testing the entire W0-W10 implementation surface | +| Destination | `tests/tools/run-layer.sh` extension (this repository already has `tests/tools/run-layer.sh` and `layer1-jobs.py` bounded-parallelism precedent) plus fake `EffectPort`/`ResourceClient` stub crates under `packages/d2b-provider-toolkit-fakes/` | +| Owner/dependencies | The Provider-toolkit implementation work item (blocked on the W0-W10 implementation request) | +| Dependency/owner | The Provider-toolkit implementation work item (blocked on the W0-W10 implementation request) | +| Current source | Existing bounded-parallel test-runner precedent in tests/tools/run-layer.sh and layer1-jobs.py; Provider toolkit fake crates are net-new | +| Reuse action | adapt | +| Implementation shape | Extend the existing `layer1-jobs.py` bounded-parallel-shard pattern with a per-Provider-crate shard definition; fake dependency crates implement the same `EffectPort`/`ResourceClient` trait surface with in-memory stand-ins | +| Detailed design | Extend the existing `layer1-jobs.py` bounded-parallel-shard pattern with a per-Provider-crate shard definition; fake dependency crates implement the same `EffectPort`/`ResourceClient` trait surface with in-memory stand-ins Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt bounded-parallel shard pattern; add net-new fake EffectPort/ResourceClient crates. | +| Integration | `make test-unit`/`make check` shard addition once Provider crates exist | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | New integration test proving a single Provider crate's test run succeeds with zero other Provider crates built | +| Adoption timing | Streamline wave (Tier B); lands after the Provider-toolkit implementation work item reaches Validation-complete | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-015 — Conflict-aware generated-artifact regeneration + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-015` | +| Tier | A for the doc-level generators (001, 002 Tier A, 009); B for regenerating real Rust-derived artifacts once implementation code exists | +| Observed friction evidence | F1, F11, F12: independent branches each re-derived the same 24-file reconciliation by hand instead of one branch regenerating the shared artifact and the others rebasing onto it | +| Desired behavior | Every generator this spec introduces (registry, provider catalog, schema JSON) detects when its own output would conflict with a concurrent branch's uncommitted regeneration (by comparing input hashes) and reports the conflict instead of silently overwriting, so two branches never independently re-derive the same generated artifact | +| Destination | Shared `packages/xtask` regeneration-conflict-detection helper consumed by every `gen-*`/`spec-registry` subcommand | +| Owner/dependencies | ADR046-streamline-001, ADR046-streamline-009, ADR046-streamline-002 (Tier A part) | +| Dependency/owner | ADR046-streamline-001, ADR046-streamline-009, ADR046-streamline-002 (Tier A part) | +| Current source | Existing packages/xtask gen-* and drift-gate pattern; no existing generated-artifact conflict detector | +| Reuse action | adapt | +| Implementation shape | Each generator hashes its input set before writing; if a concurrent regeneration on a sibling branch (detected via the reconcile helper's file-overlap report) would produce a different hash for the same output path, the generator refuses to overwrite and reports the divergent input set | +| Detailed design | Each generator hashes its input set before writing; if a concurrent regeneration on a sibling branch (detected via the reconcile helper's file-overlap report) would produce a different hash for the same output path, the generator refuses to overwrite and reports the divergent input set Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing generator/drift-gate pattern with net-new input-hash conflict detection. | +| Integration | Wired into every drift gate this spec and ADR046-streamline-001/009 introduce | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | New test simulating two divergent input sets producing conflicting output for the same generated file and confirming the conflict is reported, not silently resolved | +| Adoption timing | Tier A part ships alongside ADR046-streamline-001/009; the Tier B part (real generated Rust artifacts) ships in the streamline wave once code generation exists | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-016 — Pre-panel zero-open-decision gate + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-016` | +| Tier | A | +| Observed friction evidence | F8: three per-spec decision-ID prefixes leaked into three specs before consolidation; D014/README.md already require zero unresolved decisions before panel, but no automated gate checks it today | +| Desired behavior | A gate that fails closed if any file under `docs/specs/**` contains a "decision-required" marker, a placeholder (`TBD`/`TODO` without accompanying justification prose), or a per-spec decision-ID prefix other than the canonical `ADR-046-decision-register` numbering, run immediately before the documentation set's pre-panel snapshot | +| Destination | `packages/d2b-contract-tests/tests/policy_no_leaked_decision_prefix.rs`; invoked as a required precondition script `tests/tools/pre-panel-gate.sh` | +| Owner/dependencies | ADR046-streamline-001, ADR046-streamline-008 | +| Dependency/owner | ADR046-streamline-001, ADR046-streamline-008 | +| Current source | D014/docs/specs/README.md zero-open-decision requirement; no existing automated pre-panel gate | +| Reuse action | create | +| Implementation shape | Scans registry output plus raw Markdown for `decision-required`, `TBD`, `TODO`, and any decision-ID-shaped token (`[A-Z]+-\d+`) not matching the canonical `D\d+` register numbering | +| Detailed design | Scans registry output plus raw Markdown for `decision-required`, `TBD`, `TODO`, and any decision-ID-shaped token (`[A-Z]+-\d+`) not matching the canonical `D\d+` register numbering Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lint and precondition script. | +| Integration | Required precondition before requesting the ADR 0046 documentation set's panel round (D014) | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `policy_no_leaked_decision_prefix` | +| Adoption timing | Immediately; this is a precondition for the ADR 0046 documentation set's own remaining panel gate, not deferred tooling | +| Removal/supersession | None; graduates to permanent panel-gate precondition | +| Removal proof | None — net-new; no prior owner to remove; graduates to permanent panel-gate precondition | + +### ADR046-streamline-017 — External evidence command planning + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-017` | +| Tier | A | +| Observed friction evidence | F11, F17: correction commits and full-tree sweeps happened without a documented, reusable command sequence a reviewer or future author could re-run to reproduce the same evidence; each investigation (as in this spec's own [Observed friction evidence](#observed-friction-evidence)) was ad hoc | +| Desired behavior | A committed, reusable list of exact `git`/`grep`/`xtask` commands that reproduce every metric and evidence citation in this spec (and any future evidence-grounded spec), so panel reviewers and future authors can independently reproduce the evidence without re-deriving the search strategy | +| Destination | `docs/specs/ADR-046-streamline-evidence-commands.md` (a follow-up artifact outside this task's file scope; tracked here as a required future addition, not authored by this spec) | +| Owner/dependencies | None — no prerequisite work item; owned by this spec | +| Dependency/owner | No prerequisite; `docs/specs` evidence-command documentation owner | +| Current source | This spec's Observed friction evidence command prose; no reusable evidence-command artifact exists yet | +| Reuse action | adapt | +| Implementation shape | A plain Markdown list of the exact commands used to produce each F1-F17 citation above (already reproduced verbatim in this spec's prose), organized by friction ID, so re-running them is copy-paste rather than re-derivation | +| Detailed design | A plain Markdown list of the exact commands used to produce each F1-F17 citation above (already reproduced verbatim in this spec's prose), organized by friction ID, so re-running them is copy-paste rather than re-derivation Primary reuse disposition: `adapt`. Preserved source-plan detail: extract/adapt the already-cited evidence commands into a net-new documentation artifact. | +| Integration | Referenced from PR bodies as external evidence per this repository's "PR bodies contain... check-status summaries only... may link to external evidence" convention | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | Manual reviewer re-run of at least one command per friction ID during panel, confirmed to reproduce the cited count/SHA | +| Adoption timing | Immediately, as a follow-up documentation artifact; does not gate any other item in this spec | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-018 — Worktree/disk/target cleanup reporting + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-018` | +| Tier | A | +| Observed friction evidence | Generalizes this repository's existing disk-hygiene posture (already required for code waves) to the documentation-authoring worktrees this ADR's dossier-branch-per-scope workflow creates (F1 shows 30+ `adr0046-*` branches/worktrees existed concurrently) | +| Desired behavior | A report enumerating every `adr0046-*` worktree, its `packages/target/` size (or confirmation it is the shared-cache symlink), and its branch's merge/staleness state, with **no automatic destructive deletion**; cleanup remains an explicit human/integrator-approved action | +| Destination | `tests/tools/worktree-disk-report.sh` | +| Owner/dependencies | ADR046-streamline-010 (shares branch-staleness detection) | +| Dependency/owner | ADR046-streamline-010 (shares branch-staleness detection) | +| Current source | Existing disk-hygiene operator guidance; no ADR 0046 worktree disk report exists yet | +| Reuse action | adapt | +| Implementation shape | `git worktree list --porcelain` plus `du -sh` per worktree's `packages/target/`; output is a report only, never a `git worktree remove`/`rm -rf` invocation | +| Detailed design | `git worktree list --porcelain` plus `du -sh` per worktree's `packages/target/`; output is a report only, never a `git worktree remove`/`rm -rf` invocation Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing disk-hygiene reporting concepts into a net-new non-mutating script. | +| Integration | Run manually before requesting garbage collection or worktree removal, consistent with this repository's existing "Disk hygiene contract"-equivalent operator guidance for code waves | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | New test confirming the reporting script's exit code and output never include a mutating command string, and confirming it correctly flags a fixture worktree with a real (non-symlink) `packages/target/` | +| Adoption timing | Immediately | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-019 — Docs source-evidence old→new terminology helper + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-019` | +| Tier | A | +| Observed friction evidence | F15: `Realm`/`Workload` remains the live vocabulary in this repository's current-source `AGENTS.md`/`d2b-realm-*` crates while target ADR 0046 specs use `Zone`/`Guest`; D050's rename shows at least one dossier round used the old target name before the rename propagated | +| Desired behavior | A helper cross-referencing every current-source symbol/crate/option cited by a spec's "Current-code fit" table against `ADR-046-terminology-and-identities`/`ADR-046-current-code-migration-map`'s old→new mapping, flagging any spec that uses a target-side name not yet present in either mapping document | +| Destination | `packages/xtask/src/bin/terminology_check.rs` (`cargo run -p xtask -- terminology-check`) | +| Owner/dependencies | ADR046-streamline-001 | +| Dependency/owner | ADR046-streamline-001 | +| Current source | ADR-046 terminology and current-code migration-map specs; no automated old-to-new terminology helper exists yet | +| Reuse action | create | +| Implementation shape | Parses every "Current-code fit" table's "Current anchor" cell for symbol/crate names, cross-references against the migration map's disposition rows, and flags any current-source citation absent from the map | +| Detailed design | Parses every "Current-code fit" table's "Current anchor" cell for symbol/crate names, cross-references against the migration map's disposition rows, and flags any current-source citation absent from the map Primary reuse disposition: `create`. Preserved source-plan detail: net-new terminology checker over existing mapping specs. | +| Integration | Standing Layer-1 policy lint once wired; also usable ad hoc when authoring a new dossier's evidence section | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | New test seeding a fixture spec citing a current-source symbol absent from the migration map and confirming it is flagged | +| Adoption timing | Immediately | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-020 — Hermetic test placement lint + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-020` | +| Tier | A | +| Observed friction evidence | D094: without a lint, a slow scenario needing a real process/container/network silently lands in a `src/`/`tests/` hermetic tier and inflates the inner loop | +| Desired behavior | A policy lint asserting every `src/` `#[cfg(test)]` module and crate `tests/*.rs` file is hermetic — no process spawn, container, network, DBus, systemd, broker daemon, Nix eval/build, KVM, USB/GPU/TPM hardware, live cloud, or filesystem tree beyond tiny temp fixtures; any such need must move to `integration/`, never gain a sleep, larger timeout, or `#[ignore]` | +| Destination | `packages/d2b-contract-tests/tests/policy_test_placement.rs` | +| Owner/dependencies | ADR046-streamline-001 | +| Dependency/owner | ADR046-streamline-001 | +| Current source | D094 test-placement requirement; no existing hermetic test-placement policy lint | +| Reuse action | create | +| Implementation shape | Scans hermetic-tier Rust sources for banned API surfaces (`std::process::Command`, socket/container/DBus/systemd helpers, `#[ignore]`) and for `integration/`-only markers appearing outside `integration/` | +| Detailed design | Scans hermetic-tier Rust sources for banned API surfaces (`std::process::Command`, socket/container/DBus/systemd helpers, `#[ignore]`) and for `integration/`-only markers appearing outside `integration/` Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lint. | +| Integration | `make test-policy` row; no new top-level `tests/*.sh` gate | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | Fixture crate with an intentional process-spawning hermetic test is rejected naming the file/line; a correct crate passes | +| Adoption timing | Immediately | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-021 — Deterministic-clock/sleep lint + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-021` | +| Tier | A | +| Observed friction evidence | D094: wall-clock `sleep`/retry in a hermetic test both breaks the p95 ≤50 ms budget and makes the test flaky/non-parallel-safe | +| Desired behavior | A lint rejecting wall-clock sleep/retry (`std::thread::sleep`, `tokio::time::sleep` without a paused test clock, real `Instant::now` polling loops) in `src/`/`tests/` hermetic tiers, requiring a deterministic fake clock/RNG instead | +| Destination | `packages/d2b-contract-tests/tests/policy_test_determinism.rs` | +| Owner/dependencies | ADR046-streamline-020 | +| Dependency/owner | ADR046-streamline-020 | +| Current source | D094 deterministic-clock requirement; no existing wall-clock sleep/retry policy lint | +| Reuse action | create | +| Implementation shape | Scans hermetic sources for banned time/sleep APIs and asserts the deterministic fake-clock/RNG fixtures from the toolkit are used; classified crypto/property exceptions are allow-listed by explicit name with a declared per-test budget | +| Detailed design | Scans hermetic sources for banned time/sleep APIs and asserts the deterministic fake-clock/RNG fixtures from the toolkit are used; classified crypto/property exceptions are allow-listed by explicit name with a declared per-test budget Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lint layered on ADR046-streamline-020. | +| Integration | `make test-policy` row | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | Fixture hermetic test using `thread::sleep` is rejected; a classified crypto test on the allow-list passes | +| Adoption timing | Immediately | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-022 — Test-runtime ledger and timing gate + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-022` | +| Tier | A | +| Observed friction evidence | D094: no machine-readable record of execution-only test time exists, so budget regressions are invisible until the inner loop is already slow | +| Desired behavior | A test-runtime ledger + timing gate (reusing existing `xtask`/`libtest --format=json` output, no new framework) measuring execution-only time after build against the §10.16 budgets, recording reference runner/repetitions/p95, reporting top slow tests, applying a historical regression threshold, and emitting a CI artifact | +| Destination | `packages/xtask/src/test_runtime_ledger.rs` (shared with `ADR046-delivery-007`) | +| Owner/dependencies | ADR046-delivery-007 | +| Dependency/owner | ADR046-delivery-007 | +| Current source | ADR046-delivery-007 timing-ledger design; no existing execution-only test-runtime ledger in this spec set | +| Reuse action | adapt | +| Implementation shape | Parses per-test JSON timings, aggregates per test/crate/shard, compares against pinned budgets and the previous ledger, and fails on regression beyond the threshold | +| Detailed design | Parses per-test JSON timings, aggregates per test/crate/shard, compares against pinned budgets and the previous ledger, and fails on regression beyond the threshold Primary reuse disposition: `adapt`. Preserved source-plan detail: share/adapt ADR046-delivery-007 timing-ledger implementation for this gate. | +| Integration | Consumed by wave entry/exit (`ADR-046-validation-and-delivery` §4/§10.16); `make test-rust` and Layer-1 shards run concurrently | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | Synthetic timing regression fails the gate; ledger output is deterministic and machine-readable | +| Adoption timing | Immediately | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-023 — Legacy-test retirement generator + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-023` | +| Tier | A | +| Observed friction evidence | D094: replaced behavior otherwise leaves old duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest entries running alongside their successors indefinitely | +| Desired behavior | A generator that, from each current-code migration work item's old-selector→disposition table, produces the retirement checklist and removal gate: which old test selectors/files/`tests/layer1-jobs.json` rows/closed gate manifests/flake-matrix-Nix-unit pins/generated ledgers/CI shards are deleted once successor coverage and removal proof pass, and asserts a retired selector is absent afterward | +| Destination | `packages/xtask/src/bin/legacy_test_retirement.rs` (`cargo run -p xtask -- legacy-test-retirement`) | +| Owner/dependencies | ADR046-streamline-008, ADR046-streamline-022 | +| Dependency/owner | ADR046-streamline-008, ADR046-streamline-022 | +| Current source | Current-code migration-map disposition rows and existing gate manifests; no legacy-test retirement generator exists yet | +| Reuse action | adapt | +| Implementation shape | Reads the migration map's disposition rows, cross-references the live `tests/layer1-jobs.json`/gate manifests, and emits the delete set plus an absence assertion; never deletes automatically — it produces the gated checklist and the failing test that proves incomplete retirement | +| Detailed design | Reads the migration map's disposition rows, cross-references the live `tests/layer1-jobs.json`/gate manifests, and emits the delete set plus an absence assertion; never deletes automatically — it produces the gated checklist and the failing test that proves incomplete retirement Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new generator reading existing migration rows and gate manifests without mutating them. | +| Integration | `make test-policy`/`make test-drift` row; wired to every current-code migration work item's removal proof | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | Fixture with a replaced behavior whose old selector still appears in `tests/layer1-jobs.json` fails; once removed, the absence assertion passes | +| Adoption timing | Immediately | +| Removal/supersession | None — net-new; no prior owner to remove | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-streamline-024 — Implementation-graph generator and duplicate-generator reconciliation + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-streamline-024` | +| Tier | A (shared prep) | +| Observed friction evidence | The implementation graph (D095) is hand-regenerated per wave; separate ad hoc generators exist for the spec-set manifest and the test-runtime ledger, so their output can drift from a single canonical path | +| Desired behavior | The future `xtask implementation-graph` subcommand that deterministically emits `docs/specs/ADR-046-implementation-graph.json` and its `.md` view from `ADR-046-spec-set.json` + `ADR-046-work-items.json` + the `ADR-046-validation-and-delivery` §3 wave topology (D095/D096/D097), preserving the current node/work-item counts and never overwriting with stale counts; and the reconciliation of the duplicate generators for the spec-set manifest and the test-runtime ledger to a single canonical `xtask` path | +| Destination | `packages/xtask/src/bin/implementation_graph.rs` (`cargo run -p xtask -- implementation-graph`); folds the spec-set and test-runtime emitters into the one `xtask` dispatch | +| Owner/dependencies | ADR046-streamline-001 | +| Dependency/owner | ADR046-streamline-001 | +| Current source | The graph is regenerated by hand today; `ADR-046-spec-set.json` and the test-runtime ledger (ADR046-delivery-007) each have their own emitter — duplicate-generator finding, retained here for reconciliation | +| Reuse action | create | +| Implementation shape | Reads the two manifests plus §3 wave topology, maps every member spec and every work item exactly once to a wave/parallel-group, emits typed edges and topological rank, renders the Mermaid/table `.md`, and is wired into the existing `gen-*` drift gate; the spec-set and test-runtime emitters become subcommands of the same `xtask` binary so there is one canonical generator, not three | +| Detailed design | Deterministic (sorted keys, no timestamps/host paths); regenerated after any spec or work-item change and after the two manifests; a drift gate runs it and `git diff --exit-code`. Retains the duplicate-generator findings for the spec-set manifest and the test-runtime ledger and reconciles them to the single `xtask` path rather than leaving three independent emitters Primary reuse disposition: `create`. Preserved source-plan detail: net-new generator that consumes the two manifests and the wave topology; folds the duplicate spec-set/test-runtime emitters into one canonical path. | +| Integration | `tests/unit/gates/` drift gate alongside `xtask spec-registry`; consumed by the ready-wave query in `ADR-046-validation-and-delivery` §3.5.1 | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `implementation_graph::every_spec_and_work_item_mapped_once`; `implementation_graph::acyclic_and_wave_monotonic`; `implementation_graph::parallel_groups_are_file_disjoint`; `implementation_graph_regenerates_clean`; duplicate-generator reconciliation asserts one canonical emitter path for spec-set and test-runtime | +| Adoption timing | Immediately after ADR046-streamline-001 | +| Removal/supersession | Supersedes the hand-regeneration and the separate spec-set/test-runtime emitters once the single `xtask` path is green | +| Removal proof | The ad hoc separate emitters for spec-set and test-runtime are removed after the single `xtask implementation-graph`/`spec-registry` dispatch reaches parity | diff --git a/docs/specs/ADR-046-telemetry-audit-and-support.md b/docs/specs/ADR-046-telemetry-audit-and-support.md new file mode 100644 index 000000000..8fcae078e --- /dev/null +++ b/docs/specs/ADR-046-telemetry-audit-and-support.md @@ -0,0 +1,2411 @@ +# ADR 0046 telemetry, audit, and support + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-telemetry-audit-and-support` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 2 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-telemetry`, `d2b-audit`, `d2b-provider-observability-otel`, `d2b zone` CLI | +| Depends on | `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-store-redb`, `ADR-046-componentsession-and-bus`, `ADR-046-core-controllers`, `ADR-046-components-processes-and-sandbox`, `ADR-046-provider-model-and-packaging` | +| Supersedes | Current `d2bd` hand-rolled Prometheus registry; current daemon/broker/gateway JSONL audit paths | + +## Terminology mapping: baseline names → v3 targets + +The pre-ADR45 v3 baseline uses the following names. This spec cites baseline +code with its actual names and explicitly states the v3 target name at each +design boundary. All claims about current behavior derive from the exact +baseline symbols named below. + +| Baseline name / symbol | v3 ADR 0046 target | Evidence class | +| --- | --- | --- | +| `RealmPath` (`d2b-realm-core/src/realm.rs`) | Zone name (string, name of `Zone/` self resource) | implemented-and-reachable | +| `RealmId` (`d2b-realm-core/src/ids.rs`) | Component of Zone name | implemented-and-reachable | +| `WorkloadId` (`d2b-realm-core/src/ids.rs`) | Opaque resource UID (for `Process`, `Guest`, or null) | implemented-and-reachable | +| `NodeId` (`d2b-realm-core/src/ids.rs`) | Not retained as a standalone audit field; resolved through `Host/` or `Guest/` resource references | implemented-and-reachable | +| `PrincipalId` (`d2b-realm-core/src/ids.rs`) | `subject_digest: sha256:` in v3 audit records | implemented-and-reachable | +| `AuditEnvelope.realm: RealmPath` | `zone: ` in v3 audit record | implemented-and-reachable | +| `AuditEnvelope.node: NodeId` | Not a standalone field; execution context resolved from resource | implemented-and-reachable | +| `AuditEnvelope.workload: WorkloadId` | Opaque resource UID (for Process under the operation, or null) | implemented-and-reachable | +| `AuditEnvelope.principal: PrincipalId` | `subject_digest: sha256:` | implemented-and-reachable | +| `AuditStreamKind::Daemon` | Zone-local audit stream (Zone runtime process) | implemented-and-reachable | +| `AuditStreamKind::Gateway` | ZoneLink-boundary audit stream (gateway-backed realm → ZoneLink) | implemented-and-reachable | +| `AuditStreamKind::RemoteNode` | RemoteZone audit stream (cross-Zone link) | implemented-and-reachable | +| `VmProcessDag` / `ProcessNode` / `ProcessRole` (`d2b-core/src/processes.rs`) | `Process` or `EphemeralProcess` resource; or fixed bootstrap; per role-disposition table in ADR-046-components-processes-and-sandbox | implemented-and-reachable | +| `ProcessRole::OtelHostBridge` | `Process` resource under `observability-otel` Provider | implemented-and-reachable | +| `RunnerRole::OtelHostBridge` (`d2b-contracts/src/broker_wire.rs`) | `Process` resource under `observability-otel` Provider; broker `SpawnRunner` becomes Provider supervisor ticket | implemented-and-reachable | +| `RunnerRole::CloudHypervisor`, `QemuMedia`, `Virtiofsd`, `Swtpm`, etc. | `Process` or `EphemeralProcess` under each VM/Device Provider; see ADR-046-components-processes-and-sandbox | implemented-and-reachable | +| `WorkloadIdentity` / `WorkloadTarget` / `RealmTarget` (`d2b-core/src/workload_identity.rs`) | Zone self-resource reference `Zone/` | implemented-and-reachable | +| `d2b.realms` Nix option (`nixos-modules/options-realms.nix`) | `d2b.zones` Nix option (ADR-only target) | generated-or-eval-contract | +| `realm-controllers.json` bundle artifact | Zone runtime config (new generated artifact; existing file is retired) | generated-or-eval-contract | +| `d2b_daemon_vm_*` metrics with `vm` label (`packages/d2bd/src/metrics.rs`) | `vm` label (VM name) removed from v3 metric labels; VM identity carried only in bounded OTEL resource attributes and permitted audit fields | implemented-and-reachable | +| `vm.name`, `vm.env`, `vm.role` OTEL resource attributes (`nixos-modules/components/observability/{host,stack,guest}.nix`) | Preserved in v3 (advisory from edge collector; re-stamped at ingress boundary). Extended with `d2b.zone`, `d2b.provider`, `d2b.component` (ADR-only additions) | implemented-and-reachable | +| `d2b.observability.vmName` / `identityName` Nix options | `d2b.zones..observability.*` Nix options (ADR-only target) | generated-or-eval-contract | +| `config_source = "realm-controllers"` tracing field (`d2b-priv-broker/src/runtime.rs`) | `config_source = "zone-config"` in v3 startup tracing | implemented-and-reachable | +| `d2b-clipd/src/audit.rs::AuditEvent.source_realm`, `.destination_realm` | `source_zone`, `destination_zone` (cross-Zone clipboard audit) | implemented-and-reachable | +| `kind = "unsafe-local"` workload (`nixos-modules/options-realms-workloads.nix:221,233`) | `Host/` resource — user-only, **no isolation boundary**; reconciled by `Provider/system-core` with `defaultDomain=user`, `allowedDomains=[user]`, `defaultUserRef=User/`; child processes use normal Process Providers; **not** a v3 Provider | implemented-and-reachable | +| `UnsafeLocalWorkloadsJson` / `UnsafeLocalWorkload` / `UnsafeLocalLauncherItem` (`packages/d2b-core/src/unsafe_local_workloads.rs`) | `Host` resource spec serialized in the private bundle; `UnsafeLocalWorkload.identity.runtime_kind = "unsafe-local"` / `provider_id = "unsafe-local"` → `Provider/system-core` catalog entry | implemented-and-reachable | +| `HelperRegistry` / `HelperConnection` / `dispatch_launch` (`packages/d2bd/src/unsafe_local_helper.rs`) | user-domain process supervision; `HelperRegistry::allowed_uids` → `defaultUserRef=User/` constraint; v3 replaces with normal Process Provider supervisor ticket | implemented-and-reachable | +| `DaemonToUnsafeLocalHelper` / `UnsafeLocalHelperToDaemon` / `HelperLaunchRequest` / `HelperShellRequest` (`packages/d2b-contracts/src/unsafe_local_wire.rs`) | internal launch/shell protocol between `d2bd` and the helper binary; retired in v3 when launch moves to Process Provider supervisor ticket | implemented-and-reachable | +| `d2b-unsafe-local-helper` binary (`packages/d2b-unsafe-local-helper/src/{main,protocol,runtime,systemd}.rs`) | fixed user-domain supervisor process; v3 equivalent is a user-domain `Process` under `Provider/system-core` | implemented-and-reachable | +| `nixos-modules/unsafe-local-workloads-json.nix` (`runtimeKind = "unsafe-local"`, `providerId = "unsafe-local"`) | Nix emitter for the private bundle artifact; v3 target is the `Provider/system-core` `Host` resource spec | generated-or-eval-contract | +| `nixos-modules/unsafe-local-helper.nix` (service unit for `d2b-unsafe-local-helper`) | fixed user-supervisor Nix unit; retired after Process Provider supervisor ticket migration | generated-or-eval-contract | + +## SDK placement: resolved + +Zone/core processes (Zone runtime, core-controller, mandatory Providers) use +**lightweight bounded emitters** — `tracing` + a bounded in-process ring — to +push telemetry frames over a private local Unix datagram socket. They carry no +`opentelemetry_sdk` or `opentelemetry-otlp` dependency. This matches the current +v3 baseline (`d2bd` uses only `tracing` crate; no OTEL SDK present at +`packages/d2bd/src/lib.rs` or `packages/d2b-priv-broker/src/runtime.rs`). + +`Provider/observability-otel` is an **ordinary optional Process** that runs the +full OTEL SDK with an OTLP/gRPC exporter. It owns the per-Zone OTLP receiver +socket, drains the emitter ring, and forwards to the SigNoz backend. Because it +is an optional non-bootstrap Process: + +- It does **not** count toward the mandatory ≤64 MiB core aggregate defined in + ADR-046-resource-store-redb. That budget is unchanged. +- Zone runtime and core-controller startup proceed before and without it. +- If the `observability-otel` Provider is absent, unready, or crashes: the + emitter ring fills, oldest frames are dropped, `d2b_telemetry_drop_total` + increments, and Zone/controller health transitions to `Degraded` (not + `Failed`). Authoritative audit is unaffected. + +Current-code evidence: `d2bd` uses `tracing` crate exclusively for structured +logging/tracing (`packages/d2bd/src/lib.rs` lines 720+, +`packages/d2b-priv-broker/src/runtime.rs` lines 34–35). No `opentelemetry_sdk` +crate exists in the v3 baseline. Hand-rolled Prometheus registry in +`packages/d2bd/src/metrics.rs` (no OTEL SDK). This resolved design requires no +ADR-046-resource-store-redb budget revision. + +## Separation invariant + +Telemetry (OTEL metrics/traces/logs) and authoritative audit are two distinct +subsystems with no shared writer path. + +- OTEL telemetry is performance and health observability. It is best-effort, + buffered, and lossy under back-pressure. No OTEL field carries event payload, + authorization decision text, resource spec/status bytes, argv, secrets, paths, + or subject names. OTEL data is exported through the `observability-otel` + Provider and is never an authz input. +- Authoritative audit is a durable tamper-evident record of security-relevant + decisions. Audit records must be committed before the operation they describe + completes. Audit is never a telemetry stream and never enters an OTEL + pipeline. +- OTEL spans and audit records share an opaque `operation_id` / `correlation_id` + for cross-system joining. Neither direction carries the other's payload. + +Both subsystems must fail safely and independently. OTEL unavailability never +blocks mutations, reconciliation, or process launch. Audit unavailability for +privileged records fails the operation closed; see durability class policy +below. + +## OTEL resource attributes + +### Current baseline attribute set (implemented-and-reachable) + +`packages/d2b-contract-tests/tests/policy_observability.rs::loki_native_otel_resource_attributes` +enforces a closed allowlist: + +``` +deployment.environment, host.name, service.name, service.namespace, +source, vm.env, vm.name, vm.role +``` + +Required keys: `service.name`, `vm.env`, `vm.name`, `vm.role`. + +These are stamped advisorily by each process/collector. The SigNoz OTel +Collector re-stamps them authoritatively at the trusted ingress boundary +(ADR 0026/0033 contract, preserved in v3). + +The current `vm.name` carries the VM name (from `d2b.vms.` in +`d2b_daemon_vm_*` metrics and the Nix `identityName`/`vmName` Nix options). +In v3, a VM (current `d2b.vms.`) whose execution is VM-backed becomes a +`Guest/` resource. VM names remain as advisory `vm.name` values because +this is an OTEL resource attribute, not a metric label. + +### v3 target attribute additions (ADR-only) + +The v3 `d2b-telemetry` crate extends the allowlist with these additional keys: + +| Attribute | Source | Values | +| --- | --- | --- | +| `d2b.zone` | Zone name string (matches `Zone/` self resource name) | Advisory; re-stamped at ingress | +| `d2b.provider` | Provider name (from closed Provider name catalog) | Provider processes only | +| `d2b.component` | Component ID (from signed component descriptor) | Controller/service/worker only | +| `service.version` | `CARGO_PKG_VERSION` | All processes | + +The existing `vm.name`/`vm.env`/`vm.role`/`host.name`/`service.name` +keys are **preserved unchanged** in the v3 allowlist. No key is removed. +No key outside the allowlist may be stamped by any v3 process; the +`policy_observability.rs::loki_native_otel_resource_attributes` test is +adapted to include the new keys. + +`d2b.zone` (Zone name) is allowed in resource attributes but **not** in +metric label values; see cardinality rules below. + +## Host resource (unsafe-local) posture requirements + +### Baseline identity + +The current `kind = "unsafe-local"` workload in `nixos-modules/options-realms-workloads.nix` is the +**only** current workload kind that runs as the authenticated user with no isolation boundary. +Its Nix description (line 233–235) reads: + +> `unsafe-local` — Host-user process runtime with no isolation boundary. Requires explicit realm policy opt-in. + +The Nix module explicitly records that `stateDir` and `runDir` are null for this kind (lines +264–275): there is no host VM state path and no `/run/d2b/vms/` runtime directory, because +user scopes are owned by the authenticated user's systemd manager. + +In v3, this maps to a `Host/` resource. It is: + +- **Not** a `Guest` (no VM/sandbox execution boundary). +- **Not** a v3 Provider. `Provider/system-core` is the reconciler, not the execution substrate. +- Reconciled by `Provider/system-core` with `defaultDomain=user`, `allowedDomains=[user]`, + `defaultUserRef=User/`. +- Child processes launched via normal Process Providers (`provider=system-core-user`, + `domain=user`). +- Explicitly no-isolation: user processes share the host UID, filesystem, and environment. + +The "no isolation boundary" is a **first-class semantic property** of the +user-only `Host` resource (the unsafe-local successor with +`defaultDomain=user`, `allowedDomains=[user]`), not an implementation gap or +transitional state. It does not apply to other `Host` ResourceType variants +that may carry different execution policies. v3 must make this explicit and +persistent in four surfaces. + +### Host resource status + +The `Provider/system-core` reconciler sets a non-negotiable `isolationPosture` +status field on every user-only `Host` resource at reconciliation time. +Operators cannot suppress or override it: + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Host", + "status": { + "phase": "Ready", + "isolationPosture": "none", + "isolationPostureMessage": "This host resource runs processes as the authenticated user with no isolation boundary. All child processes share the host user environment." + } +} +``` + +`isolationPosture: "none"` is set on user-only Host resources +(`defaultDomain=user`, `allowedDomains=[user]`). It is not set on Host +resources with other execution policies. Any resource status snapshot in a +support bundle must include this field for user-only Hosts; it is not redacted. + +### CLI/UI + +`d2b zone list` and `d2b zone inspect` must render a visible warning line for +any Zone containing a user-only `Host` resource +(`isolationPosture: "none"`). Example rendering: + +``` + Host/laptop-shell Ready ⚠ no isolation boundary (user domain) +``` + +The warning annotation must not be omittable via a flag or environment +variable. The warning text must not carry the resource name, user name, UID, +or executable path. Host resources without `isolationPosture: "none"` do not +emit this annotation. + +`d2b zone doctor` includes the following named check for each user-only `Host` +resource: + +| Check name | Pass condition | +| --- | --- | +| `isolation-posture-declared` | User-only `Host` resource status has `isolationPosture: "none"` set by the reconciler | + +### Audit: ProcessEffect for Host processes + +Every process launch and stop under a `Host` resource emits a `ProcessEffect` record. +The record carries `domain=user` and `no_isolation=true`: + +```json +{ + "record_class": "process-effect", + "process_effect_fields": { + "event": "launch|stop|adopt|quarantine", + "provider": "system-core-user", + "domain": "user", + "no_isolation": true, + "execution_ref_digest": "sha256:", + "process_uid": "", + "outcome": "ok|error", + "exit_class": "exited|signaled|killed|null" + } +} +``` + +`no_isolation: true` is set whenever `domain=user` and the parent `Host` +resource has `isolationPosture: "none"` (i.e., user-only Hosts). It is a +**required field** for these records; audit consumers must not need to join +the resource status to determine the isolation posture of a record. + +**Current gap**: The existing `DaemonEvent` enum in `packages/d2bd/src/daemon_audit.rs` has +no dedicated `unsafe-local` launch or stop audit event. The `HelperRegistry::dispatch_launch` +path (`packages/d2bd/src/unsafe_local_helper.rs`) does not emit a `DaemonEvent`. This gap +is a required correction in v3: every user-only Host process launch and stop +must emit a `ProcessEffect` record with `no_isolation: true`. + +### OTEL telemetry for Host processes + +Metric labels for Host processes: + +- `provider=system-core-user` (closed-set value; the controller handler name + `system_core_user` in the handler closed set maps to this label value) +- `domain=user` + +`no_isolation=true` is **not** a metric label. It is carried only in resource status and +audit records. It must not appear as a span attribute, log field, or metric label value. + +OTEL resource attributes for Host processes: + +- `service.name`: fixed name for the system-core Provider (e.g., `d2b-provider-system-core`) +- `d2b.zone`: Zone name (resource attribute; not a metric label) +- `d2b.provider`: `system-core` (closed-set provider name) +- `d2b.component`: controller/service component ID + +No `vm.name`, no user name, no UID, no argv, no path appears in resource attributes or +span attributes for Host processes. + +## Metrics + +### Cardinality rules + +Every metric descriptor MUST use only fixed semantic label keys from the +closed `METRIC_LABEL_POLICY` registry, and every value domain MUST be a closed +enum or an explicitly bounded semantic classifier from this spec. Free-form +resource identity is never a metric dimension. The structural policy lint +rejects descriptor keys that are absent from the registry before inspecting +any emitted value. + +The exact label keys `vm`, `zone`, `zone_id`, and `zone_uid` are +unconditionally forbidden. So are bare resource-kind identity keys such as +`network`, `credential`, `guest`, `host`, `user`, `volume`, `device`, and +`process`, plus every resource-name-derived key (`credential_name`, +`resource_name`, `network_name`, `link_name_hash`, any `*_name`, +`*_name_hash`, `*_name_digest`, or `*_uid`). Fixed semantic keys such as +`resource_type`, `provider`, `handler`, `operation`, `phase`, and `outcome` +remain allowed only with their closed value domains; for example, `provider` +identifies a fixed implementation class, never `Provider.metadata.name`. + +The following values are also unconditionally forbidden in metric labels: + +- VM names, Zone names, Provider resource names, and all resource names + (`metadata.name` values) — these appear only in bounded OTEL resource + attributes +- Zone/Provider/Process UIDs +- Host/Guest/User/Volume/Network/Device names +- Filesystem paths, socket paths, executable paths +- argv or environment values +- Status detail messages or outcome text beyond stable error codes +- Subject names or principal identifiers +- PID, pidfd, or cgroup path values +- Operation IDs or correlation IDs (allowed in trace span attributes, not metric labels) +- Endpoint addresses, port numbers, or IP addresses +- ResourceExport `exportKey` values, raw stream bytes, device serials, token + values, and any path/device/socket locator + +Zone identity remains available as the bounded `d2b.zone` OTEL resource +attribute. Removing identity labels MUST NOT remove that resource attribute or +the Zone/resource identity fields permitted by the audit contract. + +### Collector ingress enforcement + +`METRIC_LABEL_POLICY` is both a descriptor registry and a runtime admission +contract. The `observability-otel` collector MUST run one shared structural +validator over metrics from every ingress: compact frames on the Unix emitter, +OTLP on the private Unix socket, OTLP forwarded over vsock, and the D096 import +stream. Validation runs after bounded decode and trusted OTEL Resource stamping, +but before SDK aggregation, queue insertion, batching, retry, or export. It +parses descriptor labels plus every data-point and exemplar attribute map. + +The whole frame is rejected and dropped when a key is outside +`METRIC_LABEL_POLICY`; is exactly `vm`, `zone`, `zone_id`, `zone_uid`, +`credential_name`, `network`, `network_name`, or `link_name_hash`; ends in +`_name`, `_name_hash`, `_name_digest`, or `_uid`; or when a value equals a +trusted producer/resource `metadata.name`, UID, ResourceRef, or other +resource-identity canary. No adapter may enqueue an unchecked frame, and a +rejected OTLP frame cannot export its otherwise-valid siblings. + +Unix datagrams are dropped without a response. Stream ingress returns only +`invalid-telemetry-frame`; three violations quarantine the connection for at +most 30 seconds and set its credits to zero. Quarantine is in-memory, capped at +64 connections per Binding, and retains only an opaque connection handle, +expiry, ingress class, and the closed error class—not payload, rejected +key/value, or producer/resource identity. Structural validation remains ahead +of queue capacity during exporter backpressure; invalid frames never consume +queue or retry capacity. + +The collector reports only: + +```text +d2b_otel_ingress_policy_total{ + ingress = emitter_unix | otlp_unix | otlp_vsock | import_stream, + outcome = accepted | rejected | quarantined, + error_class = none | key_not_allowlisted | key_forbidden | + key_suffix_forbidden | value_identity | malformed | oversize +} +``` + +These fixed domains are themselves registered by `METRIC_LABEL_POLICY`. +`d2b_telemetry_drop_total` uses only the additional closed reasons +`policy_violation` and `ingress_quarantine`. No metric, span, log, status, +protocol error, or quarantine record echoes a forbidden key or value. Valid +identity remains only in allow-listed OTEL Resource attributes. Authoritative +audit remains a separate writer, payload, and export path. + +Note: the current `d2b_daemon_vm_*` metrics in `packages/d2bd/src/metrics.rs` +use `vm` labels with VM name values (e.g. labels `["vm", "state"]`, +`["vm", "outcome"]`, `["vm", "vmm", "outcome"]`, `["vm", "reason"]`). +These existing metrics are **not adopted into v3**. They are retained in d2bd +only until that daemon is superseded; v3 metrics carry no resource-name labels. + +### Standard instruments + +#### Zone runtime and resource store + +Target crate: `d2b-resource-store-redb` (ADR-only). Current analog: none; +hand-rolled Prometheus registry in `packages/d2bd/src/metrics.rs` covers +daemon-level VM lifecycle, not a generic resource store. + +| Metric | Type | Labels | Buckets (s) | +| --- | --- | --- | --- | +| `d2b_store_write_duration_seconds` | histogram | `kind={single,group}`, `outcome={ok,conflict,error}` | 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.5, 1.0 | +| `d2b_store_read_duration_seconds` | histogram | `op={get,list,scan}` | 0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1 | +| `d2b_store_group_commit_size` | histogram | (none) | 1, 2, 4, 8, 16, 32, 64 | +| `d2b_store_conflict_total` | counter | `resource_type` | — | +| `d2b_store_watch_active` | gauge | (none) | — | +| `d2b_store_revision` | gauge | (none) | — | +| `d2b_store_compaction_duration_seconds` | histogram | `outcome={ok,error}` | 0.01, 0.05, 0.1, 0.5, 1.0, 5.0 | +| `d2b_store_backup_duration_seconds` | histogram | `outcome={ok,error}` | 0.1, 0.5, 1.0, 5.0, 10.0, 30.0 | +| `d2b_store_queue_depth` | gauge | `queue={write,read}` | — | + +`resource_type` values come from the bound closed catalog short-name set. +Unknown or vendor-qualified types use the literal string `vendor`. + +#### Resource API + +Target crate: `d2b-resource-api` (ADR-only). No current analog. + +| Metric | Type | Labels | Buckets (s) | +| --- | --- | --- | --- | +| `d2b_api_request_total` | counter | `verb={get,list,watch,create,update-spec,update-status,update-metadata,update-finalizers,delete,use-credential,admin-credential}`, `resource_type`, `outcome={ok,conflict,invalid,denied,not_found,quota,error}` | — | +| `d2b_api_request_duration_seconds` | histogram | `verb`, `resource_type` | 0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.5 | +| `d2b_api_watch_active` | gauge | (none) | — | +| `d2b_api_admission_rejected_total` | counter | `reason={auth,quota,conflict,invalid,schema}` | — | + +#### d2b-bus + +Target crate: `d2b-bus` (ADR-only). Current analog: operation routing in +`d2b-realm-router/src/route_engine.rs` and `mux_session.rs` +(implemented-but-unwired as a generic bus). + +| Metric | Type | Labels | Buckets (s) | +| --- | --- | --- | --- | +| `d2b_bus_route_total` | counter | `service`, `direction={local,host,guest,zone_link}`, `outcome={ok,denied,not_found,error}` | — | +| `d2b_bus_route_duration_seconds` | histogram | `service`, `direction` | 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1 | +| `d2b_bus_session_active` | gauge | `transport={unix,vsock,zone_link}` | — | + +`service` values are names from the closed bound service package catalog. +`direction=zone_link` replaces the current `AuditStreamKind::Gateway` / +`RemoteNode` distinction: gateway-backed realms (current `EntrypointMode::GatewayBacked` +in `d2b-realm-core/src/realm.rs`) become `ZoneLink`-connected Zones. + +#### ComponentSession + +Target crate: `d2b-session` (copied/adapted from main `a1cc0b2d` per +ADR-046-componentsession-and-bus). Current v3 analog: +`d2b-realm-router/src/secure_session.rs` and +`d2b-realm-router/src/mux_session.rs` (implemented-but-unwired for +generic ComponentSession). + +| Metric | Type | Labels | Buckets (s) | +| --- | --- | --- | --- | +| `d2b_session_connect_total` | counter | `profile={NN,KK,IKpsk2}`, `purpose_class={local,enrolled,bootstrap}`, `outcome={ok,auth,transcript,policy,timeout,error}` | — | +| `d2b_session_reconnect_total` | counter | `outcome={ok,error,abandoned}` | — | +| `d2b_session_record_total` | counter | `direction={send,recv}`, `kind={control,ttrpc,stream,attachment}` | — | +| `d2b_session_active` | gauge | `transport={unix,vsock,zone_link}` | — | + +#### Core controller + +Target crate: `d2b-core-controller` (ADR-only). Current analog: `d2bd` +provides daemon-level VM lifecycle metrics through the hand-rolled registry +in `packages/d2bd/src/metrics.rs` (implemented-and-reachable), but carries +no per-handler controller metrics. + +Key current metrics that inform bucket design: + +- `d2b_daemon_broker_request_duration_seconds`: `BROKER_REQUEST_BUCKETS_SECONDS = + [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]` +- `d2b_daemon_activation_phase_duration_seconds`: `ACTIVATION_PHASE_BUCKETS_SECONDS = + [0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 30.0, 120.0, 600.0]` + +| Metric | Type | Labels | Buckets (s) | +| --- | --- | --- | --- | +| `d2b_controller_reconcile_total` | counter | `handler`, `outcome={ok,requeue,conflict,error}` | — | +| `d2b_controller_reconcile_duration_seconds` | histogram | `handler`, `outcome` | 0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.5, 2.0 | +| `d2b_controller_queue_depth` | gauge | `handler` | — | +| `d2b_controller_hint_to_handler_seconds` | histogram | `handler` | 0.001, 0.002, 0.005, 0.010, 0.015, 0.020, 0.030, 0.050 | +| `d2b_controller_watch_revision_lag` | gauge | `handler` | — | + +`handler` values are the closed set defined in ADR-046-core-controllers: +`configuration`, `api_catalog`, `authz`, `provider`, `controller_registration`, +`ownership`, `watch_maintenance`, `ephemeral_cleanup`, `zone_link`, `budget`, +`store_lifecycle`, `system_core_host`, `system_core_user`. + +`d2b_controller_hint_to_handler_seconds` measures the interval from durable +store commit to the first instruction of the matching controller handler. The +p95 hard target is ≤5 ms per ADR 0046. + +#### ResourceExport and ResourceImport controller (D096) + +Target crate: `d2b-core-controller` export/import controller. Metrics count +state and outcomes only; Zone names, resource names, payload bytes, paths, +device identifiers, tokens, and `exportKey` values are forbidden labels. + +| Metric | Type | Labels | Buckets (s) | +| --- | --- | --- | --- | +| `d2b_resource_export_state` | gauge | `exported_type`, `arbitration={exclusive,shared,multiplexed}`, `state={advertised,ready,revoking,degraded}` | — | +| `d2b_resource_import_state` | gauge | `projection_type`, `state={pending,reachable,bound,degraded,revoked}` | — | +| `d2b_resource_export_consumers` | gauge | `exported_type`, `state={active,pending}` | — | +| `d2b_resource_share_lease_total` | counter | `operation={admit,revoke,reconnect}`, `arbitration`, `outcome={ok,denied,quota,timeout,cancel,revoked,error}` | — | + +`exported_type` and `projection_type` use the closed ResourceType catalog short +name; vendor-qualified types collapse to `vendor`. + +#### Process Providers + +Target crates: `d2b-provider-system-minijail` and `d2b-provider-system-systemd` +(ADR-only). Current analog: `d2bd` hand-rolled metrics for VM lifecycle: + +- `d2b_daemon_vm_start_duration_seconds`: `VM_START_BUCKETS_SECONDS = + [0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 30.0, 60.0, 120.0, 300.0]` + labels `["vm", "outcome"]` — current `vm` label carries a VM name + (current `d2b.vms.` → target `Guest/` or `Host/`) +- `d2b_daemon_vm_shutdown_duration_seconds`: `VM_SHUTDOWN_BUCKETS_SECONDS`, + labels `["vm", "vmm", "outcome"]` — `vmm` is the current `RunnerRole` + (`CloudHypervisor`, `QemuMedia`), renamed to `provider` in v3 +- `d2b_daemon_vm_degraded` labels `["vm", "reason"]` +- `d2b_daemon_pidfd_table_size` — adapts to `d2b_process_pidfd_active` + +v3 replaces all `vm`-name labels with closed-set `provider` and `domain` labels: + +| Metric | Type | Labels | Buckets (s) | +| --- | --- | --- | --- | +| `d2b_process_launch_total` | counter | `provider={minijail,systemd}`, `domain={system,user}`, `outcome={ok,error,quota}` | — | +| `d2b_process_launch_duration_seconds` | histogram | `provider`, `domain` | 0.001, 0.005, 0.010, 0.015, 0.020, 0.030, 0.050, 0.1, 0.5, 2.0 | +| `d2b_process_active` | gauge | `provider`, `domain` | — | +| `d2b_process_restart_total` | counter | `provider`, `class={exited,signaled,killed}` | — | +| `d2b_process_adoption_total` | counter | `provider`, `outcome={ok,quarantine,error}` | — | +| `d2b_process_pidfd_active` | gauge | (none) | — | + +`d2b_process_launch_duration_seconds` measures from the instant the +`Process` resource commits to `Ready` to the instant the first OS spawn +call (clone3 or systemd unit start) is issued. The p95 hard target is +≤20 ms per ADR 0046. + +Current `d2b_daemon_vm_shutdown_duration_seconds` maps to a new +`d2b_process_stop_duration_seconds` histogram with labels +`provider`, `stop_class={graceful,forced}`, `outcome`. + +#### Provider (all Provider processes) + +Target crates: individual Provider crates (ADR-only). + +| Metric | Type | Labels | Buckets (s) | +| --- | --- | --- | --- | +| `d2b_provider_reconcile_total` | counter | `resource_type`, `outcome={ok,requeue,conflict,error}` | — | +| `d2b_provider_reconcile_duration_seconds` | histogram | `resource_type` | 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 2.0 | +| `d2b_provider_component_phase` | gauge | `component_type={controller,service,worker}`, `phase={pending,ready,degraded,failed,unknown}` | — | + +#### Telemetry subsystem self-metrics + +| Metric | Type | Labels | Buckets | +| --- | --- | --- | --- | +| `d2b_telemetry_drop_total` | counter | `signal={metric,trace,log}`, `reason={buffer_full,export_error,policy_violation,ingress_quarantine}` | — | +| `d2b_telemetry_export_total` | counter | `signal`, `outcome={ok,error}` | — | +| `d2b_otel_ingress_policy_total` | counter | `ingress={emitter_unix,otlp_unix,otlp_vsock,import_stream}`, `outcome={accepted,rejected,quarantined}`, `error_class={none,key_not_allowlisted,key_forbidden,key_suffix_forbidden,value_identity,malformed,oversize}` | — | +| `d2b_audit_write_total` | counter | `record_class`, `outcome={ok,rate_limited,error}` | — | +| `d2b_audit_drop_total` | counter | `record_class={privileged,unprivileged}` | — | + +## Traces + +### Trace context + +`packages/d2b-realm-core/src/trace_context.rs` implements `TraceContext` +(implemented-and-reachable): + +- opaque bounded printable-ASCII `trace_id` and `span_id` fields; +- maximum field length: 64 bytes (`MAX_TRACE_FIELD_LEN`); +- validated constructor `TraceContext::new` — returns `None` on invalid + tokens; +- redacted in all Debug output; +- serialized by `d2b-realm-codec-protobuf/src/lib.rs` (`encode_trace_context`, + `decode_trace_context`) over the current constellation protobuf codec. + +v3 target: extract `TraceContext` unchanged to `d2b-telemetry`. Adapt the +protobuf codec to the v3 resource API contract framing. The current +`traceparent` field in `d2b-realm-core` (referenced from +`packages/d2bd/src/usbip_reconcile_state.rs` and `typed_error.rs`) is +preserved and extended to carry the OTEL W3C `traceparent` format. + +`TraceContext` is carried in every d2b-bus route request and every +ComponentSession operation. The existing `AuditEnvelope.trace: +Option` field (implemented-and-reachable in +`d2b-realm-core/src/audit.rs`) is preserved in v3 audit records. + +### Span catalog + +Every span carries standard `SpanKind`, the `d2b.zone` resource attribute, +an `outcome` attribute set at span end, and no path/name/argv/credential/ +PID attribute. `operation_id` and `correlation_id` are allowed as span +attributes (they are opaque digests). + +| Span name | Kind | Attributes | Notes | +| --- | --- | --- | --- | +| `d2b.store.write` | Internal | `kind`, `group_size`, `revision`, `outcome` | Per write transaction | +| `d2b.store.read` | Internal | `op`, `resource_type`, `outcome` | Per read transaction | +| `d2b.store.compaction` | Internal | `segments_removed`, `outcome` | | +| `d2b.api.request` | Server | `verb`, `resource_type`, `operation_id`, `outcome` | | +| `d2b.api.watch.event` | Internal | `resource_type`, `event_kind`, `outcome` | | +| `d2b.bus.route` | Client | `service`, `method`, `direction`, `outcome` | | +| `d2b.session.handshake` | Server | `profile`, `purpose_class`, `outcome` | | +| `d2b.session.reconnect` | Internal | `generation`, `outcome` | | +| `d2b.controller.reconcile` | Internal | `handler`, `resource_type`, `generation`, `outcome` | | +| `d2b.controller.hint` | Consumer | `handler`, `resource_type`, `hint_kind` | Commit → handler start | +| `d2b.process.launch` | Internal | `provider`, `domain`, `outcome` | Commit-to-Ready → launch attempt | +| `d2b.process.stop` | Internal | `provider`, `domain`, `stop_class`, `outcome` | | +| `d2b.provider.reconcile` | Internal | `resource_type`, `outcome` | Per Provider instance | +| `d2b.provider.install` | Internal | `provider_class`, `outcome` | Fixed implementation class, not Provider resource identity | + +### Trace context propagation + +Resource API requests entering via d2b-bus carry an incoming W3C `traceparent` +header. The request creates a child span, stores the resulting `TraceContext` +in the operation record (adapts existing `AuditEnvelope.trace` field in +`d2b-realm-core/src/audit.rs`), and propagates it to: + +- the store write transaction span; +- the post-commit controller hint; +- the controller reconcile span; +- any downstream Process launch span. + +A complete request-to-launch trace spans: +`API request → store write → controller hint → controller reconcile → process launch`. + +Cross-Zone operations propagate trace context through `ZoneLink` cursor +operations (adapts existing `d2b-realm-core/src/routing.rs` route propagation). + +## Logs + +### Structured OTEL log records + +Each component emits structured OTEL log records for lifecycle transitions. +Records use `tracing` macros (already present in d2bd: +`packages/d2bd/src/lib.rs` and `packages/d2b-priv-broker/src/runtime.rs`). +The existing policy `startup_tracing_avoids_host_path_fields` in +`packages/d2b-contract-tests/tests/policy_observability.rs` is extended to +all v3 component startup paths. + +Current startup tracing fields (implemented-and-reachable): + +```rust +// packages/d2b-priv-broker/src/runtime.rs lines 689–717 +config_source = "realm-controllers", // → v3: "zone-config" +config_present = true, +``` + +v3 target: these fields are preserved with `config_source = "zone-config"` to +match the renamed artifact. + +Forbidden log body content (extends current policies): + +- Raw provider error strings +- Resource names, paths, PIDs, argv, or environment values +- Credential bytes or digests in non-audit context +- Terminal bytes or Wayland buffer content +- `RealmPath` or `WorkloadId` string values in log fields (old names not + leaked into v3 log bodies) + +Required structured log events per component are identical to those specified +in the prior version of this spec (Zone startup, configuration publication, +controller start/stop, Provider install/ready/failed, Process launch/stop, +Session handshake failures, audit segment rotation, telemetry buffer events). + +### journald stdout/stderr ingestion + +The current `scrapeJournal` option in `nixos-modules/components/observability/host.nix` +(implemented-and-reachable) collects journald entries for the host. The v3 +`observability-otel` Provider adapts this to follow per-Zone cgroup entries: + +- cgroup filter: `z-/*` (Zone runtime processes, from + ADR-046-components-processes-and-sandbox naming) +- execution filter: `s-/*` (Process/EphemeralProcess cgroup + leaves) +- disabled by default in the Provider spec; requires explicit operator consent + +The collector applies a redaction filter before forwarding: drops `MESSAGE` +bodies matching credential/secret/path patterns, drops `_CMDLINE`, `_EXE`, +`INVOCATION_ID` fields. Retains `_SYSTEMD_CGROUP`, `PRIORITY`, `SYSLOG_IDENTIFIER`, +and structured `KEY=VALUE` pairs from the declared allow-set. + +## Private OTEL endpoints + +### Emitter architecture (resolved) + +Zone runtime, core-controller, and all other core processes use a +**lightweight bounded emitter** from `d2b-telemetry`. The emitter: + +- uses `tracing` + `tracing-subscriber` (already present in `d2bd`/broker) + for structured log/span capture; +- serializes metric increments and span events into compact frames; +- writes frames over a private Unix datagram socket to the `observability-otel` + Provider process; +- relies on the collector's mandatory structural metric admission at every + Unix-emitter, OTLP-Unix, OTLP/vsock, and import-stream ingress before any + batching or export; emitter-side validation is defense in depth, never a + substitute for the collector gate; +- holds a bounded in-process ring (default 4 MiB metrics, 4 MiB traces, 2 MiB + logs per process — configurable via the observability-otel Provider spec); +- drops oldest frames on ring-full, incrementing `d2b_telemetry_drop_total`. + +No `opentelemetry_sdk` or `opentelemetry-otlp` dependency is added to any +Zone/core crate. + +`Provider/observability-otel` runs the full OTEL SDK with an OTLP/gRPC exporter +as an **ordinary optional Process**. It: + +- is not bootstrap; +- does not count toward the mandatory ≤64 MiB core aggregate (ADR-046-resource-store-redb unchanged); +- drains frames from the per-Zone datagram socket and forwards via OTLP to the SigNoz backend. + +If the `observability-otel` Provider is absent, unready, or restarts: emitter +ring fills, frames are dropped with `d2b_telemetry_drop_total` increments, Zone +and core-controller health transitions to `Degraded` (not `Failed`). Zone/controller +startup and all authoritative audit are completely unaffected. + +### Current architecture (implemented-and-reachable) + +The current v3 baseline provides: + +- `nixos-modules/components/observability/host.nix`: host-side OTel Collector + writing to `${otelRuntimeDir}/host-egress.sock` (Unix UDS), collecting + host metrics and tailing `storeSyncExportDir/*.jsonl` audit export. +- `nixos-modules/components/observability/stack.nix`: SigNoz obs VM with + ClickHouse + ClickHouse Keeper + SigNoz + SigNoz OTel Collector. +- `nixos-modules/components/observability/guest.nix`: per-VM guest OTel + Collector with `vm.name`/`vm.env`/`vm.role` resource attributes. +- `packages/d2b-host/src/otel_host_bridge_argv.rs`: argv generator for the + `RunnerRole::OtelHostBridge` socat-based vsock OTLP forwarder + (broker-spawned, `ProcessRole::OtelHostBridge` in + `packages/d2b-core/src/processes.rs`). +- `packages/d2bd/src/otel_host_bridge_readiness.rs`: readiness gate for the + `RunnerRole::OtelHostBridge` runner. + +In v3 `ProcessRole::OtelHostBridge` / `RunnerRole::OtelHostBridge` maps to a +`Process` resource under the `observability-otel` Provider. The socat-based +vsock forwarding path (`otel_host_bridge_argv.rs`) is replaced by a native +OTLP/gRPC-over-vsock transport owned by the Provider. + +### v3 per-Zone datagram receiver (ADR-only) + +The `observability-otel` Provider owns the per-Zone telemetry socket. + +On Provider installation, the observability-otel controller creates: + +``` +$ZONE_STATE/telemetry/emitter.sock +``` + +This Unix datagram socket receives compact telemetry frames from core process +emitters. Owner: the `observability-otel` collector process UID. Mode: `0660`. +Group: a generated `d2b--otel-writers` group containing all component +UIDs for that Zone. ACL pattern follows `nixos-modules/components/observability/ +host.nix`'s `otelRuntimeDir` ACL setup (`setfacl -m u::--x` on the parent, +`d:u::rw` default ACL). + +A second socket `$ZONE_STATE/telemetry/otlp.sock` receives OTLP/gRPC from any +process that embeds the full SDK (e.g., Provider processes that opt in). + + + +### Forwarding to obs Zone + +Adapts the current vsock forwarding approach from `otel_host_bridge_argv.rs` +and the current `nixos-modules/components/observability/stack.nix` pipeline to +use native OTLP/gRPC over vsock. The SigNoz stack is unchanged; only the +forwarding transport changes. The current Nix ingress source model +(`ingressSources` in `stack.nix`) is preserved with a per-Zone entry replacing +the per-VM entry. + +### `sys-obs` ResourceExport/ResourceImport model (D096) + +One observability Zone (`sys-obs`) owns the SigNoz ingest authority. Its +`Provider/observability-otel` advertises a local telemetry-ingest `Endpoint` +through `ResourceExport`; every other Zone opts in with a local +`ResourceImport` that creates one local telemetry route/projection. The result is +many-to-one multiplexed ingest with per-import quota, credits/backpressure, +schema validation, redaction, and cardinality enforcement before data reaches +SigNoz. The import never receives a SigNoz socket, token, or FD; bytes traverse +only the bounded encrypted named stream defined by the ZoneLink session. + +Authoritative audit remains Zone-local. Exporting audit copies to `sys-obs` or +another Zone requires a separate explicit `ResourceExport`/`ResourceImport` +whose payload is a copy only and transfers no audit authority, writer role, or +authorization decision surface. + +## Nix configuration and resource bundle + +### Nix resource authoring shape + +All Zone resources are authored through one uniform option: + +```nix +d2b.zones..resources. = { + type = "..."; + spec = { ...exact ResourceType spec fields... }; +}; +``` + +`metadata.name` is derived from the `` attribute key; `metadata.zone` +from the `` attribute key; `apiVersion` defaults to `"resources.d2bus.org/v3"`. Nix +authors may also provide `metadata.ownerRef` and presentation-only +`metadata.labels`. They do **not** author `status`, `uid`, `revision`, +`finalizers`, `metadata.managedBy`, or `metadata.configurationGeneration` — +these fields are managed exclusively by the core runtime and publication +handler. + +`spec` contains exactly the canonical ResourceType spec fields. No second Nix +vocabulary renames or re-nests them. The `resources` module is schema-aware: +for a known `type`, `spec.*` option types, defaults, and documentation are +generated from the committed `ResourceTypeSchema` JSON for that type. + +**Zone self resource** (`` = ``; `spec.telemetry` and `spec.audit` +are Zone ResourceTypeSchema fields owned by this spec): + +```nix +d2b.zones.work.resources.work = { + type = "Zone"; + # Optional: metadata.ownerRef and presentation metadata.labels may be set. + # metadata.managedBy and metadata.configurationGeneration are NOT authored here. + spec = { + telemetry.emitter.ringCapacityBytes = 4194304; # default 2097152 + audit = { + retentionDays = 30; # range 1..3650 + maxSegmentBytes = 67108864; # range 1 MiB..1 GiB + }; + }; +}; +``` + +**Provider resource** — `spec` fields are defined by the Provider +ResourceTypeSchema in **ADR-046-provider-model-and-packaging**; `spec.config` +sub-fields are generated from the signed Provider schema for the installed +package (selected via `spec.artifactId`). This spec governs the +`observability-otel` Provider's placement as an optional non-bootstrap Process; +it does not redefine the Provider spec schema. +Secrets must be `Credential/` refs; secret values are never inlined: + +```nix +d2b.zones.work.resources.observability-otel = { + type = "Provider"; + # spec fields: see ADR-046-provider-model-and-packaging for exact schema. + # config.* options generated from the Provider's signed resourceTypeSchema. + spec = { + # ... Provider spec fields per ADR-046-provider-model-and-packaging + # artifactId selects the catalog entry / package. + # config.signozBackend.credentialRef = "Credential/signoz-api-key"; + }; +}; +``` + +**Credential resource** — `spec` fields are defined by the Credential +ResourceTypeSchema in the owning primitive spec +(**ADR-046-primitive-resource-composition**); this spec does not redefine that +schema. Credential secret values are injected at runtime via the credential +store and never written to the bundle or Nix store: + +```nix +d2b.zones.work.resources.signoz-api-key = { + type = "Credential"; + # spec fields: see ADR-046-primitive-resource-composition for exact schema. + spec = { + # ... Credential spec fields per ADR-046-primitive-resource-composition + }; +}; +``` + +### Canonical ResourceSpec JSON shapes + +The `nix build` derivation for a Zone emits one sorted integrity-pinned bundle. +All keys are sorted canonically (alphabetically at every level) in the emitted +file. + +The bundle contains **input** records: only the fields Nix authors and the +derivation produce. Core-set fields (`uid`, `revision`, `generation`, +`finalizers`, `managedBy`, `configurationGeneration`) are absent from the bundle +file; the configuration service populates them when persisting resources at +activation. + +**Zone self resource — Nix-rendered bundle input** (telemetry/audit fields +owned by this spec): + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { + "name": "work", + "zone": "work" + }, + "spec": { + "audit": { + "maxSegmentBytes": 67108864, + "retentionDays": 30 + }, + "telemetry": { + "emitter": { "ringCapacityBytes": 4194304 } + } + }, + "type": "Zone" +} +``` + +**Zone self resource — persisted record after activation** (core-set fields +shown; `status` is a separate read-only sub-document): + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { + "configurationGeneration": 1, + "finalizers": [], + "generation": 1, + "managedBy": "configuration", + "name": "work", + "ownerRef": null, + "revision": 1, + "uid": "01960000-0000-7000-8000-000000000000", + "zone": "work" + }, + "spec": { + "audit": { + "maxSegmentBytes": 67108864, + "retentionDays": 30 + }, + "telemetry": { + "emitter": { "ringCapacityBytes": 4194304 } + } + }, + "type": "Zone" +} +``` + +**Provider/observability-otel resource — Nix-rendered bundle input** — `spec` +fields defined in **ADR-046-provider-model-and-packaging**; secret values never +appear in the bundle: + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { + "name": "observability-otel", + "zone": "work" + }, + "spec": { + "...": "see ADR-046-provider-model-and-packaging for exact Provider spec schema" + }, + "type": "Provider" +} +``` + +**Credential/signoz-api-key resource — Nix-rendered bundle input** — `spec` +fields defined in **ADR-046-primitive-resource-composition**; secret values +injected at runtime via the credential store and never written to the bundle or +Nix store: + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "metadata": { + "name": "signoz-api-key", + "zone": "work" + }, + "spec": { + "...": "see ADR-046-primitive-resource-composition for exact Credential spec schema" + }, + "type": "Credential" +} +``` + +For Provider and Credential resources the persisted record follows the same +pattern as the Zone example above: core sets `uid`, `revision`, `generation`, +`finalizers`, `managedBy = "configuration"`, and `configurationGeneration` at +activation. + +### NixOS eval and build validation + +**Eval-time (NixOS module assertions)**: + +1. `type` must be a registered ResourceType known to the `resources` module; + unknown `type` values fail at eval with an unknown-type message. +2. `spec.*` option types, defaults, and docs are generated from the committed + `ResourceTypeSchema` for the declared `type`. For Provider resources, + `spec.config.*` options are generated from the signed Provider schema + for the package identified by `spec.artifactId`. Unknown `spec` fields are + rejected at eval time by the generated option set. +3. `credentialRef` values must match `^Credential/[a-z][a-z0-9-]*$` and the + referenced resource must be declared as a `type = "Credential"` entry in + `d2b.zones..resources`. +4. Numeric bounds, enum values, and required fields are enforced by the + generated option types — the same constraints embedded in the + `ResourceTypeSchema`. No hand-written bespoke assertions duplicate these + constraints. +5. Resource `` attribute keys are validated against `^[a-z][a-z0-9-]*$`. +6. Two `resources.` entries with the same `type` and `` within one + Zone fail with a duplicate-name assertion. +7. Any bare host path, PID, UID number, argv string, or secret token in a + `spec` leaf triggers an assertion failure. Secrets must be Credential refs. + +**Build-time (`resources-bundle` derivation)**: + +`nixos-modules/resources-bundle.nix` (ADR-only target): + +1. Serializes each resource to canonical sorted JSON. +2. Validates each resource against its `ResourceTypeSchema`: core schemas for + Zone/Credential/primitive types; the signed schema embedded in the Provider + package for Provider resources. +3. Validates Provider `spec.config` against the exact signed Provider schema + for the package identified by `spec.artifactId`. Schema mismatch or unknown + config fields fail the build. +4. Asserts no resource `spec` field contains a bare secret, host path, argv + token, or UID string (pattern-checked against the forbidden-field set — + same set used by `policy_observability.rs::startup_tracing_avoids_host_path_fields` + in current baseline `packages/d2b-contract-tests/tests/`). +5. Computes a SHA-256 digest for each resource spec (deterministic canonical + bytes). +6. Sorts resources by `(type, name)` and computes the generation digest as + SHA-256 of the newline-separated sorted digest list. +7. Emits one `zone-resources-.json` bundle in the Nix store; its store + path is the integrity pin (content-addressed). + +**Runtime activation (core-controller)**: + +1. Reads the bundle from the Nix store path embedded in the system closure + (same `git+file://` integrity guarantee used by existing `d2b_flake_ref` in + `tests/lib.sh`). +2. Re-validates Provider package identity (per ADR-046-provider-model-and-packaging + package identity contract) against the installed Provider package. +3. Resolves each `credentialRef` to a live credential store entry; fails closed + if any ref is unresolved. +4. Checks name/domain/owner/bounds conflicts with existing resources in the Zone + store. +5. Validates the Provider schema matches the installed package schema. +6. Any validation failure → reject the new generation; retain the prior + generation; emit a `generation-rejected` audit record with a closed-enum + `reason` field. + +No validation step falls back to a partial activation. + +## Authoritative audit + +### Current implementation (implemented-and-reachable) + +The v3 baseline has three separate audit implementations: + +1. **Daemon JSONL audit** (`packages/d2bd/src/daemon_audit.rs`): hash-chain + JSONL with `prev_hash`/`record_hash` (SHA-256), daily date-segmented files + `daemon-events-YYYY-MM-DD.jsonl`, structured `DaemonEvent` variants + covering VM start/stop/degraded, DetachedExec, Shell, OtelHostBridge. + +2. **Broker audit** (`packages/d2b-priv-broker/src/audit.rs`): rate-limited + O_APPEND JSONL; `AuditWriteClass::{Privileged,Unprivileged}`; + `AuditDropSummary` counting `privileged_rate_limited` / + `unprivileged_rate_limited`; `DEFAULT_AUDIT_WRITES_PER_SECOND = 4096`; + `OpAuditRecord` variants per broker op + (`packages/d2b-priv-broker/src/ops/audit_op.rs`). + +3. **Gateway/realm-core audit** (`packages/d2b-gateway/src/audit.rs`, + `packages/d2b-gateway-runtime/src/audit_jsonl.rs`): `GatewayAuditKind` + events + `AuditEnvelope` (from `d2b-realm-core/src/audit.rs`) with fields + `realm: RealmPath`, `node: NodeId`, `workload: Option`, + `principal: PrincipalId`, `scope: AuthorizationScope`, `decision: + AuthzDecision`, `trace: Option`. + +4. **Realm-core chain types** (`packages/d2b-realm-core/src/audit.rs`): + `AuditHash`, `AuditChainLink`, `AuditChainRecord` (with `realm: RealmPath`, + `node: NodeId` fields), `AuditStreamKind::{Gateway, RemoteNode, Daemon}`, + `AuditSinkHealth`, `AuditRetentionFloorStatus`. + +### v3 target audit record types + +The `d2b-audit` crate (ADR-only new crate) provides unified hash-chain JSONL +adapted from the implementations above. Every record carries: + +```json +{ + "ts_ms": 1234567890123, + "schema_version": 1, + "zone": "", + "record_class": "", + "operation_id": "", + "correlation_id": "", + "trace_id": "", + "source": "", + "prev_hash": "sha256:", + "record_hash": "sha256:", + "_fields": { ... } +} +``` + +`zone` is the Zone name (baseline: `RealmPath` → target: Zone name). +`operation_id` and `correlation_id` are the same opaque-digest types from +`d2b-realm-core/src/ids.rs` (`OperationId`, `CorrelationId`), retained +unchanged. `trace_id` is the `TraceContext.trace_id` field from +`d2b-realm-core/src/trace_context.rs`, adapted to the v3 contract. + +#### ResourceMutation + +```json +{ + "record_class": "resource-mutation", + "resource_mutation_fields": { + "verb": "create|update-spec|update-status|update-metadata|update-finalizers|delete|use-credential|admin-credential", + "resource_type": "", + "resource_uid": "", + "generation": 12, + "expected_revision": 7, + "resulting_revision":8, + "subject_digest": "sha256:", + "policy_revision": 3, + "outcome": "ok|conflict|denied|invalid|error", + "error_code": "" + } +} +``` + +`subject_digest` is SHA-256 of the normalized canonical subject string from +the v3 `AuthenticatedSubjectContext` (ADR-046-componentsession-and-bus); this +replaces the current `PrincipalId` in `AuditEnvelope`. No resource name, spec +bytes, or status bytes appear in this record. + +#### ResourceUpgrade (D091) and expedited reconcile (D090) + +An `assess_update`/`plan_upgrade`/`execute_upgrade` operation emits a +`resource-upgrade` record; an authorized expedited (`waitForReconcile`) mutation +is recorded via the existing `resource-mutation` record extended with an +`expedited: true` flag and its `operation_id`. Neither carries spec/status bytes, +secrets, or raw artifact paths — only bounded closed-enum currency/disruption +values and opaque generation/digest IDs: + +```json +{ + "record_class": "resource-upgrade", + "resource_upgrade_fields": { + "verb": "assess|plan|execute", + "resource_type": "", + "resource_uid": "", + "update_state": "Current|UpdateAvailable|UpgradeRequired|Upgrading|Blocked|Unknown", + "disruption": "None|Reload|Restart|Recycle|Replace", + "preserve_state": true, + "reasons": [""], + "observed_generation": 11, + "target_generation": 12, + "affected_owned_count": 3, + "operation_id": "", + "outcome": "ok|blocked|conflict|denied|error", + "error_code": "" + } +} +``` + +The core Operation ledger owns upgrade idempotency/progress; this record is the +authoritative security history of the upgrade decision/execution, not a second +ledger. OTEL metrics for currency/upgrade and expedited reconcile use only +bounded closed-enum labels (`update_state`, `disruption`, `disposition`, +`outcome`) plus the existing `zone`/`provider`/`component` resource attributes — +never resource names, generation digests as labels, or per-operation IDs as +labels (cardinality rules below). + +#### RBACChange + +```json +{ + "record_class": "rbac-change", + "rbac_change_fields": { + "verb": "create|update-spec|delete", + "resource_type": "Role|RoleBinding", + "resource_uid": "", + "generation": 4, + "subject_digest": "sha256:", + "policy_revision":3, + "outcome": "ok|denied|error" + } +} +``` + +#### SessionConnect + +```json +{ + "record_class": "session-connect", + "session_connect_fields": { + "event": "connect|reconnect|close", + "profile": "NN|KK|IKpsk2", + "purpose_class": "local|enrolled|bootstrap", + "transport_class": "unix|vsock|zone_link", + "subject_digest": "sha256:", + "authz_decision": "allowed|denied", + "authz_revision": 7, + "session_gen_digest": "sha256:", + "outcome": "ok|auth|policy|timeout|error", + "error_code": "" + } +} +``` + +Adapts current `GatewayAuditEvent` (from `d2b-gateway/src/audit.rs`) and +`AuditEnvelope.scope = AuthorizationScope::capability(Capability::WindowForwarding)` +pattern to cover all ComponentSession connections. `transport_class=zone_link` +covers what the current `AuditStreamKind::Gateway` and `AuditStreamKind::RemoteNode` +streams recorded. + +#### RouteAdmission + +```json +{ + "record_class": "route-admission", + "route_admission_fields": { + "service": "", + "method": "", + "direction": "local|host|guest|zone_link", + "subject_digest": "sha256:", + "authz_decision": "allowed|denied", + "authz_revision": 7, + "outcome": "ok|denied|error" + } +} +``` + +#### ResourceShare (D096) + +ResourceExport/ResourceImport advertise/admit/revoke/reconnect decisions emit a +bounded resource-share audit record. The event carries only the local Zone from +the envelope, the peer Zone, a closed capability subset, and the outcome. It +never carries payload bytes, paths, device identifiers, tokens, raw stream data, +ResourceType/resource names, arbitration internals, or `exportKey`. + +```json +{ + "record_class": "resource-share", + "resource_share_fields": { + "event": "advertise|admit|revoke|reconnect", + "peer_zone": "", + "capability_subset": [""], + "outcome": "ok|denied|quota|revoked|degraded|error" + } +} +``` + +#### BrokerEffect + +```json +{ + "record_class": "broker-effect", + "broker_effect_fields": { + "op_class": "", + "subject_digest": "sha256:", + "execution_context_digest": "sha256:", + "resource_context_digest": "sha256:", + "outcome": "ok|denied|error", + "error_code": "" + } +} +``` + +Adapts existing `OpAuditRecord` from +`packages/d2b-priv-broker/src/ops/audit_op.rs`. No raw paths, device +identifiers, or broker operation arguments. Current `SwtpmDirAudit` fields +(`base_dir_hash`, `result`, `mode`, `owner_uid`, `marker_result`) are +preserved by encoding them into `resource_context_digest` plus a +`swtpm_dir_fields` sub-object that carries the closed-set enums without paths. + +#### ProcessEffect + +```json +{ + "record_class": "process-effect", + "process_effect_fields": { + "event": "launch|stop|adopt|quarantine", + "provider": "minijail|systemd|system-core-user", + "domain": "system|user", + "no_isolation": false, + "execution_ref_digest": "sha256:", + "process_uid": "", + "outcome": "ok|error", + "exit_class": "exited|signaled|killed|null" + } +} +``` + +`no_isolation: true` is set when `domain=user` and the parent resource is a +user-only `Host` (i.e., `isolationPosture: "none"`). It is `false` for all +other process effects. The field is always present. +`provider=system-core-user` is the closed-set label for user-only Host child +processes. + +Adapts current `DaemonEvent::{VmStartRunnerExit, RunnerExitKind, VmShutdown}` variants +from `packages/d2bd/src/daemon_audit.rs`. Current `RunnerExitKind::{Exited, +Signaled, Killed}` maps directly to `exit_class`. No PID, pidfd, argv, +environment, or unit name; current `VmShutdownProvider::{CloudHypervisor, +QemuMedia, Unknown}` maps to the closed `provider` enum. + +**Current gap** (unsafe-local): the existing `HelperRegistry::dispatch_launch` path +in `packages/d2bd/src/unsafe_local_helper.rs` does not emit a `DaemonEvent` for +unsafe-local launches or stops. In v3 every user-only Host process launch and +stop must emit a `ProcessEffect` record with `no_isolation: true`, `domain=user`, +`provider=system-core-user`. + +#### StateReset + +```json +{ + "record_class": "state-reset", + "state_reset_fields": { + "scope": "zone|provider|host|guest", + "trigger": "operator|upgrade|corruption|emergency", + "generation": 5, + "prior_digest": "sha256:", + "outcome": "ok|error" + } +} +``` + +### AuditStreamKind v3 mapping + +| Current (`AuditStreamKind`) | v3 target | Notes | +| --- | --- | --- | +| `Daemon` | `Zone` (Zone-local) | Zone runtime owns this stream | +| `Gateway` | `ZoneLink` | Gateway-backed realms become ZoneLink-connected Zones | +| `RemoteNode` | `RemoteZone` | Cross-Zone audit stream over remote Zone link | + +The `AuditChainRecord` type (current: `realm: RealmPath`, `node: NodeId`) is +re-versioned in v3 with `zone: String` replacing `realm` and `node` dropped. + +### Durability classes + +| Class | Records | Durability | Failure policy | +| --- | --- | --- | --- | +| Privileged | ResourceMutation (RBAC verbs), RBACChange, SessionConnect (auth-failure/denial), ResourceShare (`admit`/`revoke`), StateReset | Durable before operation completes | Fail operation with `audit-unavailable` | +| Standard | ResourceMutation (non-RBAC), RouteAdmission, ResourceShare (`advertise`/`reconnect`), ProcessEffect | Durable within bounded window | Log warning; metric increment; continue | +| Best-effort | BrokerEffect (informational), telemetry-self records | Async append | Drop under rate-limit; no operation impact | + +Privileged records are never rate-limited. Unprivileged records follow the +existing `DEFAULT_AUDIT_WRITES_PER_SECOND = 4096` rate limit from +`packages/d2b-priv-broker/src/audit.rs` (`AuditWriteClass::Unprivileged` path). + +### Segmentation and retention + +Segments rotate at 64 MiB or UTC midnight (whichever first). Segment names: +`audit-.jsonl` (adapts the 20-digit format from ADR +0045 v3 audit journal). Files are immutable after rotation. Default retention: +30 days (adapts `DEFAULT_GATEWAY_AUDIT_RETENTION_DAYS = 14` from +`packages/d2b-gateway-runtime/src/audit_jsonl.rs`; extended for the more +authoritative Zone audit). + +### Export + +`d2b zone audit export [--zone ] [--after ] [--before ]` + +- Invokes exactly `d2b.audit.v3.AuditService/Export` and requires the + `audit-export` session verb (admin-only). +- The grant is session-only: it implies no `get`, `list`, or other Zone + resource authority. The service itself performs the bounded authoritative + segment read after method admission. +- Adapts `ExportBrokerAuditOk` response contract from + `packages/d2b-priv-broker/tests/broker_export_audit.rs`. +- Hash chain breaks reported inline in output stream. +- No plaintext resource names, paths, argv, or credential bytes. + +The canonical Role rule is: + +```yaml +resourceTypes: [Zone] +verbs: [] +sessionVerbs: [audit-export] +subresources: [d2b.audit.v3.AuditService/Export] +resourceNames: [] +zones: [] +executionRefs: [] +``` + +## Doctor and support bundles + +### `d2b zone doctor [--zone ] [--json]` + +Read-only aggregate health report. Adapts `doctor::render_summary` from +`packages/d2bd/src/lib.rs` (referenced in +`packages/d2b/tests/host_doctor_contract.rs`). Extends `d2b host doctor +--read-only` behavior to the Zone resource API. + +Current doctor probes (implemented-and-reachable): `broker_ready`, per-check +`status`/`data`, `summary`, `exitCode`, plus probes redirected via +`D2B_BROKER_SOCKET`, `D2B_PUBLIC_SOCKET`, `D2B_DAEMON_STATE_DIR`, +`D2B_METRICS_URL`, `D2B_MANIFEST_PATH`. The `audit_check.rs` pattern +(`packages/d2bd/src/audit_check.rs`) provides the `defects` array for audit +chain validation. + +v3 JSON envelope: + +```json +{ + "zone": "", + "zone_phase": "Pending|Ready|Degraded|Failed|Unknown", + "store_health": { "phase": "…", "revision": 12345, "compaction_floor": 100, "watch_active": 3 }, + "controllers": [{ "handler": "…", "phase": "…", "queue_depth": 0, "last_reconciled_at": "…" }], + "providers": [{ "provider": "…", "phase": "…", "component_phases": {} }], + "process_counts": { "active": 4, "failed": 0 }, + "audit": { "phase": "ok|rate_limited|unavailable", "segments": 3, "drop_privileged": 0, "drop_total": 2 }, + "telemetry": { "phase": "ok|buffering|unavailable", "drop_total": 0 }, + "checks": [{ "name": "…", "status": "ok|warn|error", "detail": "…" }], + "summary": { "ok": 12, "warn": 1, "error": 0 } +} +``` + +Named check set: `store-revision-monotonic`, `controller-all-ready`, +`mandatory-providers-ready`, `audit-sink-healthy`, `otel-sink-reachable`, +`schema-catalog-consistent`, `watch-quota-headroom`, `audit-hash-chain-clean`, +`isolation-posture-declared`. + +Forbidden: resource names, paths, argv, credential bytes, PID values, audit +record content, raw error messages beyond stable codes. + +### `d2b zone support-bundle [--zone ]` + +Bounded redacted diagnostic snapshot for operator/support use. Requires +the `support-bundle` session verb (admin-only) on exactly +`d2b.support.v3.SupportService/GenerateBundle`. The grant is session-only and +implies no resource `get`/`list` authority; the admitted service performs its +own bounded internal reads. Output is NDJSON. + +```yaml +resourceTypes: [Zone] +verbs: [] +sessionVerbs: [support-bundle] +subresources: [d2b.support.v3.SupportService/GenerateBundle] +resourceNames: [] +zones: [] +executionRefs: [] +``` + +Contents: + +1. Doctor output. +2. Bounded resource status snapshots: last 32 per ResourceType, 512 total; + only `metadata` (UID, zone, generation, revision, timestamps, phase) and + top-level `status` fields (phase, conditions, observedGeneration, outcome + code). No `spec` content; no resource names beyond opaque UID. +3. Controller checkpoint / queue-depth snapshots. +4. Schema catalog inventory: ResourceType names + versions only. +5. Audit segment inventory: filenames (date-derived), sizes, record counts, + drop totals per class. +6. OTEL collector metrics summary (if observability-otel Provider is available). +7. Bounded structured log ring: last 2000 structured log records already + emitted and redacted by the Zone runtime and core-controller. + +## Error, failure, and outage behavior + +### OTEL collector unavailable + +When the per-Zone emitter socket (`emitter.sock`) is absent or the +`observability-otel` Provider process is not running: + +1. Emitter ring fills; oldest frames dropped in FIFO order. +2. `d2b_telemetry_drop_total` increments per dropped frame. +3. All mutations, reconciliation, and process launch continue normally. +4. Doctor: `telemetry: { "phase": "buffering" }`. +5. Zone/core-controller health: `Degraded` (not `Failed`). Zone startup is not + blocked. +6. `observability-otel` Provider: `Degraded` phase with `telemetry-export-unavailable`. +7. On socket recovery: emitter resumes draining ring in FIFO order. + +### Audit sink unavailable + +When the audit segment file cannot be opened or written: + +1. Privileged records: fail the operation with stable error code `audit-unavailable`. +2. Standard records: log warning via `tracing::warn!`, increment + `d2b_audit_drop_total{record_class="standard"}`, continue. +3. Best-effort records: increment counter, drop. +4. Doctor: `audit: { "phase": "unavailable" }`. +5. Rate-limited records: privileged records are never rate-limited (adapts + existing `AuditWriteClass::Privileged` invariant from + `packages/d2b-priv-broker/src/audit.rs`). + +On audit file descriptor loss (rotation/restart): Zone runtime re-opens the +segment file with `O_APPEND | O_CREAT` before the next write, mirroring the +existing `packages/d2bd/src/daemon_audit.rs` file-open pattern. + +### Zone store quarantined + +During store quarantine (per ADR-046-resource-store-redb): + +1. Emitter ring fills; frames dropped (observability-otel Provider stopped with store). +2. Committed audit segments are preserved. +3. Doctor available via read-only quarantine snapshot. +4. Support bundle available from quarantine snapshot. +5. Doctor: `zone_phase: "Failed"`, condition `store-quarantined`. + +### Controller handler degraded/failed + +A single handler degrading does not prevent other handlers, OTEL export, +audit writes, or doctor/support-bundle reads. The affected handler retries +with bounded backoff; a `StateReset` audit record is emitted if owned +resources are cleared during recovery. + +### Process Provider failed + +Marks affected `Process` resources `Degraded`/`Failed`. Emits +`ProcessEffect{event:"quarantine"}`. OTEL metrics reflect phase change via +`d2b_provider_component_phase`. Other Zones/Providers continue. + +### Support bundle during Zone failure + +A bundle request during `zone_phase: "Failed"` returns available data from +the quarantine snapshot, adds `bundle_completeness: "partial"` at the top +level, and exits with code 1. + +## Configuration-owned resource cleanup contract + +### Configuration management metadata + +The core distinguishes configuration-authored resources from controller-created +and API-created resources using `metadata.managedBy` and +`metadata.configurationGeneration`. These fields are set by the core at +activation or create time; Nix authors and the bundle derivation never produce +them. + +- `metadata.managedBy = "configuration"` — resource was last written by the + Nix configuration publication service; it owns the spec and is the only + value on which the cleanup contract acts. +- `metadata.configurationGeneration = ` — the monotonically increasing + generation count at which this resource was last written by configuration + publication. +- `metadata.managedBy = "controller"` — resource was created by a controller + (e.g., a `Process` child created by the `observability-otel` Provider + lifecycle controller). +- `metadata.managedBy = "api"` — resource was created or last written through + the Zone API (e.g., by an operator tool or integration). + +The configuration publication service issues Delete only on resources where +`managedBy = "configuration"`. Resources with `managedBy = "controller"` or +`managedBy = "api"` are never touched by configuration cleanup, regardless of +whether they are absent from the new bundle. + +**Never use presentation labels as cleanup authority.** Labels may be set by +Nix authors for presentation/organization purposes but must not be used to +determine which resources to delete or retain during generation activation. + +### New generation activation algorithm + +The configuration publication handler (per ADR-046-core-controllers +§Configuration publication) follows this algorithm on new bundle arrival: + +1. **Validate** the new bundle (all eval, build, and runtime checks from the + previous section). Reject with a `generation-rejected` audit record on any + failure; retain the prior active generation. +2. **Stage** new and updated resources in inactive store transactions. Resources + are not yet visible to controllers. +3. **Atomic swap**: install the new bundle generation digest and switch resource + watchers to the staged versions in one store transaction. Returns immediately; + does not block on cleanup of removed resources. +4. **Trigger** affected Providers/controllers/resources via the reconciliation + hint system (ADR-046-resource-reconciliation). +5. **Schedule Delete** for each resource with `managedBy=configuration` present + in the prior generation but absent from the new bundle. Each scheduled Delete + sets `metadata.deletionRequestedAt` on the resource and adds a + `deletion-pending` condition (`status: "True"`, `reason: "ConfigRemoved"`). + No phase change is applied to the resource being deleted. +6. **Set condition** `pending-cleanup` (`status: "True"`) on the Zone resource + status while any such Delete has not completed. Zone phase moves to + `Degraded` (not `Failed`). +7. **Retain prior generations** count-based: default 3 prior generations + retained (range 1..16). Pruning of a prior generation is permitted only after + all cleanup from that generation is complete and the count limit is exceeded. + No time-based TTL or rollback window. + +### Cleanup ordering and safety + +**Deletion ordering**: core drives deletions in reverse-dependency (child-first) +order via the owner-child index. When a resource's `metadata.deletionRequestedAt` +is set, each configuration-managed resource that has owned children receives a +`deletion-requested` hint to its controller. The controller works through its +children and clears its own finalizer when done. After all finalizers clear, +core performs one atomic store transaction that writes the `Deleted` +revision/change event and removes the resource row and all index entries. The +audit subsystem then appends the deletion audit record from that committed +revision with dedup/exactly-once recovery. No controller needs to implement its +own deletion ordering. + +**The cleanup contract never**: +- Deletes resources with `managedBy = "controller"` or `managedBy = "api"` + solely because they are absent from the new bundle. Owner controllers + reconcile and delete their own children; API-managed resources are owned by + their creator. +- Deletes audit segment files, OTEL emitter ring state, or any non-resource + filesystem artifact. Audit data is governed exclusively by `retentionDays`, + `maxSegmentBytes`, and durability-class rules. +- Touches resources in other Zones. +- Applies broad `chmod`, `chown`, `setfacl`, or path sweeps — consistent with + the ADR 0034 no-broad-sweep invariant. + +**observability-otel Provider cleanup sequence**: + +When `Provider/observability-otel` is removed from Nix config: + +1. Config-publication sets `metadata.deletionRequestedAt` on + `Provider/observability-otel` and adds a `deletion-pending` condition + (`status: "True"`, `reason: "ConfigRemoved"`). Zone phase moves to + `Degraded`; Zone condition `pending-cleanup` is added. +2. The Provider lifecycle controller receives the `deletion-requested` hint; + adds `deleting-children` condition (`status: "True"`) to its own status. +3. Controller deletes owned `Process` (collector) and `Volume` (emitter socket + directory) children in dependency order. +4. After all children are deleted, the controller clears its own finalizer. +5. Core performs final deletion as one atomic transaction: emits + `ResourceMutation{event="deleted", trigger="config-cleanup"}` audit record + and removes the `Provider/observability-otel` row and all index entries. +6. Zone status: condition `telemetry-export-unavailable` is set; emitter ring + fills; `d2b_telemetry_drop_total` increments; Zone phase remains `Degraded`. +7. `pending-cleanup` condition is cleared when step 5 completes. + +The emitter socket is never removed while core emitter processes are still +writing to it: the `Volume` child is deleted only after the collector `Process` +has stopped and its finalizer clears. + +### Cleanup status, audit, and error surface + +**Zone resource status during cleanup** (Zone itself carries `pending-cleanup`): + +```json +{ + "status": { + "observedGeneration": 3, + "phase": "Degraded", + "conditions": [ + { + "lastTransitionTime": "...", + "message": "1 configuration-managed resource pending deletion: Provider/observability-otel", + "reason": "ConfigRemovedResources", + "status": "True", + "type": "pending-cleanup" + }, + { + "lastTransitionTime": "...", + "message": "telemetry export unavailable; emitter ring filling", + "reason": "ObservabilityProviderDeleted", + "status": "True", + "type": "telemetry-export-unavailable" + } + ] + } +} +``` + +**Resource being deleted** (`Provider/observability-otel` persisted record during +deletion; no phase change — `deletion-pending` and `deleting-children` are +typed conditions): + +```json +{ + "metadata": { + "deletionRequestedAt": "2026-07-22T21:00:00Z", + "managedBy": "configuration", + "name": "observability-otel", + "zone": "work" + }, + "status": { + "conditions": [ + { + "lastTransitionTime": "...", + "reason": "ConfigRemoved", + "status": "True", + "type": "deletion-pending" + }, + { + "lastTransitionTime": "...", + "reason": "AwaitingChildDeletion", + "status": "True", + "type": "deleting-children" + } + ] + }, + "type": "Provider" +} +``` + +**Cleanup stall detection**: if a scheduled Delete has not completed within +30 minutes, condition `cleanup-stalled` is added with the resource name and +stall duration. Zone phase remains `Degraded`. A `HealthCondition` audit record +(best-effort durability class) is emitted with a closed-enum stall reason +(`finalizer-holder-gone`, `controller-unresponsive`, `child-deletion-failed`). + +**Audit records for cleanup events**: + +| Event | Record class | Durability | +| --- | --- | --- | +| New generation accepted and staged | `ResourceMutation{event="generation-staged"}` | standard | +| Generation atomically activated | `ResourceMutation{event="generation-activated", bundle_digest="sha256:..."}` | standard | +| Generation rejected (validation failure) | `ResourceMutation{event="generation-rejected", reason=""}` | standard | +| Config-removed resource Delete scheduled | `ResourceMutation{event="delete-scheduled", trigger="config-cleanup"}` | standard | +| Config-removed resource Delete completed | `ResourceMutation{event="deleted", trigger="config-cleanup"}` | standard | +| Cleanup stall detected | `HealthCondition{type="cleanup-stalled", reason=""}` | best-effort | + +No resource name, path, argv, or secret value appears in any cleanup audit +field. The `reason` on `generation-rejected` uses only closed-enum values: +`schema-validation-failed`, `package-identity-mismatch`, `credential-unresolved`, +`conflict-detected`, `bounds-violation`. + +### Prior generation retention + +Prior generations are retained count-based (default 3, range 1..16). There is +no time-based TTL or rollback window. Activating an older generation counts as +a new activation: resources absent from the activated bundle but present in the +replaced generation receive Delete; resources present in both are reconciled. + +## Tests + +### Metric inventory policy tests + +Adapt `packages/d2b-contract-tests/tests/policy_metrics.rs`: + +- Assert every metric declared in this spec is present in the closed + `METRIC_INVENTORY` table in its owning crate. +- Parse every v3 `MetricDescriptor` and assert every label key/value domain is + present in the closed `METRIC_LABEL_POLICY` registry. +- Assert the exact keys `vm`, `zone`, `zone_id`, and `zone_uid` are absent from + every v3 descriptor. +- Assert resource-name-derived keys are absent, including exact regression + cases `credential_name`, `network`, `network_name`, and `link_name_hash`, and + reject the structural suffixes `*_name`, `*_name_hash`, `*_name_digest`, and + `*_uid`. +- Feed a canary `metadata.name` value through every metric emitter fixture and + assert it is absent from every emitted label value while `d2b.zone` remains + present in the OTEL resource-attribute fixture. +- Assert `d2b_controller_hint_to_handler_seconds` bucket list includes a + 5 ms bucket (0.005). +- Assert `d2b_process_launch_duration_seconds` bucket list includes a 20 ms + bucket (0.020). +- Assert the old `d2b_daemon_vm_state` metric's `vm` label is not present + in any v3 metric descriptor. + +### Cardinality / redaction static lint + +New test `packages/d2b-contract-tests/tests/policy_telemetry_redaction.rs`: + +- Port and extend `startup_tracing_avoids_host_path_fields` from + `packages/d2b-contract-tests/tests/policy_observability.rs` to all v3 + component startup paths (Zone runtime, core-controller, all Provider + processes). +- Scan all `tracing::` and `span!`/`event!` call sites for forbidden field + names: `path`, `socket`, `argv`, `env`, `pid`, `exe`, `realm`, `workload_id` + (old baseline names must not leak). +- Assert `TraceContext` fields only accessed via the validated constructor + `TraceContext::new`. +- Assert no `format!("{}", ...)` of a `PathBuf` appears as a span attribute. +- Assert the OTEL resource attribute initialization uses only the allowlist + from this spec (extends `loki_native_otel_resource_attributes` test from + `packages/d2b-contract-tests/tests/policy_observability.rs`). +- Parse the metric descriptor registry structurally and fail any label key not + declared by `METRIC_LABEL_POLICY`; grep-only checks are insufficient. +- Assert `config_source = "realm-controllers"` string literal does not appear + in any v3 component source; only `config_source = "zone-config"` is allowed. + +### Audit record shape tests + +New `packages/d2b-audit/tests/`: + +- `audit_record_hash_chain`: adapted from existing hash-chain test pattern + in `packages/d2b/tests/audit_contract.rs`; verifies `prev_hash` / + `record_hash` chain and truncation detection. +- `audit_record_schema`: deserialize every record class from fixture JSON; + verify `realm`, `node`, `workload_id` field names are absent (old baseline + names must not appear in v3 schema); verify `zone` is present. +- `audit_segment_rotation`: adapted from rotation logic tests in + `packages/d2b-gateway-runtime/src/audit_jsonl.rs`. +- `audit_rate_limit_privileged_never_dropped`: adapted from + `AuditWriteClass::Privileged` invariant in + `packages/d2b-priv-broker/src/audit.rs`. +- `audit_unavailable_blocks_privileged`: verifies `audit-unavailable` error. + +### Doctor contract tests + +New `packages/d2b/tests/zone_doctor_contract.rs`: + +- Adapts test scaffold from `packages/d2b/tests/host_doctor_contract.rs` + (env-redirect sandbox: `D2B_PUBLIC_SOCKET`, `D2B_DAEMON_STATE_DIR`, etc.). +- Asserts JSON envelope fields from this spec; asserts `zone_phase` field. +- Asserts old fields `broker_ready` (from current doctor) are renamed or absent. +- Asserts no resource names, paths, argv, PIDs in any check detail. +- Exit 0 on all-ready fixture; non-zero on degraded; exit code 1 on quarantine. +- Doctor works when OTEL socket absent (`telemetry.phase = "unavailable"`). +- Doctor works when Zone store is in quarantine mode. +- `isolation-posture-declared` check passes when a user-only `Host` resource + status carries `isolationPosture: "none"`; fails when the field is absent. +- Zone with no user-only `Host` resource: `isolation-posture-declared` check + is omitted from the check list (not reported as an error). + +### Host posture tests + +New `packages/d2b-provider-system-core/tests/host_posture_contract.rs`: + +- `host_status_carries_no_isolation`: reconcile a user-only `Host` resource + fixture (`defaultDomain=user`, `allowedDomains=[user]`); assert + `status.isolationPosture = "none"` is set by the reconciler; assert no + operator-supplied `isolationPosture` value is accepted; assert a Host + resource with a different execution policy does not have `isolationPosture` + set. +- `host_process_effect_carries_no_isolation_flag`: drive a user-only Host + process launch through the audit sink fixture; assert the emitted + `ProcessEffect` record has `no_isolation: true`, `domain=user`, + `provider=system-core-user`; assert `no_isolation` is absent (or `false`) + in ProcessEffect records for non-user-only-Host processes. +- `host_cli_renders_isolation_warning`: drive `d2b zone inspect` output for a + Zone containing a user-only `Host` resource; assert the warning annotation + (`⚠ no isolation boundary`) is present in the output; assert the warning is + not suppressed by any flag; assert a Zone with only non-user-only Host + resources does not emit this annotation. +- `host_otel_no_no_isolation_label`: assert that no span attribute, log field, or + metric label carries the key `no_isolation`; `no_isolation` is audit-and-status only. +- `unsafe_local_launch_emits_audit_record`: drive the unsafe-local launch path + (adapting `packages/d2bd/src/unsafe_local_helper.rs` `dispatch_launch` call site) + and assert a `ProcessEffect{no_isolation:true}` record is emitted; this test + documents the current gap in baseline `DaemonEvent` coverage. + +### Support bundle tests + +New `packages/d2b/tests/zone_support_bundle_contract.rs`: + +- All content sections from this spec are present. +- Resource status snapshots contain no `spec` bytes and no `metadata.name`. +- `bundle_completeness: "complete"` on healthy Zone; `"partial"` on quarantined. + +### Performance histogram tests + +In the performance benchmark fixture (per ADR-046-resource-store-redb): + +- `hint_to_handler_latency`: drive 1000 committed resources; assert p95 + `d2b_controller_hint_to_handler_seconds` ≤5 ms. +- `commit_to_launch_latency`: drive 100 concurrent ready Process resources; + assert p95 `d2b_process_launch_duration_seconds` ≤20 ms. + +### OTEL endpoint tests + +New `packages/d2b-provider-observability-otel/tests/`: + +- `emitter_socket_receive`: lightweight emitter writes frames to the per-Zone + datagram socket; Provider drains and forwards; assert spans arrive at the + mock OTLP sink with correct `d2b.zone` resource attribute and no forbidden + labels. +- `ingress_metric_policy`: run one table-driven corpus through the Unix + emitter, OTLP Unix, OTLP/vsock, and D096 import-stream adapters. Every adapter + rejects exact keys `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, + `network`, `network_name`, `link_name_hash`, structural suffixes + `*_name`/`*_name_hash`/`*_name_digest`/`*_uid`, and + `metadata.name`/UID/ResourceRef/resource-identity canary values before queue + insertion or batching. Assert no valid sibling of a rejected frame exports. +- The same ingress test asserts exact bounded outcome/error-class domains, no + rejected key/value echo, datagram drop, stream quarantine and bounds, zero + import credits while quarantined, policy-before-capacity under backpressure, + valid-frame passage, OTEL Resource identity preservation, and audit + separation. +- `emitter_ring_drains_on_socket_available`: emitter writes frames before socket + exists; socket appears; assert buffered frames arrive in FIFO order. +- `emitter_ring_drop_on_overflow`: fill ring past capacity; assert + `d2b_telemetry_drop_total` increments; assert Zone/controller health is + `Degraded` (not `Failed`); assert Zone startup is not blocked. +- `no_vm_label_in_metrics`: assert no emitted frame carries a label named + `vm` with a resource-name value; assert old `d2b_daemon_vm_state` metric + shape is absent from all v3 collectors. +- `zone_startup_proceeds_without_provider`: drive Zone bootstrap with + observability-otel Provider absent; assert Zone reaches `Ready`; assert + `d2b_telemetry_drop_total` > 0; assert no audit records are affected. + +### Export audit tests + +New `packages/d2b-audit/tests/export_audit.rs` (adapts +`packages/d2b-priv-broker/tests/broker_export_audit.rs` pattern): + +- Admin-only `audit-export` verb is enforced. +- NDJSON output only. +- Hash chain breaks reported inline. +- No `realm`, `node`, `workload_id` field names in exported records. +- No resource name, path, or argv appears in exported records. + +### Nix configuration and resource bundle tests + +New `tests/unit/nix/cases/resources-bundle-telemetry.nix` (nix-unit eval +cases; auto-discovered; adapts the existing eval-case pattern from +`tests/unit/nix/eval-cases/`): + +- `eval_rejects_unknown_type`: set `d2b.zones.work.resources.foo.type = "Unknown"`; + assert NixOS eval fails with an unknown-ResourceType message. +- `eval_rejects_invalid_emitter_ring_size`: set + `d2b.zones.work.resources.work.spec.telemetry.emitter.ringCapacityBytes = 0`; + assert NixOS eval fails with a schema-range violation. +- `eval_rejects_unknown_provider_config`: set an unknown key in + `d2b.zones.work.resources.observability-otel.spec.config`; assert eval + fails with an unknown-field message (schema-generated option set rejects it). +- `eval_rejects_inline_secret_in_config`: set a `spec.config` leaf to a raw + secret string matching the forbidden-field pattern; assert eval fails. +- `eval_rejects_unresolved_credential_ref`: set `spec.config.signozBackend.credentialRef = + "Credential/nonexistent"` without a matching `resources.nonexistent` Credential + entry; assert eval fails with a missing-credential assertion. +- `eval_rejects_duplicate_resource_name`: declare two `resources` entries of + type `Provider` with the same `` attr key; assert eval fails. + +New `packages/d2b-provider-observability-otel/tests/bundle_contract.rs`: + +- `bundle_is_sorted_canonically`: render a two-resource bundle; assert JSON + keys at every level are in ascending alphabetical order; assert resources + are sorted by `(type, name)`. +- `bundle_digest_is_deterministic`: render the same config twice from + independent evaluation; assert the generation digest embedded in the + `metadata.configurationGeneration` token round-trips identically. +- `bundle_contains_no_secret_values`: set a `credentialRef`; assert the + rendered bundle JSON contains no key named `secretValue`, `password`, + `token`, `key`, or any value matching a secret pattern heuristic. +- `bundle_schema_validates_against_provider_schema`: assert the rendered + `Provider/observability-otel` spec fields validate against the JSON Schema + produced by the declared Provider package's `resourceTypeSchema` output. + +### Configuration-owned cleanup contract tests + +New `packages/d2b-core-controller/tests/config_cleanup.rs`: + +- `managedby_configuration_set_on_activated_resources`: activate a bundle; + assert every resource in the Zone store carries + `metadata.managedBy = "configuration"` and a non-zero + `metadata.configurationGeneration`. +- `controller_created_resources_have_managedby_controller`: Provider lifecycle + controller creates a child `Process`; assert the child carries + `metadata.managedBy = "controller"` and no `managedBy = "configuration"`. +- `absent_resource_receives_delete_on_new_generation`: activate a bundle with + `Provider/observability-otel`; activate a new bundle without it; assert + `Provider/observability-otel` has `metadata.deletionRequestedAt` set and + carries a `deletion-pending` condition (`status: "True"`, + `reason: "ConfigRemoved"`); assert + `ResourceMutation{event="delete-scheduled", trigger="config-cleanup"}` audit + record is written. +- `cleanup_does_not_touch_controller_children`: config-owned `Provider` + deleted; assert controller-owned `Process` child is deleted by the Provider + lifecycle controller (finalizer clear), not by config-publication; assert + config-publication does not issue a direct Delete on the `managedBy=controller` + `Process`. +- `deletion_sets_deletionrequestedat_not_phase`: schedule Delete on a resource; + assert `metadata.deletionRequestedAt` is set to a non-zero timestamp; assert + no `"phase": "Deleting"` appears in the resource record. +- `final_deletion_is_atomic`: mock a resource with all finalizers cleared; + assert that the `Deleted` audit record emission and the row/index removal + occur in one store transaction; assert no intermediate state where the row + is removed but the audit record is absent, or vice versa. +- `pending_cleanup_condition_set_on_zone`: scheduled Delete pending; assert + Zone `status.conditions` contains `type="pending-cleanup"`, `status="True"`. +- `zone_is_degraded_not_failed_during_cleanup`: scheduled Delete pending; + assert Zone `status.phase = "Degraded"` (not `"Failed"`). +- `pending_cleanup_cleared_after_deletion_completes`: all scheduled Deletes + complete; assert `pending-cleanup` condition removed; assert Zone phase + transitions to `Ready` (absent other conditions). +- `prior_generation_retained_count_based`: new generation activated; assert + up to 3 prior generation bundles are retained in the Zone store; assert + generation 4 causes the oldest prior bundle to be pruned (after all its + cleanup is complete). +- `rollback_schedules_delete_for_new_generation_resources`: activate G1, + activate G2 (adds Provider), rollback to G1; assert Provider receives + Delete; assert `pending-cleanup` is set on the Zone. +- `audit_segments_preserved_on_provider_delete`: delete + `Provider/observability-otel`; assert audit segment files in + `$ZONE_STATE/audit/` are unchanged; assert no segment is deleted as part + of config cleanup. +- `cleanup_stall_condition_set`: inject a finalizer holder that never clears; + wait >30 min (simulated); assert condition `cleanup-stalled` appears with + a closed-enum `reason` field. +- `generation_rejected_emits_audit_record`: submit a bundle with a + schema-validation failure; assert no resources are changed in the Zone store; + assert a `ResourceMutation{event="generation-rejected", reason= + "schema-validation-failed"}` audit record is written. + + + +| Item | Treatment | +| --- | --- | +| Current anchor | (1) `packages/d2bd/src/metrics.rs`: 16-metric hand-rolled Prometheus registry; `vm` name labels; `VM_START_BUCKETS_SECONDS`, `BROKER_REQUEST_BUCKETS_SECONDS`, `ACTIVATION_PHASE_BUCKETS_SECONDS`. (2) `packages/d2bd/src/daemon_audit.rs`: hash-chain JSONL; `DaemonEvent` variants; `VmStartRunnerExitReason`, `RunnerExitKind`, `VmShutdownProvider` enums. **No** `DaemonEvent` for unsafe-local launches — this is a gap documented in the ProcessEffect section. (3) `packages/d2b-priv-broker/src/audit.rs`: `AuditWriteClass`, `AuditDropSummary`, rate-limit, O_APPEND, rotation. (4) `packages/d2b-realm-core/src/audit.rs`: `AuditHash`, `AuditChainLink`, `AuditChainRecord{realm: RealmPath, node: NodeId}`, `AuditStreamKind::{Gateway,RemoteNode,Daemon}`, `AuditEnvelope{realm, node, workload, principal}`, `AuditSinkHealth`. (5) `packages/d2b-realm-core/src/trace_context.rs`: `TraceContext{trace_id, span_id}`. (6) `packages/d2b-realm-core/src/ids.rs`: `RealmId`, `WorkloadId`, `NodeId`, `PrincipalId`, `OperationId`, `CorrelationId`. (7) `packages/d2b-gateway/src/audit.rs`: `GatewayAuditEvent`, `GatewayAuditKind`. (8) `packages/d2b-gateway-runtime/src/audit_jsonl.rs`: `JsonlGatewayAudit`, `DEFAULT_GATEWAY_AUDIT_RETENTION_DAYS`. (9) `packages/d2b-priv-broker/src/ops/audit_op.rs`: `OpAuditRecord`, `SwtpmDirAudit`. (10) `packages/d2b-host/src/otel_host_bridge_argv.rs`, `packages/d2bd/src/otel_host_bridge_readiness.rs`. (11) `nixos-modules/components/observability/{host,stack,guest}.nix`: `scrapeJournal`, `identityName`/`vmName`, `vm.name`/`vm.env`/`vm.role` OTEL resource attributes, SigNoz stack. (12) `packages/d2b-contract-tests/tests/{policy_observability,policy_metrics,minijail_relay_otel}.rs`. (13) `packages/d2b/tests/{audit_contract,host_doctor_contract}.rs`. (14) `packages/d2b-priv-broker/tests/broker_export_audit.rs`. (15) **unsafe-local sources**: `packages/d2b-core/src/unsafe_local_workloads.rs` (`UnsafeLocalWorkloadsJson`, `UnsafeLocalWorkload`, `UnsafeLocalLauncherItem`, `UnsafeLocalExecItem`, `UnsafeLocalShellItem`, `UnsafeLocalShellPolicy`, `UNSAFE_LOCAL_WORKLOADS_SCHEMA_VERSION`, `MAX_UNSAFE_LOCAL_WORKLOADS`); `packages/d2b-contracts/src/unsafe_local_wire.rs` (`HelperHello`, `HelperLaunchRequest`, `HelperShellRequest`, `HelperFailureCode`, `HelperScopeKind`, `DaemonToUnsafeLocalHelper`, `UnsafeLocalHelperToDaemon`); `packages/d2bd/src/unsafe_local_helper.rs` (`HelperRegistry`, `HelperConnection`, `dispatch_launch`, `allowed_uids`, `bind_helper_socket`); `packages/d2b-unsafe-local-helper/src/{main,protocol,runtime,systemd}.rs` (`HelperClient`, `ScopeRuntime`, `run_scope_supervisor`, `SystemdUserScopeManager`); `nixos-modules/options-realms-workloads.nix` (lines 221, 233–235, 264–275: `kind = "unsafe-local"`, null `stateDir`/`runDir`); `nixos-modules/unsafe-local-workloads-json.nix`; `nixos-modules/unsafe-local-helper.nix`. | +| Evidence class | (1) Hand-rolled metrics: implemented-and-reachable (no OTEL SDK). (2-4) Audit JSONL/hash/rate-limit: implemented-and-reachable. (5-6) TraceContext/IDs: implemented-and-reachable. (7-9) Gateway/broker/op audit: implemented-and-reachable. (10) OtelHostBridge runner: implemented-and-reachable. (11) Nix OTEL pipeline: implemented-and-reachable for the v1 daemon; the v3 `observability-otel` Provider is ADR-only. (12-14) Tests: implemented-and-reachable. (15) unsafe-local: implemented-and-reachable; **gap**: no `DaemonEvent` for unsafe-local launch/stop in current daemon. | +| Behavior retained | SHA-256 hash chain `prev_hash`/`record_hash`; O_APPEND JSONL segment files; privileged-never-dropped audit rate-limit invariant; `TraceContext` opaque bounded fields; SigNoz backend + OTEL Collector pipeline shape; `vm.name`/`vm.env`/`vm.role` OTEL resource attributes (advisory); journald scrape option; startup-tracing-avoids-host-path policy; `loki_native_otel_resource_attributes` closed allowlist; `broker_export_audit` admin-only / path-free / NDJSON contract; `UnsafeLocalWorkload` private-bundle-only argv/shell policy; `HelperRegistry::allowed_uids` per-UID isolation | +| Required delta | Lightweight `BoundedEmitter` crate (no OTEL SDK in core); v3 metrics with no `vm`-name labels; traces with `d2b.zone`/`d2b.provider` resource attributes; v3 resource/RBAC/session/route/state-reset audit records; `zone` field replacing `realm: RealmPath` in all audit records; per-Zone emitter socket; `observability-otel` Provider (full OTEL SDK in its own Process only); `d2b zone doctor`/`support-bundle` CLI; performance histogram benchmarks; **user-only Host resource `isolationPosture: "none"` status field**; **ProcessEffect `no_isolation: true` for all user-only Host (unsafe-local successor) process launches and stops** (gap fill); **CLI/UI isolation warning for user-only Host only**; `isolation-posture-declared` doctor check | +| Reuse path | Extract `TraceContext` + `AuditHash` + `AuditChainLink` to `d2b-telemetry` unchanged; copy hash-chain append/rate-limit/rotation logic from `daemon_audit.rs` and broker `audit.rs`; adapt `OpAuditRecord` BrokerEffect fields; copy Nix OTEL pipeline shape from `{host,stack,guest}.nix`; adapt `policy_observability`/`policy_metrics` tests; adapt `broker_export_audit` test scaffold; adapt `host_doctor_contract.rs` test harness; adapt `UnsafeLocalWorkload` private-bundle contract for Host resource spec | +| Replacement/deletion | `d2bd/src/metrics.rs` hand-rolled registry removed only after OTEL SDK metrics reach parity in all covered paths; `otel_host_bridge_argv.rs` socat runner retired after `observability-otel` Provider delivers native OTLP/vsock; daemon/broker/gateway `*_audit.rs` JSONL writers retired per-component after `d2b-audit` sink achieves parity; `AuditStreamKind` renamed `Daemon→Zone`, `Gateway→ZoneLink`, `RemoteNode→RemoteZone` in `d2b-telemetry`; `AuditChainRecord{realm, node}` re-versioned to `{zone}` in `d2b-audit`; `d2b-unsafe-local-helper` binary and `DaemonToUnsafeLocalHelper` wire protocol retired after Process Provider supervisor ticket migration for Host resources | +| Feasibility proof | `TraceContext` proven. Hash-chain JSONL proven in broker/daemon. Rate-limit/rotation proven in broker. `BoundedEmitter` datagram-socket pattern follows existing `otelRuntimeDir` ACL + socket design. OTEL SDK + Unix socket exporter proven in the v3 Nix OTEL pipeline (Provider-only). SigNoz stack operational. `broker_export_audit` path-free NDJSON proven. `host_doctor_contract` env-redirect scaffold proven | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-telem-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-telem-001` | +| Dependency/owner | W0/W1a; telemetry crate owner | +| Current source | `packages/d2b-realm-core/src/trace_context.rs` (`TraceContext`, `MAX_TRACE_FIELD_LEN`); `packages/d2b-realm-core/src/audit.rs` (`AuditHash`, `AuditHashError`, `AuditChainLink`, `AuditChainRecord`); `packages/d2b-realm-core/src/ids.rs` (`OperationId`, `CorrelationId`); `packages/d2b-realm-codec-protobuf/src/lib.rs` (`encode_trace_context`, `decode_trace_context`); `packages/d2b-contract-tests/tests/policy_observability.rs::startup_tracing_avoids_host_path_fields` | +| Reuse action | adapt | +| Destination | `packages/d2b-telemetry/src/{trace_context.rs,audit_hash.rs,emitter.rs,meter_registry.rs,metric_label_policy.rs,redaction_guard.rs}` | +| Detailed design | `d2b-telemetry` provides: (1) `TraceContext` / `AuditHash` / `AuditChainLink` extracted unchanged; (2) `BoundedEmitter`: `tracing`-subscriber layer that serializes span/metric events into compact frames and writes them over a private Unix datagram socket to the `observability-otel` Provider — no `opentelemetry_sdk` dependency; (3) the canonical closed `METRIC_LABEL_POLICY`, structural descriptor/data-point/exemplar validator, exact forbidden-key/suffix predicates, and non-serializable resource-identity canary matcher used by emitter defense in depth and the mandatory collector ingress gate; (4) `RedactionGuard` span wrapper that asserts the v3 resource attribute allowlist at span creation. No OTEL SDK in this crate. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract unchanged (`TraceContext`, `AuditHash`, `AuditChainLink`); adapt (`OperationId`/`CorrelationId` for v3 record contract); add bounded emitter. | +| Integration | Every v3 core process initializes a `BoundedEmitter` pointing at `$ZONE_STATE/telemetry/emitter.sock`; v3 audit records use `AuditHash`/`AuditChainLink` from this crate | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Unit test for `RedactionGuard` attribute gate; unit test for `BoundedEmitter` ring-full drop and FIFO drain; table tests for `METRIC_LABEL_POLICY` exact keys, suffixes, and `metadata.name`/UID/ResourceRef identity canaries; `policy_telemetry_redaction.rs::startup_tracing_avoids_host_path_fields` port; assert `config_source = "realm-controllers"` absent; assert no `opentelemetry_sdk` dependency in `d2b-telemetry` Cargo.toml | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-telem-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-telem-002` | +| Dependency/owner | ADR046-telem-001 + ADR046-store-001; store owner | +| Current source | `packages/d2bd/src/metrics.rs` (`MetricDescriptor`, `MetricKind`, `VM_START_BUCKETS_SECONDS`, `BROKER_REQUEST_BUCKETS_SECONDS`, `ACTIVATION_PHASE_BUCKETS_SECONDS` — for bucket pattern reference only; the `vm` labels are not reused) | +| Reuse action | adapt | +| Destination | `packages/d2b-resource-store-redb/src/metrics.rs`, `packages/d2b-resource-store-redb/src/tracing.rs` | +| Detailed design | Instrument the store actor, write/read/group-commit paths with the metric inventory from this spec via `d2b-telemetry` `BoundedEmitter`. Emit `d2b.store.*` spans. The p95 `d2b_store_write_duration_seconds` hard target (≤10 ms) feeds the benchmark fixture. No `vm` label; `resource_type` label only from closed catalog. No OTEL SDK in the store crate. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt bucket boundary constants (rename; remove `vm` labels); replace hand-rolled `Registry` with `d2b-telemetry` `BoundedEmitter` meter API. | +| Integration | Store actor calls `d2b-telemetry` meter/tracer via `BoundedEmitter`; spans linked to API request spans via `TraceContext` | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | p95 write ≤10 ms benchmark fixture; metric inventory policy test asserting no `vm` label; assert old `d2b_daemon_vm_state` shape absent | +| Removal proof | Hand-rolled registry in `d2bd/src/metrics.rs` retained until daemon-level ADR 0046 cutover | + +### ADR046-telem-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-telem-003` | +| Dependency/owner | ADR046-telem-001 + ADR046-session-001 + ADR046-bus-001; session/bus owner | +| Current source | `packages/d2b-realm-core/src/ids.rs` (`OperationId`, `CorrelationId`); `packages/d2b-realm-codec-protobuf/src/lib.rs` (`encode_trace_context`, `decode_trace_context` for v3 codec adaptation); `packages/d2b-realm-router/src/mux_session.rs`, `route_engine.rs` (current implemented-but-unwired generic routing) | +| Reuse action | adapt | +| Destination | `packages/d2b-resource-api/src/metrics.rs`, `packages/d2b-session/src/metrics.rs`, `packages/d2b-bus/src/metrics.rs` | +| Detailed design | Instrument resource API verb dispatch, watch delivery, bus route resolution, and session handshake/reconnect per the metric/span catalog in this spec. Propagate `TraceContext` from incoming bus request to store write transaction span as child context. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `TraceContext` protobuf codec for v3 resource API framing; adapt routing metrics patterns. | +| Integration | ResourceClient → bus → API → store span chain via `TraceContext` | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | API request metric inventory test; session profile/outcome label cardinality gate; bus direction label gate; assert no `realm` field in span attributes | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-telem-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-telem-004` | +| Dependency/owner | ADR046-telem-001 + ADR046-core-001; core-controller owner | +| Current source | `packages/d2bd/src/metrics.rs` (`BROKER_REQUEST_BUCKETS_SECONDS` for reference; `d2b_daemon_broker_request_duration_seconds` labels `["op"]` as a cardinality-safe example); `packages/d2b-realm-core/src/allocator_engine.rs` (field `trace: Option` at line 873 — existing trace context wiring pattern) | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/metrics.rs`, `packages/d2b-core-controller/src/tracing.rs` | +| Detailed design | Emit `d2b.controller.hint` span at the instant the post-commit dispatcher fires; emit `d2b.controller.reconcile` child span at handler entry. Interval = p95 ≤5 ms target. `handler` label from closed set; no resource name labels. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt bucket patterns; adapt trace-context-in-reconcile pattern from `allocator_engine.rs`. | +| Integration | Post-commit dispatcher creates hint span; handler creates child reconcile span via `TraceContext` | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `hint_to_handler_latency` benchmark with p95 ≤5 ms assertion; closed `handler` label set gate | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-telem-005 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-telem-005` | +| Dependency/owner | ADR046-telem-001 + ADR046-process-001; Process Provider owner | +| Current source | `packages/d2bd/src/metrics.rs` (`d2b_daemon_vm_start_duration_seconds` `VM_START_BUCKETS_SECONDS`; `d2b_daemon_vm_shutdown_duration_seconds` `VM_SHUTDOWN_BUCKETS_SECONDS`; `d2b_daemon_vm_shutdown_total` labels `["vm", "vmm", "outcome"]` — `vmm` = current `RunnerRole` → v3 `provider`); `packages/d2bd/src/supervisor/pidfd.rs` (pidfd adoption/launch call sites); `packages/d2b-contracts/src/broker_wire.rs::RunnerRole` (`CloudHypervisor`, `QemuMedia`, `OtelHostBridge` etc. → v3 `provider` label values) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-supervisor/src/metrics.rs`, `packages/d2b-provider-supervisor/src/tracing.rs` | +| Detailed design | `d2b_process_launch_duration_seconds`: start = instant Process controller receives commit-to-Ready hint; end = first OS spawn call (clone3 or systemd unit start). This implements p95 ≤20 ms. `provider` label replaces `vmm`/`RunnerRole` with the closed set `{minijail,systemd}`. No `vm` name label. A separate `d2b_process_ready_duration_seconds` histogram covers launch-attempt → readiness signal (not a hard target). Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt launch histogram bucket constants; rename `vm` label to no label (process identity in resource attributes); rename `vmm`/`RunnerRole` → `provider` closed enum. | +| Integration | Process Provider controller start handler → supervisor ticket delivery → first spawn call | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `commit_to_launch_latency` benchmark with p95 ≤20 ms assertion; assert no `vm` label in process metrics; `vmm→provider` label rename gate | +| Removal proof | `d2b_daemon_vm_start_duration_seconds` (with `vm` label) retained in `d2bd/src/metrics.rs` until daemon-level ADR 0046 cutover | + +### ADR046-telem-006 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-telem-006` | +| Dependency/owner | ADR046-process-001 + ADR046-provider-001; `observability-otel` Provider owner | +| Current source | `nixos-modules/components/observability/host.nix` (`otelRuntimeDir = "/run/d2b/otel"`, `hostEgressSocket`, ACL `setfacl` pattern, `scrapeJournal`, `hostCfg.identityName`); `nixos-modules/components/observability/stack.nix` (SigNoz stack, `ingressSources`, per-source `vmName`, `receiverGrpcPort`/`receiverHttpPort`, loopback binding, `cfg.signoz.listenPort`); `nixos-modules/components/observability/guest.nix` (`vm.name`/`vm.env`/`vm.role` identity stamping, guest collector); `packages/d2b-host/src/otel_host_bridge_argv.rs` (`OtelHostBridgeArgvInputs`; vsock forwarding); `packages/d2bd/src/otel_host_bridge_readiness.rs` (readiness gate pattern: `OtelHostBridgeReadiness::{Ready,Pending,Failed}`); `packages/d2b-core/src/processes.rs::ProcessRole::OtelHostBridge`; `packages/d2b-contracts/src/broker_wire.rs::RunnerRole::OtelHostBridge`; `packages/d2b-contract-tests/tests/{policy_observability.rs,minijail_relay_otel.rs}` | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-observability-otel/src/`, `nixos-modules/components/observability/` (adapted files) | +| Detailed design | `Provider/observability-otel` is an **ordinary optional non-bootstrap Process** (not counted toward the ≤64 MiB mandatory core aggregate). It owns: (1) per-Zone datagram receiver socket at `$ZONE_STATE/telemetry/emitter.sock` (drains frames from core emitters) and OTLP/gRPC Unix socket at `$ZONE_STATE/telemetry/otlp.sock`; (2) the full OTEL SDK with OTLP exporter — only this process links `opentelemetry_sdk`; (3) OTel Collector pipeline per Zone and per Host; (4) vsock OTLP forwarding to obs Zone (replaces socat-based `OtelHostBridgeArgvInputs`); (5) D096 import-stream ingest; (6) one structural `METRIC_LABEL_POLICY` gate shared by Unix emitter, OTLP Unix, OTLP/vsock, and import-stream metrics before aggregation, queueing, batching, retry, or export, with bounded non-echoing errors/quarantine/backpressure; (7) SigNoz stack Nix adapted from `stack.nix` with per-Zone `ingressSources` replacing per-VM `vmName`; (8) journald scrape (optional, disabled by default); (9) self-metrics endpoint. Trusted producer identity is stamped only into allow-listed OTEL Resource attributes; audit remains separate. Zone/controller startup does not wait for this Provider. If absent or unready, Zone health is `Degraded` (not `Failed`). Readiness: socket exists and first drain cycle completes successfully. `d2b.observability.host.identityName` option preserved; `vmName` in `ingressSources` populated from Zone name. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt Nix pipeline shape (replace per-VM `vmName` with per-Zone naming); adapt `OtelHostBridgeArgvInputs` vsock forwarding to native OTLP/gRPC-over-vsock; adapt readiness gate pattern (`OtelHostBridgeReadiness::Ready` → Provider phase `Ready`); adapt `ingressSources` per-VM → per-Zone. | +| Integration | Core process `BoundedEmitter` → `emitter.sock` → observability-otel collector → `otlp.sock` → vsock → obs Zone SigNoz; Zone startup independent of Provider readiness | +| Data migration | Existing SigNoz data not migrated; v3 starts fresh | +| Validation | `emitter_socket_receive`, `emitter_ring_drains_on_socket_available`, `emitter_ring_drop_on_overflow`, table-driven `ingress_metric_policy` across all four ingress adapters, `no_vm_label_in_metrics`, and `zone_startup_proceeds_without_provider` tests; adapted `policy_observability.rs` tests (retain `loki_native_otel_resource_attributes` and SigNoz-only backend assertions); adapted `minijail_relay_otel.rs` shape test for Provider-managed runner | +| Removal proof | `otel_host_bridge_argv.rs` socat runner and `otel_host_bridge_readiness.rs` retired after `observability-otel` Provider delivers native OTLP/vsock and passes conformance; `ProcessRole::OtelHostBridge` and `RunnerRole::OtelHostBridge` retired from `d2b-core/src/processes.rs` and `d2b-contracts/src/broker_wire.rs` after Provider migration | + +### ADR046-audit-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audit-001` | +| Dependency/owner | W0/W1a; audit crate owner | +| Current source | `packages/d2b-realm-core/src/audit.rs` (`AuditHash::parse`, `AuditChainLink::new`/`verify`, `AuditChainRecord{stream: AuditStreamKind, realm: RealmPath, node: NodeId}`, `AuditStreamKind::{Gateway,RemoteNode,Daemon}`, `AuditSinkHealth`, `AuditRetentionFloorStatus`); `packages/d2bd/src/daemon_audit.rs` (hash-chain append algorithm, `prev_hash`/`record_hash` SHA-256 pattern, daily segment files `daemon-events-YYYY-MM-DD.jsonl`, `DaemonEvent` additive contract); `packages/d2b-priv-broker/src/audit.rs` (`AuditWriteClass::{Privileged,Unprivileged}`, `AuditDropSummary`, `DEFAULT_AUDIT_WRITES_PER_SECOND = 4096`, O_APPEND CLOEXEC file open, `AuditDropWarningState`); `packages/d2b-gateway-runtime/src/audit_jsonl.rs` (`JsonlGatewayAudit`, `DEFAULT_GATEWAY_AUDIT_RETENTION_DAYS = 14`, `prune_old` rotation algorithm); `packages/d2b-priv-broker/src/ops/audit_op.rs` (`OpAuditRecord`, `SwtpmDirAudit`, `SwtpmDirResult`, `SwtpmMarkerResult`); `packages/d2b-realm-core/src/ids.rs` (`OperationId`, `CorrelationId`, `PrincipalId` — `PrincipalId` becomes `subject_digest`); `packages/d2b/tests/audit_contract.rs`; `packages/d2b-priv-broker/tests/broker_export_audit.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-audit/src/{hash_chain.rs,segment.rs,rate_limit.rs,record_types.rs,sink.rs,export.rs}` | +| Detailed design | `d2b-audit` provides: typed record structs per class; canonical serialization with `zone` replacing `realm: RealmPath`; SHA-256 hash chain (extracted from `daemon_audit.rs`); segment writer (O_APPEND CLOEXEC, 64 MiB / UTC-midnight rotation); 30-day compaction (adapts `prune_old` from `JsonlGatewayAudit`); `AuditWriteClass::{Privileged,Standard,BestEffort}` (extends current `{Privileged,Unprivileged}`); rate-limit with privileged-never-dropped invariant; export iterator with inline hash-break reporting. `AuditStreamKind` re-versioned: `Daemon→Zone`, `Gateway→ZoneLink`, `RemoteNode→RemoteZone`. `AuditChainRecord` re-versioned: `{zone: String}` replaces `{realm: RealmPath, node: NodeId}`. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract unchanged: `AuditHash`, `AuditChainLink` from `d2b-realm-core/src/audit.rs`; copy hash-chain append algorithm from `daemon_audit.rs`; copy `AuditWriteClass`/rate-limit/rotation/prune from broker `audit.rs`; adapt `JsonlGatewayAudit` segment writer; adapt `OpAuditRecord` to `BrokerEffect` record class. | +| Integration | Zone runtime, core-controller, Process Providers, broker effect bridge → `d2b-audit` sink; `d2b zone audit export` → export iterator | +| Data migration | v3 bootstrap; existing daemon/broker JSONL files not migrated | +| Validation | `audit_record_hash_chain`, `audit_record_schema` (no `realm`/`node` fields), `audit_segment_rotation`, `audit_rate_limit_privileged_never_dropped`, `audit_unavailable_blocks_privileged` | +| Removal proof | `daemon_audit.rs`, broker `audit.rs`, `JsonlGatewayAudit` retired per-component after `d2b-audit` sink achieves parity | + +### ADR046-audit-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audit-002` | +| Dependency/owner | ADR046-audit-001 + ADR046-store-001; store/authz owner | +| Current source | `packages/d2b-priv-broker/src/ops/audit_op.rs` (`OpAuditRecord` structural pattern — operation, peer_uid, decision, result fields); `packages/d2b-realm-core/src/audit.rs::AuditEnvelope{principal: PrincipalId, scope: AuthorizationScope, decision: AuthzDecision}` (principal → v3 `subject_digest`) | +| Reuse action | adapt | +| Destination | `packages/d2b-resource-store-redb/src/audit.rs`, `packages/d2b-core-controller/src/authz_audit.rs` | +| Detailed design | `ResourceMutation` records emitted by the store actor inside the write transaction before commit returns. The audit sink must durably fsync the audit record before returning the commit success (privileged durability class). `RBACChange` emitted by the authz handler in the same write transaction. `subject_digest` = SHA-256 of normalized canonical subject string from v3 `AuthenticatedSubjectContext` (ADR-046-componentsession-and-bus). Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `OpAuditRecord` structural pattern for `ResourceMutation` / `RBACChange` record classes; adapt `PrincipalId` → `subject_digest` derivation. | +| Integration | Store write transaction → `d2b-audit` sink → fsync → commit result | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Integration test: 100 mutations → verify hash-chained audit records with `zone` field, no `realm` field | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-audit-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audit-003` | +| Dependency/owner | ADR046-audit-001 + ADR046-session-001 + ADR046-bus-001; session/bus owner | +| Current source | `packages/d2b-gateway/src/audit.rs` (`GatewayAuditEvent`, `GatewayAuditKind::{DisplaySessionOpenAdmitted,DisplaySessionOpenDenied,DisplaySessionRunning,DisplaySessionClosed}`, `GatewayAudit` trait, `NoopGatewayAudit`); `packages/d2b-realm-core/src/audit.rs::AuditEnvelope{realm, principal, scope, decision, trace}` (fields adapted to v3 `SessionConnect` record class) | +| Reuse action | adapt | +| Destination | `packages/d2b-session/src/audit.rs`, `packages/d2b-bus/src/audit.rs` | +| Detailed design | `SessionConnect` records emitted at handshake completion. `GatewayAuditKind::DisplaySessionOpenAdmitted/Denied` → `event="connect"`, `authz_decision="allowed/denied"`. `GatewayAuditKind::DisplaySessionRunning` → informational `ProcessEffect`. `GatewayAuditKind::DisplaySessionClosed` → `event="close"`. `transport_class=zone_link` covers what the current `AuditStreamKind::Gateway` stream recorded for gateway-backed realm sessions. `RouteAdmission` records emitted at bus route resolution for denied routes. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `GatewayAudit` trait pattern for `SessionConnect` and `RouteAdmission` record classes; `NoopGatewayAudit` pattern reused for test sinks. | +| Integration | Session engine and bus router → `d2b-audit` sink | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Session connect/close/auth-failure audit tests; `GatewayAuditKind` → `SessionConnect` mapping test | +| Removal proof | `NoopGatewayAudit` and gateway JSONL sink retired after gateway is on v3 resource API | + +### ADR046-audit-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audit-004` | +| Dependency/owner | ADR046-audit-001; CLI owner | +| Current source | `packages/d2b/tests/audit_contract.rs` (`d2b audit --strict` returns 78; `auditResponse` relay; `authz-audit-requires-admin` denial; daemon-down exit 1 without bash fallback); `packages/d2b-priv-broker/tests/broker_export_audit.rs` (`export_audit_requires_admin_and_exports_op_audit_records`: admin-only, path-free, NDJSON `ExportBrokerAuditOk` shape, `peer_uid` field, `ApplyNftables` operation name in records) | +| Reuse action | adapt | +| Destination | `packages/d2b/src/zone_audit.rs` (new `d2b zone audit export` subcommand); `packages/d2b/tests/zone_audit_contract.rs` | +| Detailed design | `d2b zone audit export` opens segments read-only (shared flock), streams NDJSON to stdout, validates hash chain inline, reports breaks as inline error records, and invokes only `d2b.audit.v3.AuditService/Export` under the admin-only `audit-export` session verb (same `SO_PEERCRED`/Role check as current `ExportBrokerAuditOk`). The session grant provides no Zone resource authority. Assert no `realm`, `node`, `workload_id` fields in exported records. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt audit CLI contract test (daemon-down/exit behavior); adapt broker export test to new `zone` field and v3 record schema. | +| Integration | `d2b` CLI → ComponentSession `d2b.audit.v3.AuditService/Export` with `sessionVerbs=[audit-export]` → `d2b-audit` export iterator → stdout | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `export_audit.rs`: admin-only, hash break inline, no old field names (`realm`/`node`/`workload_id`), no path/argv in output, exit 0 on clean chain | +| Removal proof | `d2b audit` legacy command retained until `d2b zone audit export` covers all record classes | + +### ADR046-doctor-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-doctor-001` | +| Dependency/owner | ADR046-core-001 + ADR046-audit-001; CLI/doctor owner | +| Current source | `packages/d2b/tests/host_doctor_contract.rs` (env-redirect sandbox: `D2B_BROKER_SOCKET`, `D2B_PUBLIC_SOCKET`, `D2B_DAEMON_STATE_DIR`, `D2B_METRICS_URL`, `D2B_MANIFEST_PATH`; `doctor::render_summary` JSON envelope fields: `command`, `mode`, `broker_ready`, per-check `status`+`data`, `summary`, `exitCode`); `packages/d2bd/src/audit_check.rs` (`defects` array audit-chain validation pattern); `packages/d2bd/src/lib.rs` (doctor read-only path: `host doctor --read-only` reads from `D2B_DAEMON_STATE_DIR`, pidfd_table file, kernel-module check file, metrics URL) | +| Reuse action | adapt | +| Destination | `packages/d2b/src/zone_doctor.rs`, `packages/d2b/tests/zone_doctor_contract.rs` | +| Detailed design | `d2b zone doctor [--zone ] [--json]` reads resource status from Zone API (read-only verb), OTEL self-metrics from `observability-otel` Provider endpoint (optional), and audit segment inventory from `d2b-audit` segment reader. Named check set from this spec. Exit 0 on all-ready; 1 on any warn/error. Env-redirect sandbox for all test fixtures. Current `MANIFEST_JSON` fixture pattern adapted: `"_observability": {"enabled": false}` test ensures OTEL probe short-circuits cleanly. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt env-redirect sandbox test harness; adapt `defects` array pattern for `audit-hash-chain-clean` check; adapt `broker_ready` → `zone_phase` field. | +| Integration | `d2b` CLI → Zone resource API status reads + audit segment reader | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `zone_doctor_contract.rs`: all-ready/degraded/quarantine/otel-absent/audit-absent fixtures; no resource names/paths/argv/PIDs; `zone_phase` field present; no legacy `broker_ready` field | +| Removal proof | `d2b host doctor` retained until `d2b zone doctor` covers all check parity | + +### ADR046-doctor-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-doctor-002` | +| Dependency/owner | ADR046-doctor-001; CLI/doctor owner | +| Current source | `packages/d2b/tests/host_doctor_contract.rs` (env-redirect sandbox scaffold — no current `support-bundle` equivalent exists) | +| Reuse action | adapt | +| Destination | `packages/d2b/src/zone_support_bundle.rs`, `packages/d2b/tests/zone_support_bundle_contract.rs` | +| Detailed design | `d2b zone support-bundle [--zone ]` invokes only `d2b.support.v3.SupportService/GenerateBundle` under the admin-only `support-bundle` session verb; the caller receives no resource read authority. The service reads bounded resource status snapshots (32 per type, 512 total; metadata + status only; no spec bytes; no `metadata.name`), controller queue depths, schema catalog (names+versions only), audit segment inventory, OTEL collector metrics summary, and a bounded structured log ring (2000 entries). NDJSON output. On quarantine: `bundle_completeness: "partial"`, exit 1. Primary reuse disposition: `adapt`. Preserved source-plan detail: reuse env-redirect sandbox scaffold. | +| Integration | `d2b` CLI → ComponentSession `d2b.support.v3.SupportService/GenerateBundle` with `sessionVerbs=[support-bundle]` → bounded internal resource/controller/audit/OTEL readers | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `zone_support_bundle_contract.rs`: complete/partial bundles; no spec/name/path/argv; field completeness | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-telem-007 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-telem-007` | +| Dependency/owner | ADR046-telem-006; Nix/observability owner | +| Current source | `nixos-modules/components/observability/host.nix` (`scrapeJournal = hostCfg.scrapeJournal`, `journaldStorageDir = "/var/lib/d2b-host-otel-collector/journald"`, cgroup-path filtering pattern for host units) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-observability-otel/src/nix/journald.nix` (new Nix fragment) | +| Detailed design | `d2b.zones..observability.journald.enable = false` (default). When enabled: journald receiver follows `z-/*` and `s-/*` cgroup filters. Collector applies redaction: drops `MESSAGE` credential/path patterns, `_CMDLINE`, `_EXE`, `INVOCATION_ID`. Current `scrapeJournal` host option is preserved unchanged. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt journald receiver config for per-Zone cgroup filter (`z-/*`, `s-/*`). | +| Integration | `observability-otel` Provider Nix config → OTel Collector journald receiver → redaction filter → obs Zone | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Nix eval test: filter expression set when enabled; test that `_CMDLINE` and `INVOCATION_ID` appear in drop list | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-telem-008 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-telem-008` | +| Dependency/owner | ADR046-telem-001; policy/contract-tests owner | +| Current source | `packages/d2b-contract-tests/tests/policy_observability.rs` (`loki_native_otel_resource_attributes` allowlist: `["deployment.environment","host.name","service.name","service.namespace","source","vm.env","vm.name","vm.role"]`; `tempo_stack_signoz_backend_and_collector` SigNoz-only assertion; `startup_tracing_avoids_host_path_fields` forbidden fields); `packages/d2b-contract-tests/tests/policy_metrics.rs` (`EXPECTED_METRICS` table parity with `docs/reference/daemon-metrics.md`) | +| Reuse action | adapt | +| Destination | `packages/d2b-contract-tests/tests/policy_telemetry_redaction.rs` (new); updated `policy_observability.rs`; updated `policy_metrics.rs` | +| Detailed design | (1) Extend `loki_native_otel_resource_attributes` allowlist to include `d2b.zone`, `d2b.provider`, `d2b.component`, `service.version`. (2) Add redaction lint: scan all v3 instrumentation call sites for `realm`, `workload_id`, `node_id`, `vm` (as label key), `path`, `socket`, `argv`, `pid`, `exe`. (3) Add structural metric-label policy lint: parse every v3 `MetricDescriptor`, require each label key and value domain to exist in the closed `METRIC_LABEL_POLICY`, reject exact keys `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `network`, `network_name`, and `link_name_hash`, reject resource-name-derived key suffixes `*_name`, `*_name_hash`, `*_name_digest`, and `*_uid`, and prove `metadata.name`, UID, ResourceRef, and resource-identity canaries never enter label values. Fixed semantic labels remain allowed only with closed domains. (4) Prove the observability Provider's Unix-emitter, OTLP-Unix, OTLP/vsock, and import-stream adapters all invoke that validator before queue/batch/export; assert whole-frame rejection, bounded non-echoing error classes/quarantine, and policy-before-capacity backpressure. (5) Assert the `d2b.zone` Resource attribute remains present and audit is unchanged. (6) Add bucket boundary gates for 5 ms and 20 ms. (7) Retain: `startup_tracing_avoids_host_path_fields`; SigNoz-only backend assertion; `tempo_guest_collector_shape`; `config_source = "realm-controllers"` absence gate. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt and extend; keep existing tests; add new policy gates. | +| Integration | Contract-tests run in workspace check and `make test-drift` | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | These tests are their own validation artifact | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-host-posture-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-host-posture-001` | +| Dependency/owner | ADR046-audit-001 + ADR046-core-001; `Provider/system-core` owner | +| Current source | `packages/d2b-core/src/unsafe_local_workloads.rs` (`UnsafeLocalWorkloadsJson`, `UnsafeLocalWorkload`, `UnsafeLocalLauncherItem`, `UNSAFE_LOCAL_WORKLOADS_SCHEMA_VERSION = "v2"`, `MAX_UNSAFE_LOCAL_WORKLOADS = 256`); `packages/d2b-contracts/src/unsafe_local_wire.rs` (`HelperHello.uid: u32`, `HelperLaunchRequest`, `HelperShellRequest`, `HelperScopeKind::{Exec,Shell}`, `DaemonToUnsafeLocalHelper`, `UnsafeLocalHelperToDaemon`); `packages/d2bd/src/unsafe_local_helper.rs` (`HelperRegistry::new(daemon_uid, allowed_uids)`, `dispatch_launch`, `bind_helper_socket`); `packages/d2b-unsafe-local-helper/src/{main,protocol,runtime,systemd}.rs` (`HelperClient`, `ScopeRuntime`, `run_scope_supervisor`, `SystemdUserScopeManager`); `nixos-modules/options-realms-workloads.nix` (lines 221, 233–235 `kind = "unsafe-local"` description; lines 264–275 null `stateDir`/`runDir`); `nixos-modules/unsafe-local-workloads-json.nix` (`runtimeKind = "unsafe-local"`, `providerId = "unsafe-local"`); `nixos-modules/unsafe-local-helper.nix` (service unit) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-system-core/src/{host_reconciler.rs,host_status.rs,host_process_audit.rs}`; adapted `nixos-modules/unsafe-local-workloads-json.nix`; `packages/d2b-provider-system-core/tests/host_posture_contract.rs` | +| Detailed design | `Provider/system-core` reconciler: (1) On user-only `Host` resource creation (`defaultDomain=user`, `allowedDomains=[user]`), set `status.isolationPosture = "none"` and `status.isolationPostureMessage = "..."` unconditionally; reject any operator-supplied value for these fields. Host resources with other execution policies do not receive `isolationPosture`. (2) On every user-only Host process launch: emit `ProcessEffect{event:"launch", provider:"system-core-user", domain:"user", no_isolation:true, ...}` audit record. (3) On every user-only Host process stop: emit `ProcessEffect{event:"stop", ...}`. (4) `d2b zone list`/`inspect` CLI renders `⚠ no isolation boundary (user domain)` annotation only for `Host` resources with `isolationPosture: "none"`; annotation is not suppressible. (5) `isolation-posture-declared` doctor check: passes when user-only `Host` resource status has `isolationPosture: "none"`; omitted when Zone has no user-only `Host` resources. (6) `no_isolation=true` is emitted in `ProcessEffect` records only; it does not appear in any OTEL span attribute, log field, or metric label. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `UnsafeLocalWorkload` private-bundle contract for the `Host` resource spec payload; adapt `HelperRegistry::allowed_uids` constraint as `defaultUserRef=User/` validation; adapt Nix `unsafe-local-workloads-json.nix` emitter for the new Host resource shape; gap-fill: add `ProcessEffect{no_isolation:true}` at `dispatch_launch` / stop call sites. | +| Integration | `Provider/system-core` reconciler → `d2b-audit` sink; `d2b zone doctor` → resource status check; `d2b zone list`/`inspect` → CLI output renderer | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `host_posture_contract.rs` tests from the Host posture tests section of this spec; `d2b-contract-tests/tests/policy_telemetry_redaction.rs` asserts `no_isolation` key absent from all span/metric/log surfaces | +| Removal proof | `d2b-unsafe-local-helper` binary and `DaemonToUnsafeLocalHelper`/`UnsafeLocalHelperToDaemon` wire types retired after `Provider/system-core` Process Provider supervisor ticket migration; `nixos-modules/unsafe-local-helper.nix` Nix unit retired after migration; `nixos-modules/unsafe-local-workloads-json.nix` adapted (not deleted) to emit Host resource spec format | + +## Main-commit reuse work items + +The ADR45 W5–W9 implementation in main commit `a1cc0b2da4a08ca3240a770a972fe4da6f912bef` +provides production-grade implementations of ComponentSession v2, d2b-bus routing, +async clients, Provider proxies, attachment/stream/cancellation subsystems, and their +conformance tests. These are **reuse sources** for v3, not pre-ADR-0045 v3 baseline +evidence. Each item below records: exact main commit file/symbol, selected behavior, +exact v3 destination/integration, and the ADR45-specific assumptions that must be +excluded or adapted. + +### ADR046-reuse-001 — ComponentSession v2 runtime + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reuse-001` | +| Dependency/owner | ADR046-telem-001 (d2b-telemetry must exist first for MetricsSink injection); session owner | +| Current source | Main commit source row below: `packages/d2b-session/` and `packages/d2b-contracts/src/v2_component_session.rs` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-session/` copied verbatim; v3 contract extension renames endpoint purpose values while preserving backward wire compatibility during transition. | +| Detailed design | Noise handshake, record protection, scheduler, named streams, cancellation, attachments, deadlines, metrics injection, ttrpc serving, and generation-discovery behavior from the selected source. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy/adopt the core ComponentSession runtime; adapt EndpointPurpose/ServicePackage/RealmSessionAuthority names listed in Excluded ADR45 assumptions and inject a `d2b-telemetry` MetricsSink.. | +| Data migration | Full d2b 3.0 reset; no v2 session runtime state/config import | +| Removal proof | ADR45 topology enum names listed in Excluded ADR45 assumptions are adapted; no prior v3 session runtime owner to remove. | +| Main commit source | `packages/d2b-session/` at `a1cc0b2d`. Key files: `src/lib.rs` (full public surface), `src/handshake.rs` (`NoiseHandshake`, `HandshakeCredentials`, `HandshakeRole`, `NegotiatedOffer`, generation-discovery encode/decode, transcript-hash), `src/engine.rs` (`SessionEngine`, `SessionEvent`), `src/driver.rs` (`ComponentSessionDriver`, `SessionDriverHandle`), `src/cancellation.rs` (`Cancellation`, `RequestRegistry`, `CancelRequest`/`CancelAck`/`CancelResult`), `src/streams.rs` (`NamedStreamMux`, `StreamId`, `StreamPhase`, `StreamEvent`), `src/attachment.rs` (`OwnedAttachment`, `AttachmentPayload`, `AttachmentValidationError`), `src/metrics.rs` (`MetricEvent`, `MetricsSink`, `NoopMetrics`), `src/record.rs` (`ProtectedRecord`, `RecordProtector`), `src/scheduler.rs` (`FairScheduler`, `OutboundFrame`, `QueueClass`), `src/server.rs` (`serve_ttrpc_services`), `src/lifecycle.rs` (`SessionLifecycle`, `SessionPhase`, `KeepaliveAction`), `src/deadline.rs` (`DeadlineBudget`). Tests: `tests/component_session.rs` (noise vector tests, record-protection round-trip, schema-mismatch rejection, LimitProfile fixture), `tests/noise_vectors.rs` (committed KAT vectors). Contract source: `packages/d2b-contracts/src/v2_component_session.rs` (all `closed_enum!` tables, `LimitProfile`, `EndpointPolicy`, `EndpointPolicyIdentity`, `MetricLabels`, all 50+ constants including `MAX_ACTIVE_NAMED_STREAMS=128`, `MAX_SESSION_ATTACHMENTS=256`, `LOCAL_HANDSHAKE_DEADLINE_MS=5000`, `MAX_RECONNECT_ATTEMPTS=10`). | +| Selected behavior | Full Noise_NN/KK/IKpsk2 handshake engine; `RecordProtector` encrypt/decrypt; `FairScheduler` ttrpc/stream credit; `NamedStreamMux` open/close/reset/credit; `Cancellation`/`RequestRegistry` per-generation cancel; `OwnedAttachment` typed payload validation; `DeadlineBudget` deadline propagation; `MetricsSink` injection point; `serve_ttrpc_services` server loop; generation-discovery handshake. | +| Excluded ADR45 assumptions | `EndpointPurpose::{DaemonLocal, DaemonRemote, RealmPeer}` are ADR45 daemon topology names — v3 replaces with `Purpose::{ZoneLocal, ZoneLink, ProviderAgent}` (or adapts to closed-set). `PurposeClass::{Local, Enrolled, Bootstrap}` are retained verbatim. `ServicePackage::{DaemonV2, RealmV2, GuestV2}` are ADR45 service names — v3 replaces with the v3 bus service package catalog. `RealmId`/`WorkloadId` in `RealmSessionAuthority` (d2b-realm-router) are ADR45 realm concepts — v3 uses Zone name strings and resource UIDs. | +| v3 Destination | `packages/d2b-session/` copied verbatim (zero ADR45 topology assumptions in the core crates). Wire constants in `v2_component_session.rs` are adopted without change. `EndpointPurpose` enum values renamed in v3 contract extension; existing values kept for backward wire compatibility during transition. | +| Integration | `d2b-bus` route handler calls `serve_ttrpc_services`; `d2b-session-unix` provides `OwnedTransport` impl; `d2b-telemetry` `MetricsSink` impl feeds `d2b_session_*` metrics inventory from this spec. | +| Validation | Adopt `tests/component_session.rs` and `tests/noise_vectors.rs` unchanged; extend with v3 `EndpointPurpose` enum gate test; add `d2b-contract-tests/tests/component_session_v2_vectors.rs` (existing at `a1cc0b2d`) as-is. | + +### ADR046-reuse-002 — Unix transport substrate + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reuse-002` | +| Dependency/owner | ADR046-reuse-001; Unix transport owner | +| Current source | Main commit source row below: `packages/d2b-session-unix/` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-session-unix/` copied verbatim. | +| Detailed design | SO_PASSCRED verification, pidfd identity verification, multi-scope credit reservation, emergency headroom, seqpacket atomic CLOEXEC transfer, stream reassembly, vsock transport, and descriptor policy enforcement. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy verbatim; retain `host-socket`/`native-vsock` feature flags; no ADR45 topology adaptation required.. | +| Data migration | Full d2b 3.0 reset; no v2 transport runtime state/config import | +| Removal proof | None — net-new; no prior owner to remove | +| Main commit source | `packages/d2b-session-unix/` at `a1cc0b2d`. Key files: `src/adapter.rs` (`UnixSeqpacketTransport`, `UnixStreamTransport`, `PeerIdentityPolicy`, `DescriptorPolicyResolver`, `OwnedUnixAttachment`, `UnixAttachmentPayload`, `PathnamePeerVerifier`), `src/credit.rs` (`CreditPool`, `CreditScope`, `CreditScopeSet`, `ProcessCreditLimit`, `CreditBundle`, `CreditError` — rollback-on-failure, emergency-headroom constants), `src/descriptor.rs` (`PeerCredentials`, `FirstPacketCredentials`, `ObjectIdentity`, `PidfdIdentityPolicy`, `DescriptorPolicy`, `AcceptedAttachment`, `ReceivedPacket`), `src/pidfd.rs` (`PidfdEvidence`, `PidfdIdentityVerifier`, `verify_pidfd`), `src/socket.rs` (`SeqpacketSocket`, `StreamSocket`), `src/vsock.rs` (vsock transport). Tests: `tests/unix_session.rs` (`ancillary_capacity_is_derived_from_closed_hard_bounds`, `process_limit_preserves_emergency_headroom`, `failed_multiscope_reservation_rolls_back_every_prior_scope`, `staged_credit_reservations_release_once_at_each_scope`, `inherited_passcred_is_verified_but_never_repaired`, `first_packet_has_exact_directional_credentials`, `seqpacket_transfer_is_atomic_cloexec_and_object_exact`, `duplicate_kernel_objects_are_rejected_and_cleaned_up`, `owned_transport_adapters_transfer_packets_and_owned_files_end_to_end`, `stream_transport_reassembles_partial_and_coalesced_records`, `stream_transport_distinguishes_clean_and_partial_eof`). | +| Selected behavior | SO_PASSCRED first-packet credential verification; pidfd identity verification; multi-scope credit reservation with rollback; emergency headroom reservation; seqpacket atomic cloexec transfer; stream transport reassembly; vsock transport; `DescriptorPolicy` per-fd type enforcement. | +| Excluded ADR45 assumptions | No ADR45-specific topology assumptions in this crate. `PeerIdentityPolicy` uses Unix `SO_PEERCRED` which is host-local — vsock peers use vsock CID policy instead; no change needed. | +| v3 Destination | `packages/d2b-session-unix/` copied verbatim. Feature flags `host-socket` / `native-vsock` retained unchanged. | +| Integration | `d2b-bus` Zone-local listeners use `UnixSeqpacketTransport`; Provider agent connections use vsock transport from this crate; `CreditPool`/`CreditScopeSet` enforce per-Zone attachment FD budgets. | +| Validation | Adopt all `unix_session.rs` tests unchanged. | + +### ADR046-reuse-003 — Async client and retry layer + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reuse-003` | +| Dependency/owner | ADR046-reuse-001; client owner | +| Current source | Main commit source row below: `packages/d2b-client/` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-client/` copied; DaemonClient/GuestClient adapted to v3 service packages; `MetadataInput::with_trace` drives `TraceContext` propagation. | +| Detailed design | MetadataInput trace/correlation/idempotency, retry policy, cancellation token, generic client over resolver/connector/clock, typed service proxies, stream terminal, and host-socket verifier. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy client layer; adapt service package/method sets, local-root Zone identity, and TraceContext propagation for v3.. | +| Data migration | Full d2b 3.0 reset; no v2 client state/config import | +| Removal proof | ADR45 Daemon/Guest wrappers and RealmPath identity assumptions are adapted to v3 service packages/Zone name; no prior v3 client owner to remove. | +| Main commit source | `packages/d2b-client/` at `a1cc0b2d`. Key files: `src/client.rs` (`Client`, `ConnectedClient`, `CallOptions`, `RetryPolicy`, `CancellationToken`, `MetadataInput`, `Response`, `WallClock`, `SystemClock`), `src/service.rs` (`ServiceHandle`, `MethodHandle`, `GeneratedClient`, `ServiceKind`), `src/session.rs` (session reconnect and credential binding), `src/target.rs` (target/resolver abstraction), `src/daemon_service.rs` (`DaemonClient`, `DaemonMethod`, `DaemonLifecycleRequest`, `DaemonTerminal`, `daemon_call_options`), `src/guest_service.rs` (`GuestClient`, `GuestOperation`, `GuestInspectCall`, `GuestCancelCall`, `GuestRetainedLogCall`), `src/host_socket.rs` (`HostSocketConnector`, `local_daemon_endpoint_identity`), `src/error.rs` (`ClientError`, `RemoteErrorKind`, `RetryClass`). Tests: `client.rs` (`typed_routes_select_exact_transport_without_fallback`, `daemon_typed_list_preserves_projection_and_truncation`, `daemon_typed_errors_and_generation_mismatch_are_actionable`, `guest_exec_management_preserves_typed_state_and_cancel_correlation`, `guest_retained_log_open_binds_range_resource_and_selection`, `terminal_uses_server_stream_and_validates_bidirectional_lifecycle`, `daemon_guest_proxy_reuses_the_authenticated_session`, `absent_daemon_guest_proxy_fails_closed_without_reconnecting`). | +| Selected behavior | `MetadataInput` W3C trace-id propagation (`with_trace([u8; 16])`), correlation, idempotency-key; `RetryPolicy` max-attempts; `CancellationToken`; `Client` generics over resolver/connector/clock; typed `DaemonClient`/`GuestClient` service proxies; stream-based terminal; `local_daemon_endpoint_identity` host-socket verifier. | +| Excluded ADR45 assumptions | `DaemonClient`/`GuestClient` wrap ADR45 `DaemonV2`/`GuestV2` service packages. v3 replaces with v3 service package names. `local_daemon_endpoint_identity` uses `RealmPath::parse("local-root")` — v3 uses the Zone name for the local-root Zone. `DaemonMethod`/`GuestMethod` enums are ADR45 method sets — v3 replaces with v3 bus service method sets. | +| v3 Destination | `packages/d2b-client/` copied; `DaemonClient`/`GuestClient` adapted to v3 service packages; `local_daemon_endpoint_identity` adapted to use Zone name; `MetadataInput::with_trace` drives `TraceContext` propagation into d2b-bus route requests. | +| Integration | Every controller/service that makes outbound calls uses `Client`; `MetadataInput::with_trace` feeds `d2b_api_request_duration_seconds` trace-id into `d2b.bus.route` span. | +| Validation | Adopt typed-route, proxy-reuse, and cancel tests unchanged. Add v3 service-package name gate test. | + +### ADR046-reuse-004 — Provider registry, RPC proxy, and conformance toolkit + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reuse-004` | +| Dependency/owner | ADR046-reuse-001 + ADR046-reuse-003; Provider owner | +| Current source | Main commit source row below: `packages/d2b-provider/` and `packages/d2b-provider-toolkit/` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-provider/` and `packages/d2b-provider-toolkit/` copied with v3 session admission and bus routing adaptations. | +| Detailed design | ProviderRegistry factory registration, bounded admission, RPC proxy fail-closed behavior, generated provider service admission/dispatch, redaction wrappers, and conformance gates. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy provider runtime/toolkit; adapt SessionIdentity Zone name and route through `d2b-bus` instead of daemon embedding.. | +| Data migration | Full d2b 3.0 reset; no v2 ProviderRegistry runtime state import | +| Removal proof | Daemon-embedded ADR45 ProviderRegistry wiring is excluded; v3 provider-process registry replaces it without importing state. | +| Main commit source | `packages/d2b-provider/` at `a1cc0b2d`. Files: `src/registry.rs` (`ProviderRegistry`, `ProviderRegistryBuilder`, `RegistryLimits`, `AdmissionOptions`), `src/rpc.rs` (`RpcCall`, `RpcPayload`, `RpcResponse`, `RpcOperation`, `AuthenticatedProviderRpc`, `RpcProviderProxy`, `SessionIdentity`), `src/instance.rs` (`ProviderInstance`, `ProviderFactory`, `CancellationToken`), `src/context.rs` (`ProviderCallContext`, `OwnedOperationContext`), `src/error.rs` (`ProviderRuntimeError`, `RegistryBuildError`, `FactoryError`). Tests: `runtime.rs` (`closed_error_context_is_actionable_without_identity_leaks`, `health_uses_no_input_inspection_methods_for_every_axis`, `all_provider_traits_are_object_safe`). `packages/d2b-provider-toolkit/` at `a1cc0b2d`. Files: `src/server.rs` (`GeneratedProviderServiceServer`, session admission, ttrpc method dispatch), `src/adapter.rs` (`invoke_session` → `AuthenticatedProviderRpc` bridge), `src/conformance.rs` (`check_descriptor_conformance`, `check_provider_conformance`), `src/fixture.rs` (conformance fixture), `src/redaction.rs` (`Redacted`, `Secret`), `src/registration.rs`, `src/values.rs`. Tests: `tests/conformance.rs` (`every_axis_passes_identical_in_process_and_rpc_conformance`, `conformance_uses_the_exact_real_descriptor_placement_and_target`, `provider_values_preserve_all_descriptor_and_operation_bindings`, `exact_registration_supports_all_axes_and_shared_factories`, `adapter_rejects_authenticated_identity_mismatch`, `rpc_proxy_fails_closed_on_cancellation_and_method_mismatch`, `rpc_proxy_preserves_plan_handle_and_adoption_bindings`, `rpc_proxy_rejects_mismatched_observability_query_results`, `redaction_wrappers_do_not_expose_canaries`). | +| Selected behavior | `ProviderRegistryBuilder` factory registration with `RegistryLimits` bound enforcement; `RpcProviderProxy` `AuthenticatedProviderRpc` bridge with cancellation + method mismatch fail-closed; `GeneratedProviderServiceServer` session admission + ttrpc dispatch; `Redacted`/`Secret` Debug suppression; `check_descriptor_conformance`/`check_provider_conformance` conformance gate. | +| Excluded ADR45 assumptions | `ProviderRegistry` is wired to ADR45 `d2bd` daemon via `DaemonEffectAdapters` (`provider_effects.rs` in d2bd). In v3, Providers are independent processes registering with the Zone runtime via the resource API, not daemon-embedded. The `SessionIdentity` in `rpc.rs` carries `realm: RealmId` — v3 replaces with Zone name string. `ProviderRealmPath`/`WorkloadId` imports in `d2bd/src/provider_registry.rs` are ADR45 daemon coupling — excluded. | +| v3 Destination | `packages/d2b-provider/` and `packages/d2b-provider-toolkit/` copied; `SessionIdentity` adapted to use Zone name; `ProviderRegistry` wired to v3 bus service via `d2b-bus` route handler instead of daemon embedding; `GeneratedProviderServiceServer` session admission adapted to v3 `EndpointPurpose` values. | +| Integration | Each v3 Provider process embeds `ProviderRegistry` + `GeneratedProviderServiceServer`; `check_provider_conformance` runs in Provider install-time conformance check (feeds `d2b_provider_reconcile_total{outcome="error"}` on failure). | +| Validation | Adopt all `conformance.rs` and `runtime.rs` tests unchanged. Add v3 `SessionIdentity` zone-name gate. Add conformance-failure → `d2b_provider_reconcile_total` metric integration test. | + +### ADR046-reuse-005 — Provider agent process and gateway-runtime audit bridge + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reuse-005` | +| Dependency/owner | ADR046-reuse-004 + ADR046-audit-003; Provider agent / observability-otel owner | +| Current source | Main commit source row below: `packages/d2b-gateway-runtime/src/provider_agent.rs` at `a1cc0b2d` plus `tests/provider_agent_v2.rs`. | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-observability-otel/src/agent.rs` adapted; ComponentSessionDriver mock becomes shared Provider session fixture. | +| Detailed design | Session-bound ProviderAgentProcess construction, bounded audit snapshot, closed-set ProviderAgentError paths, and full ComponentSessionDriver mock for hermetic Provider tests. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt provider-agent session wrapper and audit ring; replace gateway-runtime audit envelope with v3 `d2b-audit` records.. | +| Data migration | Full d2b 3.0 reset; no v2 provider-agent runtime state import | +| Removal proof | ADR45 gateway-runtime AuditEnvelope/realm custody and launch path are excluded; v3 provider agent is supervised by Provider/system-core or the owning Provider supervisor. | +| Main commit source | `packages/d2b-gateway-runtime/src/provider_agent.rs` at `a1cc0b2d`. Types: `ProviderAgentProcess` (`from_registry`, `from_registry_with`, `provider_type`, `service_names`, `audit_snapshot`), `ProviderAgentError` (closed-set: `SessionClosed`, `ProtocolViolation`, `InvalidAuditCapacity`, `RegistryNotAccepting`, `UnregisteredAdapter`, `RegistrationRejected`), `ProviderAgentAuditOutcome`, `ProviderAgentAuditEvent`. Tests: `tests/provider_agent_v2.rs` (full `ComponentSessionDriver` mock implementing all 20 async driver methods: `start_ttrpc`/`complete_ttrpc`/`cancel`/`send_ttrpc`/`receive_ttrpc`/`register_inbound_call`/`complete_inbound_call`/`remove_inbound_call`/`send_attachments`/`receive_attachments`/`open_named_stream`/`send_named_stream`/`receive_named_stream`/`grant_named_stream_credit`/`close_named_stream`/`reset_named_stream`/`drive_keepalive`/`receive_control`/`close`). | +| Selected behavior | `ProviderAgentProcess::from_registry` constructs a session-bound provider agent from a `ProviderRegistry`; bounded audit ring (`audit_snapshot`); `ProviderAgentError` closed-set for session-level fail paths; full `ComponentSessionDriver` mock trait for hermetic tests. | +| Excluded ADR45 assumptions | `ProviderAgentProcess` in main is wired to the ADR45 gateway-runtime which uses `GatewayAuditKind` / `AuditEnvelope` (realm-scoped). In v3, the provider-agent audit bridge uses `d2b-audit` `SessionConnect`/`RouteAdmission` record classes with `zone` field. Gateway-runtime `AuditEnvelope.realm: RealmPath` is excluded. `bin/d2b-provider-agent.rs` launch path is ADR45-specific; v3 provider agent is launched via `Provider/system-core` or the owning Provider supervisor. | +| v3 Destination | `packages/d2b-provider-observability-otel/src/agent.rs` (adapted); the `ComponentSessionDriver` mock from `tests/provider_agent_v2.rs` becomes the reusable test fixture for all v3 Provider session tests. | +| Integration | `observability-otel` Provider embeds a `ProviderAgentProcess`; session connect/disconnect emits `SessionConnect` audit records via `d2b-audit`; `ProviderAgentAuditEvent` ring feeds `d2b_provider_reconcile_total` metric on session error. | +| Validation | Adopt `provider_agent_v2.rs` mock harness unchanged as shared v3 Provider session fixture. Add v3 audit-bridge test: provider-agent session → `SessionConnect{transport_class="zone_link"}` record emitted. | + +### ADR046-reuse-006 — Realm service v2 routing and remote-node routing state + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reuse-006` | +| Dependency/owner | ADR046-reuse-001 + ADR046-bus-001; bus routing owner | +| Current source | Main commit source row below: `packages/d2b-realm-router/` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/routing.rs` adapted from `service_v2.rs`; RemoteNodeErrorKind stable code pattern becomes v3 BusErrorKind. | +| Detailed design | Realm/Zone service ttrpc handler table, session authority constructors, host/gateway credential custody split, stable low-cardinality error codes, and binding/shortcut bounds. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt realm service routing to Zone service routing; preserve credential-custody, bounded routing, and stable error-code behavior.. | +| Data migration | Full d2b 3.0 reset; no v2 routing state import | +| Removal proof | ADR45 RealmId and RealmService names are adapted to Zone/resource UID routing; no prior v3 bus routing owner to remove. | +| Main commit source | `packages/d2b-realm-router/` at `a1cc0b2d`. Key files: `src/service_v2.rs` (`RealmServiceServer`, `RealmSessionAuthority`, `RealmAuditEvent`, `RealmServiceProcess`, `RealmServiceLimits`, `CredentialCustody`, `REALM_SERVICE_NAME = "d2b.realm.v2.RealmService"`, `DEFAULT_MAX_REALM_BINDINGS = 256`, `DEFAULT_MAX_SHORTCUTS = 256`, `DEFAULT_MAX_MUTATION_RECORDS = 1024`, `DEFAULT_AUDIT_CAPACITY = 1024`), `src/remote_node.rs` (`RemoteNodeAvailability`, `RemoteNodeErrorKind` with stable `code()` method, `RemoteNodeError`), `src/session_lifecycle.rs`, `src/target_resolver.rs`, `src/execution.rs`. Tests: `tests/realm_service_v2.rs` (`authority_keeps_remote_credentials_in_gateway_guests`, `authenticated_bootstrap_enrollment_route_and_shortcut_lifecycle`). | +| Selected behavior | `RealmServiceServer` ttrpc handler table and per-session authority; `RealmSessionAuthority::local_controller`/`gateway_peer`/`new` for local/ZoneLink/remote purposes; `CredentialCustody::{Host,Gateway}` — host-local sessions retain only public pins; `RemoteNodeErrorKind::code()` stable low-cardinality code method (safe for audit/error labels); bounds `DEFAULT_MAX_REALM_BINDINGS`/`DEFAULT_MAX_SHORTCUTS`. | +| Excluded ADR45 assumptions | `RealmSessionAuthority` carries `realm: RealmId` — in v3, the routing authority carries a Zone name string and resource UID. `REALM_SERVICE_NAME = "d2b.realm.v2.RealmService"` is the ADR45 wire name — v3 replaces with the v3 bus service package name from the closed catalog. `RealmId::parse(request.stream_id)` in route dispatch — v3 uses opaque resource UID. `PurposeClass::Enrolled` maps to gateway-backed enrollment — v3 ZoneLink replaces "gateway". | +| v3 Destination | `packages/d2b-bus/src/routing.rs` (adapted from `service_v2.rs`); `RemoteNodeErrorKind` → v3 `BusErrorKind` with same `code()` stable-label pattern; bounds adopted verbatim. | +| Integration | `d2b-bus` route handler adapts `RealmServiceServer` dispatch table; `RemoteNodeErrorKind::code()` values feed `d2b_bus_route_total{outcome}` metric labels; `CredentialCustody::Host` maps to `purpose_class=local` in `d2b_session_connect_total`. | +| Validation | Adopt `authority_keeps_remote_credentials_in_gateway_guests` test renamed to `authority_keeps_remote_credentials_in_zone_link_sessions`; adapt `RealmId` → Zone name; adopt `authenticated_bootstrap_enrollment_route_and_shortcut_lifecycle` renamed with zone terminology. | + +### ADR046-reuse-007 — d2bd service routing, provider effects, and daemon session tests + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reuse-007` | +| Dependency/owner | ADR046-reuse-004 + ADR046-reuse-006; core-controller routing owner | +| Current source | Main commit source row below: `packages/d2bd/` routing/provider effects files and tests at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/service_router.rs` and `packages/d2b-core-controller/src/provider_effects.rs`. | +| Detailed design | Closed-set service.package route gates, provider lifecycle effect-adapter composition, closed-set error mapping, fixed local policy invariants, and bootstrap/enrolled credential binding shape. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt route-gate and ProviderLifecycleDispatch patterns; exclude daemon-embedded effect composition and route through v3 bus/provider processes.. | +| Data migration | Full d2b 3.0 reset; no v2 daemon routing/provider-effects state import | +| Removal proof | ADR45 daemon-embedded effect adapters and realm child supervisor assumptions are excluded; v3 bus/provider effects own the adapted behavior. | +| Main commit source | `packages/d2bd/` at `a1cc0b2d`. Key files: `src/provider_registry.rs` (full `ProviderCompositionError` mapping, factory composition, `DaemonEffectAdapters` wiring), `src/provider_effects.rs` (`ProviderLifecycleDispatch`, `DaemonEffectAdapterError`, effect adapter structs per domain: `DeviceEffectAdapter`, `AudioEffectAdapter`, etc.), `src/control_services/provider.rs` (`owns`: `service.package == "d2b.provider.v2"` route gate), `src/control_services/daemon.rs` (daemon service route gate), `src/realm_child_supervisor.rs` (realm supervisor with pidfd adoption), `src/realm_stubs.rs`. Tests: `tests/daemon_service_v2.rs` (`every_generated_daemon_method_has_one_typed_adapter`, `local_daemon_policy_is_fixed_and_has_no_negotiation_or_fd_surface`, `public_daemon_handshake_rejects_daemon_or_guest_proxy_schema_mismatch`, `daemon_uses_shared_bootstrap_and_enrolled_guest_credential_bindings`, `shared_guest_session_credential_rejects_zero_authority`, `daemon_guest_paths_do_not_call_broker_signing_or_define_a_private_codec`), `tests/realm_child_supervisor_v2.rs`, `tests/realm_service_v2.rs`. | +| Selected behavior | `service.package == "d2b.provider.v2"` route gate pattern (closed-set package matching without reflection); `ProviderLifecycleDispatch` effect-adapter composition; `DaemonEffectAdapterError` closed-set; `local_daemon_policy_is_fixed` test invariant (no negotiation surface, no fd surface on local policy); bootstrap/enrolled credential binding shape. | +| Excluded ADR45 assumptions | `DaemonEffectAdapters` is daemon-embedded effect composition — v3 effect adapters are per-Provider-process, not daemon-embedded. `control_services/provider.rs::owns` routes to the ADR45 `d2bd` daemon service; v3 routes through `d2b-bus`. `RealmPath as ProviderRealmPath`/`WorkloadId` imports are ADR45 realm concepts — excluded from v3 bus routing. `realm_child_supervisor.rs` uses `RealmId`/`WorkloadId` to supervise child realm processes — v3 replaces with Zone resource UID supervision. `realm_stubs.rs` stubs out ADR45 realm creation — excluded. | +| v3 Destination | `packages/d2b-bus/src/service_router.rs` (adapts route-gate pattern); `packages/d2b-core-controller/src/provider_effects.rs` (adapts `ProviderLifecycleDispatch`); route-gate policy tests adapted from `daemon_service_v2.rs` invariants. | +| Integration | Bus service router uses `service.package` closed-set matching from route-gate pattern; `ProviderLifecycleDispatch` feeds `d2b_provider_component_phase` metric. | +| Validation | Port `local_daemon_policy_is_fixed_and_has_no_negotiation_or_fd_surface` invariant to v3 bus local policy test; port `every_generated_daemon_method_has_one_typed_adapter` to v3 bus method adapter completeness test. | + +### ADR046-reuse-008 — ComponentSession v2 vector tests and contract conformance + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reuse-008` | +| Dependency/owner | ADR046-reuse-001; contract-tests owner | +| Current source | Main commit source row below: ComponentSession vector/conformance tests at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-contract-tests/tests/component_session_v2_vectors.rs` and `tests/noise_vectors.rs` copied verbatim. | +| Detailed design | Pinned Noise KAT vectors, transcript/PSK mutation rejection, typed bootstrap admission fixture, and public-key corruption rejection remain the session wire security ground truth. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy the vector and conformance tests verbatim; add v3 constant gate for ComponentSession major/minor version.. | +| Data migration | None — docs/tooling only; no runtime state | +| Removal proof | None — net-new; no prior owner to remove | +| Main commit source | `packages/d2b-contract-tests/tests/component_session_v2_vectors.rs` at `a1cc0b2d` (`committed_noise_vectors_verify_with_pinned_snow`, `declared_noise_public_key_corruption_is_rejected`, `bootstrap_fixture_mutations_execute_typed_admission_state`, `transcript_and_psk_mutations_are_rejected`). `packages/d2b-contracts/tests/component_session_v2.rs` at `a1cc0b2d`. `packages/d2b-session/tests/noise_vectors.rs` (pinned KAT vectors). | +| Selected behavior | Pinned Noise KAT vectors against the exact `snow` version; transcript+PSK mutation rejection; bootstrap fixture typed-admission state machine; public-key corruption detection. These tests are the ground-truth for session wire security. | +| Excluded ADR45 assumptions | None — these tests have no topology dependency. They test only the cryptographic layer. | +| v3 Destination | `packages/d2b-contract-tests/tests/component_session_v2_vectors.rs` copied verbatim. `tests/noise_vectors.rs` copied verbatim. Neither file requires modification. | +| Integration | These tests run in `make test-rust` / `cargo test -p d2b-contract-tests` and `cargo test -p d2b-session`. They are gating for any Noise library update. | +| Validation | These tests are self-validating. Add one gate: assert `COMPONENT_SESSION_MAJOR = 2` and `COMPONENT_SESSION_MINOR = 0` constants are unchanged in v3 contract. | + +### ADR046-reuse-009 — Session MetricsSink → d2b-telemetry bridge + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-reuse-009` | +| Dependency/owner | ADR046-reuse-001 + ADR046-telem-001 + ADR046-telem-003; telemetry/session owner | +| Current source | Main commit source row below: `packages/d2b-session/src/metrics.rs` and `MetricLabels` in `packages/d2b-contracts/src/v2_component_session.rs` at `a1cc0b2d`. | +| Reuse action | adapt | +| Destination | `packages/d2b-telemetry/src/session_metrics_sink.rs`. | +| Detailed design | Map every MetricEvent variant to the `d2b_session_*` inventory; convert MetricLabels closed sets to allowed v3 label strings; keep NoopMetrics for hermetic tests. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt MetricsSink bridge to v3 OTEL instruments and rename ADR45 endpoint-purpose label values to v3 closed-set labels.. | +| Data migration | Full d2b 3.0 reset; no v2 telemetry metric state import | +| Removal proof | ADR45 EndpointPurpose metric label strings are replaced by v3 purpose names; no prior v3 MetricsSink bridge owner to remove. | +| Main commit source | `packages/d2b-session/src/metrics.rs` at `a1cc0b2d` (`MetricEvent` enum: `ActiveSessions`, `Handshake`, `ConnectAttempt`, `ReconnectAttempt`, `Close`, `ControlCreditExhaustion`, `QueueDepth`, `QueueCapacity`, `SchedulingDelay`, `RejectedRecord`; `MetricsSink` trait; `NoopMetrics`). `packages/d2b-contracts/src/v2_component_session.rs::MetricLabels` (`transport: TransportClass`, `purpose: EndpointPurpose`, `channel_class: ChannelClass`, `noise: NoiseProfile`, `locality: Locality`). | +| Selected behavior | `MetricsSink` injection interface: `record(event: MetricEvent, labels: MetricLabels, value: u64)`. Every `MetricEvent` variant maps to a v3 metric instrument from the `d2b_session_*` inventory in this spec. `MetricLabels` fields provide the closed-set label values for those instruments. `NoopMetrics` used in hermetic tests. | +| Excluded ADR45 assumptions | `EndpointPurpose` variants in `MetricLabels` carry ADR45 purpose names (`DaemonLocal`, `DaemonRemote`, `RealmPeer`) — these become v3 purpose names in the OTEL label. `MetricLabels.channel_class` and `MetricLabels.locality` are currently unexported in main but map directly to `session_active{transport}` and `session_connect_total{purpose_class}` labels. | +| v3 Destination | `packages/d2b-telemetry/src/session_metrics_sink.rs`: implements `MetricsSink` backed by the OTEL `d2b_session_*` instruments. `MetricEvent::ActiveSessions` → `d2b_session_active` gauge; `MetricEvent::Handshake` → `d2b_session_connect_total` counter; `MetricEvent::ReconnectAttempt` → `d2b_session_reconnect_total`; `MetricEvent::ControlCreditExhaustion` → `d2b_telemetry_drop_total{signal="session"}`. `MetricLabels.noise` → `profile` label using `NoiseProfile::as_str()`. | +| Integration | `serve_ttrpc_services` receives a `Box` from `d2b-telemetry`; all session endpoints call through this bridge. | +| Validation | New test `packages/d2b-telemetry/tests/session_sink_bridge.rs`: drive `MetricEvent` variants through the sink; assert OTEL counter/gauge values; assert `MetricLabels` closed-set values map only to allowed label strings (no `DaemonLocal` string in v3 metric output). | + +### ADR046-telem-009 — Nix resource authoring shape, schema-driven options, and bundle emission + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-telem-009` | +| Dependency/owner | W0/W1a; Nix integrator (adapts `nixos-modules/options-observability.nix` and `options-realms.nix`) | +| Current source | `nixos-modules/options-observability.nix` (`d2b.observability.host.identityName`, `scrapeJournal`, `otlpIngest.enable`, `signoz.jwtSecretFile`, `signoz.rootPasswordFile`, `signoz.clickhousePasswordFile`, `retention.*`, `sampling.*` — all predecessor options); `nixos-modules/options-realms.nix` (submodule/option-type/assertion pattern); `nixos-modules/components/observability/{host,stack,guest}.nix` (Nix pipeline shape, ACL pattern, `identityName`/`vmName`); `nixos-modules/manifest.nix` (resource-bundle emission pattern); `packages/d2b-contract-tests/tests/policy_observability.rs::loki_native_otel_resource_attributes` (closed allowlist enforcement — adapt as bundle schema policy test) | +| Reuse action | adapt | +| Destination | `nixos-modules/resources.nix` (uniform `d2b.zones..resources` schema-aware option; `spec.*` option types generated from `ResourceTypeSchema` for each `type`); `nixos-modules/resources-bundle.nix` (ADR-only: sorted integrity-pinned bundle derivation) | +| Detailed design | (1) Implement `d2b.zones..resources = lib.mkOption { type = lib.types.attrsOf (schemaAwareResourceSubmodule); }` where the submodule, given `config.type`, loads the registered `ResourceTypeSchema` and generates `spec.*` option types from it. For `type = "Provider"`, `spec.config.*` options are generated from the signed Provider schema for the package identified by `spec.artifactId` (see ADR-046-provider-model-and-packaging). No second bespoke vocabulary; `spec` fields mirror the canonical JSON fields exactly. (2) `resources-bundle.nix` derivation: serialize each resource to canonical sorted JSON (keys alphabetically sorted at every level); sort resources by `(type, name)`; compute generation digest; emit `zone-resources-.json` as Nix store output. Publication handler sets `metadata.managedBy = "configuration"` and `metadata.configurationGeneration` on activation — these fields are NOT authored in Nix. (3) `status`, UID, generation, revision, and timestamps are absent from Nix authoring; core fills them. Primary reuse disposition: `adapt`. Preserved source-plan detail: Implement uniform `d2b.zones..resources. = { type; spec; }` option with schema-driven `spec.*` generated option types; adapt option submodule pattern from `options-realms.nix`; adapt pipeline shape from `{host,stack,guest}.nix`; emit canonical sorted ResourceSpec JSON from `resources-bundle.nix`. | +| Integration | `d2b-core-controller` reads the Nix store path from the activated system closure; secrets never appear in the bundle | +| Data migration | Current `d2b.observability.*` options are retained with compat warnings (same pattern as current `retention.*`/`sampling.*` compat options); the v3 `d2b.zones..resources.*` option is the authoritative surface | +| Validation | `eval_rejects_unknown_type`, `eval_rejects_invalid_emitter_ring_size`, `eval_rejects_unknown_provider_settings`, `eval_rejects_inline_secret_in_settings`, `eval_rejects_unresolved_credential_ref`, `eval_rejects_duplicate_resource_name` nix-unit cases; `bundle_is_sorted_canonically`, `bundle_digest_is_deterministic`, `bundle_contains_no_secret_values`, `bundle_schema_validates_against_provider_schema` contract tests | +| Removal proof | `nixos-modules/options-observability.nix` predecessor options retained with compat warnings until `d2b.observability.enable` migration is complete | + +### ADR046-telem-010 — Build-time and runtime ResourceTypeSchema validation + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-telem-010` | +| Dependency/owner | ADR046-telem-009 + ADR046-telem-006 + ADR046-store-001; schema/validation owner | +| Current source | `packages/d2b-contract-tests/tests/policy_observability.rs::startup_tracing_avoids_host_path_fields` (forbidden-field pattern enforcement — adapt as bundle forbidden-field gate); `packages/d2b-contract-tests/tests/policy_metrics.rs` (metric inventory policy test pattern); `packages/d2b-priv-broker/src/runtime.rs` (current runtime schema load/verify pattern); `packages/d2b-contracts/src/provider_registry_v2.rs::ProviderBindingV2` (non-exhaustive signed schema contract) | +| Reuse action | adapt | +| Destination | `nixos-modules/resources-bundle.nix` (build-time validation step 4 in the `resources-bundle` derivation); `packages/d2b-core-controller/src/configuration.rs` (runtime activation checks) | +| Detailed design | Build-time: (1) For each `Provider` resource, fetch the `resourceTypeSchema` output from the package; validate `settings` JSON against the JSON Schema; fail the build on schema mismatch or unknown fields. (2) Assert no resource spec field contains a bare secret/path/argv (forbidden-field pattern from `startup_tracing_avoids_host_path_fields`). Runtime: (3) Core-controller re-validates Provider package identity (per ADR-046-provider-model-and-packaging) against the installed package; resolves Credential refs; checks conflict/bounds; rejects with closed-enum `generation-rejected` reason on any failure; no partial activation. (4) Provider schema mismatch between the bundle's schema and the installed Provider's live schema → reject, emit `generation-rejected{reason="package-identity-mismatch"}`. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt `startup_tracing_avoids_host_path_fields` forbidden-field pattern for bundle schema gate; adapt `ProviderBindingV2` non-exhaustive contract for Provider-specific settings schema fingerprint. | +| Integration | Nix `resources-bundle.nix` derivation gate + core-controller `configuration.rs` activation path | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `bundle_schema_validates_against_provider_schema` bundle contract test; `generation_rejected_emits_audit_record` cleanup contract test with a `schema-validation-failed` reason; add a nix-unit case `eval_rejects_unknown_fields_against_signed_schema` that runs the bundle derivation with a schema mismatch and asserts build failure | +| Removal proof | None — net-new; no prior owner to remove; this is new tooling | + +### ADR046-telem-011 — Configuration-owned resource cleanup contract implementation + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-telem-011` | +| Dependency/owner | ADR046-telem-009 + ADR046-telem-010 + ADR046-audit-001 + ADR046-store-001; core-controller owner | +| Current source | `packages/d2bd/src/daemon_audit.rs` (hash-chain `ResourceMutation`-like append pattern — adapt for cleanup audit records); `packages/d2b-priv-broker/src/audit.rs` (`AuditWriteClass::{Standard,Unprivileged}` — cleanup audit records use `Standard` durability); `packages/d2b-realm-core/src/audit.rs::AuditChainLink::new` (hash-chain append for cleanup audit records); `nixos-modules/manifest.nix` (prior-generation retention pattern in the current bundle contract) | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/{configuration.rs, ownership.rs}` | +| Detailed design | (1) On new generation activation, every stored `managedBy=configuration` resource absent from the new configured set receives `deletionRequestedAt` plus `deletion-pending`; controller/API-managed resources are untouched. (2) Activation returns after durable intent queueing and does not wait for cleanup. (3) The ownership handler drives child-before-parent finalizers. (4) When finalizers clear, one atomic store transaction writes the `Deleted` revision/change event and removes the row and indexes. After commit, the audit subsystem appends `ResourceMutation{event="deleted", trigger="config-cleanup"}` from that revision using a dedup/exactly-once recovery key; audit append is not part of the store transaction. (5) Stall detection sets `cleanup-stalled` without force-removing finalizers. (6) Prior generations use count retention, default 3 and range 1..16, with no TTL. (7) Core sets `managedBy`/`configurationGeneration` in persisted resources; input bundles omit both. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt hash-chain append from `daemon_audit.rs` for `ResourceMutation{trigger="config-cleanup"}` records; adapt prior-generation retention window from `manifest.nix` pattern. | +| Integration | `d2b-core-controller::configuration.rs` (generation activation); `d2b-core-controller::ownership.rs` (cleanup ordering and atomic final deletion); `d2b-audit` sink (cleanup audit records) | +| Data migration | None — the `managedBy`/`configurationGeneration`/`deletionRequestedAt` fields are new; existing resources gain them on first v3 activation | +| Validation | All tests in "Configuration-owned cleanup contract tests" subsection; additionally: `managedby_configuration_set_on_activated_resources`, `controller_created_resources_have_managedby_controller`, `absent_resource_receives_delete_on_new_generation`, `deletion_sets_deletionrequestedat_not_phase`, `final_deletion_is_atomic`, `cleanup_does_not_touch_controller_children`, `pending_cleanup_condition_set_on_zone`, `zone_is_degraded_not_failed_during_cleanup`, `pending_cleanup_cleared_after_deletion_completes`, `prior_generation_retained_count_based`, `rollback_schedules_delete_for_new_generation_resources`, `audit_segments_preserved_on_provider_delete`, `cleanup_stall_condition_set`, `generation_rejected_emits_audit_record` | +| Removal proof | None — net-new; no prior owner to remove; this is new behavior | diff --git a/docs/specs/ADR-046-terminology-and-identities.md b/docs/specs/ADR-046-terminology-and-identities.md new file mode 100644 index 000000000..334bac951 --- /dev/null +++ b/docs/specs/ADR-046-terminology-and-identities.md @@ -0,0 +1,302 @@ +# ADR 0046 terminology and identities + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-terminology-and-identities` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-contracts`, Zone runtime, Nix resource compiler | +| Depends on | `ADR-046-decision-register` | +| Supersedes | Public Realm terminology selected by ADR 0043 for d2b v3 | + +## Purpose + +This spec defines the names, identities, references, and scoping rules used by +every ADR 0046 specification. + +## Terms + +### Zone + +A Zone is the d2b 3.0 resource, policy, authorization, routing, state, and audit +boundary. Every resource belongs to exactly one Zone and is stored in that +Zone's redb database. + +Every Zone store contains exactly one authoritative: + +```text +Zone/ +``` + +The resource's metadata.zone equals ``. Each non-root child stores +one child-local `ZoneLink/` uplink. Its compiler-only `parentZone` +setting selects the parent allocator and is never emitted as a resource. +Parent access uses the child Zone API; resources and ordinary refs are not +copied across Zones. + +`Realm` remains current v3 baseline terminology and migration evidence. New d2b +3.0 public schemas, CLI, APIs, errors, and docs use `Zone`. + +### ResourceType and ResourceSpec + +A ResourceType is a Zone-bound schema/controller contract. A ResourceSpec is +one resource's desired-state object under that type. + +The public envelope field is `type`. ADR 0046 does not use Kubernetes +ResourceKind/kind terminology. + +Standard ResourceTypes use a short Zone-unique name: + +```text +Host +Guest +Process +Volume +``` + +Vendor ResourceTypes use a qualified name: + +```text +acme.io.Widget +``` + +ResourceApiBinding rejects type-name collisions. A type cannot be selected by +an ambiguous short name. + +### Resource reference + +Every field ending in `Ref` contains: + +```text +/ +``` + +Examples: + +```text +Zone/dev +Provider/system-core +Host/host-system +Process/wayland-proxy +User/alice +Volume/work-state +``` + +Rules: + +- refs resolve only in the caller/resource's Zone; +- ResourceType and name are both required; +- `resource_name` matches `^[a-z][a-z0-9-]*$`; +- the serialized form has no scheme, Zone prefix, query, fragment, relative + segment, or implicit default type; +- a plain enum/inline value never has a `Ref` suffix; +- a ref resolves both canonical type/name and the target's immutable UID; +- a deleted/recreated object with the same type/name has a different UID and + does not silently inherit old ownership/operation state. + +Cross-Zone ResourceRefs do not exist in the initial contract. A future need is +a decision-required architecture change. + +### Provider + +A Provider is installed as: + +```text +Provider/ +``` + +Its resource binds exact package/config/schema/controller/service/process/ +state/trust generations and status. A providerRef cannot resolve merely because +a package exists in the Nix store or catalog; the Provider resource must exist +and be Ready in the Zone. + +One Provider maps to one independently buildable crate/package. It may contain +several separately sandboxed process binaries. + +### Host + +A Host is a physical/local host execution, policy, and budget parent: + +```text +Host/ +``` + +Provider/system-core reconciles Host and local User. A Zone may define several +Hosts for separate system/user policy and budgets. + +### Guest + +A Guest is a non-host VM, sandbox, cloud, or remote execution parent: + +```text +Guest/ +``` + +Each Guest selects an installed runtime Provider such as Cloud Hypervisor, +QEMU, ACA, or Azure VM. + +### ExecutionPolicy + +Host and Guest share: + +- `providerRef`; +- `defaultDomain: system|user`; +- `allowedDomains`; +- `defaultUserRef` when user is the default. +- budgets; +- Volume/Network/Device attachment defaults. + +### Process domain + +Process and EphemeralProcess placement consists of: + +- required `executionRef`; +- optional `domain: system|user`, defaulting from the referenced Host/Guest; +- `userRef` when user domain does not inherit its default. + +executionRef must resolve to Host or Guest. Remote/nested are Guest Provider/ +Zone properties, not Process domains or duplicate Process ResourceTypes. + +### Process implementation + +Process and EphemeralProcess use `providerRef` to select an installed +implementation such as: + +```text +Provider/system-systemd +Provider/system-minijail +``` + +Both implement one common execution schema and mandatory local pidfd +conformance. The pidfd is local ephemeral controller authority, not a resource +or ref. + +### AuthenticatedSubjectContext + +The shared identity/authorization seam is: + +```text +AuthenticatedSubjectContext { + subjectRef + subjectUid + zoneRef + evidenceClass + executionRef? + providerRef? + processRef? + controllerGeneration? + providerGeneration? + sessionPurpose + service + schemaFingerprint + transportBinding + reconnectGeneration + transcriptHash +} +``` + +This spec owns the field contract. ComponentSession owns mapping trusted +Unix/Noise/bootstrap/vsock/enrollment evidence into it. The resource API and +d2b-bus consume it when building native authorization attributes. Peers cannot +self-assert or mutate it. + +### Owner + +Each resource has zero or one `metadata.ownerRef`. It is same-Zone and resolves +to canonical ref plus immutable UID. + +Ownership means: + +- a child mutation triggers owner reconciliation; +- owner deletion orders child finalization first; +- owner cycles fail at commit; +- unrelated dependencies use ordinary typed refs, not ownerRef. + +### Generation, revision, and UID + +- `metadata.uid` is immutable store-generated identity. +- `metadata.generation` starts at 1 and increments only on spec change. +- `metadata.revision` is the opaque Zone-local optimistic concurrency/watch + token of the resource's latest committed mutation. +- `status.observedGeneration` is the latest spec generation the controller has + observed and accounted for. + +None is a timestamp. Human-readable generation names are not part of the common +contract. + +### Time + +Persistent datetimes are RFC 3339 UTC with bounded precision. Wall time never +extends an already-admitted monotonic deadline. Common metadata/status times +are described in +[`ADR-046-resource-object-model`](ADR-046-resource-object-model.md). + +## Canonical process identities + +Fixed Zone bootstrap process: + +```text +z-@ +``` + +System-domain Host/Guest Process: + +```text +s-@ +``` + +User-domain Host/Guest Process: + +```text +u--@ +``` + +The logical name is diagnostic only. Adoption also verifies Provider, +component/template, executable/config/sandbox generations, executionRef/domain, +cgroup/scope, and provider-specific process identity. + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | `d2b-realm-core/src/ids.rs`, `realm.rs`, `target.rs`, `workload.rs`; `nixos-modules/options-realms*.nix`; `index.nix` | +| Evidence class | Current Realm/Workload IDs are implemented-and-reachable; Zone/Host/Guest/ResourceRef/Provider resources are ADR-only | +| Behavior retained | Bounded fail-closed IDs, canonical target parsing, opaque token redaction, stable current Workload identity | +| Required delta | Zone term/type, universal ResourceRef, UID/generation/revision, Host/Guest split, Process domains | +| Reuse path | Adapt current ID validators/serde/redaction; map current Workload/Realm only where evidence says reachable | +| Replacement/deletion | Realm public types/options remain until the v3 cutover work item supplies Zone successors | +| Feasibility proof | Golden ref/ID vectors shared by Rust/Nix/other SDKs; collision and UID-recreate tests | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-identities-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | W0 shared contract root; `d2b-contracts` | +| Current source | `packages/d2b-realm-core/src/ids.rs`, `realm.rs`, `target.rs`, `workload.rs` | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/identity.rs`, `packages/d2b-contracts/src/v3/resource_ref.rs` | +| Detailed design | Add ZoneId, ResourceTypeName, ResourceName, ResourceUid, ResourceRef, generation/revision newtypes, exact parsing/serde/Debug/redaction, and golden vectors Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Resource API/store/controllers/SDK/Nix import only these canonical types | +| Data migration | Destructive d2b 3.0 reset; no RealmRef parser compatibility | +| Validation | Rust property/vector tests; pure-Nix vector parity; malformed/collision/UID-recreate tests | +| Removal proof | Old public Realm target types removed only after all v3 callers consume Zone/ResourceRef | + +### ADR046-identities-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-identities-001; Nix integrator | +| Current source | `nixos-modules/options-realms.nix`, `options-realms-workloads.nix`, `index.nix` | +| Reuse action | adapt | +| Destination | `nixos-modules/options-zones.nix`, `nixos-modules/resources.nix`, `nixos-modules/index.nix` | +| Detailed design | Validate Zone names, ResourceTypes/names/refs, shared Host/Guest ExecutionPolicy, and canonical sorted resource identities | +| Integration | Nix resource objects serialize exactly the Rust contract | +| Data migration | Full reset and new Zone declarations | +| Validation | nix-unit vectors and rendered contract tests | +| Removal proof | Realm-facing declarations removed only in the reset/purge wave | diff --git a/docs/specs/ADR-046-validation-and-delivery.md b/docs/specs/ADR-046-validation-and-delivery.md new file mode 100644 index 000000000..8b40cb52e --- /dev/null +++ b/docs/specs/ADR-046-validation-and-delivery.md @@ -0,0 +1,1412 @@ +# ADR 0046 validation and delivery contract + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-validation-and-delivery` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | ADR 0046 integrator, `xtask` delivery tooling, panel/validator process owners | +| Depends on | `ADR-046-decision-register`, `ADR-046-terminology-and-identities`, `ADR-046-resource-object-model`, `ADR-046-resource-store-redb`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-componentsession-and-bus`, `ADR-046-primitive-resource-composition`, `ADR-046-zone-routing`, `ADR-046-provider-model-and-packaging`, `ADR-046-components-processes-and-sandbox`, `ADR-046-core-controllers`, `ADR-046-resources-network`, `ADR-046-resources-credential`, `ADR-046-provider-state`, `ADR-046-resources-zone-control`, `ADR-046-resources-host-guest-process-user`, `ADR-046-resources-volume`, `ADR-046-resources-device`, `ADR-046-telemetry-audit-and-support`, `ADR-046-cli-and-operations`, `ADR-046-nix-configuration`, `ADR-046-current-code-migration-map`, every `ADR-046-provider-*` dossier, and the `ADR-046-security-and-threat-model`, `ADR-046-streamline`, `ADR-046-reset-and-cutover`, `ADR-046-feasibility-and-spikes` closing specs | +| Supersedes | This repository's current `AGENTS.md` "Panel review" phase-gate as the *sole* review mechanism for ADR 0046 work (extended, not replaced, per §12); ad hoc per-agent validation ordering for ADR 0046 implementation | + +## 1. Purpose and scope + +This spec is the normative implementation delivery contract for ADR 0046 / +d2b 3.0. It defines: the exact wave/dependency graph derived from every +`ADR-046-*` spec's declared `Depends on` edges; per-wave entry/exit criteria; +Git Town stack shape and worktree/branch ownership; speculative readiness and +the anti-serialization file-overlap graph; the shared-prep pattern for +contended files; codegen/schema pin management; current-code deletion gates; +the full validation matrix from Tier0 through manual hardware/live/cloud +tiers; the sole heavy-gate mechanism; the immutable candidate snapshot, +validator-evidence, and ten-role panel/attest/seal/eligibility process; PR +opening versus final-lane semantics; merge order; post-wave +branch/worktree/GC cleanup policy; and the release/cutover gate. + +This spec is documentation only. It creates no crates, dependencies, Nix +modules, services, controllers, Providers, state stores, CI workflows, or +`xtask` subcommands. Per ADR 0046 decision D024, future W0–W7 implementation +(§3) requires a separate request. This spec is the binding contract that +request must follow; it does not itself begin that work, and no cleanup, +branch deletion, or worktree removal described in §14 is performed by this +change. + +## 2. Manifest closure gate (Gate 0 — precondition for any implementation wave) + +No `ADR046-W*` implementation wave in §3 may open until **all** of the +following are true, per the parent ADR's "Review and acceptance" contract and +`docs/specs/README.md`: + +1. Every file in the `docs/specs/ADR-046-*` manifest — the exact 28 top-level + specs and 27 `docs/specs/providers/ADR-046-provider-*` dossiers — is + `Status: Accepted`. The five closing specs are + `ADR-046-feasibility-and-spikes`, `ADR-046-reset-and-cutover`, + `ADR-046-security-and-threat-model`, `ADR-046-streamline`, and this spec. +2. `ADR-046-decision-register` has zero rows under "Open decisions." +3. `docs/specs/ADR-046-spec-set.json` and `docs/specs/ADR-046-work-items.json` + exist and enumerate every spec above with matching content digests (§8). + Every declared Markdown work-item heading is in exact bijection with the + work-item manifest, its owner/path/prefix and mandatory fields match, and + its `reuseAction` is one closed scalar. The future generator and its + fail-closed policy tests are `ADR046-delivery-004` and + `ADR046-delivery-009`; no checked-in generator exists in this Proposed + documentation candidate. +4. The ADR/spec PR has both required human review gates from the parent ADR: + approval before the immutable final panel snapshot, and approval after + unanimous panel signoff (this is the spec-authoring panel, distinct from + the per-implementation-wave panel defined in §12). +5. No spec contains an unresolved decision, a missing ResourceType/ + core-controller/Provider dossier, an undefined ref/owner/controller/ + process/state/limit/error/test, a work item without exact source and + destination paths, or a claim that proposed v3 implementation is already + live. + +Gate 0 is re-evaluated, not waived, if any manifest member changes content +after being marked Accepted (parent ADR: "Any content change invalidates +validation and panel evidence"). + +## 3. Delivery wave topology + +### 3.1 Derivation rule + +Each spec's wave is `1 + max(wave(dep) for dep in "Depends on" if dep is an +ADR-046-* spec)`, with `ADR-046-decision-register` at wave floor. This is a +plain topological layering over the exact `Depends on` edges recorded in +every spec's metadata table (verified directly against each file at baseline +`b5ddbed6`, not inferred). Two specs in the same wave are parallel-safe by +*dependency* satisfaction only; §6 additionally requires file-disjointness +before two specs may run in literally concurrent worktrees. Where a spec's +own `Depends on` list is a strict subset of another same-wave spec's +prerequisites, its implementation branch MAY open earlier than its assigned +wave boundary under the speculative-readiness rule in §6 — the wave number +below is the latest-safe placement, not the earliest-possible one. + +### 3.2 Wave assignment table + +| Wave | Specs (all must be `Accepted`; Gate 0 already covers this) | New/changed crates and modules (destination roots) | +| --- | --- | --- | +| `ADR046-W0` | `ADR-046-terminology-and-identities` → `ADR-046-resource-object-model` → `ADR-046-resource-store-redb` → `ADR-046-resource-api-and-authorization` (serial sub-steps, one integrator branch) | `packages/d2b-contracts/src/v3/{identity,resource_ref,resource,resource_status,resource_schema}.rs`; `packages/d2b-resource-store/`, `packages/d2b-resource-store-redb/`; `packages/d2b-contracts/proto/d2b-resource-v3.proto`; `packages/d2b-resource-api/`; `nixos-modules/{options-zones,resources,index}.nix` | +| `ADR046-W1` | `ADR-046-resource-reconciliation` ‖ `ADR-046-componentsession-and-bus` | `packages/d2b-controller-toolkit/`; `packages/d2b-core-controller/src/{hints,dependencies,owner_reconcile}.rs`; `packages/d2b-contracts/src/v3/component_session.rs`; `packages/d2b-session/`; `packages/d2b-session-unix/`; `packages/d2b-bus/` | +| `ADR046-W2` | `ADR-046-primitive-resource-composition` ‖ `ADR-046-zone-routing` | `packages/d2b-contracts/src/v3/{host,guest,execution_policy,process,volume,user,network,device,credential}.rs`; `packages/d2b-process/`; `packages/d2b-provider-supervisor/`; `packages/d2b-zone-routing/` | +| `ADR046-W3` | `ADR-046-provider-model-and-packaging` (single spec; strictly serial — every downstream Provider dossier depends on it) | `packages/d2b-provider/`; `packages/d2b-provider-toolkit/`; one `packages/d2b-provider--/` skeleton generator | +| `ADR046-W4` | `ADR-046-components-processes-and-sandbox` ‖ `ADR-046-core-controllers` ‖ `ADR-046-resources-network` ‖ `ADR-046-resources-credential` ‖ `ADR-046-provider-state` (five parallel specs) | `packages/d2b-process/`, `d2b-provider-supervisor/` (process effect ports); `packages/d2b-core-controller/`; `packages/d2b-provider-network-local/` schema half; `packages/d2b-provider-credential-*/` schema half; Volume `stateSchema`/`persistenceClass`/`sensitivityClass` extension | +| `ADR046-W5` | `ADR-046-resources-zone-control` ‖ `ADR-046-resources-host-guest-process-user` ‖ `ADR-046-resources-volume` ‖ `ADR-046-resources-device` ‖ `ADR-046-telemetry-audit-and-support` ‖ `ADR-046-cli-and-operations` ‖ `ADR-046-nix-configuration` (seven parallel specs) | `packages/d2b-provider-system-{core,systemd,minijail}/`; `packages/d2b-provider-volume-{local,virtiofs}/` schema half; `packages/d2b-provider-device-*/` schema half; `packages/d2b-telemetry/`, `d2b-audit/`; `packages/d2b/` CLI; `nixos-modules/resources-*.nix` | +| `ADR046-W6` | All 27 `ADR-046-provider-*` dossiers, grouped into five file-disjoint provider families (§3.3) | One `packages/d2b-provider--/` per Provider (27 crates) | +| `ADR046-W7` | `ADR-046-feasibility-and-spikes` ‖ `ADR-046-reset-and-cutover` ‖ `ADR-046-security-and-threat-model` ‖ `ADR-046-streamline` ‖ `ADR-046-validation-and-delivery` | Cross-cutting friction fixes, reset/cutover mechanics, feasibility closure, security closure, and the release gate (§15) | + +Waves are numbered `ADR046-W0`…`ADR046-W7` — an ADR-046-scoped namespace, +distinct from this repository's general per-plan `Wn` commit-tag convention +in `AGENTS.md`. Commit subjects for ADR 0046 implementation work use +`( ADR046-W )`, `( ADR046-Wfu )`, and +`( ADR046-Wfu )` following the same severity/ordinal grammar +`AGENTS.md` already defines, so existing tooling and human reviewers read one +consistent tag shape. + +### 3.3 Wave 6 provider families (file-disjoint parallel tracks) + +`ADR046-W6` is the largest wave (27 crates). Every dossier's `Depends on` list +resolves to wave ≤5 prerequisites only (verified per-dossier against the +metadata tables read directly from `docs/specs/providers/*.md`), so all 27 +are dependency-parallel. File-disjointness (one crate directory per Provider, +per D012/`ADR-046-provider-model-and-packaging` crate boundary) makes them +worktree-parallel too, grouped into five independently staffed tracks so no +single agent/reviewer owns all 27 at once: + +| Track | Providers (crate `packages/d2b-provider--/`) | +| --- | --- | +| System/Host/Guest (7) | `system-core`, `system-systemd`, `system-minijail`, `runtime-cloud-hypervisor`, `runtime-qemu-media`, `runtime-azure-container-apps`, `runtime-azure-virtual-machine` | +| Storage/network/device (7) | `volume-local`, `volume-virtiofs`, `network-local`, `device-tpm`, `device-usbip`, `device-security-key`, `device-gpu` | +| Interaction (5) | `display-wayland`, `audio-pipewire`, `clipboard-wayland`, `notification-desktop`, `shell-terminal` | +| Credentials (3) | `credential-secret-service`, `credential-entra`, `credential-managed-identity` | +| Transport/observability/activation (5) | `transport-unix`, `transport-vsock`, `transport-azure-relay`, `observability-otel`, `activation-nixos` | + +Within a track, the 3–7 Providers are further parallel (each is its own +crate, its own `ADR-046-provider-.md` dossier, and its own +`tests/`/`integration/` tree per D059). The only intra-track ordering +constraint is `volume-local` before `volume-virtiofs` (D083: volume-virtiofs +never writes Volume layout/spec/ownership fields, but its controller +`Depends on` `ADR-046-resources-volume` and reads Ready Volume rows created +by volume-local in integration tests) and `network-local` before +`device-usbip` (device-usbip's dossier lists `ADR-046-resources-network` as a +dependency for its firewall/export attachment). Both are soft integration-test +orderings, not authoring blockers — the crates themselves may be authored +concurrently; only their `integration/` scenario tests need the peer Provider +present. + +### 3.4 Full dependency edge table + +The edges below were read directly from every spec's `Depends on` metadata +row at baseline `b5ddbed6`; they are the source of truth for §3.1's wave +placement and for the speculative-readiness check in §6. + +| Spec | Depends on (ADR-046-* only) | Computed wave | +| --- | --- | --- | +| `decision-register` | none | floor | +| `terminology-and-identities` | `decision-register` | W0 (step 1) | +| `resource-object-model` | `decision-register`, `terminology-and-identities` | W0 (step 2) | +| `resource-store-redb` | `terminology-and-identities`, `resource-object-model` | W0 (step 3) | +| `resource-api-and-authorization` | `terminology-and-identities`, `resource-object-model`, `resource-store-redb` | W0 (step 4) | +| `resource-reconciliation` | `resource-object-model`, `resource-store-redb`, `resource-api-and-authorization` | W1 | +| `componentsession-and-bus` | `terminology-and-identities`, `resource-api-and-authorization` | W1 | +| `primitive-resource-composition` | `resource-object-model`, `resource-reconciliation` | W2 | +| `zone-routing` | `terminology-and-identities`, `componentsession-and-bus`, `resource-api-and-authorization`, `resource-object-model`, `resource-reconciliation` | W2 | +| `provider-model-and-packaging` | `resource-object-model`, `resource-api-and-authorization`, `resource-reconciliation`, `primitive-resource-composition` | W3 | +| `components-processes-and-sandbox` | `provider-model-and-packaging`, `primitive-resource-composition`, `componentsession-and-bus` | W4 | +| `core-controllers` | `resource-store-redb`, `resource-api-and-authorization`, `resource-reconciliation`, `provider-model-and-packaging` | W4 | +| `resources-network` | `resource-object-model`, `primitive-resource-composition`, `resource-reconciliation`, `provider-model-and-packaging`, `terminology-and-identities` | W4 | +| `resources-credential` | `terminology-and-identities`, `resource-object-model`, `resource-api-and-authorization`, `resource-reconciliation`, `provider-model-and-packaging`, `primitive-resource-composition`, `componentsession-and-bus` | W4 | +| `provider-state` | `terminology-and-identities`, `resource-object-model`, `primitive-resource-composition`, `provider-model-and-packaging`, `resource-store-redb`, `componentsession-and-bus` | W4 | +| `resources-zone-control` | `decision-register`, `terminology-and-identities`, `resource-object-model`, `resource-api-and-authorization`, `resource-store-redb`, `core-controllers`, `provider-model-and-packaging`, `resource-reconciliation` | W5 | +| `resources-host-guest-process-user` | `decision-register`, `terminology-and-identities`, `resource-object-model`, `resource-api-and-authorization`, `resource-reconciliation`, `components-processes-and-sandbox`, `provider-model-and-packaging`, `primitive-resource-composition`, `core-controllers` | W5 | +| `resources-volume` | `resource-object-model`, `primitive-resource-composition`, `resource-reconciliation`, `provider-model-and-packaging`, `components-processes-and-sandbox` | W5 | +| `resources-device` | `resource-object-model`, `primitive-resource-composition`, `resource-api-and-authorization`, `resource-reconciliation`, `provider-model-and-packaging`, `components-processes-and-sandbox` | W5 | +| `telemetry-audit-and-support` | `terminology-and-identities`, `resource-object-model`, `resource-store-redb`, `componentsession-and-bus`, `core-controllers`, `components-processes-and-sandbox`, `provider-model-and-packaging` | W5 | +| `cli-and-operations` | `terminology-and-identities`, `resource-object-model`, `resource-api-and-authorization`, `provider-model-and-packaging`, `components-processes-and-sandbox`, `componentsession-and-bus` | W5 | +| `nix-configuration` | `terminology-and-identities`, `resource-object-model`, `primitive-resource-composition`, `provider-model-and-packaging`, `core-controllers` | W5 | +| all 27 `provider-*` dossiers | see §3.3; every dossier's deepest edge resolves to a W5 spec (`resources-host-guest-process-user`, `resources-volume`, `resources-device`, `resources-zone-control`, `resources-credential`, `telemetry-audit-and-support`, `cli-and-operations`, or `nix-configuration`) | W6 | +| `security-hardening`, `streamline`, `reset-and-cutover`, `feasibility-proofs` (forthcoming) | the entire manifest (cross-cutting closing review) | W7 | + +### 3.5 Machine-readable implementation graph (D095) + +The wave topology in §3.1–§3.4 and the file-overlap graph in §6 are also emitted +as a single generated, committed, machine-readable artifact so no author +re-derives launch order or parallelism from this prose. Per D095: + +- **Artifacts.** `docs/specs/ADR-046-implementation-graph.json` (canonical) and + `docs/specs/ADR-046-implementation-graph.md` (rendered human view: Mermaid DAG, + the `W0`–`W7` table, shared-prep and parallel groups, this ready-wave + algorithm, the critical path, and counts). Both are **generated non-member + artifacts**: they are NOT part of the 55 `ADR-046-spec-set.json` members and do + not change that count. +- **Contract.** The JSON declares `artifactKind` + (`d2b-adr-implementation-graph`), `schemaVersion`, `adr` (`0046`), and `status`. + It carries one `node` for every one of the 55 member specs and every work item + in `ADR-046-work-items.json`, each mapped **exactly once** to a `wave` + (`W0`–`W7`), a file-disjoint `parallelGroup`, `owner`/`destinations`, + `entryContracts`, `prerequisites`, `blockers` (empty in a `Proposed` plan + unless an explicit blocker is recorded), `exitGate`, and `topologicalRank`. It + carries typed `edges`: `spec-depends-on` (cross-wave spec dependency), + `shared-contract` (same-wave spec prep barrier), `work-item-depends-on`, + `implements-spec` (work item → its spec), and `file-overlap-order` (same-wave + file contention ordering). +- **Source of truth.** `spec-depends-on`/`shared-contract` edges derive from + `ADR-046-spec-set.json.members[].dependsOn`; waves derive from §3.1–§3.4; + work-item mapping and `work-item-depends-on` derive from + `ADR-046-work-items.json`; the W6 `file-overlap-order` edges derive from §3.3. +- **Generation.** This Proposed documentation set has no implemented repository + generator yet. Until `ADR046-streamline-001` (and the duplicate-generator + reconciliation in `ADR046-streamline-024`) lands, the integrator regenerates + the artifacts deterministically from the exact Markdown bytes with a + disposable untracked script, validates the bytes, and removes that script. + The future `cargo run -p xtask -- implementation-graph` command will read the + two manifests plus this section, write sorted output with no timestamps or + host paths, and run under a drift gate. Regenerate the graph after any spec or + work-item edit, always **after** `ADR-046-spec-set.json` and + `ADR-046-work-items.json` are regenerated. +- **Validation.** Every one of the 55 spec nodes and every work item appears + exactly once; all edge endpoints resolve to a declared node; the graph is + acyclic; waves are monotonic (every edge's dependency resolves to an earlier + wave, or to an explicit same-wave `shared-contract`/`file-overlap-order` prep + barrier whose `topologicalRank` precedes its consumers); every work item is + mapped to a wave; a `parallelGroup` never implies ordering absent a + dependency or file-overlap edge; the JSON is deterministic; and every Mermaid + node ID is a valid identifier. +- **Anti-serialization.** The graph embodies §6: all ready, file-disjoint + parallel groups launch concurrently. A same-wave dependency is a + `shared-contract`/`file-overlap-order` prep barrier before its specific + consumers — never a reason to serialize a whole wave. + +#### 3.5.1 Ready-wave query + +A node is **ready to launch** when every node it lists in `prerequisites` is +`done` (or, before implementation begins, when every prerequisite is in an +earlier wave than the current open wave and any same-wave prep barrier has +landed). Against `ADR-046-implementation-graph.json` the exact query is: + +```bash +# All ready nodes: no unfinished prerequisite. $done is a JSON array of done node ids. +jq --argjson done "$DONE" ' + .nodes[] + | select((.prerequisites - $done) | length == 0) + | {id, kind, wave, parallelGroup, topologicalRank} +' docs/specs/ADR-046-implementation-graph.json + +# Ready and NOT yet launched, grouped by file-disjoint parallelGroup so every +# concurrently-launchable track is visible at once (anti-serialization check): +jq --argjson done "$DONE" --argjson launched "$LAUNCHED" ' + [ .nodes[] + | select((.prerequisites - $done) | length == 0) + | select([.id] - $launched | length == 1) ] + | group_by(.parallelGroup) + | map({parallelGroup: .[0].parallelGroup, wave: .[0].wave, ready: [.[].id]}) +' docs/specs/ADR-046-implementation-graph.json +``` + +A scope that is ready but absent from `$launched` and has no recorded blocker is +an anti-serialization violation (see §6 and `ADR046-streamline-013`). + +## 4. Per-wave entry/exit criteria + +Every wave (`ADR046-W0`…`ADR046-W7`) uses this template. A wave's exit +criteria are its successor's entry criteria; there is no partial-wave +advance. + +**Entry criteria (all required):** + +1. Gate 0 (§2) has passed, or — for `ADR046-W1` onward — every spec assigned + to this wave and every wave before it has a `Merged` implementation state + recorded in `docs/specs/ADR-046-work-items.json` (§8). +2. Every destination path this wave's work items name (§3.2, §7) is free of + an open, unresolved contention flag from an earlier wave. +3. The wave's Git Town stack (§5) has been proposed against the exact parent + commit named in its dependency edges (§3.4), not against a stale `main`. +4. The `cargo xtask heavy-gate` semaphore (§11) is available (not held past + its 30-minute timeout by a stale prior-wave validation run). +5. The fast hermetic suite (§10.16) passes within its execution budgets on the + wave's entry tree; it is the required default inner loop for every change + in the wave and must be green before any `integration/` lane is scheduled. + +**Exit criteria (all required):** + +1. Every spec's work items assigned to this wave show `Validation` evidence + satisfying §10's applicable matrix rows, imported per §12.2, including the + §10.16 runtime-ledger artifact showing every hermetic budget met (or a + classified crypto/property exception) with no timing regression. +2. The immutable candidate snapshot (§12.1) for this wave's integrated tree + has all required CI, local, and host validator lanes reporting (pending is + acceptable only while the PR is open, per §13; not at wave close). +3. The ten-role panel (§12.3) has returned unanimous `signoff: true` against + that exact snapshot, with zero outstanding `recommendations`. +4. `cargo xtask delivery wave seal` (§12.4) has produced a sealed record + binding this wave's `candidate_id`/`content_id`/`snapshot_sha256`. +5. `cargo xtask delivery wave merge-eligibility` reports eligible for every + PR in the wave's stack, and each has merged root-to-leaf through GitHub + (§13). +6. Post-wave cleanup (§14) is recorded as pending for the integrator (not + necessarily executed before advancing — advancing needs the merge, not the + worktree teardown). + +No wave may begin implementation subagent dispatch before its entry criteria +hold; no wave may be marked delivered before its exit criteria hold. This +mirrors and tightens this repository's existing `AGENTS.md` "Phase gate" +rule (`## Panel review` → `### Phase gate`): where that rule allows a panel +per implementation round, ADR 0046 restricts the **binding** panel to exactly +one occurrence per wave, run only against the wave's single immutable final +snapshot (§12), never against interim implementation rounds within the wave. + +## 5. Git Town stack shape and worktree/branch ownership + +ADR 0046 implementation is large, cross-cutting, and only partially +file-disjoint by default (`ADR046-W0` in particular is one shared contract +surface). It therefore follows this repository's existing +`AGENTS.md` §"Stacked PR workflow for large waves" as its baseline, tightened +to the exact contract below (adapted, per D001/D041, from the more granular +stacked-wave/anti-serialization workflow already proven on this +codebase's sibling ADR-0045 lineage): + +1. **One branch/worktree per file-disjoint slice**, never per person. A + slice is one row of §3.2/§3.3 that shares no destination path with any + other concurrently open slice (checked against §7's contention list). + Branch names are `adr046-w-`, for example `adr046-w4-network`, + `adr046-w6-device-tpm`, `adr046-w6-credential-entra`. +2. **Stack only real dependencies.** A slice branch targets `main` if every + one of its `Depends on` specs already merged to `main`; it targets the + exact prerequisite PR branch if that prerequisite is still open but + dependency-complete-enough per §6's speculative rule. `ADR046-W0`'s four + serial steps are one branch each, stacked linearly + (`adr046-w0-identities` → `adr046-w0-object-model` → `adr046-w0-store` → + `adr046-w0-api`), proposed with + `git town propose --stack --non-interactive --no-browser`. +3. **`ADR046-W1`/`ADR046-W2`/`ADR046-W4`/`ADR046-W5`/`ADR046-W6` parallel + slices** each branch from the exact merged (or, speculatively, exact + open) tip of their prerequisite branch and target `main` once that + prerequisite merges — never targeting an unrelated parallel sibling slice. +4. **The integrator owns**: shared-prep commits (§7), Cargo.toml workspace + member list and `flake.nix` output additions, `docs/specs/ADR-046-spec-set.json` + / `ADR-046-work-items.json` regeneration, cross-slice conflict resolution, + root-to-leaf merge order (§13.3), branch retargeting after a lower PR + merges, and post-wave cleanup (§14). The integrator is not the default + implementation sink for any slice that can be assigned independently + (mirrors the sibling repository's anti-serialization invariant, item 4). +5. **PR bodies** contain only dependency, base/head/tree, `candidate_id`/ + `content_id`, and check-status summaries, per §12.5 — never raw + validation output, panel transcripts, or AI/tool/model attribution. +6. **Reviewers and panel roles** inspect the plan/diff and supplied evidence; + they do not re-run tests/builds/evals unless the integrator explicitly + asks, per this repository's existing `AGENTS.md` panel-prompt rule. +7. Slice worktrees are removed only after their commits are integrated and + their real `packages/target/` (if any, distinct from the shared-cache + symlink) is cleaned, per §14. + +## 6. Speculative readiness and the anti-serialization file-overlap graph + +### 6.1 Speculative-start rule + +A slice's implementation branch **may** open before its assigned wave number +in §3.2 closes, provided: + +1. every spec it `Depends on` (§3.4) already has `Merged` work-item state + (not merely "wave complete" — the precise edge, not the coarse wave), and +2. no destination path it will write (per its work items' `Destination` + field) is currently claimed by another **still-open** branch, per the + contention index in §6.2/§7. + +For example, `resources-network` (computed wave W4) and `resources-credential` +(also W4) may each open as soon as `provider-model-and-packaging` (W3) merges +— they need nothing from `components-processes-and-sandbox` or +`core-controllers`, which are their W4 siblings, not their prerequisites. This +is a positive launch requirement, not merely permission: when a slice's exact +dependency edges are satisfied and its destination paths are uncontended, the +integrator MUST open it in the same coordination cycle as any other +newly-ready slice, mirroring the sibling repository's "Anti-serialization +invariant" item 2 (a ready slice sitting idle without a recorded file- +ownership/tooling blocker is a process failure to correct, not a preference). + +### 6.2 File-overlap graph (build procedure) + +Before opening any wave's slices, the integrator builds a file-overlap graph: + +1. List every work item's `Destination` field for every spec entering that + wave (§3.2's crate/module column, expanded to the exact paths recorded in + each spec's "Implementation work items" section). +2. Two slices sharing a destination path are one connected component and + MUST be either (a) internally ordered within one branch/worktree, or (b) + split by a shared-prep commit (§7) that lands the contended symbol once, + after which both slices branch from that prep commit and touch disjoint + regions of the (now-existing) file. +3. Distinct connected components run concurrently in separate worktrees. + Partition by actual file paths, never by a desire to avoid all future + conflict — avoiding possible conflicts is not, by itself, grounds to + serialize two components that do not share a file. +4. Record the connected-component count, the launched-slice count, and any + blocked slice with its exact blocker (contended path, missing shared-prep + commit, or genuine cross-cutting security invariant per §6.3) at wave + entry and after every panel round. A launch count below the ready count + without a recorded blocker fails wave entry criteria (§4). + +### 6.3 Narrow serial exception + +A security-sensitive cross-cutting invariant (for example: the +`AuthenticatedSubjectContext` mapping in `componentsession-and-bus`, or the +bootstrap-authorization narrowing in `resource-api-and-authorization` §"Native +RBAC resources") may stay serial only when the wave plan names the exact +files, the invariant, and the unblock commit. Every downstream slice is +dispatched the moment that unblock commit lands; the exception never +silently expands to cover the rest of the wave. + +## 7. Shared-prep pattern and known contention files + +The following destination paths are written by more than one spec/work item +and require a shared-prep commit landed on the wave's root branch (by the +integrator, or by whichever slice's work item is listed first below) before +the other claimant's worktree opens: + +| Contended path | Claimed by | Resolution | +| --- | --- | --- | +| `packages/d2b-contracts/src/v3/volume.rs` | `ADR046-primitives-001` (base struct), `ADR-046-resources-volume` (full schema), `ADR-046-provider-state` (`stateSchema`/`persistenceClass`/`sensitivityClass`/quota/sealing fields) | `resources-volume`'s base Volume struct lands first (small commit) in `ADR046-W5`; `provider-state`'s extension fields land as an immediate fast-follow commit on the same branch before both fan out to `volume-local`/`volume-virtiofs`/interaction Provider slices in `ADR046-W6` | +| `packages/Cargo.toml` workspace member list | every new crate across `ADR046-W0`–`ADR046-W6` | integrator-only trailing commit per merged slice; never edited inside a slice's own PR diff except to add that slice's own single new member line, which the integrator rebases to the current tail before merge | +| `flake.nix` package/output list | every new Provider crate (`ADR046-W6`) | integrator-only trailing commit, batched per track (§3.3), same rule as Cargo.toml | +| `nixos-modules/index.nix`, `nixos-modules/default.nix` | `ADR046-identities-002` (zones/resources), every `ADR-046-provider-*` Nix authoring section (`ADR046-W5`/`ADR046-W6`) | `ADR046-W0` lands the base zones/resources wiring; each `ADR046-W6` Provider slice appends its own resource-type Nix module import as a single line, rebased by the integrator at merge time, never touching another Provider's import line | +| `packages/d2b-contract-tests/tests/workspace_policy.rs` | every Provider crate-layout assertion (D059/`ADR046-pstate-011`-equivalent gates), one row per Provider | integrator batches one appended assertion per merged `ADR046-W6` slice; a slice's own PR adds only its own assertion function, appended after the current last function, never reordering existing ones | +| `docs/specs/ADR-046-spec-set.json`, `docs/specs/ADR-046-work-items.json` | regenerated after every spec status/work-item-state change (§8) | integrator-only; regenerated and committed as the last commit of each wave, never inside a slice's own PR | +| `packages/d2b-core-controller/src/rbac.rs`, `authz_audit.rs` | `resource-api-and-authorization` (W0-adjacent api-002 work item), `resources-zone-control` (Role/RoleBinding schema), `telemetry-audit-and-support` (audit hooks) | `resources-zone-control` (W5) lands the concrete Role/RoleBinding schema atop the W0 `authz.rs` skeleton; `telemetry-audit-and-support` (W5, parallel) adds only its own `authz_audit.rs` audit-emission hooks, a distinct file, so this is a false-positive overlap once split at the file (not module) level — recorded here so the integrator does not accidentally serialize two already-disjoint files under one shared symbol name | + +Any newly discovered contention during wave execution is added to this table +in the same PR that discovers it; the parent decision register process +(`decision-required` protocol in `docs/specs/README.md`) governs disputes +about *how* to split a contended file, not *whether* contention blocks +parallelism (it does, until resolved). + +## 8. Codegen/schema pin management + +1. `docs/specs/ADR-046-spec-set.json` and `docs/specs/ADR-046-work-items.json` + are generated indexes (per `docs/specs/README.md`) binding exact member + files, versions, statuses, dependency edges, content digests, and + implementation work items. They are generated once the initial member set + exists (already true — all 28 top-level specs and 27 dossiers exist at + this baseline) and regenerated as the last commit of every wave in §3.2. + A wave's exit criteria (§4) include this regeneration; `make test-drift` + (extended per work item `ADR046-delivery-004`, §17) fails if the committed + index is stale relative to the specs it indexes. +2. Every ResourceType/Provider spec's committed + `docs/reference/schemas/v3/.json` (per `ADR-046-nix-configuration` + §"Build-time JSON validation") is generated by `xtask gen-schemas` and + drift-checked exactly as this repository's existing v2 schemas are today + (`tests/unit/gates/drift-check.sh`, extended to the `v3/` tree). A schema + change without a matching `apiVersion`/`schemaVersion` bump fails this + gate, per the existing manifest-contract convention in `AGENTS.md` + ("Critical subsystems" → "Manifest contract"). +3. Generated Nix option types/docs for every `d2b.zones..resources.` + surface are derived from the same `ResourceTypeSchema` and signed Provider + schema used for build-time validation (per `ADR-046-nix-configuration`); + `make test-drift` gains one row per `ADR046-W5`/`ADR046-W6` spec asserting + the two sources never diverge (`xtask gen-nix-options` + `git diff + --exit-code`). +4. The artifact catalog digest format (`ADR-046-current-code-migration-map` + §0.2 "Artifact catalog") is pinned once in `ADR046-W0` and never + re-derived per Provider; every later spec's `d2b.artifacts.` usage + validates against that one pinned encoding. +5. `packages/d2b-contract-tests/tests/policy_*.rs` gains one policy lint per + wave for that wave's frozen bounds (Provider/Role/RoleBinding bounds from + D073; Volume bounds from D062; Device arbitration bounds from D063), so a + later spec cannot silently loosen an earlier wave's frozen limit without + a visible policy-test diff. + +## 9. Current-code deletion gates + +No `RETAIN`/`ADAPT` current-v3 path is deleted merely because a wave's specs +are Accepted; per the parent ADR's "Current-code fit" table and +`docs/specs/README.md`'s work-item field "Removal proof", deletion requires: + +1. The exact successor ResourceType/Provider/controller is integrated + (its own wave's exit criteria, §4, are met) **and** covered by the tests + named in that work item's `Validation` field. +2. The specific removal-proof test named in the migration map + (`ADR-046-current-code-migration-map` §8.2, e.g. `systemctl list-units + 'd2b-r-*'` returns empty; `grep -r WorkloadOp packages/ --include='*.rs'` + returns zero results; `grep -r RelayProvider packages/ --include='*.rs'` + returns zero results) passes on the candidate snapshot. +3. The deletion is its own commit, tagged with the wave/finding that proved + the successor, and is never bundled into the same commit that lands the + successor (so a revert of one does not silently also revert the other). +4. `DELETE`-disposition rows (per the migration map's disposition-code + table, §0/§1–§9) are deleted only in the wave whose successor spec closes + them — for example, the per-realm PID1 broker/controller systemd units + (§5.2 of the migration map) are deleted only after `ADR046-W5`'s + `resources-zone-control`/`core-controllers` successors are integrated and + the removal-proof test passes; `d2b-realm-router` session types are + deleted only after `ADR046-W1`'s `componentsession-and-bus` successor + routes every v3 peer path. +5. A `REPLACE`-disposition row (e.g. `d2b-realm-router/src/router.rs` → + Zone-local resource routing) follows the same rule but may retain its old + file as a dead, test-gated stub for one wave beyond its successor's + landing if — and only if — a still-open sibling slice's integration test + fixture references it; the stub's removal is then a follow-up commit in + the same wave, not deferred indefinitely. +6. This spec's own `ADR046-W7` ("streamline & cutover") is the wave that + performs bulk final deletion of every remaining `RETAIN`-until-parity row, + gated by `ADR-046-reset-and-cutover`'s destructive-cutover mechanics and + by the release/cutover gate in §15. No deletion happens in this + documentation-only change. + +## 10. Validation matrix + +Every row below maps to the Layer taxonomy in `tests/AGENTS.md` and +`tests/README.md`. New ADR 0046 test surfaces are added to the **existing** +closed Layer-1 set (nix-unit cases, Rust unit/integration/contract/policy-lint, +flake checks) wherever hermetically possible; Layer-2 tiers (container, +runNixOSTest, live-host, hardware) are used only where Layer 1 provably +cannot cover the behavior, exactly as `tests/AGENTS.md`'s "one rule" requires. +No new top-level `tests/*.sh` gate is added; new coverage extends the +existing orchestrators (`tests/static.sh`, `make test-*` targets) by +manifest/fixture, per that file's closed-set rule. + +### 10.1 Tier0 and Layer-1 shards + +| Row | Coverage | Tier/Layer | Location | +| --- | --- | --- | --- | +| Tier0 | Syntax + shellcheck for every new/changed shell/doc surface introduced by ADR 0046 tooling (§17) | `make check-tier0` | existing target, no change | +| Layer-1 lint | `cargo fmt`/`clippy` for every new `d2b-*` crate in §3.2/§3.3 | `make test-lint` | existing target, extended by new crate membership | +| Layer-1 rust | `cargo test --workspace` across every new crate, including the three broker feature passes where a new crate touches `d2b-priv-broker` (none does, per D077 — no Provider process imports the broker) | `make test-rust` | existing target | +| Layer-1 proofs | Any new `proofs/` crate for redb/session invariants (only if a wave's feasibility spike needs a separate proof crate; see `ADR-046-feasibility-and-spikes`) | `make test-proofs` | existing target | +| Layer-1 flake | `eval-*` checks extended with Zone/resource examples once `ADR046-W5`'s `nix-configuration` lands; `examples/minimal` gains a `d2b.zones.dev.resources.*` block | `make test-flake` | existing target, new fixture | +| Layer-1 drift | Schema/Nix-option/spec-set drift gates from §8 | `make test-drift` | existing target, extended rows | +| Layer-1 policy | Workspace-policy, provider-crate-layout, and telemetry/audit-redaction policy lints (§10.4, §10.9) | `make test-policy` | existing target, extended rows | + +### 10.2 Rust unit/property/fuzz/fault/conformance + +| Row | Coverage | Location | +| --- | --- | --- | +| Unit | Every DTO/schema/validator introduced by `ADR046-object-001/002`, `ADR046-store-001..003`, `ADR046-api-001/002` — canonical JSON, bounds, redaction, unknown-field rejection | `packages/d2b-contracts/src/v3/**` `#[cfg(test)]`, `packages/d2b-resource-store-redb/src/**` | +| Property | Owner cycle/depth/reparent property tests (`ADR046-object-002`); expected-revision conflict storms; watch replay/no-gap; ResourceRef parse/collision vectors (`ADR046-identities-001`) | `packages/d2b-contracts/src/v3/resource_ref.rs`, `packages/d2b-resource-store-redb/tests/` | +| Fuzz | Canonical offer/record fuzzing carried over from main's `d2b-session` Noise vectors (`ADR046-session-001`); redb key/encoding fuzz for `type_index`/`owner_index`/`revision_log` | `packages/d2b-session/tests/noise_vectors.rs` (ported), `packages/d2b-resource-store-redb/fuzz/` | +| Fault injection | Forced crash at every commit boundary (resource-store-redb performance contract fixture list); controller-spawned Process exits unexpectedly → `phase: Degraded`/`Failed` (D059 `tests/` requirement); disconnect/relist/lease-withdrawal | `packages/d2b-resource-store-redb/tests/fault.rs`; every Provider crate's `tests/*.rs` | +| Conformance | `check_provider_conformance` from the toolkit run against every Provider's declared axis; zero `ConformanceError` (D059) | `packages/d2b-provider-toolkit/tests/conformance.rs`, every `packages/d2b-provider-*/tests/` | + +### 10.3 Generated schema/Nix parity + +Covered by §8's drift gates. Explicit assertions required per wave: rendered +JSON matches `ResourceTypeSchema`; provider-specific `spec.*` matches the +signed Provider schema; inline secret bytes fail the build (credential-ref +marker enforcement); duplicate/undeclared `d2b.artifacts` IDs fail eval; +`artifactId`/`systemArtifactId` type-mismatch fails the build with resource +name, field name, expected/actual type in the error. + +### 10.4 redb 10k/RSS/perf + +Exact benchmark fixtures from `ADR-046-resource-store-redb`'s performance +contract, run in `packages/d2b-resource-store-redb/benches/`: + +| Fixture | Hard target | +| --- | --- | +| Empty store readiness | <=500 ms | +| Aggregate Zone resource service/store + fixed system-core + system-minijail controllers idle RSS | <=64 MiB | +| p95 local Get/bounded List | <=2 ms | +| p95 crash-safe single-resource mutation | <=10 ms | +| p95 durable commit → matching controller handler start | <=5 ms | +| p95 ready Process commit → launch-attempt start | <=20 ms | +| 10,000 resources | list/get/watch fixture — must meet the above p95s under load | +| 100 live watches | fan-out fixture | +| 1/10/100 concurrently ready Process resources | fast-launch concurrency fixture (`ADR046-reconcile-003`) | +| Expected-revision conflict storm | no silent merge; every stale write returns `resource-conflict` with current revision | +| Owner-trigger fan-in/chain | bounded depth/work budget, no amplification | +| Revision compaction | durable floor advances; below-floor cursor gets `revision-expired` | +| Forced crash at every commit boundary | no partial/ambiguous commit observable after recovery | +| Backup/restore/internal schema upgrade | staged validate → atomic publish → rollback-window retention | +| Repeated open/close and long-reader rejection | no reader starves the single writer | + +Failure to meet a hard target changes the Proposed design (per the spec); +it is never resolved by weakening durability, authorization, or audit. + +### 10.5 Bus/Noise/auth/RBAC/attachment + +Ported verbatim (per `ADR046-session-001/002`) from main's +`d2b-session`/`d2b-session-unix` test suites, plus v3-specific additions: + +- All Noise NN/KK/IKpsk2 profile-strictness, transcript-mismatch, and + bootstrap single-use-consumption vectors (11 functions enumerated in + `ADR-046-current-code-migration-map` §9.1, e.g. + `fixed_negotiation_and_all_noise_profiles_are_strict`, + `bootstrap_is_operation_bound_expiring_single_use_and_redacted`). +- SO_PEERCRED/SCM_RIGHTS/pidfd/object-identity/credit tests (8 functions, + `ADR-046-current-code-migration-map` §9.2, e.g. + `duplicate_kernel_objects_are_rejected_and_cleaned_up`). +- New v3 additions: `AuthenticatedSubjectContext` mapping tests per evidence + class (Unix pathname/socketpair, enrolled KK key, bootstrap IKpsk2, native + vsock); Role/RoleBinding decision-matrix property tests; revocation-latency + test (relevant resource revision invalidates cache immediately after + durable commit); parent/child Zone access tests (disconnected child-local + outbound intent, reconnect reauthorization against current revision); attachment + descriptor validation (encrypted, service/method/request/operation/ + generation bound, CLOEXEC, duplicate-object rejection). + +### 10.6 EffectPort/broker + +- `ProcessLaunchEffectPort` (ProviderSupervisor) ticket verification, + package/template/resource-output resolution, and identity/pidfd-evidence + observation tests (`ADR046-process-001`). +- `VolumeLayoutEffectPort`/`VolumeSourceEffectPort`, + `NetworkEffectPort`/`DeviceEffectPort` — every call carries only opaque + resource/intent/template/policy IDs; a policy test + (`packages/d2b-contract-tests/tests/policy_no_raw_host_path_to_provider.rs`) + asserts no Provider crate source references a raw host path, broker DTO + type, or `clone3`/`pidfd_open` symbol directly (D077 enforcement). +- Broker remains sole privileged executor: integration test spawns a + Provider process under a restricted seccomp/namespace profile that has no + socket to the broker and asserts the effect call still succeeds only + through the injected EffectPort. + +### 10.7 Process/adoption/reap + +- system-minijail: `clone3(CLONE_PIDFD)`, cgroup-at-birth placement, adoption + identity revalidation (pid/start-time/cgroup/executable/template/ + generation) before `pidfd_open`; ambiguity → Unknown/quarantine, never + broad kill/reuse. +- system-systemd: InvocationID+cgroup+MainPID/start-time binding, pidfd open + after unit start, no daemonizing/forking unit type, adoption revalidation + of all stable identity before pidfd open. +- Shared conformance suite run against both Providers asserting identical + ResourceType/status/error shape (`ADR046-process-002`). +- Restart/adoption integration: `d2bd`-successor restart while a Process + Provider's children are running; pidfd re-adoption without kill (mirrors + this repository's existing `KillMode=process` continuation-event + invariant in `AGENTS.md`). + +### 10.8 State/Volume/ACL/marker/quota/migration + +- Volume layout/ACL/no-follow/inheritance/repair/cleanup tests carried + forward from current storage-lifecycle coverage (`ADR046-primitives-003`), + plus new: 1,024 layout-entry / 64-view / 64-attachment bound tests (D062); + `sourcePolicyId` opacity test (raw host path never in spec/status/audit, + never reaches the Provider process as a literal path — D082). +- Provider state Volume tests: a component receives a state Volume **only** + when it declares one under the storage-need test (D087); a stateless + component declares none and receives none, and no empty identity-only Volume + exists (revised D076); each declared state Volume has a `User/` layout + principal from a bounded Nix-preprovisioned pool; no + cross-component/cross-Provider mount sharing (D076/D079 enforcement); status + bound/schema/conformance tests (total canonical status ≤ 64 KiB, + provider-specific detail ≤ 32 KiB, condition/list/map cardinality caps, + `status-oversize` rejection) and status-first restart-revalidation tests + (controller re-derives observed state from status/core ledger/external + observation and never treats status as authority); optional-state admission + tests asserting an unjustified namespace is rejected `component-state-not- + justified`; no bootstrap state Volume or bootstrap-storage mechanism exists — + fixed bootstrap components reach Ready from status/the core Operation ledger + (D086, superseded by D087), and a Guest still bootstraps its own Guest-local + `volume-local` without a parent-Host dirfd leak. +- Three-layer status shape tests (D088): every resource carries the universal + `ResourceStatus` base; the ResourceType-common `status.resource` schema + validates provider-neutral; the optional `status.provider` extension carries + `providerRef`/`schemaId`/`schemaVersion`/`observedProviderGeneration`/`details`. + Schema-parity/conformance across implementations: for each multi-implementation + ResourceType (Guest, Device, Credential, Volume, Process) assert every + implementation populates the same `status.resource` fields with identical + shape, and no shared field is duplicated in any `status.provider` + (`status-provider-overlap` rejection). Provider-extension tests: unregistered + `schemaId`/`schemaVersion` → `status-provider-schema-invalid`; unknown field in + `details` → rejected; version mismatch (installed Provider vs written + extension) → rejected; `status.provider.details` over 32 KiB or over cardinality + → `status-oversize`; `status.provider` restating a universal/`status.resource` + field → `status-provider-overlap`. Base-only projection/watch compatibility: + a generic consumer that requests only the universal base + `status.resource` + reads and watches successfully with `status.provider` absent, unknown, or from + a different Provider version, and never parses `details`. Atomic layered write: + all present layers are committed in one status mutation with one expected + revision; a partial-layer write is rejected. +- Three-layer spec shape tests (D089): every resource carries the universal + envelope and a ResourceType base spec at `spec.*` (including `spec.providerRef`); + the optional canonical `spec.provider = { schemaId, schemaVersion, settings }` + is the only provider-extension shape (no `providerSettings`/Device `settings`/ + ad hoc extension survives). Base-schema parity across implementations: for each + multi-implementation ResourceType (Guest, Device, Credential, Volume, Process) + assert every Provider `ResourceApiBinding` implements the identical base spec + schema version/fingerprint and accepts the canonical minimal valid base Spec. + Capability-declared optional rejection: a Provider whose signed capability + matrix marks an optional base capability unsupported returns provider-neutral + `unsupported-capability`; a Provider that ignores/reinterprets/renames/ + duplicates/weakens a base field, or requires `spec.provider` for base-required + behavior, fails conformance. Extension tests: unregistered/version-mismatched + `spec.provider.schemaId`/`schemaVersion` → `spec-provider-schema-invalid`; + unknown field in `settings` → rejected; `settings` restating a base field → + `spec-provider-shadow`; `spec.provider` validated against `spec.providerRef` at + both Nix build and API admission; over-limit envelope → spec bounds error. + Spec+status atomic schema binding: the selected Provider's registered + `spec.provider` and `status.provider` schemas align, and a binding missing + either base fingerprint is rejected. Generic CLI/controller base-only tests: + author and reconcile succeed on base spec + base status with `spec.provider`/ + `status.provider` absent or from a newer Provider version. +- Expedited reconcile tests (D090): commit-fails/`Abort` → no external effect; + controller finishes preflight before commit but gates all effects on + `CommittedRevisionProof`; effects-gate (no finalizer release / status write + pre-proof); status-write-delayed → response carries `statusPersistence: + pending` and last persisted status revision; normally-queued reconcile + no-ops/rejoins after the expedited pass (idempotency key from + UID/generation/revision/operationId; no duplicate effect); concurrent + mutation conflict handling; delete returns event-only Deleted projection / + not-found; expedited timeout → committed-but-reconcile-pending with the queue + continuing; restart mid-expedited → no duplicate effect; `expedited-not- + authorized`/`expedited-quota-exceeded` enforcement and priority-lane fairness. +- Currency and disruptive-upgrade tests (D091): `status.update` reports + `Current` when converged; a non-disruptive change reconciles without + `UpgradeRequired`; each trigger (`CoreGenerationChanged`, + `ProviderGenerationChanged`, `ArtifactChanged`, `ImageOrSystemGenerationChanged`, + `SpecChanged`, `DependencyChanged`, `SecurityPolicyChanged`) drives the correct + state/reason; dependency propagation aggregates owned/dependency currency for + list/get; GPU dependency blocking → `Blocked`/`UpgradeRequired`, planner drains + dependent Processes/Guests, recycles the GPU realization, restarts dependents; + state-preservation (durable/state Volumes) and TPM-identity preservation across + upgrade/recycle; `Replace` only with ownership/state transfer; crash/re-entry + resumes from the core Operation ledger without duplicating; per-resource + single-flight serializes reconcile-vs-upgrade; CLI projections (`list + --updates`, `get`, `upgrade` plan-by-default vs `--recursive --apply`) with + stable `--json`; `spec.updatePolicy` manual-disruptive default; `spec.provider` + cannot bypass disruption policy. +- Endpoint resource tests (D092): endpoint ownership/lifecycle (owned `Endpoint` + child, `producerRef` set, child-first deletion with the producer/owner); no + raw locator in Endpoint spec/status/CLI (only closed + class/transport/locality/purpose + bounded fingerprints); ref resolution (a + consumer `Endpoint/` ref gains a dependency edge and resolves to a + private transport/FD only via EffectPort/LaunchTicket); provider base-schema + conformance for the Endpoint ResourceType; endpoint update/recycle (recycle + with producer); producer restart bumps `endpointGeneration`/`status.update`; + consumer dependency trigger fires on that bump; unauthorized resolve → + `endpoint-resolve-denied` with no locator; CLI update visibility (`get + Endpoint/` shows readiness/currency, no locator); high-churn-handle + non-resource lint asserts pidfd/fd-index/named-stream/`OwnedTransport`/ + `operationId` are NOT promoted to resources and stay internal; the + `ProcessSpec`-has-no-inline-`endpoints` lint; standard ResourceType count is 19 + (Endpoint present in the catalog); every retained public `*Id`/`Handle` has a + documented rationale row; schema vectors accept exactly + `visibility=owner|provider|zone` and reject aliases; controller admission + tests cover all three scopes plus each + `consumerPolicy.allowedSubjects`/`allowedProviderComponents`/ + `allowedOperations` mismatch; a docs drift test parses every Endpoint + example and fails if visibility is outside the enum, if a finer restriction + is encoded as another visibility value, or if `consumerPolicy` is a + scalar/array alias. ResourceExport visibility is explicitly outside this + Endpoint-only drift check. +- Entra identity-Guest login tests (D093), all against a **fake Entrablau Guest + login service** (no live Entra in CI; a manual real-Guest login is a separate + non-CI check): fake Guest login success → `interactionState: Authenticated` + and a subsequent on-demand access-token lease delivered end-to-end (KK) only to + the exact `consumerRef` consumer (Host/bus see ciphertext); login-required → + `interactionState: Required`/`AwaitingUser`; cancel (`CancelLogin`) → + `login-cancelled`; timeout past `loginDeadline` → `login-deadline-exceeded` + with the durable Credential unchanged; controller/agent restart mid-login + resumes/re-derives without leaking secrets; login `Endpoint` unavailable or + generation mismatch → typed error, no token; Host placement of the + login/token service rejected (`host-placement-rejected`); token/refresh-state + redaction (no token, URL, cookie, authority-conferring device code, or user + PII in status/audit/OTEL); end-to-end record only (intermediate controllers + observe ciphertext); same-Zone `identityGuestRef`/`loginEndpointRef`/ + `consumerRef` accepted and cross-Zone rejected; Nix composition validation + (identity Guest composes `inputs.entrablau.nixosModules.default`, login + Endpoint bound, Credential fields same-Zone-consistent, no store path/token); + identity-Guest TPM/login-state preserved on reset unless explicitly destroyed; + ACA and Azure VM consumers obtain their access token over KK from the identity + Guest; `credential-entra` controller does no direct Entra network egress. +- Quota/EmergencyPolicy tests: hierarchical Host/Guest allocation against + Zone capacity, overcommit blocking, digest/Provider/Host/Guest/Zone/global + emergency disable and route/session/grant revocation, incident-held + Volume/Provider state preservation. +- Migration: every `RETAIN`/`ADAPT`/`REPLACE` row in the migration map has a + test asserting the v3 successor round-trips the retained semantic (e.g. + `unsafe_local_posture_round_trips_as_closed_typed_fields`-equivalent test + for the user-only Host no-isolation posture, D042). + +### 10.9 Network/Device/credential/interactions + +- Network: bridge/namespace/address/DHCP/DNS/NAT/firewall/egress + reconciliation tests for `network-local`; Host/Guest attachment tests. +- Device: first-probe-failure → `Unknown`, absent-after-3-failures → cleared + (D063); render-node sharing vs. full/VFIO exclusive arbitration; USBIP + firewall/export tests; security-key unprivileged relay/frontend tests + (fixed broker only opens/passes hidraw, never more). +- Credential: raw-token/SignChallenge delivery only over a dedicated + Noise\_KK end-to-end sensitive ComponentSession (D055/D056/D068); NN/ + IKpsk2 rejection tests for token delivery; zero-token-bytes-in-audit/ + telemetry/store policy lint. +- Interactions (display/audio/clipboard/notification/shell-terminal): one + ResourceType-conformance + fault-injection pass per Provider dossier, + per D059's mandatory `tests/` tree. + +### 10.9a Cross-Zone sharing (ResourceExport/ResourceImport, D096) + +Hermetic (fake ZoneLink/stream/clock/adapter) fast tests and slower integration +(real bounded encrypted streams) tests cover, per +[`ADR-046-resources-zone-control.md` §8A.7](ADR-046-resources-zone-control.md): + +- signed projection factory binds qualified semantic/provider-neutral Service + type, Binding type, allowed owner-Service backing refs, allowed Binding target + refs, projection schema/fingerprint, and factory fingerprint; absent/unsigned/ + tampered/mismatched metadata fails closed at Provider install, Nix build, and + API admission; +- `ResourceExport.resourceRef` accepts only the owner Service; Device, Endpoint, + Binding, Credential, backend, and cross-Zone targets reject; +- one import creates exactly one same-qualified-type projection Service with + `ownerRef: ResourceImport/`; it creates no Device/Endpoint/Binding; +- one or more separately authored same-Zone Bindings reference that Service plus + an allowed Guest/User/Zone and own Process/Endpoint children; Binding spec is + desired consumer intent only and observations appear only in status; import + never exports, auto-creates, or auto-deletes Binding; +- opt-in required on both sides; unauthorized Zone and capability/fingerprint + mismatches reject; +- exact frozen type/Provider mapping: + `audio.d2bus.org.AudioService` + `audio.d2bus.org.AudioBinding` with + `audio-pipewire`, `security-key.d2bus.org.SecurityKeyService` + + `security-key.d2bus.org.SecurityKeyBinding` with `device-security-key`, + `telemetry.d2bus.org.TelemetryService` + + `telemetry.d2bus.org.TelemetryBinding` with `observability-otel`, and + policy-gated `usb.d2bus.org.UsbService` + `usb.d2bus.org.UsbBinding` with + `device-usbip`; +- canonical minimal Service/Binding base admission succeeds without + `spec.provider`; export/import preserves semantic type across independently + selected conformant implementations; every Core projection has ResourceImport + ownership, `providerRef`, semantic base/import fields, and no `spec.provider`; + provider/adapter identity changes leave the semantic factory fingerprint + unchanged while signed-descriptor authentication still detects substitution; + common semantic observations exist only under `status.resource` and + implementation observations only under `status.provider`; mismatched factories and + PipeWire/CTAPHID/OTEL/USBIP detail in base spec/status/conditions/errors/ + fingerprints reject; +- quota/fairness/deadline enforcement; reconnect revalidation and revocation + degrade the projection Service; D091 update propagation owner Service → + export → import → projection Service → Binding → children; finalizer waits + visibly for Bindings to be deleted/retargeted; +- audio: speaker mix with per-Zone volume/quota and microphone + exclusivity/consent/fair-queue; security-key: CTAP serialization with one + exclusive per-device lease/deadline/cancel; observability: one SigNoz ingest + with many producer Zones under quota/backpressure/redaction/cardinality; + USBIP: all Provider/Zone/export/device policy gates required; one physical + token selected by USBIP and security-key resolves to a byte-identical + Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` tuple, and the + second claimant receives `physical-usb-backing-conflict` before any effect; +- only those four Provider families are admitted (USBIP policy-gated); all other + Providers and every Binding are non-exportable; +- Nix canonical resources and projection names are inspectable/byte-stable even + when transparent sugar lowers to them; CLI renders import → projection + Service → Binding → owned Process/Endpoint without hidden nodes; +- **no FD, secret, Credential, backing/remote Ref, raw path/locator, device/ + socket handle, token, or payload bytes crosses a Zone or appears in status/ + CLI/audit**. High-churn sessions/streams stay internal; intermediaries see + ciphertext. + +All metadata, admission, graph, cleanup, and update-propagation cases are fast, +deterministic, parallel-safe, hermetic tests using fake adapters/streams/clocks. +Real cross-Zone audio, security-key, observability, and policy-gated USBIP +streams run only in the slower integration tier and prove the same invariants +with the production encrypted named-stream implementation. + +### 10.10 Container integration and host runNixOSTest KVM/TCG + +| Row | Coverage | Command | +| --- | --- | --- | +| Container | Provider controller against a real Zone runtime in a container; resource lifecycle under real broker calls; cleanup-contract scenarios (§0.2 of the migration map) | `make test-integration` (podman; local host/manual pre-PR, per this repo's existing tier) | +| Host runNixOSTest, KVM | Live daemon/broker/socket-activation/host-posture/kernel behavior for every new Zone runtime, Provider process, and cleanup/rollback scenario | `make test-host-integration` (x86_64-linux, KVM) | +| Host runNixOSTest, TCG fallback | Same suite when `/dev/kvm` is absent — slower, still required before Wave exit for waves touching kernel-adjacent behavior (process/adoption, cgroup, virtiofs) | `make test-host-integration` (TCG fallback path, already documented in this repo's `AGENTS.md`) | + +### 10.11 Hardware/live/cloud manual + +| Row | Coverage | Command | +| --- | --- | --- | +| Hardware | Real GPU/YubiKey/hardware-TPM passthrough for `device-gpu`, `device-security-key`, `device-tpm` | `make test-hardware`, manual, on a host with the devices | +| Live-host | Destructive/stateful checks against a real deployed Zone (store adoption, restart/power-loss, USBIP guestd lifecycle equivalents) | `D2B_LIVE=1 bash tests/integration/live/.sh`, manual, never CI | +| Cloud | `runtime-azure-container-apps`, `runtime-azure-virtual-machine`, `transport-azure-relay`, `credential-managed-identity`, `credential-entra` against real Azure resources | manual tier, gated by `ADR-046-feasibility-and-spikes`; never run in CI or as a required wave-exit lane — recorded as external evidence only | + +### 10.12 Restart/power-loss + +- Zone runtime restart: relist owned resources, resume from durable + checkpoints, revalidate Provider/controller leases, preserve + Unknown/ambiguous states, no cleanup before owners observe/adopt + (`ADR-046-core-controllers` "Restart" section). +- Power-loss/forced-crash: covered by §10.4's "forced crash at every commit + boundary" fixture plus a runNixOSTest scenario that `kill -9`s the Zone + runtime process mid-write and asserts redb recovers to the last durable + commit with no torn write observable. +- Store identity fail-closed: a previously provisioned database that is + missing, replaced, bound to another Zone/UID, newer than the binary + schema, or internally inconsistent fails closed — never silently + recreated (`ADR-046-resource-store-redb` "Store identity"). + +### 10.13 Reset/cutover + +Deferred in content to `ADR-046-reset-and-cutover` (forthcoming, required +before `ADR046-W7` per §3.4); this spec fixes only the **gate shape**: +`ADR046-W7`'s exit criteria (§4) additionally require every destructive-reset +test named by that spec to pass on the `ADR046-W7` candidate snapshot before +the release/cutover gate (§15) opens. + +### 10.14 CLI/Nix examples/docs + +- Every `d2b ` surface in `ADR-046-cli-and-operations` gets one + `packages/d2b/tests/*.rs` hermetic test (`CARGO_BIN_EXE_*`, + `D2B_PUBLIC_SOCKET` pointed at a fixture) plus one non-TTY-JSON-output + test. +- `examples/minimal`, `examples/multi-env`, and `templates/default` gain a + `d2b.zones..resources.*` block once `nix-configuration` lands + (`ADR046-W5`); `flake.checks..eval-*` extended accordingly (§10.1). +- Every Provider dossier's required Nix-authoring section (minimal Nix + snippet + exact rendered canonical JSON, per `docs/specs/README.md` + "Evidence and current-code fit") is itself a drift-checked fixture: a + contract test in `packages/d2b-contract-tests/tests/` renders the snippet + and diffs it against the dossier's committed JSON block. +- `docs/reference/*` pages affected by ADR 0046 land in the same wave as + their implementation, not deferred — per this repository's existing + `AGENTS.md` docs-review-role expectation ("Diataxis adherence... AGENTS.md + updates landing with load-bearing changes"). + +### 10.15 Mocking shared dependencies for parallel safety + +Every wave-parallel slice (§3.2, §3.3) hermetically mocks its not-yet-landed +or concurrently-in-flight peers so Layer-1 tests never require a second +slice's crate to exist: + +- Resource store/API: `packages/d2b-provider-toolkit` ships fake + core/store/bus/supervisor/effect clients (per `ADR-046-provider-model-and-packaging` + "Toolkit"); every Provider crate's `tests/` tree uses these fakes, never a + real Zone runtime. +- ComponentSession/bus: an in-process loopback transport substitutes for a + real Unix/vsock/relay transport in unit/integration tests; only + `integration/` and host-integration tiers use a real transport. +- EffectPort: `FakeProvider`/fake effect adapters from the conformance kit + stand in for ProviderSupervisor and the volume/network/device effect + adapters in every Provider crate's `tests/`. +- Cross-Provider dependencies (manifest `dependency` aliases, e.g. a Guest + runtime Provider's `volume` alias) resolve to a fake alias target in + `tests/`, and only to a real installed Provider resource in `integration/`. + +This is what makes §3.3's 27-way parallel wave safe: no Provider crate's +hermetic test suite requires another Provider crate to be compiled, merged, +or even to exist yet. + +### 10.16 Hermetic execution budgets, placement, and the runtime ledger (D094) + +The fast hermetic suite (`src/` `#[cfg(test)]` units and crate `tests/*.rs`) +is the default inner loop, required on every change; all slower coverage +lives only in `integration/`. Budgets below are **execution-only**, measured +after build with a warm cache (compilation excluded) against the recorded +reference runner, and enforced by the runtime ledger + timing gate — not by a +new test framework. + +| Budget | Threshold (warm cache, execution only) | +| --- | --- | +| Individual normal hermetic test | p95 ≤50 ms, no wall-clock sleep | +| Per Provider crate `cargo test -p d2b-provider-- --lib --tests` | ≤2 s | +| All 27 Provider hermetic suites, sharded | ≤30 s aggregate wall | +| All 27 Provider hermetic suites, single host | ≤60 s | +| Each Layer-1 hermetic shard (`make test-rust` split) | ≤60 s | +| Classified bounded crypto/property exception | named per test; capped case count; declared higher per-test budget | + +**Placement rules (a violation must move, never gain a sleep/timeout/`#[ignore]`):** + +- `src/` units and `tests/` are in-process only — deterministic fake + clock/RNG; fake `ResourceClient`/EffectPort/broker/transport/credential/ + systemd; in-memory or tiny-temp bounded redb fixtures; parallel-safe with + no global mutable or shared ports/paths; exact bounded property case counts. + No sleep/retry on wall clock, process spawn, containers, network, DBus, + systemd, broker daemon, Nix eval/build, KVM, USB/GPU/TPM hardware, live + cloud, or filesystem trees beyond tiny temp fixtures. +- `integration/` owns any real process, socket rendezvous, container, Nix + eval/build, guest VM/KVM/TCG, broker/systemd/DBus, real filesystem + quota/mount/namespace, hardware, or live cloud/Entrablau. It may be slower + but still has a lane timeout/budget, parallel isolation, and fake external + services by default; live/hardware remain a separate manual tier. + +**Runtime ledger + timing gate.** A machine-readable test-runtime ledger and +a timing gate reuse the existing `xtask`/Make tooling (no new top-level +`tests/*.sh` gate): they record the reference runner, repetition count, and +per-test/crate/shard p95, enforce the budgets above, report the top slow +tests, apply a historical regression threshold, and emit a CI artifact. +`make test-rust` and the Layer-1 hermetic shards run concurrently; expensive +integration lanes take the sole heavy-gate slot (§11). CI caches compile +outputs, but correctness never depends on the cache, and cold compile time is +tracked and optimized separately via shared cache and dependency discipline. + +**Legacy retirement.** When ADR 0046 replaces a behavior, the minimum +reusable semantic assertions migrate into the new hermetic suite and the old +duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and +manifest entries are deleted once successor coverage and the §9 removal proof +pass — old and new suites never run indefinitely. Every current-code +migration/replacement work item names the exact old test selectors/files with +a keep/adapt/move/delete disposition and a removal gate, and updates +`tests/layer1-jobs.json`, closed gate manifests, flake/matrix/Nix-unit pins, +generated ledgers, and CI workflow shards. Policy self-tests assert that an +intentional slow/sleep/process/network test in a hermetic tier is rejected, a +timing regression fails the gate, parallel isolation holds, and a retired +legacy selector is absent. + +## 11. Heavy-gate: sole use + +Every Layer-2/hardware/live/perf-heavy command anywhere in ADR 0046 +delivery — `make test-integration`, `make test-host-integration`, +`make test-hardware`, `D2B_LIVE=1 bash tests/integration/live/*.sh`, the +redb benchmark suite (§10.4), and any cloud-tier manual run (§10.11) — MUST +run only through one shared semaphore: + +```bash +cargo xtask heavy-gate -- [args...] +``` + +This is adopted by copy/adapt (per D001/D041) from the equivalent tooling +already proven on this codebase's sibling ADR-0045 lineage: a two-slot +per-UID OFD-locked semaphore under +`${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/d2b-heavy-gates`, nonblocking +acquisition retried every 250 ms for up to 30 minutes, fail-closed (no +`flock` fallback) on unsupported locking or timeout, with the child process +receiving a duplicated locked-FD handle and the wrapper retaining +group-signal/reap ownership exactly as documented. Building this tool (if +not already present at ADR 0046 implementation time) is work item +`ADR046-delivery-001` (§17); every wave's `make heavy-check`, +`make heavy-test-integration`, `make heavy-test-host-integration`, and +`make heavy-test-hardware` targets route through it. "Sole use" means: no +wave, no Provider crate, and no panel/validator role may create a second +ad hoc lock file, a bespoke sleep-and-retry loop, or a per-crate heavy-lane +guard — every heavy lane in every wave shares the exact same two slots, so +concurrent waves' heavy validation cannot silently oversubscribe the shared +Nix store, cargo target, or KVM device. + +## 12. Candidate snapshot, validator evidence, ten-role panel, attest/seal/eligibility + +### 12.1 Immutable candidate snapshot + +For each wave (§3.2), once its stack of PRs is open and has passed the +smallest focused local preflight (`make check-tier0` plus the wave's +directly affected `make test-*` shard), the integrator creates one immutable +snapshot binding: + +- the exact base commit and every open PR's head commit in the wave's stack; +- the wave's dependency graph edges (§3.4) and repository set; +- a `content_id` (digest of the wave's integrated tree) and a `candidate_id` + (digest of `content_id` + dependency graph + repository set); +- a `snapshot_sha256` covering the same inputs, byte-for-byte. + +This mirrors `cargo xtask delivery wave help`'s `snapshot` subcommand as +already specified for this codebase's sibling ADR-0045 lineage (built here as +work item `ADR046-delivery-002`, §17, by copy/adapt per D001/D041). Any +content change after the snapshot — including generated output, dependency +metadata, contract fingerprints, or repository-set membership — invalidates +both validator and panel evidence; the wave re-snapshots and both lanes +rerun. A history-only rebase or retarget may reuse panel evidence only when +the canonical proof tool (§12.6) verifies byte-identical integrated content. + +### 12.2 CI/local/host validator evidence + +Three lanes run concurrently against the exact snapshot, never sequentially +gating each other: + +1. **Required GitHub CI**: the existing Layer-1 `check` rollup + (`.github/workflows/pr-l1-static-fast.yml`, generated from + `tests/layer1-jobs.json`), extended with the ADR 0046 rows from §10.1. +2. **Final local/host validators**: `make test-integration`, + `make test-host-integration`, and (for waves touching device Providers) + `make test-hardware`, all wrapped in the heavy-gate (§11), run by the + integrator on the development host and imported as evidence via + `cargo xtask delivery wave validate-import` (built as work item + `ADR046-delivery-003`, §17). +3. **The ten-role panel** (§12.3), run against the same snapshot. + +A pending lane is valid only while the wave's PR stack is open (§13.1); it +never permits merge (§13.3). Command/result evidence is imported into an +external, candidate-ID-addressed state directory — never committed to Git, +copied into generated artifacts, or pasted into a PR body (§12.5). + +### 12.3 Ten-role final panel, bound to Gemini 3.1 Pro + +Every ADR 0046 wave's binding panel — run exactly once, at wave close, +against the wave's one immutable snapshot, never per implementation round — +uses this repository's existing ten-role default roster (`AGENTS.md` → +"Panel review" → "Default panel"), with every role's provider/model bound by +the wave's `panel-request` record to: + +```text +provider: github-copilot +model_version: gemini-3.1-pro-preview +``` + +| Role | Focus (unchanged from this repository's existing default panel) | +| --- | --- | +| `software` | Shell + Nix shape, daemon instrumentation, sidecar idempotency, metric-exporter error handling | +| `test` | New option-schema coverage, restart-policy gates, manifest/schema drift, invisible-regression risk | +| `nixos` | Module wiring, `lib.mkForce`/`lib.mkDefault` correctness, option declarations, activation ordering | +| `networking` | Network surface changes, firewall posture, DHCP/DNS regressions, bridge isolation, routing invariants | +| `security` | Attack surface, capability sets/syscall filters, authz boundaries, PII/telemetry-label review, retention | +| `rust` | API shape, error propagation, unsafe/FFI boundaries, schema generation, workspace-dependency direction | +| `product` | Operator UX, naming surface, migration/deprecation policy, actionable errors | +| `docs` | Diataxis adherence, CHANGELOG, schema md↔json drift, AGENTS.md updates landing with load-bearing changes | +| `observability` | Metric-label cardinality, span-attribute hygiene, log/audit shape, retention, exporter correctness | +| `kernel` | pidfd, cgroup, namespace, mount, signal, ioctl, filesystem semantics; kernel-version assumptions | + +`cargo xtask delivery wave panel-request` writes the candidate-bound request +(binding `candidate_id`/`content_id`/`snapshot_sha256`, the exact ten-role +roster, and the required `gemini-3.1-pro-preview` model). `cargo xtask +delivery wave panel-attest` validates a directory containing exactly one +record per role, each shaped exactly as this repository's sibling +ADR-0045-lineage panel-receipt artifact: + +```json +{ + "artifact_kind": "d2b-delivery/panel-receipt", + "schema_version": 1, + "role": "software", + "candidate_id": "", + "content_id": "", + "snapshot_sha256": "", + "model_version": "gemini-3.1-pro-preview", + "provider": "github-copilot", + "run_id": "run-001", + "receipt_locator": "github-copilot://runs/run-001/software", + "output_sha256": "", + "signoff": true, + "recommendations": [] +} +``` + +`signoff` is `true` iff `recommendations` is `[]`; any finding requires a +content change, which creates a new snapshot and invalidates every prior +validation/panel record for that wave. Green tests never waive this gate — +every wave, including a documentation-only or single-crate wave, requires +unanimous 10/10 signoff before its exit criteria (§4) are met. Building this +tooling (if not already present) is work item `ADR046-delivery-004`/`-005` +(§17), copy/adapted from the equivalent sibling-lineage tooling per D001/D041. + +### 12.4 Seal and merge eligibility + +`cargo xtask delivery wave seal` requires all ten panel records +present, unanimous, and bound to the same `candidate_id`/`content_id`/ +`snapshot_sha256`, plus every §12.2 validator lane reporting success on that +exact snapshot. `cargo xtask delivery wave merge-eligibility` then confirms, +per PR in the wave's stack: the seal exists, the PR's current base/head +still matches the sealed snapshot's recorded OIDs (or a history-only rebase +has passed the byte-identical proof in §12.6), and every required GitHub +check is green. + +### 12.5 No raw evidence or AI metadata in Git/PR + +Validation command output, panel transcripts, and attestation payloads +never enter Git, generated source, a PR body, or a release archive. PR +bodies carry only: dependency list, base/head/tree OIDs, `candidate_id`/ +`content_id`, and check-status summaries, with optional links to external +evidence. No PR description, commit message, CHANGELOG entry, or shipped doc +names or lists the AI agent, assistant, tool, or model used to author or +review the change (per this repository's existing `AGENTS.md` "AI/tool +attribution" rule, extended here to also cover panel attestation records — +the panel's own `model_version`/`provider` fields exist only inside the +external, non-Git delivery-state directory, never inside a committed file). + +### 12.6 Content invalidation and byte-identical history proof + +Any content change to the wave's integrated tree — including generated +output, dependency metadata, contract/index content, or repository-set +membership — invalidates both the validator and panel lanes for that wave. +The wave re-snapshots (§12.1) and reruns both lanes. A history-only rebase or +retarget (no tree content change, only a new base commit) may reuse prior +panel records only when a canonical proof tool verifies: + +- byte-identical integrated content across the old and new history; +- byte-identical generated artifacts (schemas, `spec-set.json`/ + `work-items.json`, Nix-rendered fixtures); +- byte-identical dependency diff and repository set. + +Required CI still reruns on the new history even when the proof succeeds +(the proof only preserves the *panel* record, never the CI requirement). +This tool is work item `ADR046-delivery-006` (§17), copy/adapted from the +sibling-lineage history-proof tool per D001/D041. + +## 13. PR opening vs. final lanes; merge order + +### 13.1 PR opening + +After a slice's candidate passes the smallest focused local preflight +(`make check-tier0` + its directly affected `make test-*` shard), the +integrator immediately opens or updates its PR and creates the wave's +immutable snapshot (§12.1) from that exact open-PR/stack state — it does not +wait for `make test-integration`, `make test-host-integration`, or the panel +to finish first. This is the same ordering this repository's existing +`AGENTS.md` "Landing changes (PR workflow)" section already requires; ADR +0046 adds only the explicit snapshot-creation step at that same point. + +### 13.2 Final lanes + +GitHub CI, the local/host validators, and the ten-role panel then run +concurrently against that exact snapshot (§12.2/§12.3). The PR may report +these lanes as pending while it stays open; pending is never sufficient to +merge (§13.3). + +### 13.3 Merge order + +1. Merge proceeds only after `cargo xtask delivery wave merge-eligibility` + reports eligible (§12.4) for every PR in the wave's stack. +2. Merges follow the wave's Git Town stack root-to-leaf: `ADR046-W0`'s four + serial steps merge in their stacked order; a wave's parallel slices + (§3.2/§3.3) merge in any order relative to each other (they are + file-disjoint by construction, §6), but every slice depending on another + still-open slice (the intra-track ordering in §3.3) merges after its + prerequisite. +3. The integrator retargets/rebases any still-open dependent PR immediately + after its prerequisite merges, and reruns only the smallest relevant + validation for that retarget (full re-panel only if content changed, per + §12.6). +4. A wave does not advance to the next wave's entry criteria (§4) until + every PR in its own stack has merged through GitHub — never through a + local octopus merge or a direct push to `main` for ADR-scale work + (per this repository's existing `AGENTS.md` "Finish-of-work invariant" + and "Stacked PR workflow" sections, which remain binding for ADR 0046). + +## 14. Post-wave cleanup (policy only — no deletion performed by this change) + +After each wave's every PR merges (§13.3), the integrator: + +1. Deletes the merged remote feature branches for that wave's slices. +2. Cleans any slice worktree's real `packages/target/` (if it is not the + shared-cache symlink) before removing the worktree. +3. Removes the finished local worktrees and deletes their local branches. +4. Runs `nix-collect-garbage` and verifies `git worktree list` contains only + active work for the next wave. +5. Confirms no abandoned/superseded branch is silently dropped — any + worktree branch whose tip is unmerged but represents abandoned work is + flagged for the operator, per this repository's existing `AGENTS.md` + worktree-audit rule. + +This is a policy statement only. This documentation-only change creates no +worktrees, branches, or generated artifacts, so no cleanup step above is +executed as part of landing this spec. + +## 15. Release/cutover gate + +`ADR046-W7` ("streamline & cutover") does not close, and d2b 3.0 does not +release, until all of: + +1. `ADR-046-streamline`, `ADR-046-security-and-threat-model`, + `ADR-046-reset-and-cutover`, `ADR-046-feasibility-and-spikes`, and this + validation spec are `Accepted` and their own work items' `Validation` + evidence is imported per §12.2. +2. Every `DELETE`/`REPLACE` row in `ADR-046-current-code-migration-map` has + satisfied its removal-proof test (§9) on the `ADR046-W7` candidate + snapshot — this is the destructive-cutover gate; d2b 3.0 does not ship + with both the v3-pre-ADR-0046 code path and its successor coexisting + indefinitely. +3. The `ADR046-W7` snapshot has passed §10's complete matrix, including the + manual hardware/live/cloud tiers (§10.11) at least once with recorded + external evidence (not required to be green in CI, but required to be + evidenced), and the reset/cutover scenarios (§10.13) defined by + `ADR-046-reset-and-cutover`. +4. The ten-role panel (§12.3) has returned unanimous signoff on the + `ADR046-W7` snapshot with zero recommendations, and + `cargo xtask delivery wave seal` + `merge-eligibility` both pass. +5. `CHANGELOG.md` carries a new version header under the project's existing + Keep-a-Changelog convention (`AGENTS.md` → "Changelog & Releases"), + summarized by version with every internal wave/finding process marker + stripped, per that same file's "Process markers stay out of shipped + artifacts" rule — ADR 0046's `ADR046-W` tags are exactly such a + process marker and never appear in the released CHANGELOG section. +6. Every prior wave's post-wave cleanup (§14) has been performed, so the + release cuts from a tree with no dangling ADR 0046 implementation + worktrees or branches. + +Only after all six hold does the auto-release mechanism already documented +in this repository's `AGENTS.md` ("Auto-release") apply unchanged: a new +version header merged to `main` tags `vX.Y.Z` and builds/releases the host +binaries. + +## 16. Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | This repository's `AGENTS.md` "Panel review" (8/N-role phase gate, no candidate snapshot/seal), "Stacked PR workflow for large waves," "Worktrees for parallel agents," `tests/AGENTS.md`/`tests/README.md` Layer-1/Layer-2 taxonomy, and `Makefile` targets (`make check-tier0`, `test-unit`, `test-lint`, `test-rust`, `test-proofs`, `test-flake`, `test-drift`, `test-policy`, `check`, `check-static`, `test`, `test-integration`, `test-host-integration`) | +| Evidence class | The Layer-1/Layer-2 test taxonomy and Makefile targets are `production-reachable` (verified directly in `tests/AGENTS.md`, `tests/README.md`, and this repository's `Makefile` target list); the ten-role panel roster is `production-reachable` (verified verbatim in this repository's own `AGENTS.md`); the candidate-snapshot/`xtask delivery`/seal/attest machinery, `cargo xtask heavy-gate`, and the byte-identical history-proof tool are `ADR-only` in this repository today — they exist as a proven, documented process on this codebase's sibling ADR-0045 lineage and are adopted here by explicit copy/adapt under D001/D041, not invented fresh | +| Behavior retained | Layer-1-first bias, closed drift/meta-gate set, hermetic mocking discipline, commit-before-build convention, no-AI-metadata-in-Git convention, worktree/branch hygiene, `KillMode=process` restart-continuation semantics | +| Required delta | Candidate-snapshot immutability, ten-role panel bound to one fixed model/provider and run exactly once per wave (not per round), `xtask delivery` subcommands, `xtask heavy-gate` semaphore, attest/seal/eligibility/history-proof tooling, the exact `ADR046-W0`–`ADR046-W7` wave graph and its file-overlap/shared-prep contracts | +| Reuse path | Copy/adapt the sibling-lineage `xtask delivery`/`xtask heavy-gate` implementations named in §11/§12; extend (never replace) the existing Layer-1/Layer-2 taxonomy and Makefile targets; extend the existing ten-role panel table unchanged | +| Replacement/deletion | Nothing in this repository's current validation/delivery tooling is removed by this spec; `ADR046-delivery-00x` work items (§17) are additive tooling built alongside, not instead of, the existing `Makefile`/panel-review process, until `ADR046-W7` explicitly retires any tooling the migration map marks `DELETE`/`REPLACE` | +| Feasibility proof | The sibling-lineage candidate-snapshot/panel/seal contract supplies the reuse design named in §11/§12; `ADR-046-feasibility-and-spikes` owns the ADR-0046-specific redb/reconciliation/session/package/state numeric proofs cited in §10.4 | +| Future owner | Work items in §17 | + +## 17. Implementation work items + +### ADR046-delivery-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-delivery-001` | +| Dependency/owner | `ADR046-W0`; delivery-tooling integrator | +| Current source | none in this repository; `Makefile` heavy-lane targets do not yet exist | +| Reuse source | sibling-lineage `cargo xtask heavy-gate` implementation (per D001/D041 unrestricted-reuse policy) | +| Reuse action | adapt | +| Destination | `packages/xtask/src/heavy_gate.rs`; `Makefile` targets `heavy-check`, `heavy-test-integration`, `heavy-test-host-integration`, `heavy-test-hardware`, `heavy-cargo-test`, `heavy-flake-check` | +| Detailed design | Two-slot per-UID OFD-locked semaphore, 250 ms nonblocking retry up to 30 minutes, fail-closed on unsupported locking, duplicated locked-FD handoff to child, wrapper-owned group-signal/reap, as specified in §11 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt paths/crate names to this repository's `packages/xtask` layout. | +| Integration | Every heavy lane in §10.4/§10.10/§10.11 routes through this one binary; no wave adds a second lock mechanism | +| Data migration | None — net-new tooling | +| Validation | Unit tests for slot acquisition/timeout/fail-closed paths; integration test spawning two concurrent heavy-gate invocations and asserting the second blocks until the first releases | +| Removal proof | Not applicable (net-new; nothing to remove) | + +### ADR046-delivery-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-delivery-002` | +| Dependency/owner | `ADR046-W0`; delivery-tooling integrator | +| Current source | none in this repository | +| Reuse source | sibling-lineage `cargo xtask delivery wave snapshot` implementation | +| Reuse action | adapt | +| Destination | `packages/xtask/src/delivery/snapshot.rs` | +| Detailed design | Binds base/head OIDs, dependency graph, repository set into `candidate_id`/`content_id`/`snapshot_sha256` per §12.1 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt. | +| Integration | Called by the integrator immediately after PR opening (§13.1), before any validator/panel lane starts | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Unit tests asserting identical inputs produce identical digests and any single-byte content change produces a different `content_id` | +| Removal proof | Not applicable | + +### ADR046-delivery-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-delivery-003` | +| Dependency/owner | `ADR046-delivery-002`; delivery-tooling integrator | +| Current source | none in this repository | +| Reuse source | sibling-lineage `cargo xtask delivery wave validate-import` implementation | +| Reuse action | adapt | +| Destination | `packages/xtask/src/delivery/validate_import.rs`; external candidate-ID-addressed evidence directory (never under Git) | +| Detailed design | Imports CI/local/host validator command/result evidence, keyed by `candidate_id`, per §12.2 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt. | +| Integration | Consumed by `wave seal` (§ADR046-delivery-005) as one of the seal's required inputs | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Test asserting evidence for a stale `candidate_id` is rejected; test asserting raw command output never lands in a tracked file | +| Removal proof | Not applicable | + +### ADR046-delivery-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-delivery-004` | +| Dependency/owner | `ADR046-delivery-002`; spec-set integrator | +| Current source | `docs/specs/README.md`'s described-but-not-yet-generated `ADR-046-spec-set.json`/`ADR-046-work-items.json` contract | +| Reuse source | none required — this generator is specific to the `docs/specs/ADR-046-*` manifest shape | +| Reuse action | adapt | +| Destination | `packages/xtask/src/gen_spec_set.rs`; `docs/specs/ADR-046-spec-set.json`, `docs/specs/ADR-046-work-items.json` | +| Detailed design | Enumerates every `docs/specs/ADR-046-*.md` and `docs/specs/providers/ADR-046-provider-*.md` file, its metadata table, bytewise-sorted `workItemPrefixes` registry, content digest, and every `### ADR046--` work item, per §8. Each prefix belongs globally to exactly one member, and generation resolves ownership only through the registry rather than by splitting IDs or filenames. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (new generator, following the existing `xtask gen-schemas`/`gen-nix-options` pattern already used for other generated artifacts). | +| Integration | `make test-drift` gains a row running this generator and `git diff --exit-code`; every wave's exit criteria (§4) require it committed as the wave's last commit | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Golden-fixture test against a small synthetic spec directory; drift test against the real `docs/specs/` tree | +| Removal proof | Not applicable | + +### ADR046-delivery-005 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-delivery-005` | +| Dependency/owner | `ADR046-delivery-002`, `ADR046-delivery-003`; panel-tooling integrator | +| Current source | none in this repository; this repository's existing `AGENTS.md` panel-review process is host-local script tooling (`/etc/nixos/scripts/panel-review.{md,sh}`), not a candidate-bound `xtask` subcommand | +| Reuse source | sibling-lineage `cargo xtask delivery wave panel-request`/`panel-attest` implementation | +| Reuse action | adapt | +| Destination | `packages/xtask/src/delivery/panel.rs` | +| Detailed design | `panel-request` writes the candidate-bound request naming the exact ten roles and required model; `panel-attest` validates a directory of exactly ten strict 13-field records, rejecting wrong model/candidate binding, duplicate provider/run provenance, or inconsistent `signoff`/`recommendations`, per §12.3 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt to bind the fixed `gemini-3.1-pro-preview`/`github-copilot` model/provider pair and this repository's existing ten-role roster (§12.3). | +| Integration | Every wave's exit criteria (§4) require ten unanimous attested records before `wave seal` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Unit tests for every rejection class (wrong model, missing role, duplicate run_id, `signoff:true` with non-empty `recommendations`); integration test with ten synthetic valid records passing | +| Removal proof | Not applicable | + +### ADR046-delivery-006 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-delivery-006` | +| Dependency/owner | `ADR046-delivery-002`, `ADR046-delivery-004`, `ADR046-delivery-005`; delivery-tooling integrator | +| Current source | none in this repository | +| Reuse source | sibling-lineage `cargo xtask delivery wave seal`, `merge-eligibility`, and history/byte-identity proof implementation | +| Reuse action | adapt | +| Destination | `packages/xtask/src/delivery/{seal,eligibility,history_proof}.rs` | +| Detailed design | `seal` requires all ten panel records unanimous and bound to the same candidate/content/snapshot digests plus every validator lane passing; `merge-eligibility` checks each stacked PR's current base/head against the sealed OIDs or a passing history-proof; `history_proof` verifies byte-identical integrated content/generated artifacts/dependency diff/repository set across a rebase, per §12.4/§12.6 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt. | +| Integration | `make check` gains no new required step for ordinary contributors; this tooling is invoked only by the wave integrator per §4/§13 | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Unit tests for seal rejection on any missing/mismatched record; integration test proving a history-only rebase with identical content passes `history_proof` and reuses panel evidence, while any content change fails it | +| Removal proof | Not applicable | + +### ADR046-delivery-007 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-delivery-007` | +| Dependency/owner | `ADR046-W0`; delivery-tooling integrator | +| Current source | none in this repository; this codebase's existing `tests/tools/` timing logs (`d2b-static-timing.$$/`) are ad hoc, not a candidate-bound ledger | +| Reuse source | existing `xtask`/`libtest --format=json` timing output; no new test framework | +| Reuse action | adapt | +| Destination | `packages/xtask/src/test_runtime_ledger.rs`; a `make`-invokable timing gate reusing `make test-rust`/Layer-1 shard targets | +| Detailed design | Measures execution-only time (after build, warm cache) per test/crate/shard against §10.16 budgets, records the reference runner/repetitions/p95, reports the top slow tests, applies a historical regression threshold, and emits a machine-readable CI artifact; the placement lint rejects a hermetic-tier test that sleeps, spawns a process, or touches network/containers/DBus/systemd/broker/Nix/KVM/hardware/live cloud, and the deterministic-clock/sleep lint rejects wall-clock sleep/retry in `src/`/`tests/` | +| Integration | Every wave's entry/exit criteria (§4) consume the ledger artifact; `make test-rust` and Layer-1 shards run concurrently; no new top-level `tests/*.sh` gate is added | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Policy self-tests: an intentional slow/sleep/process/network hermetic test is rejected; a synthetic timing regression fails the gate; parallel isolation holds under shuffled/parallel execution; a retired legacy selector is absent from `tests/layer1-jobs.json`, closed gate manifests, and CI shards | +| Removal proof | Not applicable | + +### ADR046-delivery-008 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-delivery-008` | +| Dependency/owner | `ADR046-streamline-001`, `ADR046-W0`; delivery-tooling integrator | +| Current source | none in this repository; launch order and parallelism were previously derived only from this spec's §3/§6 prose | +| Reuse source | `ADR-046-spec-set.json`, `ADR-046-work-items.json`, and §3.1–§3.4/§3.5 of this spec; no new framework | +| Reuse action | adapt | +| Destination | `docs/specs/ADR-046-implementation-graph.json`, `docs/specs/ADR-046-implementation-graph.md` (generated by `ADR046-streamline-001`'s `xtask implementation-graph`); the artifact contract, generation, validation, and ready-wave query are owned by §3.5 of this spec | +| Detailed design | Owns the D095 implementation-graph contract: `artifactKind`/`schemaVersion`/`adr`/`status`; one node per member spec and per work item mapped exactly once to a `W0`–`W7` wave and a file-disjoint `parallelGroup`, with `owner`/`destinations`/`entryContracts`/`prerequisites`/`blockers`/`exitGate`/`topologicalRank`; work-item nodes additionally embed the manifest's exact `detailedDesign` and `validation` text byte-for-byte; typed `spec-depends-on`/`shared-contract`/`work-item-depends-on`/`implements-spec`/`file-overlap-order` edges; the §3.5.1 ready-wave query; and the anti-serialization invariant that every ready file-disjoint group launches concurrently while a same-wave dependency is a prep barrier, not whole-wave serialization. The graph is a generated non-member artifact and does not change the 55-member `ADR-046-spec-set.json` count. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (D095 artifact contract). | +| Integration | Consumed by §4 wave entry/exit and §6 anti-serialization checks and by `ADR046-streamline-013`; a `tests/unit/gates/` drift gate regenerates and `git diff --exit-code`s the graph after any spec/work-item edit | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | Every 55 spec node and every work item present exactly once; all edge endpoints resolve; graph acyclic; waves monotonic (dependencies earlier or explicit same-wave prep barrier); parallel groups claim no ordering absent a dependency/file-overlap edge; deterministic JSON with no timestamps/host paths; every Mermaid node ID valid; the ready-wave query returns the expected concurrently-launchable groups on a seeded fixture | +| Removal proof | Not applicable | + +### ADR046-delivery-009 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-delivery-009` | +| Dependency/owner | `ADR046-delivery-004`, `ADR046-delivery-008`; spec-set policy-test owner | +| Current source | The fail-closed completeness, identity, and closed-action contract in `docs/specs/README.md`; the Proposed tree has no checked-in generator or policy test | +| Reuse source | `ADR046-delivery-004` generator shape and the existing `d2b-contract-tests` fixture-driven policy-test pattern | +| Reuse action | adapt | +| Destination | `packages/xtask/src/gen_spec_set.rs`; `packages/d2b-contract-tests/tests/policy_adr046_work_items.rs`; generated spec-set, work-item, and implementation-graph drift checks | +| Detailed design | Parse every normative member's exact level-three `### ADR046--` headings and tables; `##` or `####` item declarations are invalid. Require an exact Markdown/manifest bijection; exact `specId` and `specPath`; a bytewise-sorted, nonempty `workItemPrefixes` list for each item-owning member; global one-member ownership for every registered prefix; registry-based ID ownership; three-digit nonzero ordinals; every mandatory field exactly once and nonempty; one closed scalar `reuseAction`; and `reuseSource: null` for `create`. Reject dropped, extra, malformed, wrong-level, duplicate, ambiguous, heuristic-split, unregistered-prefix, or unconsumed items before writing any artifact. Validate all dependency endpoints, DAG acyclicity, wave monotonicity, and single-wave parallel groups before atomically publishing all generated files. | +| Integration | `make test-policy` runs negative fixtures; `make test-drift` regenerates all ADR 0046 artifacts and requires a clean diff; `ADR046-delivery-008` consumes only a manifest that passed this policy | +| Data migration | None — documentation/build-policy contract only | +| Validation | Fixtures fail for a dropped heading, `##`/`####` item heading, extra manifest row, duplicate ID, duplicate cross-member prefix, unsorted/empty required prefix registry, wrong owner/path/prefix, heuristic-only prefix match, two-digit/zero ordinal, missing/duplicate mandatory field, free-form/compound action, `create` with a reuse source, dangling dependency, cyclic DAG, backward-wave dependency, and cross-wave parallel group; the exact 55-spec real tree passes with every item once | +| Removal proof | Not applicable; the policy remains the permanent generated-artifact closure gate | diff --git a/docs/specs/ADR-046-work-items.json b/docs/specs/ADR-046-work-items.json new file mode 100644 index 000000000..bdd2dc3ac --- /dev/null +++ b/docs/specs/ADR-046-work-items.json @@ -0,0 +1,8153 @@ +{ + "adr" : "0046", + "artifactKind" : "d2b-adr-work-items", + "items" : [ + { + "currentSource" : "`packages/d2b-provider-aca/src/lib.rs`: `AcaWorkloadProvider`, 2841 lines production-reachable; `packages/d2b-provider-runtime-azure-container-apps/src/provider.rs`: `AzureContainerAppsRuntimeProvider`, 2796 lines (test-only at v3 baseline)", + "dataMigration" : "Full d2b 3.0 reset; no v2 provider state compatibility", + "dependencyOwner" : "ADR046-provider-001; runtime-aca owner", + "destination" : "`packages/d2b-provider-runtime-azure-container-apps/src/controller.rs`", + "detailedDesign" : "Async `Guest` reconcile loop: `describe` → `validateSpec` → `plan` → `reconcile` → `observe` → `finalize`. Adoption before first `RuntimeEnsure`; operation/requeue truth remains in the core Operation ledger and no Provider state Volume is created. Credential lease acquire/revoke per call. The controller creates a same-Zone Provider-owned semantic sandbox-agent Endpoint (`ownerRef` remains the Guest lifecycle edge), resolves opaque transport carriage, and performs Noise KK enrollment for the ACA Provider service/session. `providerPhase` and `guestIdentityDigest` stay in `status.provider.details`; Endpoint readiness/generation/availability stay only in Endpoint status; no raw endpoint/path, cross-Zone ref, route cursor, transport handle, or authority appears in status. **ProviderDeployment creates both static Processes; ACA controller never instantiates its own Processes and never writes Provider status directly. All Processes run inside the gateway Guest. The managed sandbox remains a Guest in the owning Zone and is not a Zone. No Host Process, no Host Credential, no Host Azure HTTP socket. Long-running cloud ops return `progressing`/`requeue-at` immediately; never block watch loop.** Primary reuse disposition: `replace`. Preserved source-plan detail: REPLACE (old) + ADAPT (main types/traits).", + "integration" : "Zone ResourceClient → ProviderDeployment → Process launch inside gateway Guest → d2b-bus → deployment service", + "removalProof" : "`packages/d2b-provider-aca/` removed only after conformance suite green", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-container-apps.md", + "validation" : "Controller conformance suite; adoption/ambiguity tests; Endpoint create/adopt/finalize and generation tests; deadline/cancellation/backpressure matrix; redaction coverage; **gateway Guest placement validation: assert no Process has `executionRef: Host/*`**; Process spec field schema tests (`spec.template`, canonical `sandbox`/`budget`/`networkUsage`/`endpoints`/`readiness`/`restartPolicy` fields, `mounts` with `required: true`, `providerRef: Provider/system-minijail`); ProviderDeployment creates both Processes (controller never self-spawns); no raw endpoint/path or ZoneLink status/cursor/authority in Guest or Endpoint status", + "workItemId" : "ADR046-aca-001" + }, + { + "currentSource" : "`packages/d2b-gateway-runtime/src/provider_agent.rs` (main)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-aca-001; deployment service owner", + "destination" : "`packages/d2b-provider-runtime-azure-container-apps/src/deployment_service.rs`", + "detailedDesign" : "`ProviderAgentProcess`-shaped binary; bounded dispatch (64 in-flight); bounded audit ring (1024 capacity); shutdown within 5 s; serves `d2b.aca.v3.deployment` service schema including `GuestHealth` (health probing folded in from former health worker). All ACA API calls go through the injected `AcaControl` port — no ambient network call, no SDK default chain. Long-running ops return `progressing`/`requeue-at` to the caller; no blocking on Azure API completion. Primary reuse disposition: `adapt`. Preserved source-plan detail: COPY/ADAPT (partial); exclude `aca_workload.rs`.", + "integration" : "ProviderDeployment spawns service; d2b-bus routes GuestProvision/Start/Stop/Destroy/Adopt/Inspect/Health methods", + "removalProof" : "Old `GuestControlEndpointProvider` dispatch removed per ADR046-aca-001", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-container-apps.md", + "validation" : "Service dispatch matrix; RBAC refusal tests; redaction tests; shutdown deadline tests", + "workItemId" : "ADR046-aca-002" + }, + { + "currentSource" : "`packages/d2b-provider-runtime-azure-container-apps/src/control.rs` (main): `AcaCredentialLeaseClient`, `AcaCredentialLease`, `AcaCredentialLeaseRequest`, `AcaCredentialPurpose`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-aca-001; credential integration owner", + "destination" : "`packages/d2b-contracts/src/provider_effects/aca.rs` (shared `d2b-contracts` provider-effects module; no new crate; provider crate remains one package)", + "detailedDesign" : "`AcaCredentialLeaseClient`, `AcaCredentialLease`, `AcaCredentialLeaseRequest`, and `AcaCredentialPurpose` live in the shared `d2b-contracts` provider-effects module. Adapt `CredentialLease` to v3 Credential resource opaque lease handle. `AcaCredentialPurpose` maps to `allowedOperations` check against `Credential.spec`. Lease expiry capped at call deadline. Cleanup job pattern retained verbatim. Primary reuse disposition: `adapt`. Preserved source-plan detail: RETAIN+ADAPT.", + "integration" : "Controller acquires lease per reconcile step via injected `AcaCredentialLeaseClient`; raw token delivered only via Noise KK E2E channel through `d2b.credential.v3.AcquireToken` method", + "removalProof" : "Old `CredentialProvider` trait deleted after `credential-managed-identity` Provider conformance", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-container-apps.md", + "validation" : "Mock credential client tests; lease cleanup timeout tests; token non-exposure assertion", + "workItemId" : "ADR046-aca-003" + }, + { + "currentSource" : "`packages/d2b-provider-relay/src/lib.rs`: `AzureRelayTransportProvider`; `packages/d2b-provider-aca/src/lib.rs`: `AcaRelayTransportConfig`", + "dataMigration" : "No relay session compatibility; re-enroll on first `RuntimeAdopt`", + "dependencyOwner" : "ADR046-session-001; ACA Endpoint/session and transport-capability integration owner", + "destination" : "ACA sandbox-agent Endpoint/session controller (§§7, 12); `Provider/transport-azure-relay` private configuration and carriage dossier (separate)", + "detailedDesign" : "Split `AcaRelayTransportConfig`: relay-private namespace/connection/credential fields move behind `Provider/transport-azure-relay`; this Provider retains only `sandboxTransportAlias`. The ACA controller creates the Provider-owned semantic Endpoint (with the Guest lifecycle edge), resolves an opaque `OwnedTransport`, and owns KK enrollment, authenticated `d2b.aca.v3.sandbox-agent` service/session generation, deadlines, cancel, bounded queues, credits/backpressure, reconcile, adoption, and finalization. The ordinary managed sandbox is explicitly not a Zone; no cross-Zone ResourceRef or ZoneLink status/cursor/authority is accepted or emitted. Primary reuse disposition: `replace`. Preserved source-plan detail: REPLACE (both); ADAPT relay fields only into the carriage Provider's private config.", + "integration" : "ACA controller resolves the semantic Endpoint and transport Provider capability, then establishes the enrolled KK ComponentSession after `GuestProvision`; transport Provider supplies carriage only", + "removalProof" : "`packages/d2b-provider-relay/` removed after `transport-azure-relay` Provider conformance; no ACA schema, Nix emitter, controller, status adapter, fixture, or test retains an ordinary-sandbox ZoneLink path", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-container-apps.md", + "validation" : "Endpoint ownership/resolution tests; relay unavailability tests; KK re-enrollment after sandbox/controller restart; deadline/cancel/credit-backpressure tests; schema/status tests reject ZoneLink refs, phase, cursors, and authority", + "workItemId" : "ADR046-aca-004" + }, + { + "currentSource" : "`packages/d2b-provider-runtime-azure-container-apps/src/types.rs` (main): `AcaRuntimeConfig`, `AcaSandboxProfile`, `AcaResourceBinding`, `AcaWorkloadQuery` — test-only at v3 baseline", + "dataMigration" : "None — no state Volume at v3 `1.0`", + "dependencyOwner" : "ADR046-aca-001; state/migration owner", + "destination" : "`packages/d2b-provider-runtime-azure-container-apps/src/types.rs`", + "detailedDesign" : "Replace `RealmId`/`WorkloadId` with v3 owning-`Zone`/`Guest` UID types. `AcaResourceBinding` keys the adoption query but never names a child Zone. The provider declares **no** Provider state Volume: bounded, non-secret sandbox binding/adoption metadata lives in `Guest.status` (latest bounded observed handle digests) and in-flight operation/requeue truth lives in the core Operation ledger (D087). Neither Process mounts a state Volume; there is no `sandbox-state`/`service-state` Volume, no `User/d2b-aca-controller`/`User/d2b-aca-deployment-service` state-layout principal, and no empty identity-only Volume. On restart the controller re-derives observed binding from `Guest.status`, the core Operation ledger, and an external `find_sandboxes` query, ensures the Provider-owned Endpoint, resolves fresh transport carriage, and authenticates a fresh Provider session, treating all status as observation and never authority. Host never holds cloud binding, admission, PSK, operation, Endpoint, or session state. Primary reuse disposition: `adapt`. Preserved source-plan detail: RETAIN+ADAPT.", + "integration" : "No Provider state Volume is created before Processes start; the controller writes bounded observed binding/adoption metadata to `Guest.status`, writes only standard Endpoint observations to Endpoint status, reads in-flight operation state from the core Operation ledger adapter, and retains no transport or ZoneLink cursor across restart", + "removalProof" : "Old in-memory-only operation ledger removed after core Operation ledger adapter passes; `operation-ledger` stateNamespace absent from component descriptor", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-container-apps.md", + "validation" : "Controller/service declare empty `stateNamespaces`; no `sandbox-state`/`service-state` Volume created; neither Process mounts a state Volume; `Guest.status` binding/adoption fields and standard Endpoint status are bounded, non-secret, and carry no credential/endpoint/poll-URL/ZoneLink cursor or authority bytes; restart re-derivation from status/core ledger/external `find_sandboxes`, Endpoint ensure, and fresh KK session without a Volume; core Operation ledger adapter integration test", + "workItemId" : "ADR046-aca-005" + }, + { + "currentSource" : "`nixos-modules/options-realms-workloads.nix`: `kind = \"ProviderManaged\"` → ACA; `packages/d2b-provider-aca/src/lib.rs`: tracing fields", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-aca-001; Nix/telemetry owner", + "destination" : "`nixos-modules/` (generated Guest resource options); `packages/d2b-provider-runtime-azure-container-apps/src/{audit,metrics}.rs`", + "detailedDesign" : "Eval-time assertions for ACA-specific invariants (§15.7), including rejection of ZoneLink/child-Zone fields for an ordinary sandbox and exact same-Zone sandbox-agent Endpoint template ownership. Closed OTEL label set (§13.4). Audit event schema (§13.3). Tracing target constant `d2b_provider_runtime_azure_container_apps::credential_lease_cleanup` retained. Primary reuse disposition: `replace`. Preserved source-plan detail: REPLACE (Nix emitter) + ADAPT (metric/audit shapes).", + "integration" : "Nix eval gate; `observability-otel` Provider OTEL pipeline", + "removalProof" : "Old Nix `ProviderManaged` workload options retired after Guest resource Nix emitter parity", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-container-apps.md", + "validation" : "Label cardinality policy test; audit commit-before-complete test; Nix assertion eval tests; generated resource scan proves no ACA sandbox ZoneLink is emitted", + "workItemId" : "ADR046-aca-006" + }, + { + "currentSource" : "n/a — new requirement (gateway Guest placement)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-aca-001; Nix/gateway wiring owner", + "destination" : "`nixos-modules/` (gateway Guest declaration, Process template wiring, Credential scope assertion); eval-time validation module", + "detailedDesign" : "Nix eval-time assertions for: (a) `gatewayExecutionRef` resolves to a `Guest` resource, not `Host/*`; (b) Credential `scope.executionRef` matches `gatewayExecutionRef`; (c) all Process templates emitted for this Provider have `executionRef` equal to `gatewayExecutionRef`; (d) `sandboxTransportAlias` resolves the signed carriage capability; and (e) an ordinary ACA sandbox cannot declare/reference ZoneLink, child-Zone, route-cursor, or ZoneLink-authority fields. The controller, not Nix, creates each same-Zone Provider-owned sandbox-agent Endpoint with the Guest as lifecycle `ownerRef`. No `User` resource or `users.users.*` declarations required — component principals are framework-assigned and not OS accounts. Gateway Guest NixOS closure includes only the two ACA component binaries (§15.6). Assertion error messages name the offending resource and the required `gatewayExecutionRef`; Zone-shaped ACA config fails `aca-sandbox-is-not-zone`.", + "integration" : "Nix eval gate; `d2b.zones.*.resources` validation pass; consumer flake usage example", + "removalProof" : "n/a — ongoing eval-time constraint", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-container-apps", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-container-apps.md", + "validation" : "Nix eval assertion tests (wrong `executionRef` → assertion fires; correct setup → passes; ACA ZoneLink/child-Zone fields → `aca-sandbox-is-not-zone`); Endpoint template ownership/shape and §15.7 assertion coverage tests", + "workItemId" : "ADR046-aca-007" + }, + { + "currentSource" : "packages/d2b-host/src/bin/d2b-activation-helper.rs", + "dataMigration" : "Full d2b 3.0 reset; no v2 activation-helper invocation compatibility", + "dependencyOwner" : "Provider/activation-nixos runner owner; reused helper owner in d2b-host", + "destination" : "packages/d2b-host/src/bin/d2b-activation-helper.rs", + "detailedDesign" : "Replace the helper CLI flag interface with structured JSON input and JSON output, accept bounded systemArtifactId and activationMode, resolve store path internally, emit bounded outcome code, write no resource metadata, and preserve the no-bash-fallback invariant. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt in place.", + "integration" : "activation-runner invokes the helper through the pre-opened activation portal and system-manager effect resources, then reports structured outcome to activation-nixos status.", + "removalProof" : "Legacy flag-based helper invocation is removed from activation-nixos paths once runner JSON protocol tests pass.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-activation-nixos", + "specPath" : "docs/specs/providers/ADR-046-provider-activation-nixos.md", + "validation" : "Unit tests for JSON protocol, bounded outcomes, no resource metadata writes, and no Command::new bash fallback.", + "workItemId" : "ADR046-activation-001" + }, + { + "currentSource" : "None — net-new v3 ResourceType; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 generation resource import", + "dependencyOwner" : "ADR-046-resource-object-model and ADR-046-core-controllers; d2b-contracts activation-nixos owner", + "destination" : "docs/reference/schemas/v3/activation-nixos.d2bus.org.NixosGeneration.json and packages/d2b-contracts/src/activation_nixos.rs", + "detailedDesign" : "Define JSON schema and Rust DTOs for activation-nixos.d2bus.org.NixosGeneration, with systemArtifactId as the only artifact reference, no store path fields, activationDetail as a closed status.resource enum absent from spec, and phase constrained to the common framework enum values. Primary reuse disposition: `create`. Preserved source-plan detail: net-new schema and DTOs.", + "integration" : "Resource API, resource store, Nix compiler, activation-nixos controller, and CLI projections consume the schema and DTOs.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-activation-nixos", + "specPath" : "docs/specs/providers/ADR-046-provider-activation-nixos.md", + "validation" : "Schema golden vectors, serde unknown-field rejection, phase enum tests, activationDetail-not-in-spec test, and no-store-path-in-spec-or-status test.", + "workItemId" : "ADR046-activation-002" + }, + { + "currentSource" : "Current top-level activation behavior in packages/d2b/src/lib.rs and hardlink-farm/store ownership split described in this dossier", + "dataMigration" : "Full d2b 3.0 reset; adopt mode records an existing active generation but does not import v2 controller state", + "dependencyOwner" : "ADR046-activation-002; activation-nixos controller owner", + "destination" : "packages/d2b-provider-activation-nixos/src/controller/", + "detailedDesign" : "Implement the reconcile loop for activation-nixos.d2bus.org.NixosGeneration: validate executionRef, systemArtifactId, and priorGenerationRef; dispatch one activation-runner EphemeralProcess with canonical startRoot=true shape; observe runner status; mark superseded generations; prune by retainedGenerations through the finalizer protocol; emit §12.3 metrics with fixed `mode`/`outcome` semantics and no Zone or resource-name-derived labels; never perform direct store-path operations, nix-collect-garbage, explicit VolumeGcRequest, raw argv composition, or store path writes to resources. Primary reuse disposition: `replace`. Preserved source-plan detail: replace top-level imperative activation flow with resource controller logic.", + "integration" : "Controller watches NixosGeneration resources through Zone resource API, creates activation-runner EphemeralProcesses, releases ownership references for Provider/volume-local, and writes bounded status.", + "removalProof" : "Direct hardlink-farm and garbage-collection calls from activation-nixos reachable paths are absent after controller and runner tests pass.", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-activation-nixos", + "specPath" : "docs/specs/providers/ADR-046-provider-activation-nixos.md", + "validation" : "Controller tests for retention, finalizer sequence, no TTL retention, no direct store ops, no store path in status, deleted event-only removal, runner shape, and a structural metric descriptor assertion that `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys are absent and a generation/Zone-name canary never enters labels.", + "workItemId" : "ADR046-activation-003" + }, + { + "currentSource" : "packages/d2b-host/src/bin/d2b-activation-helper.rs for helper invocation; runner process is net-new", + "dataMigration" : "Full d2b 3.0 reset; no v2 runner state import", + "dependencyOwner" : "ADR046-activation-003; activation-runner owner", + "destination" : "packages/d2b-provider-activation-nixos/src/runner/", + "detailedDesign" : "Implement target-local activation-runner worker that executes on NixosGeneration.spec.executionRef for Host and Guest targets using the same contract, reads private artifact-catalog.json through the integrity channel, resolves systemArtifactId to a store path in memory only, invokes d2b-activation-helper through structured JSON, executes target-local switch-to-configuration through typed helper dispatch with no raw exec or SSH, emits structured outcome JSON, and never outputs store paths. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new runner invoking adapted helper.", + "integration" : "Controller-created EphemeralProcess runs under Provider/system-minijail on the target execution context and returns outcome through the activation portal for status update.", + "removalProof" : "No old SSH or raw command fallback path exists in activation-runner after tests assert typed helper dispatch only.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-activation-nixos", + "specPath" : "docs/specs/providers/ADR-046-provider-activation-nixos.md", + "validation" : "Runner tests for artifact lookup, JSON helper invocation, Host and Guest target parity, no raw argv, no SSH, no store path in output, and terminal nonzero handling.", + "workItemId" : "ADR046-activation-004" + }, + { + "currentSource" : "packages/d2b/src/lib.rs top-level cmd_switch, cmd_boot, cmd_test, cmd_rollback, cmd_build, cmd_generations, cmd_gc, and cmd_migrate", + "dataMigration" : "Full d2b 3.0 reset; CLI command surface changes with no runtime state import", + "dependencyOwner" : "ADR-046-cli-and-operations; activation CLI owner", + "destination" : "packages/d2b/src/activation.rs", + "detailedDesign" : "Implement d2b activation build, switch, boot, test, rollback, adopt, generations, gc, and migrate subcommands, projecting ADR-046 CLI and Operations behavior and ensuring no store path, digest, or artifact catalog field appears in JSON or human output. Primary reuse disposition: `replace`. Preserved source-plan detail: replace with grouped CLI projection.", + "integration" : "d2b CLI dispatcher calls resource API and activation-nixos controller by creating or listing NixosGeneration resources; legacy top-level verbs are removed by ADR046-activation-007 after integration tests pass.", + "removalProof" : "Legacy top-level verb removal is gated on ADR046-activation-007 after the d2b activation integration matrix passes.", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-activation-nixos", + "specPath" : "docs/specs/providers/ADR-046-provider-activation-nixos.md", + "validation" : "CLI integration tests for subcommand parsing, authorization, resource creation/listing, rollback priorGenerationRef, gc ownership release, and output redaction.", + "workItemId" : "ADR046-activation-005" + }, + { + "currentSource" : "Current VM Nix configuration emits activation inputs implicitly; this item creates the explicit Provider and NixosGeneration resource emitter", + "dataMigration" : "Full d2b 3.0 reset; existing d2b.vms activation settings are reauthored as Zone resources rather than imported", + "dependencyOwner" : "ADR-046-nix-configuration; activation-nixos Nix owner", + "destination" : "nixos-modules/providers/activation-nixos.nix", + "detailedDesign" : "Emit Provider spec and activation-nixos.d2bus.org.NixosGeneration resources per target, flow retainedGenerations only through Provider.spec.config.retainedGenerations, reference systems by systemArtifactId only, omit store paths from all emitted resources, and avoid dedicated state-layout User or ComponentPrincipal because ProviderStateSet is empty. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new resource emitter adapted from existing Nix activation inputs.", + "integration" : "Nix compiler emits Provider and NixosGeneration resources plus private artifact catalog entries consumed by core configuration publication and the activation-nixos controller.", + "removalProof" : "Old implicit activation Nix paths are unused by activation-nixos once resource emitter parity tests pass.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-activation-nixos", + "specPath" : "docs/specs/providers/ADR-046-provider-activation-nixos.md", + "validation" : "Nix eval tests for Provider config, NixosGeneration shape, retainedGenerations source, no systemStorePath in bundle, no state Volume or state-layout principal, and artifact ID resolution.", + "workItemId" : "ADR046-activation-006" + }, + { + "currentSource" : "packages/d2b/src/lib.rs cmd_switch, cmd_boot, cmd_test, cmd_rollback, cmd_build, cmd_generations, cmd_gc, cmd_migrate and dispatcher registrations", + "dataMigration" : "Full d2b 3.0 reset; no command alias compatibility window", + "dependencyOwner" : "ADR046-activation-005; d2b CLI dispatcher owner", + "destination" : "packages/d2b/src/lib.rs", + "detailedDesign" : "Remove the legacy top-level activation command functions and their dispatcher registrations after the grouped d2b activation namespace passes integration tests. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete legacy top-level commands.", + "integration" : "CLI dispatcher routes only d2b activation subcommands for activation operations; documentation and tests use the new namespace.", + "removalProof" : "cmd_switch, cmd_boot, cmd_test, cmd_rollback, cmd_build, cmd_generations, cmd_gc, cmd_migrate and their registrations are deleted from packages/d2b/src/lib.rs.", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-activation-nixos", + "specPath" : "docs/specs/providers/ADR-046-provider-activation-nixos.md", + "validation" : "CLI integration matrix for d2b activation passes; grep or contract test confirms old cmd_* symbols and dispatcher registrations are absent.", + "workItemId" : "ADR046-activation-007" + }, + { + "currentSource" : "`packages/d2b-contracts/src/public_wire.rs`, `broker_wire.rs`; `d2b-daemon-access/src/lib.rs`; `d2b-realm-router/src/lib.rs`", + "dataMigration" : "None; v3 clean break", + "dependencyOwner" : "W0; resource API integrator", + "destination" : "`packages/d2b-contracts/proto/d2b-resource-v3.proto`, `packages/d2b-resource-api/src/service.rs`, `client.rs`", + "detailedDesign" : "Async methods, contexts, preconditions, limits, errors, status/finalizer separation, batch API Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "d2b-bus exact service → Zone auth → redb actor", + "removalProof" : "Old command/resource-equivalent paths removed only per integration wave", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resource-api-and-authorization", + "specPath" : "docs/specs/ADR-046-resource-api-and-authorization.md", + "validation" : "Protocol vectors; malformed/oversize/conflict/status-owner tests", + "workItemId" : "ADR046-api-001" + }, + { + "currentSource" : "`d2bd` public admission; `d2b-daemon-access` policy evidence; `d2b-realm-core/src/access.rs`, `audit.rs`", + "dataMigration" : "Generate initial Roles/Bindings from Nix v3 config", + "dependencyOwner" : "ADR046-api-001; authorization integrator", + "destination" : "`packages/d2b-resource-api/src/authz.rs`, `packages/d2b-core-controller/src/rbac.rs`", + "detailedDesign" : "Role/RoleBinding schemas/evaluator/cache/revision invalidation, canonical resource/session verb enums including ZoneLink-scoped `relay`, ComponentSession subject mapping, parent Zone access, and independent per-hop relay plus target-verb admission", + "integration" : "Every resource/runtime method invokes one native evaluator before structural checks", + "removalProof" : "Legacy auth remains until every v3 route is covered", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resource-api-and-authorization", + "specPath" : "docs/specs/ADR-046-resource-api-and-authorization.md", + "validation" : "Decision matrix/property tests; closed-enum and relay-origin/scope rejection; relay-missing and target-verb-missing fail-closed vectors; revocation/cache/outage/parent-child tests", + "workItemId" : "ADR046-api-002" + }, + { + "currentSource" : "`packages/d2b-core/src/audio_policy.rs` (all symbols); `packages/d2b-core/tests/audio_policy.rs`", + "dataMigration" : "Parse v1/v2 once; require exactly one configured owner Service; write grants plus `serviceRef`; fail closed on missing/ambiguous Service; remove prior file only after successful commit.", + "dependencyOwner" : "No prerequisites; `d2b-provider-audio-pipewire` crate", + "destination" : "`packages/d2b-provider-audio-pipewire/src/audio_policy.rs`; re-exported from crate root", + "detailedDesign" : "`LevelPercent`, `AudioGrant`, `AudioPolicyState`, `parse_audio_state`, `to_v2_bytes`, `AudioPolicyError` copy unchanged. `AudioPolicyState` is the canonical in-memory representation of `AudioBinding.spec.grants`. `parse_audio_state`/`to_v2_bytes` are used only once during first-activation migration from a prior v1/v2 on-disk file; there is no ongoing state file in v3. Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: `copy-unchanged` (no daemon imports; pure DTO library).", + "integration" : "First-activation migration uses `parse_audio_state` and writes grants only into a per-Guest AudioBinding that also names the explicitly configured same-Zone owner AudioService.", + "removalProof" : "`d2b-core/src/audio_policy.rs` deleted when no `d2bd` caller references it; confirmed by `cargo check --no-default-features`.", + "reuseAction" : "copy-unchanged", + "reuseSource" : "Same baseline paths", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "`tests/audio_policy.rs`: all existing tests from `d2b-core/tests/audio_policy.rs` plus AudioBinding spec serialization tests", + "workItemId" : "ADR046-audio-001" + }, + { + "currentSource" : "`packages/d2b-host/src/audio_argv.rs` (all symbols + tests); `tests/golden/runner-shape/audio-argv-minimal.txt`", + "dataMigration" : "No runtime state migration; argv template output is regenerated from the v3 component template, and live Process specs never store argv.", + "dependencyOwner" : "Depends on `ADR046-audio-001`; Process Provider template schema", + "destination" : "`packages/d2b-provider-audio-pipewire/src/argv.rs` (component template renderer)", + "detailedDesign" : "`generate_audio_argv` remains the canonical argv builder for the `vhost-user-sound-worker` component template. The resulting argv/env/executableRef are sealed into the LaunchTicket. The per-Guest binary copy path enforcement remains via the LaunchTicket verifier. The live Process resource spec contains no argv or executableRef. The `--socket` argument is removed; the vhost-user service identity is `Endpoint/corp-vm-audio-vhost-user`, while the backing locator is resolved into the LaunchTicket under authorization. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — argv builder retained; becomes a signed component-template projection, not a live Process spec field.", + "integration" : "The component template for `vhost-user-sound-worker` embeds the output of `generate_audio_argv`; the Process Provider resolves arg0 from the artifact catalog.", + "removalProof" : "`d2b-host/src/audio_argv.rs` deleted after `d2bd` has no callers; confirmed by `cargo check -p d2b-host`.", + "reuseAction" : "adapt", + "reuseSource" : "Same baseline paths", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "`tests/argv.rs`: rejection matrix (Nix store path, symlink, cross-guest copy, empty name); no-socket-in-argv assertion; no-argv-in-process-spec assertion", + "workItemId" : "ADR046-audio-002" + }, + { + "currentSource" : "`packages/d2bd/src/audio_host_controller.rs` `PipeWireHostController` enforcement logic; `QemuAudioController`", + "dataMigration" : "No state migration; mediator applies current AudioBinding grants and levels from resource state during reconcile, replacing host-controller direct writes.", + "dependencyOwner" : "Depends on `ADR046-audio-001`; ComponentSession service contract; libpipewire 1.x API", + "destination" : "`packages/d2b-provider-audio-pipewire/src/mediator/enforcement.rs`", + "detailedDesign" : "Owner AudioService only: speaker `SetGrant` maps `\"off\"` to `pw_node_set_param(SPA_PARAM_Props, mute=true, target.object=-1)` on the worker's node and `\"on\"` to `mute=false`; `SetLevel` maps to a bounded volume. Microphone `\"on\"` is applied only after the owner authority grants its single capture slot; queued/blocked consumers remain muted, and release mutes/disconnects before handoff. `captureAlias` resolves privately through the registry. A projection AudioService routes the operation to the remote owner over its import stream and is denied any local mediator/PipeWire open. `FakeAudioMediator` is the hermetic test double. No state file, wpctl, EphemeralProcess, or node ID in any external surface. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — enforcement logic becomes a libpipewire API implementation behind the `SetGrant`/`SetLevel` ComponentSession service.", + "integration" : "AudioBinding controller calls its resolved AudioService. Owner Service dispatches locally to AudioMediator; projection Service dispatches over the encrypted import route to the remote owner.", + "removalProof" : "`d2bd/src/audio_host_controller.rs` retired after `d2bd` audio dispatch path is replaced.", + "reuseAction" : "adapt", + "reuseSource" : "Same baseline paths", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "`tests/mediator.rs` and `tests/enforcement.rs`: owner-Service SetGrant/SetLevel round-trip; speaker mixing; microphone queued consumers remain muted; release/lease-expiry mute-before-handoff and no-overlap proof; projection routing with fake streams; projection-PipeWire-open denial; no-node-id-in-bus-message; ProviderSessionUnavailable; captureAlias registry resolution", + "workItemId" : "ADR046-audio-004" + }, + { + "currentSource" : "None (ADR-only); structured after `d2b-contracts/src/public_wire.rs` audio types", + "dataMigration" : "Full d2b 3.0 reset; owner Services and per-Guest AudioBindings are authored as new v3 resources; projection Services are core-generated from ResourceImport.", + "dependencyOwner" : "ADR046-provider-004 common audio Service/Binding base; Core resource-api foundation; `d2b-provider-audio-pipewire` crate", + "destination" : "`packages/d2b-provider-audio-pipewire/src/{resource_type,admission,provider_extension}.rs` (strict implementation extensions and binding only; common base lives under ADR046-provider-004)", + "detailedDesign" : "Bind the shared D098 `audio.d2bus.org.AudioService` and `audio.d2bus.org.AudioBinding` base schema versions/fingerprints from ADR046-provider-004 and define only strict audio-pipewire Provider extensions/admission. AudioService validates immutable `serviceRole`, same-Zone local Endpoint refs, owner-only D097 AuthorityDescriptor, projection-only `ownerRef: ResourceImport/`, Core-only projection creation, and projection `spec.provider` rejection. Initial-v3 owner admission requires speaker `multiplexed` and microphone `exclusive` and rejects multiplexed capture plus every consent/approval/priority placeholder. Owner Service status carries only bounded aggregate microphone active/request/Zone counts; Binding status carries only its own arbitration state. AudioBinding validates Guest ownership, required immutable same-Zone `serviceRef`, grants/levels/users, and forbids authority/export/projection semantics. Semantic authority/import/attachment observations stay under `status.resource`; implementation observations stay under `status.provider`. PipeWire fields are rejected from base spec/status. Register no provider-qualified or AudioState identifier and no serde/schema alias. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` provider-neutral closed enums; remove `AudioProviderKind` from base status; `ADR-only` for schema/admission.", + "integration" : "`Provider/audio-pipewire` signs and publishes implementation support for both neutral qualified ResourceTypeSchemas and strict provider-envelope schemas. Core import controller may create/delete only projection AudioService; ordinary resource API admission handles owner Services and AudioBindings.", + "removalProof" : "None — both ResourceTypes are net-new", + "reuseAction" : "adapt", + "reuseSource" : "`public_wire.rs` `AudioChannel`, `AudioEnforcementPosture`, `AudioErrorKind`, `AudioProviderKind`, `AudioSetApplied`; `AudioProviderKind` is removal evidence, not a base-status field", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "`tests/resource_type.rs`: consume the ADR046-provider-004 common fixtures/fingerprints; canonical minimal base without `spec.provider`; neutral qualified-name registration; both schema/status round-trips including bounded aggregate owner queue status and per-Binding arbitration state; clean-break rejection of provider-qualified names, every AudioState spelling, and all aliases; fake alternate-provider base conformance; strict base/provider unknown-field matrices; projection `spec.provider` rejection; D088 `status.resource`/`status.provider` placement; PipeWire fields only in strict provider envelopes/config; Service role/AuthorityDescriptor/ownerRef/Endpoint-locality rules; initial-v3 exclusive-mic/mixed-speaker schema and consent/approval/priority/concurrent-capture rejection; Core-only projection admission; AudioBinding required same-Zone serviceRef and Guest owner; immutable refs; out-of-range levels/users; explicit tests that AudioBinding cannot be exported or projected", + "workItemId" : "ADR046-audio-005" + }, + { + "currentSource" : "`packages/d2bd/src/audio_dispatch.rs` lines 250–end (dispatch ordering reference)", + "dataMigration" : "v1/v2 audio policy file migration is handled by ADR046-audio-001 before reconcile; the controller keeps no Provider state Volume and imports no additional runtime state.", + "dependencyOwner" : "Depends on `ADR046-audio-001` through `ADR046-audio-005`; core ResourceImport controller; system Process Provider; AudioMediator (`ADR046-audio-007`); GuestAudioAgent (`ADR046-audio-011`); no Provider state Volume under D087", + "destination" : "`packages/d2b-provider-audio-pipewire/src/controller/audio_service.rs`; `src/controller/audio_binding.rs`", + "detailedDesign" : "One controller binary registers deterministic handlers for the two neutral ResourceTypes, constrained to immutable `spec.providerRef: Provider/audio-pipewire`. Service handler watches AudioService, its ResourceImport owner and local Endpoints; owner semantics claim/revalidate D097, local mediator, and aggregate exclusive-mic queue state, while projection semantics bind only encrypted import streams and deny PipeWire. It cannot create/delete projection Service. Binding handler watches AudioBinding, same-Zone serviceRef, Guest/User, owned Process, and private Endpoints; creates the vhost-user worker, GuestAudioAgents, and private Endpoints, then calls the resolved Service and guest agents. It maps `Applied|Queued|MicQueueFull` into the closed conditions/status/phase, and release/delete/revocation cancels queue state before child teardown. A Service event enqueues serviceRef-indexed Bindings. A resource selecting another conforming Provider is ignored and cannot be status/finalizer-mutated. Neither handler uses broker/pidfd/EphemeralProcess/Volume/User operations or direct filesystem access. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — dispatch logic is the reference for step ordering only.", + "integration" : "Registered with Zone core as a controller under `Provider/audio-pipewire`.", + "removalProof" : "Supersedes `audio_dispatch.rs`; `d2bd` audio dispatch deleted after e2e parity test confirms", + "reuseAction" : "adapt", + "reuseSource" : "None directly; reconcile flow is new async controller", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "Fast hermetic `tests/audio_service_controller.rs`: neutral type/provider selection, foreign-provider ignore/deny, owner authority, bounded aggregate microphone status, projection ownerRef/import chain, core-only create/delete, projection no-PipeWire-open, revocation queue cancellation, and D091 propagation. `tests/audio_binding_controller.rs`: neutral type/provider selection, required same-Zone serviceRef, owner/projection dispatch, child Process/private Endpoint state machine, `Applied|Queued|MicQueueFull` status mapping, off/delete/revocation cancellation, grant changes, absence/failures/deletion. Conformance asserts no AudioBinding export/projection, no broker/pidfd/EphemeralProcess/Volume/User ops. ProviderDeployment integration remains fake-only and validates empty ProviderStateSet.", + "workItemId" : "ADR046-audio-006" + }, + { + "currentSource" : "`packages/d2bd/src/audio_host_controller.rs` PipeWire session access patterns (reference only)", + "dataMigration" : "No persisted mediator state migration; the service rebuilds its PipeWire node map from the registry on start and consumes current AudioBinding through controller calls.", + "dependencyOwner" : "Depends on `ADR046-audio-004`; ComponentSession service (ADR-046-componentsession-and-bus); libpipewire 1.x", + "destination" : "`packages/d2b-provider-audio-pipewire/src/mediator/mod.rs`; `src/bin/audio_pipewire_mediator.rs`", + "detailedDesign" : "Owner AudioService implementation only. Long-lived user-session Process maintains per-AudioBinding nodes under the single owner backing, receives the pre-opened local PipeWire portal FD, and exposes `SetGrant`/`SetLevel` through `Endpoint/audio-pipewire-authority`. It enforces the authority arbiter's single microphone slot and mute-before-handoff result while speaker nodes remain mixed. Projection Services never start/call a local mediator and cannot receive its FD. No EphemeralProcess, wpctl, remote Ref, or node identity in external surfaces.", + "integration" : "Second binary in the `d2b-provider-audio-pipewire` package. Registered as a user-session service under `Provider/audio-pipewire`.", + "removalProof" : "Supersedes `d2bd`'s `PipeWireHostController` direct session access; `d2bd` audio host controller deleted after e2e parity", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "`tests/mediator.rs`: owner-Service FD handoff and calls; captureAlias; node-id sealing; session-unavailable; concurrent speaker Guest isolation; exclusive microphone no-overlap and mute-before-handoff; teardown; projection Service cannot resolve mediator Endpoint or portal attachment", + "workItemId" : "ADR046-audio-007" + }, + { + "currentSource" : "`nixos-modules/components/audio/host.nix` and `guest.nix`", + "dataMigration" : "Full d2b 3.0 reset; legacy Nix audio options emit/deprecate to one owner AudioService plus per-Guest AudioBindings that reference it; no projection Service is authored.", + "dependencyOwner" : "Depends on `ADR046-audio-005`; Nix resource compilation framework; `ADR-046-nix-configuration`", + "destination" : "`nixos-modules/components/audio/v3-resource.nix`; `nixos-modules/components/audio/host-config.nix`; `nixos-modules/components/audio/guest-config.nix`", + "detailedDesign" : "`v3-resource.nix` emits only `audio.d2bus.org.AudioService` and `audio.d2bus.org.AudioBinding`, selected by `Provider/audio-pipewire`, with required same-Zone serviceRef. For projection identity, ResourceExport emits `resourceRef` plus canonical `serviceType`, `projectionSchemaFingerprint`, and `factoryFingerprint`; its Service retains the Endpoint. ResourceImport emits the matching three `expected*` fields. Projection Services are never authored: core materializes them from ResourceImport. Eval rejects every obsolete Export/Import field in the explicit rejection matrix, provider-qualified ResourceTypes, every AudioState spelling, aliases, PipeWire fields in neutral base spec/status, AudioBinding export/projection, non-exclusive microphone or consent/approval/priority/concurrent-capture fields, Service role/ownerRef/authority mismatches, cross-Zone Endpoint/service refs, and duplicate owner authority. Existing captureAlias stays in Provider config; guestUsers/group injection, runtime-audio derivation, host stream rules, and guest stack remain.", + "integration" : "`nixos-modules/default.nix` imports all three modules.", + "removalProof" : "`host.nix` and `guest.nix` kept as compat shims until v3 module deployed on all Zones", + "reuseAction" : "replace", + "reuseSource" : "Same", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "`tests/unit/nix/cases/audio-v3-resource.nix`: exact neutral type names; provider-qualified/AudioState/alias rejection; strict provider-field placement; owner Service and Binding round-trip; same-Zone serviceRef; exact canonical Export/Import Service type and both fingerprints; obsolete Export/Import field rejection; Export Endpoint-field rejection; projection core-only ownerRef chain; AudioBinding export/projection rejection; exclusive-mic/mixed-speaker and consent/approval/priority/concurrent-capture rejection; authority uniqueness; Endpoint locality; plus existing grants/users/captureAlias/deprecation/no-wpctl/no-audioFrontend assertions", + "workItemId" : "ADR046-audio-008" + }, + { + "currentSource" : "`packages/d2b-contract-tests/tests/minijail_audio_usbip.rs` audio section", + "dataMigration" : "None — test migration only; no runtime state.", + "dependencyOwner" : "Depends on `ADR046-audio-005`; `d2b-contract-tests` crate", + "destination" : "`packages/d2b-provider-audio-pipewire/tests/minijail_contract.rs` (provider-local); retain cross-bundle source greps in `d2b-contract-tests`", + "detailedDesign" : "Retain worker sandbox assertions and add role-sensitive attachment checks: an owner-Service worker may receive only the local AudioMediator attachment; a projection-Service worker receives only its same-Zone route Endpoint and can never receive a PipeWire FD. Binding-owned private Endpoint ownership/producerRef is explicit. All Service/Binding/Process/Endpoint serialized forms remain locator-free.", + "integration" : "Provider-local contract tests run in `d2b-provider-audio-pipewire`; retained cross-bundle greps in `d2b-contract-tests` ensure bundle-wide invariants still hold.", + "removalProof" : "The superseded duplicate shell validator `tests/minijail-validator-audio.sh` is deleted only after the successor Rust gate (`minijail_audio_usbip.rs` cross-bundle + provider-local `minijail_contract.rs`) is green and its removal-proof check passes; the `seccomp_policy_ref == \"w1-audio\"` assertion migrates to `spec.sandbox.seccompClass == \"audio-pipewire-worker\"` before removal. Cross-bundle Rust tests are retained.", + "reuseAction" : "adapt", + "reuseSource" : "Same", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "`cargo test -p d2b-provider-audio-pipewire -- minijail` must pass; existing cross-bundle tests must continue to pass", + "workItemId" : "ADR046-audio-009" + }, + { + "currentSource" : "`packages/d2bd/src/audio_dispatch.rs` audit call sites (redaction pattern reference)", + "dataMigration" : "No telemetry/audit data migration; v3 emits new closed-label OTEL/audit records after cutover and old audio_dispatch audit sites are removed.", + "dependencyOwner" : "Depends on `ADR046-audio-006`, `ADR046-audio-007`, `ADR046-audio-011`; `d2b-telemetry` lightweight emitter", + "destination" : "`packages/d2b-provider-audio-pipewire/src/telemetry.rs`", + "detailedDesign" : "Emit closed-label Service and Binding metrics plus post-commit audit. Service events distinguish only `owner|projection` and closed outcomes; microphone arbitration emits closed transition/outcome plus bounded aggregate counts only. Metrics expose active count `0|1`, aggregate queue depth, and closed arbitration state without Zone/Binding/handle/position labels. Events omit authority keys, import/export keys, remote identity, stream/session ids, endpoints, client timestamps, and queue entries. Enforcement metrics cover owner-local and projection-routed calls without exposing route identity. ProcessEffect audit remains Process Provider-owned.", + "integration" : "Audio controller and mediator call telemetry/audit emitters after commit or enforcement; d2b-telemetry exporter and policy_observability consume the resulting records.", + "removalProof" : "`audio_dispatch.rs` audit call sites deleted after cutover", + "reuseAction" : "adapt", + "reuseSource" : "Same; adapt redaction pattern", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "`tests/audio_telemetry.rs`: Service/Binding event separation, microphone aggregate metrics and transition audit, redaction of Zone/Binding/handle/position/timestamp, post-commit ordering, label cardinality, forbidden authority/import/stream/path fields, no ProcessEffect duplication", + "workItemId" : "ADR046-audio-010" + }, + { + "currentSource" : "`packages/d2b-guestd/src/audio_set.rs` (guestd wpctl dispatch — reference only)", + "dataMigration" : "No guest runtime state migration; GuestAudioAgent reconnects to guest PipeWire and applies current AudioBinding grants and levels on reconcile, replacing guestd wpctl dispatch.", + "dependencyOwner" : "Depends on `ADR046-audio-004`; ComponentSession service contract; libpipewire 1.x; system-systemd Process Provider for guest domain", + "destination" : "`packages/d2b-provider-audio-pipewire/src/guest_agent/mod.rs`; `src/guest_agent/enforcement.rs`; `src/bin/audio_pipewire_guest_agent.rs`", + "detailedDesign" : "Long-lived user-domain Process running in the Guest under the guest workload user's UID. One Process resource per entry in `AudioBinding.spec.guestUsers`; each named by opaque UID digest (`ag-`) and carrying label `audio-pipewire.d2bus.org/role: guest-audio-agent`. `userRef` is the corresponding `User/` Zone resource. Opens a PipeWire connection in the Guest's compositor session (same-UID, natural access). Exposes a typed `AudioSet` ComponentSession service through an owned `Endpoint/ag--audio-set` (vsock transport, Guest→Zone d2b-bus). `AudioSet(mic, speaker, speakerLevel, micGain)` applies changes via libpipewire API (`pw_node_set_param` with `SPA_PARAM_Props`, `pw_stream_set_control`) on the guest virtio-snd PipeWire node. No wpctl binary, no command path, no EphemeralProcess. Controller calls ALL active GuestAudioAgent instances in parallel for each grant change and aggregates failures. `FakeGuestAudioAgent` is a test double behind `#[cfg(test)]`. Primary reuse disposition: `adapt`. Preserved source-plan detail: `ADR-only` (new component; supersedes guestd wpctl dispatch path).", + "integration" : "Third binary in the `d2b-provider-audio-pipewire` package. Declared as GuestAudioAgent Process resources by the audio-binding-controller (one per guestUser; template: `guest-audio-agent`). System Process Provider (`Provider/system-systemd`) launches each inside the Guest under the respective guest workload user's UID.", + "removalProof" : "`d2b-guestd` wpctl audio dispatch path deleted after all Guests have GuestAudioAgent deployed and e2e parity test passes", + "reuseAction" : "adapt", + "reuseSource" : "`packages/d2bd/src/audio_host_controller.rs` libpipewire enforcement patterns (reference only)", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "`tests/guest_agent.rs`: AudioSet service call → libpipewire apply; mute/route/level; session-unavailable path; reconnect state restore; no wpctl binary; no command path; N-agent creation (one per guestUser); parallel call and aggregated failure", + "workItemId" : "ADR046-audio-011" + }, + { + "currentSource" : "None — net-new ADR 0046 cross-Zone sharing (D096)", + "dataMigration" : "None — full d2b 3.0 reset", + "dependencyOwner" : "ADR046-zone-control-019, ADR046-zone-control-020; audio Provider owner", + "destination" : "`packages/d2b-provider-audio-pipewire/src/share_adapter.rs`", + "detailedDesign" : "Implement signed `Provider/audio-pipewire` adapters only for canonical `serviceType: audio.d2bus.org.AudioService` when `spec.providerRef` selects this Provider. ResourceExport carries that `serviceType` plus the signed `projectionSchemaFingerprint` and semantic `factoryFingerprint`; ResourceImport carries the matching `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`. Export adapter admits only the owner Service. Its local authority Endpoint remains Service-owned and is never an Export field. Core creates/deletes the projection AudioService with `ownerRef: ResourceImport/`, `providerRef`, and semantic base/import fields but no `spec.provider`; routing derives from the signed local descriptor and ResourceImport record. The semantic factory fingerprint binds factory metadata plus projection-protocol version, never Provider/adapter identity, which the signed descriptor authenticates separately. The import adapter reconciles its semantic route and never creates AudioBinding or opens PipeWire. Per-Guest AudioBindings are ordinary consumer resources with same-Zone serviceRef. No provider-qualified type alias, FD/path/socket/remote Ref crosses a Zone. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (implement the signed audio export/import adapter).", + "integration" : "Core export/import controller (ADR046-zone-control-019); local projection lifecycle (ADR046-zone-control-020); ComponentSession bounded encrypted named streams", + "removalProof" : "Not applicable (new surface)", + "reuseAction" : "adapt", + "reuseSource" : "audio authority/mediator service (this dossier); `packages/d2b-provider/src/share_adapter.rs` `ExportAdapter`/`ImportAdapter` traits", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "Fast hermetic `tests/share_adapter.rs`: exact neutral AudioService `serviceType`; exact projection-schema/factory fingerprint match; explicit rejection of obsolete `endpointRef`, `exportedType`, `baseSchemaFingerprint`, `expectedType`, `expectedBaseSchemaFingerprint`, and `projectionType`; reject every Export Endpoint field, provider-qualified alias, and AudioBinding export/projection; accept owner AudioService export; Core-only projection creation/deletion; exact ResourceImport -> projection AudioService ownerRef chain with no `spec.provider`; semantic factory fingerprint unchanged by Provider/adapter identity mutation while signed identity authentication remains exact; projection never opens PipeWire; reconnect/revocation/D091 propagation with fake streams. Only `integration/real_stream.rs` exercises a real encrypted named stream.", + "workItemId" : "ADR046-audio-012" + }, + { + "currentSource" : "`packages/d2bd/src/audio_host_controller.rs` (`HostAudioController` trait, `PipeWireHostController::{from_audio_node,find_audio_node}`, `QemuAudioController`, `FakeHostController`); `packages/d2b-core/src/audio_policy.rs` (`LevelPercent`, `AudioGrant`, `AudioPolicyState`)", + "dataMigration" : "None — full d2b 3.0 reset; grants are authoritative in `AudioBinding.spec` (no state file).", + "dependencyOwner" : "Depends on `ADR046-audio-001`, `ADR046-audio-004`, `ADR046-zone-control-019`; audio Provider owner", + "destination" : "`packages/d2b-provider-audio-pipewire/src/authority.rs` (speaker mixer + mic arbiter); `AuthorityDescriptor` on owner `AudioService`", + "detailedDesign" : "Exactly one owner AudioService holds the real PipeWire connection and D097 AuthorityDescriptor. Projection Services and AudioBindings cannot carry it. Speaker streams remain multiplexed/mixed. Microphone capture has exactly one slot across owner and importing Zones. The arbiter keys requests by authenticated Zone plus route-scoped opaque consumer handle, permits one pending entry per handle, bounds pending entries to 16 per Zone and 64 total, uses owner-sequenced FIFO per Zone and round-robin across non-empty Zones, and ignores client timestamps/priority. Its 30-second active lease renews only while no other Zone waits; contended expiry mutes/disconnects, dequeues the next Zone, and atomically requeues a still-requesting old holder at its Zone FIFO tail. Off/delete/revoke/disconnect cancel idempotently. Queue entries are memory-only and restart rebuild fails closed with capture muted. Multiplexed capture and consent/approval/priority/concurrent-capture surfaces are rejected; a future spec must define a concrete consent authorization ResourceType and resource-API verb before concurrent capture exists. Core rejects duplicate owner Services before open and adopts by ownerProof. No new ProcessRole, broker path, or state file. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — the host controller becomes the single authority service; no daemon `Mutex`/state-file wrapping.", + "integration" : "Owner AudioService references `Endpoint/audio-pipewire-authority`; same-Zone AudioBindings and remote projection Services call it. Core authority index admits exactly one owner Service.", + "removalProof" : "`audio_host_controller.rs` daemon-side controller deleted after the authority service reaches parity; confirmed by `cargo check`.", + "reuseAction" : "adapt", + "reuseSource" : "Same baseline controller/policy symbols", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "Fast hermetic `tests/authority.rs`: AuthorityDescriptor accepted only on owner AudioService; Binding/projection rejection; duplicate conflict; multiplexed speaker mix/quota; one active mic across local/imported Zones; per-Zone FIFO and cross-Zone round-robin; one-entry-per-handle, per-Zone/total bounds and `MicQueueFull`; idempotent cancellation; contended 30-second lease; mute-before-handoff/no overlap; restart-muted rebuild; multiplexed-capture and consent/approval/priority/concurrent-verb rejection; ownerProof adoption; D091 drain/recycle with fake clock/FakeHostController", + "workItemId" : "ADR046-audio-013" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/stream.rs` (`StreamKind::{AudioPlayback,AudioCapture}` → `Capability::{AudioPlayback,AudioCapture}`, `StreamAuthz`, `StreamChannel` split-direction); `packages/d2b-realm-core/src/mux.rs` (credit-based flow); `packages/d2bd/src/guest_control_bridge.rs` (`audio_set_authenticated`/`audio_status_authenticated`, `GuestAudioSetRequest`/`GuestAudioStatus`)", + "dataMigration" : "None — full d2b 3.0 reset", + "dependencyOwner" : "Depends on `ADR046-audio-013`, `ADR046-zone-control-019`; audio Provider owner", + "destination" : "`packages/d2b-provider-audio-pipewire/src/streams.rs`", + "detailedDesign" : "Per-import audio frames flow only over bounded encrypted named streams: one stream with two `StreamChannel`s for playback/capture split direction and a single `StreamAuthz` (a consumer never opens two authz contexts to split direction), credit-based backpressure (a sender spends only receiver-granted credit), per-import session generation, cancel, and deadline. `StreamKind::AudioPlayback`/`AudioCapture` require `Capability::AudioPlayback`/`AudioCapture`. Playback streams may run concurrently; the owner activates capture frames for only its single granted opaque consumer handle, and revocation/disconnect cancels that Zone's active/pending requests before route teardown. No PipeWire FD/socket crosses a Zone; intermediaries see ciphertext. Guest audio calls (`audio_set`/`audio_status`) are issued to all active guests concurrently and results aggregated. Volume/gain (`LevelPercent`), queue identity/position, and node identity are redacted from audit/OTEL/logs. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — audio frames ride the existing ComponentSession named-stream credit machinery.", + "integration" : "Owner and projection AudioService adapters allocate per-import streams over ComponentSession; AudioBinding only consumes the same-Zone Service Ref; core routes encrypted records only.", + "removalProof" : "Not applicable (new surface)", + "reuseAction" : "adapt", + "reuseSource" : "Same baseline stream/mux/bridge symbols", + "specId" : "ADR-046-provider-audio-pipewire", + "specPath" : "docs/specs/providers/ADR-046-provider-audio-pipewire.md", + "validation" : "Fast hermetic `tests/streams.rs`: projection-Service ownerRef/import binding, split-direction single-authz stream, credits, generation isolation, cancel/deadline, concurrent playback, one active capture across imports, route loss cancels that Zone's active/pending capture, ciphertext-only intermediary, redaction. Only `integration/real_stream.rs` runs the slower real encrypted stream.", + "workItemId" : "ADR046-audio-014" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/audit.rs` (`AuditHash::parse`, `AuditChainLink::new`/`verify`, `AuditChainRecord{stream: AuditStreamKind, realm: RealmPath, node: NodeId}`, `AuditStreamKind::{Gateway,RemoteNode,Daemon}`, `AuditSinkHealth`, `AuditRetentionFloorStatus`); `packages/d2bd/src/daemon_audit.rs` (hash-chain append algorithm, `prev_hash`/`record_hash` SHA-256 pattern, daily segment files `daemon-events-YYYY-MM-DD.jsonl`, `DaemonEvent` additive contract); `packages/d2b-priv-broker/src/audit.rs` (`AuditWriteClass::{Privileged,Unprivileged}`, `AuditDropSummary`, `DEFAULT_AUDIT_WRITES_PER_SECOND = 4096`, O_APPEND CLOEXEC file open, `AuditDropWarningState`); `packages/d2b-gateway-runtime/src/audit_jsonl.rs` (`JsonlGatewayAudit`, `DEFAULT_GATEWAY_AUDIT_RETENTION_DAYS = 14`, `prune_old` rotation algorithm); `packages/d2b-priv-broker/src/ops/audit_op.rs` (`OpAuditRecord`, `SwtpmDirAudit`, `SwtpmDirResult`, `SwtpmMarkerResult`); `packages/d2b-realm-core/src/ids.rs` (`OperationId`, `CorrelationId`, `PrincipalId` — `PrincipalId` becomes `subject_digest`); `packages/d2b/tests/audit_contract.rs`; `packages/d2b-priv-broker/tests/broker_export_audit.rs`", + "dataMigration" : "v3 bootstrap; existing daemon/broker JSONL files not migrated", + "dependencyOwner" : "W0/W1a; audit crate owner", + "destination" : "`packages/d2b-audit/src/{hash_chain.rs,segment.rs,rate_limit.rs,record_types.rs,sink.rs,export.rs}`", + "detailedDesign" : "`d2b-audit` provides: typed record structs per class; canonical serialization with `zone` replacing `realm: RealmPath`; SHA-256 hash chain (extracted from `daemon_audit.rs`); segment writer (O_APPEND CLOEXEC, 64 MiB / UTC-midnight rotation); 30-day compaction (adapts `prune_old` from `JsonlGatewayAudit`); `AuditWriteClass::{Privileged,Standard,BestEffort}` (extends current `{Privileged,Unprivileged}`); rate-limit with privileged-never-dropped invariant; export iterator with inline hash-break reporting. `AuditStreamKind` re-versioned: `Daemon→Zone`, `Gateway→ZoneLink`, `RemoteNode→RemoteZone`. `AuditChainRecord` re-versioned: `{zone: String}` replaces `{realm: RealmPath, node: NodeId}`. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract unchanged: `AuditHash`, `AuditChainLink` from `d2b-realm-core/src/audit.rs`; copy hash-chain append algorithm from `daemon_audit.rs`; copy `AuditWriteClass`/rate-limit/rotation/prune from broker `audit.rs`; adapt `JsonlGatewayAudit` segment writer; adapt `OpAuditRecord` to `BrokerEffect` record class.", + "integration" : "Zone runtime, core-controller, Process Providers, broker effect bridge → `d2b-audit` sink; `d2b zone audit export` → export iterator", + "removalProof" : "`daemon_audit.rs`, broker `audit.rs`, `JsonlGatewayAudit` retired per-component after `d2b-audit` sink achieves parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "`audit_record_hash_chain`, `audit_record_schema` (no `realm`/`node` fields), `audit_segment_rotation`, `audit_rate_limit_privileged_never_dropped`, `audit_unavailable_blocks_privileged`", + "workItemId" : "ADR046-audit-001" + }, + { + "currentSource" : "`packages/d2b-priv-broker/src/ops/audit_op.rs` (`OpAuditRecord` structural pattern — operation, peer_uid, decision, result fields); `packages/d2b-realm-core/src/audit.rs::AuditEnvelope{principal: PrincipalId, scope: AuthorizationScope, decision: AuthzDecision}` (principal → v3 `subject_digest`)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-audit-001 + ADR046-store-001; store/authz owner", + "destination" : "`packages/d2b-resource-store-redb/src/audit.rs`, `packages/d2b-core-controller/src/authz_audit.rs`", + "detailedDesign" : "`ResourceMutation` records emitted by the store actor inside the write transaction before commit returns. The audit sink must durably fsync the audit record before returning the commit success (privileged durability class). `RBACChange` emitted by the authz handler in the same write transaction. `subject_digest` = SHA-256 of normalized canonical subject string from v3 `AuthenticatedSubjectContext` (ADR-046-componentsession-and-bus). Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `OpAuditRecord` structural pattern for `ResourceMutation` / `RBACChange` record classes; adapt `PrincipalId` → `subject_digest` derivation.", + "integration" : "Store write transaction → `d2b-audit` sink → fsync → commit result", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "Integration test: 100 mutations → verify hash-chained audit records with `zone` field, no `realm` field", + "workItemId" : "ADR046-audit-002" + }, + { + "currentSource" : "`packages/d2b-gateway/src/audit.rs` (`GatewayAuditEvent`, `GatewayAuditKind::{DisplaySessionOpenAdmitted,DisplaySessionOpenDenied,DisplaySessionRunning,DisplaySessionClosed}`, `GatewayAudit` trait, `NoopGatewayAudit`); `packages/d2b-realm-core/src/audit.rs::AuditEnvelope{realm, principal, scope, decision, trace}` (fields adapted to v3 `SessionConnect` record class)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-audit-001 + ADR046-session-001 + ADR046-bus-001; session/bus owner", + "destination" : "`packages/d2b-session/src/audit.rs`, `packages/d2b-bus/src/audit.rs`", + "detailedDesign" : "`SessionConnect` records emitted at handshake completion. `GatewayAuditKind::DisplaySessionOpenAdmitted/Denied` → `event=\"connect\"`, `authz_decision=\"allowed/denied\"`. `GatewayAuditKind::DisplaySessionRunning` → informational `ProcessEffect`. `GatewayAuditKind::DisplaySessionClosed` → `event=\"close\"`. `transport_class=zone_link` covers what the current `AuditStreamKind::Gateway` stream recorded for gateway-backed realm sessions. `RouteAdmission` records emitted at bus route resolution for denied routes. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `GatewayAudit` trait pattern for `SessionConnect` and `RouteAdmission` record classes; `NoopGatewayAudit` pattern reused for test sinks.", + "integration" : "Session engine and bus router → `d2b-audit` sink", + "removalProof" : "`NoopGatewayAudit` and gateway JSONL sink retired after gateway is on v3 resource API", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "Session connect/close/auth-failure audit tests; `GatewayAuditKind` → `SessionConnect` mapping test", + "workItemId" : "ADR046-audit-003" + }, + { + "currentSource" : "`packages/d2b/tests/audit_contract.rs` (`d2b audit --strict` returns 78; `auditResponse` relay; `authz-audit-requires-admin` denial; daemon-down exit 1 without bash fallback); `packages/d2b-priv-broker/tests/broker_export_audit.rs` (`export_audit_requires_admin_and_exports_op_audit_records`: admin-only, path-free, NDJSON `ExportBrokerAuditOk` shape, `peer_uid` field, `ApplyNftables` operation name in records)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-audit-001; CLI owner", + "destination" : "`packages/d2b/src/zone_audit.rs` (new `d2b zone audit export` subcommand); `packages/d2b/tests/zone_audit_contract.rs`", + "detailedDesign" : "`d2b zone audit export` opens segments read-only (shared flock), streams NDJSON to stdout, validates hash chain inline, reports breaks as inline error records, and invokes only `d2b.audit.v3.AuditService/Export` under the admin-only `audit-export` session verb (same `SO_PEERCRED`/Role check as current `ExportBrokerAuditOk`). The session grant provides no Zone resource authority. Assert no `realm`, `node`, `workload_id` fields in exported records. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt audit CLI contract test (daemon-down/exit behavior); adapt broker export test to new `zone` field and v3 record schema.", + "integration" : "`d2b` CLI → ComponentSession `d2b.audit.v3.AuditService/Export` with `sessionVerbs=[audit-export]` → `d2b-audit` export iterator → stdout", + "removalProof" : "`d2b audit` legacy command retained until `d2b zone audit export` covers all record classes", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "`export_audit.rs`: admin-only, hash break inline, no old field names (`realm`/`node`/`workload_id`), no path/argv in output, exit 0 on clean chain", + "workItemId" : "ADR046-audit-004" + }, + { + "currentSource" : "`d2bd/src/provider_registry.rs`: `AzureVmForbidden`, `AZURE_VM_IMPLEMENTATION_ID`; `d2b-realm-provider/src/provider.rs`: `InfrastructureProvider` (dead-reachable)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import. Existing registry sentinels are deleted only after the Provider resource model replaces them.", + "dependencyOwner" : "Provider contract owner", + "destination" : "`src/{lib.rs,config.rs,schema.rs,error.rs,effect/mod.rs}`", + "detailedDesign" : "Provider descriptor/manifest; `spec.config` schema; Guest spec.provider.settings schema; `AzureEffectPort` trait + `AzureOperationHandle`; `AzureVmError` enum; `SandboxSpec` with semantic classes; `BudgetSpec` with SI suffix memory fields; `restartPolicy` class/backoffBase/backoffMax; `networkUsage.allowEgress=false`; Endpoint ResourceType templates with name/transport/purpose Primary reuse disposition: `adapt`. Preserved source-plan detail: Extract and adapt; DELETE `InfrastructureProvider` after this Provider is operational.", + "integration" : "ProviderDeployment loads the descriptor/catalog and ResourceType schemas; Nix and Guest specs reference the provider settings; controller and EffectPort modules consume the shared config/error types.", + "removalProof" : "`InfrastructureProvider` deleted; `AzureVmForbidden` removed after Provider resource model replaces registry", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-virtual-machine.md", + "validation" : "Provider catalog; descriptor fingerprint; schema/conformance tests", + "workItemId" : "ADR046-azure-vm-001" + }, + { + "currentSource" : "`d2b-realm-provider/src/rate_limit.rs` (implemented-and-reachable)", + "dataMigration" : "No persistent data migration; in-flight ARM operation handles are new v3 status/core-ledger records and are re-derived or adopted on reconcile when absent.", + "dependencyOwner" : "ADR046-azure-vm-001", + "destination" : "`src/effect/{mod.rs,real.rs,fake.rs,rate_limit.rs}`", + "detailedDesign" : "`AzureEffectPort` async trait; opaque `AzureOperationHandle` (bounded bytes, no poll URL); real `azure_core`/`azure_mgmt_compute` impl; `FakeAzureEffectPort` for hermetic tests; ARM 429/503/409 handling Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy and adapt.", + "integration" : "Azure VM controller lifecycle/idempotency code calls `AzureEffectPort`; the real implementation talks to ARM in production and `FakeAzureEffectPort` drives hermetic lifecycle tests.", + "removalProof" : "Old `InfrastructureProvider` ARM simulation deleted after parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-virtual-machine.md", + "validation" : "`tests/lifecycle_hermetic.rs`; all ARM paths via `FakeAzureEffectPort`; no ARM URL in test assertions", + "workItemId" : "ADR046-azure-vm-002" + }, + { + "currentSource" : "`d2b-realm-provider/src/conformance.rs`; main `a1cc0b2d`: `d2b-provider-toolkit/src/reconciler_loop.rs`", + "dataMigration" : "Full d2b 3.0 reset; old WorkloadProvider lifecycle state is not imported. Existing ARM resources may be adopted by tag/idempotency checks during reconcile.", + "dependencyOwner" : "ADR046-azure-vm-001; ADR046-azure-vm-002; Guest ResourceType controller contract", + "destination" : "`src/controller/{mod.rs,lifecycle.rs,idempotency.rs}`", + "detailedDesign" : "Non-blocking reconcile: `start_*(...)` → persist `AzureOperationHandle` → `requeue-at`; `poll_lro` on subsequent ticks; controller as authorized `update-status` writer for Guest resources; finalizer held until ARM delete confirmed; top-level `phase`, `status.resource`, and Azure `status.provider.details.providerPhase` written atomically Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy/adapt main toolkit; adapt conformance shape.", + "integration" : "Zone core dispatches Guest resource events to the Azure VM controller; ResourceClient updates status/finalizers; `AzureEffectPort` starts, polls, and deletes ARM LROs.", + "removalProof" : "Old `WorkloadProvider::provision`/`deprovision` paths retired", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-virtual-machine.md", + "validation" : "`tests/lifecycle_hermetic.rs`; `tests/conformance.rs`", + "workItemId" : "ADR046-azure-vm-003" + }, + { + "currentSource" : "`d2b-realm-provider/src/types.rs`: `ProviderGuestdBootstrapContract` (implemented-and-reachable); main `a1cc0b2d`: `d2b-session/src/bootstrap.rs`", + "dataMigration" : "No v2 bootstrap state import; the new sealed recovery Volume is initialized on first v3 activation, and old vsock bootstrap material is retired at cutover.", + "dependencyOwner" : "ADR046-azure-vm-001; ComponentSession IKpsk2", + "destination" : "`src/controller/bootstrap.rs`; `src/bootstrap_svc/{mod.rs,admission.rs,enrollment.rs}`", + "detailedDesign" : "PSK generation; sealed PSK/admission/enrollment recovery material (ciphertext) in the controller's single guest-local sealed recovery Volume; `GrantBootstrapAdmission` typed bus call; IKpsk2 in bootstrap-svc; enrollment record; enrolled KK; the bootstrap-svc declares **no** state Volume (session state in process memory; obtains sealed PSK/admission from the controller only); the controller's sealed recovery Volume is an ordinary Volume resource created by core ProviderDeployment (before component Process start) from the controller's single `stateNamespaces` declaration with a Nix-preprovisioned `User/azure-vm-controller` layout principal; ARM operation/idempotency records live in the core Operation ledger and non-secret observed cloud phase lives in `Guest.status` (D087); controller does not own, create, or add Volume to exported ResourceTypes; it consumes its view dirfd only Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy/adapt main `BootstrapPsk`/`BootstrapAdmission`.", + "integration" : "Controller creates and seals recovery material in its state Volume, grants bootstrap admission over the bus, and bootstrap-svc performs IKpsk2 enrollment for Guest sessions.", + "removalProof" : "Old vsock bootstrap path removed at v3 cutover", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-virtual-machine.md", + "validation" : "`tests/bootstrap_hermetic.rs`; `tests/error_redaction.rs`", + "workItemId" : "ADR046-azure-vm-004" + }, + { + "currentSource" : "`d2b-realm-provider/src/credential.rs`: `AzureControlPlaneRef`, `OpaqueAzureRef`, `ManagedIdentityRef` (implemented-and-reachable)", + "dataMigration" : "No ambient credential migration; v3 requires ResourceType Credential/ManagedIdentityRef plus enrolled KK, and the old direct IMDS fallback is removed.", + "dependencyOwner" : "ADR046-azure-vm-003; Credential ResourceType; D055/D056", + "destination" : "`src/credential.rs`", + "detailedDesign" : "ARM credential via enrolled KK `AcquireToken`; zeroizing token handling; no ambient credential fallback; `credential-managed-identity` guest-agent placement Primary reuse disposition: `adapt`. Preserved source-plan detail: Retain `OpaqueAzureRef` directly; adapt credential acquisition to enrolled KK.", + "integration" : "Controller obtains ARM credentials through enrolled KK and the Credential ResourceType before EffectPort operations; the credential-managed-identity guest agent provides the token source.", + "removalProof" : "Old direct IMDS calls from controller removed", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-virtual-machine.md", + "validation" : "`tests/credential_hermetic.rs`; `tests/error_redaction.rs`", + "workItemId" : "ADR046-azure-vm-005" + }, + { + "currentSource" : "`d2bd/src/provider_registry.rs`: `NEXT_LIFECYCLE_OPERATION_ID: AtomicU64` (production-reachable)", + "dataMigration" : "Old `AtomicU64` operation IDs are not imported; v3 operations use deterministic keys, while missing handles are re-derived or adopted from ARM.", + "dependencyOwner" : "ADR046-azure-vm-003", + "destination" : "`src/controller/idempotency.rs`", + "detailedDesign" : "Deterministic ARM request ID derivation; `AzureOperationHandle` opaque persistence (no poll URL in state); ARM 409 adoption; finalizer held through async deletion Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt to deterministic per-Guest keys.", + "integration" : "Lifecycle controller stores deterministic request IDs and opaque handles in the core Operation ledger/status; restart recovery reads them before polling or adopting ARM operations.", + "removalProof" : "`AtomicU64` lifecycle op ID removed after all ARM callers migrate", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-virtual-machine.md", + "validation" : "`tests/idempotency.rs`; restart-recovery scenario", + "workItemId" : "ADR046-azure-vm-006" + }, + { + "currentSource" : "`nixos-modules/options-realms-workloads.nix`: `WorkloadProviderKind::ProviderManaged`", + "dataMigration" : "Full d2b 3.0 reset; old `d2b.realms..workloads.` config is replaced by v3 resource authoring with no automatic v2 config import.", + "dependencyOwner" : "ADR046-azure-vm-001; ADR-046-provider-state; ADR-046-nix-configuration", + "destination" : "`nixos-modules/` (Provider/Guest resource emitters); crate Nix build", + "detailedDesign" : "Nix `spec.config` shape; `controllerExecutionRef`/`networkRef` eval-time assertions; no Volume refs for data disks; `systemArtifactId=null` enforcement; the single controller sealed recovery Volume is an ordinary Volume resource created by core ProviderDeployment (not in Zone bundle; not operator-authored); the bootstrap-svc declares no state Volume; guest-local placement — reconciled by the Guest-local volume-local instance and expressed by `source.executionRef` = config gateway Guest; host MUST NOT hold ARM binding, admission, PSK, or operation state; ARM operation/idempotency records live in the core Operation ledger and non-secret observed cloud phase in `Guest.status` (D087); no virtiofs or host-to-guest attachment; manifest freezes guest-local with no fallback; controller does not create, own, or list Volume in exported ResourceTypes; `Provider/volume-local` is the sole Volume reconciler; controller consumes required view dirfd only; **the recovery Volume is `kind: state`, `persistenceClass: persistent`, `storageNeed: secret`, sealed via `sealingCredentialRef`, with nonzero `quotaBytes`, `quota.maxBytes`, `quota.maxInodes`, and `source.settings.sourcePolicyId`; `persistenceClass: ephemeral` and zero quotas are rejected**; it survives component/Provider restart and participates in upgrade/destroy/reset; full canonical Volume spec including `stateSchema`, `source`, `layout` with a Nix-preprovisioned `User/` principal (not ComponentPrincipal), `views`, `identityMarker`, `snapshotPolicy: null`, `retentionPolicy: null`; `sensitivityClass: private` and `volume-domain-mismatch` isolation enforced; canonical `SandboxSpec` fields with `namespaceClasses`/`capabilityClasses`/`seccompClass`/`noNewPrivileges`/`startRoot`/`environmentClass`/`readOnlyRoot`; `BudgetSpec` with SI suffix; `restartPolicy` class/backoffBase/backoffMax; Endpoint ResourceType templates with name/transport/purpose", + "integration" : "Nix emitters produce Provider, Guest, Volume, and Endpoint resource specs consumed by ProviderDeployment, `Provider/volume-local`, the Process Provider, and the Azure VM controller.", + "removalProof" : "`d2b.realms..workloads.` removed at v3 cutover", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-virtual-machine.md", + "validation" : "Nix eval tests; `make test-flake`; `make test-drift`", + "workItemId" : "ADR046-azure-vm-007" + }, + { + "currentSource" : "`d2bd/src/metrics.rs` (production-reachable)", + "dataMigration" : "No metrics/audit data migration; new OTEL/audit surfaces start at v3 cutover and the old Prometheus registry is retired.", + "dependencyOwner" : "ADR046-azure-vm-003; ADR-046-telemetry-audit-and-support", + "destination" : "`src/{telemetry.rs,audit.rs}`", + "detailedDesign" : "Closed metric labels; OTEL span attributes; audit durability classes; `azure-vm-deleted` appended post-commit; no ARM URI, ARM resource ID, or cloud endpoint in any telemetry surface Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt audit shape; replace Prometheus with d2b-telemetry emitter.", + "integration" : "Controller/error paths call telemetry and audit emitters after status commits; d2b-telemetry consumes the metrics/spans and policy_observability enforces redaction.", + "removalProof" : "`d2bd/src/metrics.rs` hand-rolled registry removed after observability-otel Provider integration", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-virtual-machine.md", + "validation" : "`tests/error_redaction.rs`; `d2b-contract-tests/tests/policy_observability.rs` updated", + "workItemId" : "ADR046-azure-vm-008" + }, + { + "currentSource" : "No existing Azure VM tests at baseline; fake/hermetic patterns from main `a1cc0b2d`", + "dataMigration" : "None — test-only work; no runtime state. Old mock tests are removed only after parity.", + "dependencyOwner" : "All ADR046-azure-vm-*", + "destination" : "`tests/`; `integration/`", + "detailedDesign" : "See §Test requirements Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy/adapt fake toolkit; write new tests.", + "integration" : "Provider crate tests, fake toolkit, and integration harness run under cargo/Layer-1 and validate all ADR046-azure-vm-* outputs together.", + "removalProof" : "Old `InfrastructureProvider` mock tests deleted after parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-azure-virtual-machine", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-azure-virtual-machine.md", + "validation" : "All tests pass", + "workItemId" : "ADR046-azure-vm-009" + }, + { + "currentSource" : "v3 `d2b-realm-router`, target resolver, CLI routing, operation router", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Sessions + resource API; bus owner", + "destination" : "`packages/d2b-bus/src/{router,registry,authorization,streams,operations}.rs`", + "detailedDesign" : "Exact service/resource routes, RBAC, pinned reverse route, cancellation, named stream bridge, no wildcard pub/sub Primary reuse disposition: `adapt`. Preserved source-plan detail: extract/adapt.", + "integration" : "Every ResourceClient/controller/Provider/CLI service", + "removalProof" : "Old direct dispatch branches removed only after route parity", + "reuseAction" : "adapt", + "reuseSource" : "Any useful main d2b-client/provider/session routing symbols named by implementation sub-items", + "specId" : "ADR-046-componentsession-and-bus", + "specPath" : "docs/specs/ADR-046-componentsession-and-bus.md", + "validation" : "Message isolation; closed session-verb enum including `relay`, `audit-export`, and `support-bundle`; exact diagnostic service/method binding with no implied resource grant; relay missing/target verb missing/provider self-assertion fail-closed vectors; named-target and nameless List/Watch selector/filter preservation at every hop; route/auth revocation; fairness; reconnect; no direct-store path", + "workItemId" : "ADR046-bus-001" + }, + { + "currentSource" : "`d2b-host/src/runtime_provider.rs`; `d2b-host/src/ch_argv.rs`; `d2bd/src/supervisor/dag.rs`", + "dataMigration" : "None (spike)", + "dependencyOwner" : "Provider toolkit / system-minijail; W1 spike owner", + "destination" : "`packages/d2b-provider-runtime-cloud-hypervisor/src/controller.rs`", + "detailedDesign" : "End-to-end: single Guest reconcile → synchronous dependency-readiness check via ResourceClient → VMM Process creation → guest-control health check in observe handler → status write. Uses fake bus/store/supervisor stubs from toolkit. Proves fast-path latency gates (≤5 ms hint, ≤20 ms VMM Process creation when all deps ready). No EphemeralProcess resources at any step. Primary reuse disposition: `adapt`. Preserved source-plan detail: Extract and adapt.", + "integration" : "Zone ResourceClient + system-minijail Process Provider + fake broker effect", + "removalProof" : "Not applicable (new crate)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-cloud-hypervisor.md", + "validation" : "Unit: reconcile state machine, fast-path latency, adoption/ambiguity, finalize ordering. Integration: end-to-end VMM boot with real KVM and guest-control session (requires `make test-host-integration`)", + "workItemId" : "ADR046-ch-001" + }, + { + "currentSource" : "`d2b-core/src/processes.rs`; `nixos-modules/processes-json.nix`; `d2b-priv-broker/src/ops/swtpm_dir.rs`; `d2b-host/src/swtpm_argv.rs`", + "dataMigration" : "v3 reset; no v2 process graph migration", + "dependencyOwner" : "Depends on `ADR046-ch-001` and `ADR046-network-005`; owner: cloud-hypervisor bootstrap-graph integration", + "destination" : "`packages/d2b-provider-runtime-cloud-hypervisor/src/bootstrap_graph.rs`", + "detailedDesign" : "Single owned VMM Process resource; synchronous ResourceClient dependency check (Device/kvm + all declared Devices, Networks, virtiofs Volumes); immediate Process creation when all deps ready; no EphemeralProcess resources; conditional net-VM Guest creation; per-dependency readiness tracking in reconcile loop. For each Network attachment the launch resolution carries only an opaque ref; network-local declares the semantic effect, the Core-owned `NetworkEffectPort` adapter performs `CreatePersistentTap → SetBridgePortFlags`, and `ProviderSupervisor` receives the connected CLOEXEC `OwnedFd` directly for LaunchTicket inheritance. The runtime controller receives no fd or broker operation. Primary reuse disposition: `replace`. Preserved source-plan detail: EXTRACT and REPLACE.", + "integration" : "Depends on `Provider/volume-virtiofs`, `Provider/device-tpm`, `Provider/device-kvm`, `Provider/network-local` ResourceType readiness, the ADR046-network-005 effect chain, and direct ProviderSupervisor LaunchTicket fd handoff", + "removalProof" : "`ProcessRole::CloudHypervisor`, `ProcessRole::Swtpm`, `ProcessRole::NetVm` variant callers deleted; `nixos-modules/processes-json.nix` VMM emitter deleted after parity", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-cloud-hypervisor.md", + "validation" : "Golden VMM Process spec vectors; dependency-ordering tests; parallel Guest tests (8 concurrent); net-VM creation tests; Device/kvm explicit-ref enforcement; tap launch routing proves `CreatePersistentTap → SetBridgePortFlags → ProviderSupervisor LaunchTicket`; fd-lifetime tests prove CLOEXEC, one inheritable child slot, and close-before-generation-fenced-delete on every failed launch; boundary tests prove the controller and bus payloads contain only opaque refs", + "workItemId" : "ADR046-ch-002" + }, + { + "currentSource" : "`d2b-host/src/ch_argv.rs::ChArgvInput`, `generate_ch_argv`; `tests/golden/runner-shape/cloud-hypervisor-argv-*.txt`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-ch-002; artifact catalog foundation", + "destination" : "`packages/d2b-provider-runtime-cloud-hypervisor/src/vmm_argv.rs`; `tests/vmm_argv_golden_test.rs`", + "detailedDesign" : "`VmmArgvInput` derived from validated `GuestSpec.spec.provider.settings`; kernel/initrd/rootfs paths resolved privately from artifact catalog at dispatch time; no path in spec/status; tap argv accepts only the declared child fd slot inherited from the sealed LaunchTicket, with no runtime-selected handoff mode or host tap name; golden tests for headless/q35/microvm/gpu/video/macvtap variants Primary reuse disposition: `adapt`. Preserved source-plan detail: COPY/ADAPT.", + "integration" : "ProviderSupervisor LaunchTicket resolution, including direct inheritance of the precreated connected tap fd", + "removalProof" : "`d2b-host/src/ch_argv.rs::generate_ch_argv` callers removed; the old network-handoff mode and tap-name branches have no v3 callers; old golden test files adapted", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-cloud-hypervisor.md", + "validation" : "Golden argv vectors matching `cloud-hypervisor-argv-*.txt` shapes with v3 adaptations; tap vector accepts only the declared LaunchTicket child fd slot; redaction test (no store path in Debug output)", + "workItemId" : "ADR046-ch-003" + }, + { + "currentSource" : "`nixos-modules/options-realms-workloads.nix`; `nixos-modules/options-vms.nix`; `nixos-modules/processes-json.nix`; `nixos-modules/store.nix`", + "dataMigration" : "`d2b.vms.` → `d2b.zones..resources.` documented in migration guide", + "dependencyOwner" : "ADR046-ch-002; nix-configuration foundation (`ADR046-identities-002`)", + "destination" : "`packages/d2b-provider-runtime-cloud-hypervisor/nix/` (Nix emitter); `nixos-modules/` option extension for `runtime-cloud-hypervisor` Guest schema", + "detailedDesign" : "`d2b.zones..resources.` with `type = \"Guest\"` and `spec.provider.settings` validated against signed Provider schema; `spec.systemArtifactId` top-level field; artifact catalog `type = \"nixos-system\"` enforced by rule 17; Guest-control `Endpoint` resource emitted without raw locator; `make test-drift` gate for schema/Nix drift Primary reuse disposition: `adapt`. Preserved source-plan detail: ADAPT and REPLACE.", + "integration" : "Zone resource bundle emission; private artifact catalog; `xtask gen-resource-nix-options` for auto-generated Nix option types", + "removalProof" : "`options-vms.nix`; `options-realms-workloads.nix` (LocalVm path); `nixos-modules/processes-json.nix` (VMM emitter); `nixos-modules/store.nix` removed after integration parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-cloud-hypervisor.md", + "validation" : "nix-unit eval tests: rule CH-1 through CH-4 + rules 1–17; golden resource bundle JSON (no store path); type-mismatch eval errors; raw locator rejection; `spec.systemArtifactId` at top-level in JSON (not in `spec.provider.settings`)", + "workItemId" : "ADR046-ch-004" + }, + { + "currentSource" : "`packages/d2bd/src/provider_shutdown.rs::GracefulVmShutdown`; `packages/d2b-host/src/runtime_provider.rs::RuntimeProvider::plan_guest_update`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-ch-001; ComponentSession/d2b-bus (`ADR046-session-001`)", + "destination" : "`packages/d2b-provider-runtime-cloud-hypervisor/src/health.rs`; `src/adoption.rs`", + "detailedDesign" : "Authenticated KK ComponentSession health check over vsock; adoption verification (pid/cgroup/executable/generation) within `adoptionWindow`; ambiguity → Unknown/Degraded, never broad kill; graceful shutdown via guest-control session before SIGTERM", + "integration" : "ComponentSession enrolled KK; guest bootstrap credential from `d2b-gctl` virtiofs share; `GuestReachable` condition write", + "removalProof" : "`ProcessRole::GuestControlHealth` observation path; `ProcessRole::GuestSshReadiness` deleted at cutover", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-cloud-hypervisor.md", + "validation" : "Fake guest-control server test; health check timeout/failure/retry; adoption property test (ambiguity, gone, stale pid); graceful shutdown ordering", + "workItemId" : "ADR046-ch-005" + }, + { + "currentSource" : "`packages/d2bd/src/metrics.rs` (`d2b_daemon_vm_*`); `packages/d2b-contract-tests/tests/policy_observability.rs`", + "dataMigration" : "`d2b_daemon_vm_*` metrics retired; consumers must update dashboards", + "dependencyOwner" : "ADR046-ch-001; telemetry foundation (`ADR046-telem-001`)", + "destination" : "`packages/d2b-provider-runtime-cloud-hypervisor/src/metrics.rs`; `src/audit.rs`", + "detailedDesign" : "`d2b_runtime_ch_*` metrics from §18.3 with closed semantic labels and no `vm`, `zone`, `zone_id`, `zone_uid`, or resource-name-derived key; bounded durable audit records from §17.3; no path/argv/socket in any field; identity retained in the closed OTEL resource-attribute allowlist extended per §18.4", + "integration" : "Zone lightweight bounded emitter; `Provider/observability-otel` forwarding", + "removalProof" : "Hand-rolled Prometheus registry (`d2bd/src/metrics.rs` `d2b_daemon_vm_*` section) deleted after migration", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-cloud-hypervisor.md", + "validation" : "`policy_observability.rs` updated with v3 allowlist; structural descriptor tests assert exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus Guest/Zone-name canary absence; bounded message/field tests; audit record schema golden vectors", + "workItemId" : "ADR046-ch-006" + }, + { + "currentSource" : "`packages/d2b-core/src/storage.rs` (`StoragePathSpec`, `SensitivityClass`) — to be retired", + "dataMigration" : "v3 reset; no v2 state storage migration", + "dependencyOwner" : "ADR046-ch-001; `ADR046-pstate-001` (common status types)", + "destination" : "`packages/d2b-provider-runtime-cloud-hypervisor/src/state.rs`; `packages/d2b-provider-runtime-cloud-hypervisor/tests/state_status_test.rs`", + "detailedDesign" : "`state.rs` owns the controller's bounded non-secret operational-state projection into the owning resource's `status` subresource (reconcile stage, per-Guest launch/adoption observations, bounded counters, closed-enum error detail) — the controller declares no Provider state Volume and mounts no `/state`; on restart it re-derives observed state from the Zone resource store, the core Operation ledger, and external observation (running VMM/virtiofsd re-adopted from cgroup leaves + fresh pidfds), treating `status` as observation, never authority (D087); status writes occur only on material change and stay within the status bounds. The superseded state-Volume integration, migration, validation, and removal rows are rejected: this Provider has no state Volume, state mount, or `StateEnvelope` startup path. Primary reuse disposition: `replace`. Preserved source-plan detail: REPLACE (storage.rs).", + "integration" : "The controller reads Volume/Device/Network dependency status through its ComponentSession/ResourceClient and writes its own bounded `status`; no Provider state Volume is provisioned or mounted", + "removalProof" : "`d2b-core/src/storage.rs` `StoragePathSpec` / `SensitivityClass` retired only after all Provider state consumers migrate to v3 status/optional-Volume helpers", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-cloud-hypervisor", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-cloud-hypervisor.md", + "validation" : "`state_status_test.rs` (hermetic): status projection round-trip and bound enforcement; restart re-derivation from store/ledger/external observation without a state Volume; no secret/path/argv/PID in status", + "workItemId" : "ADR046-ch-007" + }, + { + "currentSource" : "`packages/d2b/src/lib.rs`: `Context::from_env` (reads `D2B_PUBLIC_SOCKET` env or `/run/d2b/public.sock` default; single flat socket path — no zone-qualified path, no `ZoneContext`), `NativeCli`, `NativeCommand`, `dispatch`, `CliFailure`, `report_failure`, `stdout_is_tty`; socket path model: old `Context { public_socket, broker_socket }` → target: `ZoneContext { zone_name, socket_path, session_client }`", + "dataMigration" : "None; context discovery replaces env-var path lookups", + "dependencyOwner" : "ADR046-identities-001, ADR046-api-001; CLI crate owner", + "destination" : "`packages/d2b/src/lib.rs`, `packages/d2b/src/context.rs`, `packages/d2b/src/dispatch.rs`", + "detailedDesign" : "Introduce `ZoneContext` (zone name, socket path, ComponentSession client); implement `--zone`/`D2B_ZONE`/nearest-socket discovery using adapted `LocalUnixDaemonAccess::connect_component_session()` chain; introduce `ResourceRef` argument parser; introduce unified `--json`/`--human`/`--deadline` flag infrastructure bounded by `MAX_REQUEST_LIFETIME_MS=900s`; freeze `--json` schema version 1; stabilize exit code table. Excluded ADR 0045 assumptions: `TargetInput::Realm`, `TargetInput::Workload`, `TargetInput::Provider` variants; `RealmPath::parse(\"local-root\")` / `RealmId::derive` pattern; `RealmPath`-based service owner types.", + "integration" : "All command functions receive `ZoneContext`; resource API client (`Client`) is injected for testing", + "removalProof" : "Old `Context` struct removed only after all command functions use `ZoneContext`", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d` — copy/adapt these exact symbols: (1) `packages/d2b-client/src/client.rs` `Client`, `ConnectedClient`, `CallOptions`, `CancellationToken`, `MetadataInput`, `RetryPolicy`, `Response` — copy unchanged as the async client foundation; (2) `packages/d2b-client/src/host_socket.rs` `HostSocketConnector::from_seqpacket_fd`, `local_daemon_endpoint_identity` — adapt: replace fixed `d2b.daemon.v2` service lookup with zone-scoped service identity; replace hardcoded `RealmPath::parse(\"local-root\")` with `Zone/`-derived target; (3) `packages/d2b-daemon-access/src/component_session.rs` `LocalUnixDaemonAccess::connect_component_session()` connect chain — adapt: replace `TargetInput::LocalRoot(realm)` with the v3 Zone target variant; adapt socket path discovery for per-Zone paths; (4) `packages/d2b-client/src/session.rs` `ComponentSessionConnector`, `ConnectedSession`, `NamedStream`, `SessionCall`, `SessionReply`, `SharedDriver` — copy unchanged; (5) `packages/d2b-client/src/target.rs` `RouteTable`, `RouteRecord`, `TargetInput`, `ResolvedTarget`, `TransportKind`, `TransportSelection` — copy unchanged, excluding `TargetInput::Realm/Workload/Provider` variants which carry ADR 0045 assumptions; (6) `packages/d2b-contracts/src/v2_component_session.rs` `LimitProfile::local_default()` constants: `MAX_REQUEST_LIFETIME_MS=900000`, `LOCAL_HANDSHAKE_DEADLINE_MS=5000`, `MAX_RECONNECT_ATTEMPTS=10`, `MAX_ACTIVE_NAMED_STREAMS=128`, `MAX_LOGICAL_MESSAGE_BYTES=1048576`, `named_stream_queue_bytes=262144`, `aggregate_named_stream_queue_bytes=4194304` — copy unchanged; these bound every CLI deadline and stream operation", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "Zone-unavailable/fallback tests; ResourceRef parse/reject vectors; exit-code round-trip tests; TTY detection tests; adapt `client.rs:typed_routes_select_exact_transport_without_fallback` (line 1053), `connector_discovers_and_authenticates_the_driver_generation` (line 1254), `daemon_transport_rejects_ancillary_data_and_oversized_packets` (line 1312)", + "workItemId" : "ADR046-cli-001" + }, + { + "currentSource" : "`packages/d2b/src/lib.rs`: `cmd_vm_start`, `cmd_vm_stop`, `cmd_vm_restart`, `cmd_vm_status`, `cmd_vm_list`, `cmd_list`, `cmd_status`; wire types: `ListResponse { vms: Vec }`, `StatusResponse { entries: Vec }`, `VmLifecycleState` (old: Stopped/Starting/Booted/Running/Stopping/Restarting/Failed/Unknown) from `packages/d2b-contracts/src/public_wire.rs:2152,2158,2605`; `ListEntry.vm: String` = `WorkloadId`; `VmStatus.lifecycle.state: VmLifecycleState` → target Guest `phase` (Pending|Ready|Succeeded|Degraded|Failed|Deleted|Unknown; Starting/Stopping/Restarting → conditions/reasons); `WorkloadPublicSummary.execution_posture: WorkloadExecutionPosture` from `public_wire.rs:267` (carries `IsolationPosture`; unsafe-local entries have `IsolationPosture::UnsafeLocal`)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-cli-001, ADR046-api-001; CLI crate owner", + "destination" : "`packages/d2b/src/guest.rs` (`d2b guest start/stop/restart/list/status`); unsafe-local workloads go to `packages/d2b/src/host.rs` (`d2b host list/status/get`), NOT guest.rs", + "detailedDesign" : "Route Guest lifecycle (WorkloadProviderKind: LocalVm/QemuMedia/ProviderManaged) through `d2b.resource.v3` Get/UpdateSpec/Watch; map dry-run/apply to resource API precondition; `--no-wait-ready` exits on accepted; with-wait uses `d2b status --watch` loop. WorkloadProviderKind::UnsafeLocal entries MUST route to `d2b host` commands only; any code path that would return an unsafe-local entry from `d2b guest list` is a correctness violation. v2 commands (`d2b up/down/restart/list/status`, `d2b vm start/stop/restart/list/status`) are deleted at 3.0; replacement guidance remains documentation-only, with no migration-diagnostic command.", + "integration" : "ZoneContext → resource API client → Guest resource; status watch uses Watch stream", + "removalProof" : "Old `cmd_vm_start/stop/restart` seqpacket paths removed after Guest resource API paths are live with full test coverage", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/daemon_service.rs` `DaemonClient::lifecycle()` (line 210), `DaemonClient::list_workloads()` (line 148), `DaemonClient::inspect()` (line 179); `DaemonMethod::Apply/Start/Stop/Restart/ListWorkloads` variants (lines 31-46) — adapt: replace `WorkloadLifecycleRequest`/`WorkloadName` with `Guest/` ResourceRef; replace `TargetInput::Workload`-scoped calls with zone-root resource API calls; (2) `packages/d2b-contracts/src/generated_v2_services/daemon.rs` `WorkloadLifecycleProjection`, `DeploymentProjection`, `RuntimeProjection` — adapt field mapping to Guest resource spec/status; (3) `packages/d2b/src/lib.rs` `cmd_launch` (`LaunchArgs`) — adapt: the typed ComponentSession target resolution pattern applies but realm/workload-model types (`RealmPath`, `WorkloadName`) are excluded; behavior selected: idempotent apply with dry-run/apply precondition", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "Dry-run/apply/wait/no-wait-ready tests; zone-unavailable degraded path; JSON output schema tests; confirm v2 command paths are absent (compilation failure if any cmd_vm_start/stop alias re-introduced)", + "workItemId" : "ADR046-cli-002" + }, + { + "currentSource" : "`packages/d2b/src/exec_client.rs` (entire FSM); `packages/d2b/src/terminal_client.rs`; `packages/d2b/src/lib.rs`: `cmd_vm_exec`, `cmd_vm_exec_management`, `VmExecArgs`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-cli-001, ADR046-api-001; CLI crate owner", + "destination" : "`packages/d2b/src/exec.rs` (`d2b exec run/attach/wait/status/list/logs/kill`)", + "detailedDesign" : "Map EphemeralProcess resource lifecycle; `exec run` creates resource and returns ref; `exec attach` opens named stream via adapted `DaemonClient::open_terminal(DaemonMethod::Exec, ...)` → `DaemonTerminal`; retain full `exec_client.rs` FSM and TTY machinery from baseline; retain `--json` envelope fields `source`/`reason`/`guestExitCode`/`signal`/`transportExitCode`; retain reserved exit codes 42/69/70/75/76/77. v2 commands (`d2b vm exec *`) are deleted at 3.0; no dispatch wiring. Excluded ADR 0045: `GuestClient` vsock/guest-control proxy path; `TargetInput::Workload`; old `WorkloadName`-keyed exec management. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-then-adapt.", + "integration" : "ZoneContext → EphemeralProcess Create → named stream attach via `DaemonClient::open_terminal`", + "removalProof" : "Old `cmd_vm_exec` seqpacket path removed after `d2b exec` paths have equivalent coverage", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/daemon_service.rs` `DaemonClient::open_terminal(method, resource_id, operation_id, selection, options, cancellation)` returning `DaemonTerminal` (line 248) — copy-then-adapt: replaces the existing seqpacket Exec call; `DaemonMethod::Exec` (line 40) maps to `d2b exec run`; (2) `packages/d2b-contracts/src/generated_v2_services/terminal.rs` `TerminalOpenRequest`, `TerminalOpenResponse`, `TerminalStreamFrame`, `TerminalSelection`, `TerminalKind` — copy unchanged as the named-stream terminal wire protocol; (3) `packages/d2b-client/src/session.rs` `NamedStream` (`send`, `receive`, `cancel`, `close`, `is_terminal`) — copy unchanged; provides async stdio routing and cancel on disconnect; (4) `packages/d2b-client/src/daemon_service.rs` `GuestClient::inspect_exec()`, `cancel_exec()`, `open_exec_retained_log()` — adapt: rename from `WorkloadName`/`GuestClient` to `EphemeralProcess/` resource API; (5) `packages/d2b-session/src/streams.rs` `NamedStreamMux` limits (`MAX_ACTIVE_NAMED_STREAMS=128`, `named_stream_queue_bytes=262144`) — copy unchanged; bounds the exec stream pipeline; (6) `packages/d2b-session/src/cancellation.rs` `Cancellation`, `RequestRegistry` — copy unchanged; provides generation-bound per-request cancel; tests to adapt/import: `client.rs:terminal_uses_server_stream_and_validates_bidirectional_lifecycle`, `terminal_rejects_response_generation_and_non_server_stream_ids`, `invalid_terminal_selection_is_rejected_before_open_rpc`, `guest_exec_management_preserves_typed_state_and_cancel_correlation`, `guest_retained_log_open_binds_range_resource_and_selection`, `named_stream_fragments_over_queue_credit_and_has_terminal_actions`, `named_stream_grants_only_consumed_data_and_releases_blocked_sender`; excluded ADR 0045 assumptions: `GuestClient` internal `TargetInput::Workload`-scoped vsock routing (guest-control proxy uses old `WorkloadName`/`RealmPath` — these are excluded; v3 routes through resource API only)", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "Full `exec_client.rs` test suite migrated; adapted tests from main `client.rs:terminal_*` and `guest_exec_*`; TTY/raw-mode/RAII/signal tests; `--json` envelope/disambiguation tests; capacity/transport/auth/protocol exit-code tests; confirm v2 `cmd_vm_exec` path is absent", + "workItemId" : "ADR046-cli-003" + }, + { + "currentSource" : "`packages/d2b/src/lib.rs`: `cmd_shell` (`ShellArgs.vm: String` = `WorkloadId` or `RealmTarget`; routes through `route_vm_target()` → `VmTargetRoute::Local|Gateway`; gateway `Attach` fails closed via `shell_gateway_attach_failure()` with error class `gateway-shell-attach-unavailable` at lib.rs:1697,1780), `cmd_shell_attach`, `run_shell_fsm`, `ShellOwnerTransport`; wire: `ShellOp`, `ShellOpResponse`, `ShellAttachArgs { vm: String }`, `ShellListEntry`, `ShellSessionState` from `packages/d2b-contracts/src/public_wire.rs:1319,1394,1452,1409`; `exec_client.rs` signal/TTY machinery", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-cli-001, `shell-terminal` Provider dossier; CLI crate owner", + "destination" : "`packages/d2b/src/shell.rs` (`d2b shell open/attach/list/detach/kill/status`)", + "detailedDesign" : "Route ShellSession resource lifecycle through resource API using adapted `ShellService` generated types; `shell open` → `ShellCreate` → `DaemonClient::open_terminal(Shell)` → `DaemonTerminal`; retain FSM/TTY/signal/RAII behavior from `run_shell_fsm`; `--name` required for kill; SIGHUP detaches without kill. v2 commands (`d2b shell *`) are deleted at 3.0; no dispatch wiring. Excluded: gateway relay path (`VmTargetRoute::Gateway`); old `ShellOp`/`ShellOpResponse` seqpacket protocol; `TargetInput::Workload`-keyed realm routing. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-then-adapt.", + "integration" : "ZoneContext → ShellSession Create via `DaemonClient::open_terminal` → `NamedStream` I/O", + "removalProof" : "Old `cmd_shell` seqpacket path removed after new shell commands have equivalent coverage", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/daemon_service.rs` `DaemonClient::open_terminal(DaemonMethod::Shell, ...)` returning `DaemonTerminal` — copy-then-adapt; same `TerminalOpenRequest`/`TerminalOpenResponse`/`DaemonTerminal` flow as cli-003, applied to shell open/attach; (2) `packages/d2b-contracts/src/generated_v2_services/shell.rs` and `shell_ttrpc.rs` `ShellService` methods: `ShellCreate`, `ShellAttach`, `ShellDetach`, `ShellList`, `ShellInspect`, `ShellKill`, `ShellCancel` (service definition); `ShellCreateRequest`/`ShellAttachRequest`/`ShellListResponse`/`ShellInspectResponse` — copy-then-adapt: these are the target ShellSession resource CRUD wire types; adapt field names from `workload_id`/`shell_name` to `Guest/` ResourceRef; (3) `packages/d2b-client/src/session.rs` `NamedStream` — copy unchanged; used for shell I/O stream; (4) `packages/d2b-session/src/cancellation.rs` `Cancellation`, `RequestRegistry` — copy unchanged; (5) `packages/d2b-session/src/deadline.rs` `DeadlineBudget` — copy unchanged; shell sessions use per-operation deadline tracking; tests to adapt/import: `client.rs:shell_management_uses_typed_selection_result_and_terminal_outcome`, `named_stream_fragments_over_queue_credit_and_has_terminal_actions`, `named_stream_grants_only_consumed_data_and_releases_blocked_sender`, `concurrent_named_streams_route_events_without_cross_consumption`; excluded ADR 0045 assumptions: `VmTargetRoute::Gateway` shell routing and `realm_router` relay path; old `ShellOp`/`ShellOpResponse` seqpacket wire; unsafe-local helper shell protocol v2", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "Shell list/detach/kill/attach unit tests (adapted from existing); adapted `client.rs:shell_management_*` and `named_stream_*` tests; TTY RAII/signal tests; confirm v2 `cmd_shell` path is absent", + "workItemId" : "ADR046-cli-004" + }, + { + "currentSource" : "`packages/d2b/src/lib.rs`: `cmd_audio`, `cmd_clipboard_arm`, `cmd_vm_display`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-cli-001, Provider model spec; CLI crate owner", + "destination" : "`packages/d2b/src/provider.rs` (`d2b provider list/get/status/inspect`; dynamic projection loading)", + "detailedDesign" : "`d2b provider list/get/status/inspect`; InspectSchema call returns dynamic projection descriptor using `ConnectedClient::invoke` with generated provider service types; projection bounds enforcement (64 KiB, 32 sub-verbs, 2s deadline, shell-escape, newline strip); define the single canonical built-in command registry consumed by parser/help/completion and Provider projection binding; make bind-time rejection the sole authority for built-in and Provider/Provider top-level-name collisions, with no dispatch fallback; render projected commands from their declared top-level subcommand names; audio/clipboard/display are the first providers to migrate their projections", + "integration" : "ZoneContext → Provider resource + InspectSchema via `ConnectedClient::invoke`", + "removalProof" : "Built-in `cmd_audio`/`cmd_clipboard_arm`/`cmd_vm_display` removed only after Provider projection paths pass equivalence tests", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-provider/src/rpc.rs` `RpcProviderProxy` — adapt: the CLI uses the inverse side (client calling provider via `ConnectedClient::invoke`); `RpcCall`, `RpcPayload`, `RpcResponse`, `RpcOperation` define the typed call shape for dynamic provider commands; (2) `packages/d2b-contracts/src/generated_v2_services/provider_runtime.rs`, `provider_display.rs`, `provider_audio.rs`, `provider_infrastructure.rs` — adapt: the generated service method types show what CLI projection verbs can be mapped to typed service calls; use as shape reference for the first audio/clipboard/display migration; (3) `packages/d2b-provider-toolkit/src/conformance.rs` `check_provider_conformance`, `check_descriptor_conformance` — copy-then-adapt into CLI-side projection conformance validation (bounds: 64 KiB, 32 sub-verbs, 2s deadline, shell-escape, newline strip); (4) `packages/d2b-provider-toolkit/src/server.rs` `GeneratedProviderServiceServer::generated_services()` — server-side only; use as reference for what CLI InspectSchema receives; excluded ADR 0045 assumptions: `ProviderRegistry`/`ProviderAgentAdapter` are server-side and not used in CLI; `RpcProviderProxy` internal `AuthenticatedProviderRpc` pattern is server-side; tests to adapt: `conformance.rs:every_axis_passes_identical_in_process_and_rpc_conformance`, `generated_server_dispatches_closed_methods_over_authenticated_session`", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "Projection size/name/timeout bounds tests; registry-completeness test asserting exact equality with all built-in parser commands; bind-time built-in and Provider/Provider collision rejection tests; negative test proving no collision fallback dispatch exists; audio/clipboard/display projection conformance tests asserting `d2b audio ...`, `d2b clipboard ...`, and `d2b display ...` rendering; completion script safety tests; adapted `conformance.rs` tests", + "workItemId" : "ADR046-cli-005" + }, + { + "currentSource" : "None (no completion exists in v3 baseline)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-cli-001, ADR046-routing-012, ADR046-routing-016; CLI crate owner", + "destination" : "`packages/d2b/src/complete.rs` (`d2b complete bash/zsh/fish`)", + "detailedDesign" : "`d2b complete ` emits completion script; uses clap `CommandFactory::command()` plus dynamic projection fetch (2s per-Provider, 10s total); result bounded at 256 KiB; shell-escaped; newlines stripped Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged (clap_complete).", + "integration" : "Standalone command; no Zone API required for static completion; Zone API used for dynamic Provider projection", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "Optional: clap_complete crate (version to be pinned); no main-branch source", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "Completion script tests (bash/zsh/fish syntax valid); projection injection safety tests; deadline/partial-Provider tests", + "workItemId" : "ADR046-cli-006" + }, + { + "currentSource" : "`packages/d2b/src/lib.rs`: `cmd_build`, `cmd_generations`, `cmd_switch`, `cmd_boot`, `cmd_test`, `cmd_rollback`, `cmd_gc`, `cmd_migrate`, `cmd_keys_*`, `cmd_keys_trust`, `cmd_keys_rotate_known_host`, `ConfigCommand` variants", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-cli-001; CLI/activation Provider owner", + "destination" : "`packages/d2b/src/activation.rs` (`d2b activation build/generations/switch/boot/test/rollback/gc/migrate/keys/trust/rotate-known-host/config`)", + "detailedDesign" : "Route through `activation-nixos` Provider service via `ConnectedClient::invoke` using adapted `DaemonMethod::Apply`/lifecycle dispatch pattern; retain dry-run/apply; retain guest-control transport for config sync (no SSH). v2 top-level activation commands (`d2b build/switch/boot/test/rollback/gc/migrate/keys/trust/rotate-known-host/config`) are deleted at 3.0; no dispatch wiring.", + "integration" : "ZoneContext → activation-nixos Provider service → resource API via `ConnectedClient::invoke`", + "removalProof" : "Old top-level activation verbs removed only after `d2b activation *` paths have equivalent coverage", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/daemon_service.rs` `DaemonClient::lifecycle()` with `DaemonMethod::Apply`, `DaemonMethod::Start`, `DaemonMethod::Stop`, `DaemonMethod::Restart` — copy-then-adapt: the apply/lifecycle dispatch pattern maps cleanly to `d2b activation switch/boot/test/rollback`; retain idempotency token and dry-run/apply precondition from `DaemonMethod::Apply`; (2) `packages/d2b-contracts/src/generated_v2_services/activation.rs` activation service method types — adapt: map `ActivationBuildRequest`, `ActivationSwitchRequest`, `ActivationGenerationsRequest` to typed CLI args; excluded ADR 0045 assumptions: `DaemonMethod::ListRealms` / `DaemonMethod::ListWorkloads` are not used; old `WorkloadName`-keyed dispatch is excluded", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "All existing switch/boot/test/rollback/keys tests adapted; config sync/diff/approve/reject tests; confirm v2 top-level activation paths are absent; adapted `client.rs:daemon_typed_list_preserves_projection_and_truncation` apply pattern", + "workItemId" : "ADR046-cli-007" + }, + { + "currentSource" : "`packages/d2b/src/lib.rs`: `cmd_host_check`, `cmd_host_prepare`, `cmd_host_destroy`, `cmd_host_doctor`, `cmd_host_install`, `cmd_host_reconcile`, `cmd_host_validate`; `host_validate.rs`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-cli-001; CLI crate owner", + "destination" : "`packages/d2b/src/host.rs` (all `d2b host` subcommands)", + "detailedDesign" : "Route `host prepare/destroy` through Zone resource API Host reconcile operation via `ConnectedClient::invoke`; retain broker-mediated mutation and ownership-marker semantics; `host doctor` prefers Zone resource API status, falls back to local state files; `host check` retains exit-code 3; `host validate` retains wave/evidence-dir/scripts-dir/signature", + "integration" : "ZoneContext → Host resource; broker op path retained for emergency/shutdown-hook", + "removalProof" : "Raw broker-socket paths removed only after Host resource API routes have equivalent coverage", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-daemon-access/src/component_session.rs` `LocalUnixDaemonAccess::connect_component_session()` connect chain — adapt: the zone-local host commands use the same connect chain as cli-001; `d2b host prepare` and `d2b host doctor` both require a live `ZoneContext`; (2) `packages/d2b-client/src/client.rs` `ConnectedClient::invoke()` with `CallOptions` and `CancellationToken` — copy unchanged; used for Host resource Get/UpdateSpec/Status calls and broker-op dispatch; (3) `packages/d2b-contracts/src/generated_v2_services/broker.rs` broker operation request/response types — adapt: `BrokerHostPrepareRequest`, `BrokerHostDestroyRequest`, `BrokerHostDoctorRequest` (or equivalent) types define the CLI argument shape; retain broker-mediated ownership-marker semantics from baseline; excluded ADR 0045 assumptions: `TargetInput::Workload`-scoped broker routing is excluded; broker operation routing uses zone-root LocalRoot pattern only", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "All existing host-check/prepare/destroy/doctor/install/reconcile/validate tests; exit-code 3 regression; doctor Zone-fallback/local-state-fallback tests", + "workItemId" : "ADR046-cli-008" + }, + { + "currentSource" : "`packages/d2b/src/lib.rs`: `cmd_realm_list` (reads static `realm-entrypoints.json` via `realm_policy_rows_raw()`), `cmd_realm_inspect`, `cmd_realm_enter` (→ `realm_gateway_exec_args` → `cmd_vm_exec` with `-it bash -l`), `cmd_realm_run` (→ `cmd_vm_exec` with caller argv); wire output types: `RealmListOutputV1 { realms: Vec }`, `RealmInspectOutputV1 { realm: RealmPolicyOutputV1 }` from `packages/d2b-contracts/src/cli_output.rs:285,292,345`; `RealmPolicyOutputV1` fields: `realm` (= `RealmId`), `mode`, `gateway_vm`, `gateway_target`, `gateway_state`, `cross_realm_policy`, `credential_boundary`; `target_routing.rs`: `Route::Local { vm }`, `Route::Gateway { gateway, target }`, `resolve_access_route()`, `VmTargetRoute`; `d2b-realm-router::RealmEntrypointTable`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-cli-001; CLI crate owner", + "destination" : "`packages/d2b/src/zone.rs` (`d2b zone get/list/status`)", + "detailedDesign" : "`d2b zone get []` fetches the current Zone self resource only for the local name; another declared name is inspected through the read-only Zone topology projection. `d2b zone list` lists sorted compiler-only `{ childZone, parentZone }` rows joined with authenticated route/projection status. `d2b zone status --watch` watches the corresponding projection revision. None of these commands lists or gets a parent-local ZoneLink: the sole ZoneLink resource and handler are child-local, while the parent owns only sealed allocator topology and authenticated route state. Generated DTO/golden tests reject ZoneLink names/spec/status/provider refs/fingerprints/transport settings in parent-side output. v2 commands (`d2b realm list/inspect/enter/run`) are deleted at 3.0; no dispatch wiring. Excluded ADR 0045: `RealmServiceServer`/`RealmServiceProcess` multi-realm service; `RemoteNodeRegistration` constellation routing; `TargetInput::Realm`; `RealmMethod::ResolveRoute`/`AuthorizeShortcut`/`RevokeShortcut`.", + "integration" : "ZoneContext → local Zone self-resource Get or `ZoneService` topology/route projection via `ConnectedClient::invoke`", + "removalProof" : "`cmd_realm_*` and `target_routing.rs` removed only after zone routes pass equivalence tests", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d` — reference only (no copy): `packages/d2b-realm-router/src/service_v2.rs` `RealmServiceServer`, `RealmServiceProcess`, `RealmMethod::Inspect`, `RealmMethod::ResolveRoute` — server-side multi-realm routing; this is the ADR 0045 multi-Zone topology and is **excluded** from v3 CLI as a direct reuse source; `packages/d2b-realm-router/src/remote_node.rs` `RemoteNodeRegistration`, `RemoteNodeEntry` — constellation remote routing; also excluded; note: `packages/d2b-client/src/daemon_service.rs` `DaemonClient::list_workloads()` and `DaemonMethod::ListRealms` are the closest live list-call patterns, but their zone/workload scoping uses `RealmPath`/`RealmId` types that are ADR 0045-specific; adapt `ConnectedClient::invoke()` with the v3 `ZoneService` topology-projection request instead; no main symbols are copied unchanged for cli-009; the projection type is an ADR-only deliverable owned by zone routing", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "Zone self-get plus topology list/get/status/watch tests; disconnected and stale authenticated-route projections; golden output contains `{ childZone, parentZone }` and route/projection status but no ZoneLink fields; parent store has no ZoneLink row or watch; confirm v2 `cmd_realm_*` paths are absent", + "workItemId" : "ADR046-cli-009" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-cli-001; CLI crate owner", + "destination" : "`packages/d2b/src/resource.rs` (standard `d2b get/list/watch/create/update-spec/delete/status` top-level verbs)", + "detailedDesign" : "Generic typed dispatch to resource API Get/List/Watch/Create/UpdateSpec/Delete using `ConnectedClient::invoke` (Get/List/Create/UpdateSpec/Delete) and `ConnectedClient::open_server_stream` + `NamedStream` (Watch); ResourceRef argument parsing and validation; page token pagination; `--phase`/`--label-selector` filters; `--deadline` bounded by `MAX_REQUEST_LIFETIME_MS=900s` via `DeadlineBudget`; Watch output streams resource events as JSON lines; JSON schema version field; `CancellationToken` wired to process signal handlers. Excluded: `GuestClient` vsock exec/shell routing; `TargetInput` realm/workload/provider variants. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-then-adapt.", + "integration" : "ZoneContext → `ConnectedClient` → resource API", + "removalProof" : "Not applicable (new surface)", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/client.rs` `ConnectedClient::invoke()`, `ConnectedClient::invoke_with_attachments()`, `ConnectedClient::open_server_stream()` — copy unchanged; these are the three primitives for resource Get/List/Watch respectively; (2) `packages/d2b-client/src/session.rs` `NamedStream` (`send`, `receive`, `cancel`, `close`, `is_terminal`) — copy unchanged; Watch stream output arrives over a named stream; (3) `packages/d2b-session/src/deadline.rs` `DeadlineBudget` — copy unchanged; `--deadline` flag maps to `DeadlineBudget::admit_metadata` wall deadline; `MAX_REQUEST_LIFETIME_MS=900000` caps all Watch/List deadlines; (4) `packages/d2b-client/src/client.rs` `CancellationToken::cancel()` — copy unchanged; `SIGINT`/SIGTERM → `CancellationToken::cancel()` → propagated to `ConnectedClient::invoke` and `NamedStream`; (5) `packages/d2b-client/src/client.rs` `MetadataInput`, `RetryPolicy`, `CallOptions` — copy unchanged; `--idempotency-token` maps to `MetadataInput`; `RetryPolicy::mutating_once()` is the default for Create/UpdateSpec/Delete; tests to adapt/import: `client.rs:metadata_retries_and_cancellation_use_canonical_driver`, `mutating_retries_require_stable_idempotency`, `concurrent_named_streams_route_events_without_cross_consumption`, `named_stream_grants_only_consumed_data_and_releases_blocked_sender`; excluded ADR 0045 assumptions: `TargetInput::Workload/Realm/Provider` routing variants; `GuestClient` cross-realm proxy routing; old `DeploymentProjection`/`RuntimeProjection` ADR 0045-specific field types", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "Get/list/watch/create/update-spec/delete tests per ResourceType; pagination/filter/watch-deadline tests; error-class/exit-code tests; adapted `client.rs:metadata_retries_*` and `mutating_retries_*` and `concurrent_named_streams_*` tests", + "workItemId" : "ADR046-cli-010" + }, + { + "currentSource" : "Nix emitters: `nixos-modules/options-realms-workloads.nix` (current `d2b.envs..vms..*`), `nixos-modules/options-realms.nix` (`d2b.realms.*`), `nixos-modules/unsafe-local-workloads-json.nix` (unsafe-local source), `nixos-modules/bundle-artifacts.nix`, `nixos-modules/manifest.nix`, `nixos-modules/assertions.nix`; JSON output: `/etc/d2b/processes.json` (old bundle), `/etc/d2b/realm-entrypoints.json` (static realm index); Zone runtime apply path: `packages/d2bd/src/` (activation apply handler — pre-ADR 0046 path through `cmd_host_prepare`/broker; no live resource bundle apply); cleanup: no current resource-deletion-on-bundle-apply path at baseline", + "dataMigration" : "Full reset from current manifest/processes/realm-entrypoints JSON format; prior Nix-generated artifacts (`/etc/d2b/processes.json`, `/etc/d2b/realm-entrypoints.json`) deleted after Zone resource bundle activates", + "dependencyOwner" : "ADR046-identities-002, ADR046-cli-001, ADR046-cli-002, ADR046-cli-007, ADR046-nix-014; Nix module owner + Zone runtime owner; `nixos-modules/assertions.nix` base migration precedes this item's unified-resource update", + "destination" : "Nix: `nixos-modules/options-zones.nix` (unified `d2b.zones..resources` attrset; per-type `spec` sub-options generated from ResourceTypeSchema/Provider schema), `nixos-modules/bundle-emit.nix` (canonical JSON emit + SHA256 pin), `nixos-modules/assertions.nix` (updated); core controller: `packages/d2b-core-controller/src/configuration.rs`, `packages/d2b-core-controller/src/cleanup.rs`; Contracts: `packages/d2b-contracts/src/zone_bundle.rs` (new)", + "detailedDesign" : "**Nix shape:** `d2b.zones..resources` is `attrsOf (submodule { type; optional metadata { ownerRef; labels; annotations }; spec })`. `spec` sub-options per `type` are generated from ResourceTypeSchema and signed Provider schemas; field names remain identical. `metadata.name`/`metadata.zone`/`apiVersion` are derived; status and all core metadata are rejected in input. Vendor-qualified types are admitted only when their schema is installed. **Nix emit:** `bundle-emit.nix` emits `/etc/d2b/zones//resource-bundle.json` plus its integrity pin with canonical resource ordering and schema digests. **Core-controller apply:** `configuration.rs` verifies bundle/catalog integrity, applies Create/Update/no-op intents with bounded async concurrency, refreshes `configurationGeneration` for unchanged configuration-managed resources without waking their controller, handles controller/API name collisions per-item without seizing them, and asynchronously deletes only persisted `managedBy=configuration` resources absent from the new configured set. `cleanup.rs` consumes `Deleted` revision watches and maintains `PendingCleanup`; it never force-removes finalizers. **Prior generation retention:** `d2b.zones..retainedGenerations`, default 3 and range 1–16, is a compiler setting outside `Zone.spec`; no TTL. Rollback reapplies a retained bundle as a new higher generation.", + "integration" : "Nix build → per-Zone `resource-bundle.json` + global private artifact catalog → `d2b activation switch` → `d2b-core-controller` configuration service → resource API Create/Update/Delete → owner controllers → finalizer cascade → cleanup watcher → Zone status update", + "removalProof" : "Old `nixos-modules/manifest.nix`, `nixos-modules/bundle-artifacts.nix` emitters removed only after `bundle-emit.nix` produces equivalent-or-superseding output and all downstream consumers of the old bundle format are migrated", + "reuseAction" : "replace", + "reuseSource" : "None (new implementation; no main `a1cc0b2d` reuse — this is the Nix/Zone side, not the CLI client side)", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "Runtime integration: all CLI-visible cleanup/status/rollback/gc/audit tests (§CLI-visible tests for activation and cleanup), including no force-finalizer path; Nix unit and build tests owned by ADR-046-nix-configuration spec; canonical-example fixture validates every authored resource against the authoritative Host, Guest, and Provider schemas, resolves every artifact/resource reference, rejects unsupported fields, and proves the compiled Guest `spec` is field-for-field identical to the adjacent JSON", + "workItemId" : "ADR046-cli-011" + }, + { + "currentSource" : "None — net-new v3 work; `Endpoint` is a new standard ResourceType added by D092; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-cli-001, ADR046-cli-010; CLI crate owner", + "destination" : "`packages/d2b/src/endpoint.rs` (`d2b endpoint get/list/watch/status/resolve`)", + "detailedDesign" : "Add `Endpoint` to the frozen standard ResourceType set (19 types; local ResourceRef parser accepts `Endpoint/` with no Zone round-trip). Implement `d2b endpoint get/list/watch/status` over the ADR046-cli-010 generic verbs and `d2b endpoint resolve ` as a provider-neutral resolution projection printing `producerRef`, `endpointClass`, transport class, readiness, and capability/locality observations. Endpoint output is the base envelope only (base spec + base status per D092); any `status.provider` projection is bounded, redacted, and deny-unknown. **No raw locator** (path/address/CID/port/fd/credential) appears in any CLI field or the `resolve` projection. `status.update` currency and `d2b upgrade Endpoint/` visibility follow the standard verbs and D091. `create`/`update-spec`/`delete` are rejected on the operator surface except for statically Nix/API-authored Endpoints whose schema permits it.", + "integration" : "ZoneContext → `ConnectedClient` → resource API for `Endpoint`; consumers reference endpoints only through `Endpoint/` ResourceRefs", + "removalProof" : "Not applicable (new surface)", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d` — copy/adapt: (1) `packages/d2b-client/src/client.rs` `ConnectedClient::invoke()` and `ConnectedClient::open_server_stream()` — copy unchanged; used for Endpoint Get/List/Status and Watch; (2) `packages/d2b-client/src/session.rs` `NamedStream` — copy unchanged; Watch stream output; (3) the ADR046-cli-010 generic resource-verb dispatch — reuse for `d2b endpoint get/list/watch/status`; the `d2b endpoint resolve` projection verb is net-new. No raw transport/FD handle type is imported into the CLI; resolution to a private transport/FD remains inside Core/ProviderSupervisor via EffectPort/LaunchTicket", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "`Endpoint` accepted as a frozen standard type; `Endpoint/` parses locally without a Zone round-trip; `d2b endpoint get/list/watch/status/resolve` parse and route correctly; `get`/`resolve` output carries no raw path/address/CID/port/fd/credential; provider status projection is bounded/redacted; `d2b list Endpoint --updates` and `d2b upgrade Endpoint/` expose `status.update` currency and disruption; operator `create`/`update-spec`/`delete` rejected for controller-owned Endpoints", + "workItemId" : "ADR046-cli-012" + }, + { + "currentSource" : "None — net-new v3 work; `ResourceExport`/`ResourceImport` are new standard ResourceTypes added by D096; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-cli-001, ADR046-cli-010, ADR046-zone-control-019; CLI crate owner", + "destination" : "`packages/d2b/src/share.rs` (`d2b export …` and `d2b import …` nouns)", + "detailedDesign" : "Add `ResourceExport` and `ResourceImport` to the frozen 19-type standard set while leaving qualified semantic/provider-neutral Service/Binding types dynamically cataloged. Implement export/import generic verbs, `d2b import projection`, and `d2b import graph`. Export accepts only a factory-bound owner Service. Import status exposes the same-semantic-type projection Service and verified factory/schema fingerprints; local `providerRef` selects the implementation. Graph renders import → projection Service → authored Bindings → controller-owned Process/Endpoint children with deterministic ownership/dependency edges. Binding spec is desired intent only and observations are status-only. Reject Device/Endpoint/Binding export targets, cross-Zone refs, absent/mismatched factory/type/fingerprints, implementation detail in the base, forbidden Providers, USB policy denial, unauthorized Zones, and capability-ceiling violations. No backing/remote refs, internal sessions/streams, FD, secret, path, locator, or bytes in output. Delete waits for authored Bindings to be removed/retargeted and never cascades them.", + "integration" : "ZoneContext → resource API; signed Provider factory catalog; owner/dependency indexes for graph rendering; consumers use authored Binding → local projection Service, never ResourceImport directly", + "removalProof" : "Not applicable (new surface)", + "reuseAction" : "adapt", + "reuseSource" : "ADR046-cli-010 generic resource-verb dispatch; `packages/d2b-client/src/client.rs` `ConnectedClient::invoke`/`open_server_stream` — copy unchanged", + "specId" : "ADR-046-cli-and-operations", + "specPath" : "docs/specs/ADR-046-cli-and-operations.md", + "validation" : "Both standard types parse locally; qualified semantic Service/Binding resolve dynamically; verbs route; Service-only export and factory/type/fingerprint/policy/capability rejections; projection preserves semantic Service type across local implementation selection; canonical minimal base works without `spec.provider`; graph shape exact and bounded; Binding never auto-created/deleted; Binding spec intent-only/status observations; no implementation detail/backing/remote-ref/session/stream/FD/secret/path/locator/bytes leakage", + "workItemId" : "ADR046-cli-013" + }, + { + "currentSource" : "main `a1cc0b2d`: `packages/d2b-client/src/client.rs` (`WallClock`, `MetadataInput`, `RetryPolicy` 1..8 attempts, `CallOptions`, `CancellationToken`, `Client::new/with_clock/connect`, `ConnectedClient` methods incl. `session_generation/session_limits/service/invoke/invoke_with_attachments/named_stream/open_server_stream`, `prepare_typed_request/prepare_operation_context`, `can_retry/retryable_failure/validate_outbound_attachments/validate_reply/service_package/map_remote_kind/map_retry` — lines 35–921); `packages/d2b-client/src/session.rs` (`ComponentSessionConnector` trait, `NamedStream` lifecycle, `StreamDispatcher`, `SharedDriver`, aggregate-queue-bound test — lines 24–626); `packages/d2b-client/src/target.rs` (`ServiceOwner`, `TargetInput`, `TransportKind`, `RouteRecord`, `ResolvedTarget`, `TargetResolver`, `RouteTable` — lines 7–228); `packages/d2b-client/src/service.rs` (`ServiceKind`, `GeneratedClient`, `MethodHandle`, `ServiceHandle::new/kind/generated/proxy/method/invoke` — lines 21–184); `packages/d2b-client/src/daemon_service.rs` (`DaemonClient::new/session_generation/connected/resolve/inspect/lifecycle/open_terminal`, `DaemonTerminal`, `daemon_call_options`, `ensure_daemon_outcome`, `map_ttrpc_error`, test `redacted_terminal_debug_payload` — lines 29–689); `packages/d2b-client/src/host_socket.rs` (`HostSocketConnector::new/from_seqpacket_fd`, `local_daemon_endpoint_identity`, `ComponentSessionConnector::connect` — lines 252–383); `packages/d2b-client/src/error.rs` (`RemoteErrorKind`, `RetryClass`, `ClientError` — lines 5–128)", + "dataMigration" : "Not applicable; updated in place", + "dependencyOwner" : "ADR046-zone-control-011, ADR046-zone-control-012, ADR046-zone-control-013, ADR046-zone-control-018", + "destination" : "`packages/d2b-client/src/` (updated for v3 Zone API, replacing ADR45 daemon verbs with Zone resource operations)", + "detailedDesign" : "**Selected**: `Client::connect()` target-resolve → ComponentSession-open → `ConnectedClient` lifecycle; `RetryPolicy` 1..8 bound + `retryable_failure()` safe-only retry detection; `NamedStream::send/receive/close_local/reset` lifecycle; `ComponentSessionConnector` trait as connector abstraction; `HostSocketConnector::from_seqpacket_fd` + `local_daemon_endpoint_identity` for Zone runtime socket connector; `RouteTable` ambiguous-route rejection; `ServiceHandle`/`GeneratedClient`/`MethodHandle` typed service client pattern; `map_ttrpc_error`/`validate_reply`/`map_retry` error-handling chain; `ClientError`/`RemoteErrorKind`/`RetryClass` error taxonomy; `DaemonClient` call-options and outcome helpers (infrastructure only). **Excluded ADR45 assumptions**: `DaemonMethod` enum (lines 29–56 of daemon_service.rs): ADR45 daemon verbs (`vm_start`, `vm_stop`, `list_realms`, etc.) — replaced with Zone API verbs. `GuestClient`/`guest_service.rs`: ADR45 guest operations; excluded until v3 Guest transport work item. Hardcoded socket path `PUBLIC_SOCKET_PATH = \"/run/d2b/public.sock\"` in `host_socket.rs`: replaced by Zone-resource-managed path. `TransportKind::LocalUnix` restriction in daemon-access: v3 allows multiple transport kinds per ZoneLink binding. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "integration" : "`d2b` CLI uses `d2b-client` to connect to Zone runtime via `HostSocketConnector`; a child Zone runtime uses `d2b-client` for its allocator-bound uplink while the parent route engine uses the established session for child calls; Provider toolkit conformance tests use `Fixture`/`FakeProvider` with `d2b-client` service handles", + "removalProof" : "`DaemonMethod` v2 verb enum retired after all v2 daemon operations migrated to Zone API", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`client-retry-policy-max-8-attempts-enforced`, `client-named-stream-close-local-then-remote-close-transitions-closed`, `client-route-table-ambiguous-route-rejected`, `client-host-socket-peer-uid-verified-on-connect`, `client-retryable-failure-only-safe-mutations`", + "workItemId" : "ADR046-client-001" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent for the provider crate skeleton", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR-046-provider-model-and-packaging; Provider/clipboard-wayland crate owner", + "destination" : "packages/d2b-provider-clipboard-wayland/ with src, tests, integration, README.md, and binaries clipboard-controller, clipd-host, picker-session", + "detailedDesign" : "Create the provider crate skeleton, required source layout, three binaries, and README covering purpose, component map, local build instructions, test commands, and display-wayland fake dependency for integration tests.", + "integration" : "Workspace package manifest and Provider artifact catalog consume the crate; provider packaging registers component templates for core ProviderDeployment.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "Workspace provider layout policy plus README content review and follow-on make test-rust -p d2b-provider-clipboard-wayland once implementation exists.", + "workItemId" : "ADR046-clipboard-001" + }, + { + "currentSource" : "packages/d2b-clipd/ algorithms and types listed in the Reuse from baseline section", + "dataMigration" : "Full d2b 3.0 reset; clipboard history is bounded process memory and no v2 clipboard runtime state is imported", + "dependencyOwner" : "ADR046-clipboard-001; clipd-host service owner", + "destination" : "packages/d2b-provider-clipboard-wayland/src/clipd_host/ service binary modules such as service, display_client, bridge, picker_coord, policy, fd, audit, history", + "detailedDesign" : "Adapt d2b-clipd into clipd-host: call RequestPickerSession over d2b.clipboard.picker-coord.v3, consume d2b.display.host-clipboard.v3 instead of WAYLAND_DISPLAY or NIRI_SOCKET, serve d2b.clipboard.bridge.v3 on Endpoint/clipboard-bridge, remove filesystem bridge and peer group ACL logic, and preserve MIME allowlist, FD safety, fail-closed audit, loop suppression, and LRU history algorithms. Primary reuse disposition: `adapt`. Preserved source-plan detail: port and adapt algorithms; replace direct compositor, picker subprocess, Unix bridge, SO_PEERCRED, bridge directory, and group ACL paths.", + "integration" : "Core creates Process/clipd-host; display-wayland wayland-proxy consumes Endpoint/clipboard-bridge; clipd-host consumes display-wayland host-clipboard service and controller picker coordination service.", + "removalProof" : "Baseline picker subprocess, direct compositor/Niri clients, Unix bridge socket server, bridge directories, SO_PEERCRED peer config, and per-Guest groups are absent from the provider crate and covered by invariant tests.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "make test-rust -p d2b-provider-clipboard-wayland plus unit coverage for MIME policy, FD safety, audit fail-closed queue, history bounds, lifecycle purge and suspension, no filesystem bridge, and no bytes in status or audit.", + "workItemId" : "ADR046-clipboard-002" + }, + { + "currentSource" : "None — net-new v3 controller; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no controller durable state import because ProviderStateSet is empty", + "dependencyOwner" : "ADR046-clipboard-001; clipboard-controller owner", + "destination" : "packages/d2b-provider-clipboard-wayland/src/controller/ and clipboard-controller binary", + "detailedDesign" : "Implement Process/clipboard-controller as a system-domain system-minijail Process serving d2b.clipboard.picker-coord.v3, creating picker EphemeralProcesses from the signed picker-session template, observing picker status, relaying Guest lifecycle messages to clipd-host, creating clipboard RBAC Roles and RoleBindings, writing only bounded redacted operational observations through the optimistic status writer, and never owning or mounting Provider state Volumes. Primary reuse disposition: `create`. Preserved source-plan detail: net-new controller using existing resource API and ComponentSession contracts.", + "integration" : "Core ProviderDeployment creates the controller Process; controller uses Zone resource API for EphemeralProcess and RBAC resources and ComponentSession to clipd-host for picker and purge or suspend coordination.", + "removalProof" : "None — net-new; no prior controller owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "Controller unit tests for picker request validation, EphemeralProcess spec shape, terminal status callback, GuestStopped and GuestLocked handling, RBAC resources, status bounds, and empty ProviderStateSet.", + "workItemId" : "ADR046-clipboard-003" + }, + { + "currentSource" : "packages/d2b-clipd/src/picker.rs subprocess flow is replacement context only; picker-session worker is net-new", + "dataMigration" : "Full d2b 3.0 reset; picker state is per-operation EphemeralProcess status only", + "dependencyOwner" : "ADR046-clipboard-001 and ADR046-clipboard-003; picker worker owner", + "destination" : "packages/d2b-provider-clipboard-wayland/src/picker_session/ and picker-session binary", + "detailedDesign" : "Implement picker-session as a user-domain worker EphemeralProcess with minimal environment, metadata over inherited ComponentSession named stream, restricted WAYLAND_SOCKET FD from display-wayland presentation portal, GTK4 closure-contained runtime, one Select or Cancel response, no clipboard FDs, no clipboard bytes, no compositor credentials, no socket paths, and typed PickerStartFailed on install or start failure instead of bypass. Primary reuse disposition: `adapt`. Preserved source-plan detail: rewrite as EphemeralProcess worker.", + "integration" : "clipboard-controller creates picker EphemeralProcess per paste request; ProviderSupervisor pre-opens restricted Wayland FD; picker returns result to controller which notifies clipd-host.", + "removalProof" : "Old d2b-clipd subprocess picker path is absent once RequestPickerSession and picker EphemeralProcess tests pass.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "Contract test that picker cannot bind zwlr_data_control_manager_v1 plus unit tests for processClass worker, no FDs or payload in picker config, TTL defaults, response framing, and requirePickerForPaste false bypass semantics in clipd-host.", + "workItemId" : "ADR046-clipboard-004" + }, + { + "currentSource" : "None — net-new clipboard-wayland ComponentSession service definitions; display-wayland client stubs are consumed from the display-wayland contract", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-clipboard-001; ADR-046-componentsession-and-bus; clipboard service contract owner", + "destination" : "packages/d2b-provider-clipboard-wayland service descriptors and generated Rust async ttrpc bindings, plus any shared contracts crate selected by ADR-046-componentsession-and-bus", + "detailedDesign" : "Generate stubs for d2b.clipboard.bridge.v3, d2b.clipboard.picker-coord.v3, and d2b.clipboard.v3, consume display-wayland d2b.display.host-clipboard.v3 client stubs, reject service-name collisions, and declare attachment classes clipboard-transfer-fd, host-selection-transfer-fd, and host-selection-supply-fd in the signed descriptor for ComponentSession handshake validation. Primary reuse disposition: `create`. Preserved source-plan detail: net-new generation of service stubs and named-stream types.", + "integration" : "Service registry, Zone ComponentSession enrollment, clipd-host, clipboard-controller, display-wayland wayland-proxy, and CLI/operator clients all consume the generated bindings.", + "removalProof" : "Shared filesystem bridge path and SO_PEERCRED contract tests are removed in ADR046-clipboard-011 after ComponentSession contracts pass.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "Contract tests for wire format, service-name collision rejection, attachment class matching, and descriptor handshake validation.", + "workItemId" : "ADR046-clipboard-005" + }, + { + "currentSource" : "nixos-modules/clipboard.nix and current d2b.clipboard.* options", + "dataMigration" : "Full d2b 3.0 reset; operators translate old d2b.clipboard.* options using the dossier mapping table", + "dependencyOwner" : "ADR-046-nix-configuration; Provider/clipboard-wayland Nix owner", + "destination" : "nixos-modules/providers/clipboard-wayland.nix and d2b.artifacts.clipboard-wayland catalog entry", + "detailedDesign" : "Implement Nix module emitting d2b.zones..resources. Provider resources with spec.artifactId and spec.config, validate hostExecutionRef, hostUserRef, displayWaylandRef, and pickerArtifactId, forbid spec.componentPlacements, spec.settings, and spec.status, and remove nixos-modules/clipboard.nix in the same landing sequence as the new module. Primary reuse disposition: `replace`. Preserved source-plan detail: replace option surface with Provider resource Nix module.", + "integration" : "Nix resource compiler emits Provider resource and artifact catalog data consumed by core configuration publication and ProviderDeployment.", + "removalProof" : "nixos-modules/clipboard.nix import is removed and examples/static checks no longer reference old d2b.clipboard.* options.", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "Nix eval tests for resource shape, reference validation, null displayWaylandRef host-only mode, artifact catalog lookup, and absence of deprecated spec fields.", + "workItemId" : "ADR046-clipboard-006" + }, + { + "currentSource" : "None — net-new Zone RBAC resources for clipboard-wayland; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 RBAC state import", + "dependencyOwner" : "ADR046-clipboard-003; clipboard RBAC owner", + "destination" : "packages/d2b-provider-clipboard-wayland/src/controller/rbac.rs or equivalent controller reconcile module", + "detailedDesign" : "Controller creates Role/clipboard-admin, Role/clipboard-viewer, Role/clipboard-bridge-peer, Role/clipboard-picker-worker and RoleBindings display-wayland-bridge, host-admin-clipboard, picker-session-worker, all Zone-scoped, owned by Process/clipboard-controller, selector-bound for Process/picker-*, and cleaned up when Provider is deleted.", + "integration" : "Resource API stores RBAC resources; ComponentSession authorization checks consume Roles and RoleBindings for management, bridge, and picker worker services.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "Controller RBAC reconcile tests for create, idempotent update, provider deletion cleanup, selector scoping, bridge peer authorization, and denied unauthorized management calls.", + "workItemId" : "ADR046-clipboard-007" + }, + { + "currentSource" : "packages/d2b-clipd/src/audit.rs and policy types from packages/d2b-clipd/src/policy.rs", + "dataMigration" : "Full d2b 3.0 reset; audit stream is v3 Zone-local and no v2 audit records are imported", + "dependencyOwner" : "ADR046-clipboard-002; ADR-046-telemetry-audit-and-support; clipboard observability owner", + "destination" : "packages/d2b-provider-clipboard-wayland/src/service/audit.rs and packages/d2b-provider-clipboard-wayland/src/service/metrics.rs", + "detailedDesign" : "Implement ClipboardAuditEvent and fail-closed Zone audit queue by porting baseline audit code, renaming realm fields to source_zone_id and dest_zone_id, making ReasonCode a closed enum with unknown protobuf fields rejected, replacing exact byte counts with SizeBucket, emitting to d2b.audit.v3, and adding closed-semantic-label OTEL metrics and spans from the dossier tables. Metric descriptors carry no Zone/resource-name-derived identity; `d2b.zone` remains a resource attribute. Primary reuse disposition: `adapt`. Preserved source-plan detail: port and adapt audit plus resource-name-free metrics and redaction changes.", + "integration" : "clipd-host emits audit events to the Zone audit sink and OTEL metrics/spans to the observability Provider pipeline during clipboard operations.", + "removalProof" : "Old audit shape with realm field names and exact byte counts is absent after ported tests assert the v3 ClipboardAuditEvent schema.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "Audit tests for no bytes in events, closed ReasonCode deserialization, fail-closed queue rejection, SizeBucket discretization, excluded span attributes, and structural metric descriptor assertions for exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus clipboard/Zone-name canary absence while preserving `d2b.zone` resource attributes.", + "workItemId" : "ADR046-clipboard-008" + }, + { + "currentSource" : "packages/d2b-clipd/ behavior and this dossier invariants; no single baseline test path is declared for every assertion", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-clipboard-001 through ADR046-clipboard-008; clipboard test owner", + "destination" : "packages/d2b-provider-clipboard-wayland/tests/", + "detailedDesign" : "Create hermetic unit and Cargo integration tests covering closed MIME policy, secret-hint suppression, FD validation and bounds, LRU and TTL history, fail-closed audit, lifecycle purge and suspend, picker EphemeralProcess invariants, no filesystem bridge, core-created Processes, empty ProviderStateSet, no state mounts or state-layout principals, status-first observation, and no clipboard bytes in status, audit, metrics, Operations, or Volumes. Primary reuse disposition: `extract`. Preserved source-plan detail: extract semantic assertions into hermetic provider tests.", + "integration" : "cargo test -p d2b-provider-clipboard-wayland --lib --tests consumes the provider crate and fake clocks/effect ports without live Wayland, systemd, broker, or Nix eval.", + "removalProof" : "Replaced current-code tests receive explicit keep/adapt/move/delete dispositions and old duplicate tests are deleted once successor coverage passes.", + "reuseAction" : "extract", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "All tests listed in the Required test coverage table must pass under make test-rust -p d2b-provider-clipboard-wayland.", + "workItemId" : "ADR046-clipboard-009" + }, + { + "currentSource" : "None — net-new v3 provider integration scenarios; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-clipboard-009; display-wayland fake and clipboard integration owner", + "destination" : "packages/d2b-provider-clipboard-wayland/integration/", + "detailedDesign" : "Implement e2e paste, host capture, bridge backpressure, rate limiting, echo suppression, dependency absent host-only mode, GuestDestroyed purge, audit fail-closed, picker start timeout, and cross-zone denied scenarios using fake d2b.display.host-clipboard.v3 server and fake wayland-proxy bridge client without requiring a live compositor. Primary reuse disposition: `create`. Preserved source-plan detail: net-new integration suite with fake display-wayland and fake wayland-proxy.", + "integration" : "Provider integration lane exercises clipd-host, clipboard-controller, generated ComponentSession services, fake display-wayland service, and fake bridge client end-to-end.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "All integration scenarios in the dossier table pass and assert no live Wayland compositor dependency.", + "workItemId" : "ADR046-clipboard-010" + }, + { + "currentSource" : "packages/d2b-contract-tests/tests/policy_clipboard.rs", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-clipboard-005; packages/d2b-contract-tests owner", + "destination" : "packages/d2b-contract-tests/tests/policy_clipboard.rs", + "detailedDesign" : "Add contract tests for d2b.clipboard.bridge.v3 and d2b.clipboard.picker-coord.v3 wire formats, ReasonCode numeric stability, and attachment class descriptor names while removing tests that assume shared filesystem bridge paths or SO_PEERCRED config. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt contract tests and delete obsolete filesystem bridge assumptions.", + "integration" : "Contract test suite consumes generated service descriptors and guards downstream ComponentSession consumers.", + "removalProof" : "Tests for shared filesystem bridge paths and SO_PEERCRED config are removed from policy_clipboard.rs after v3 ComponentSession contract coverage lands.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "packages/d2b-contract-tests policy_clipboard.rs passes with v3 wire format and attachment descriptor assertions.", + "workItemId" : "ADR046-clipboard-011" + }, + { + "currentSource" : "nixos-modules/clipboard.nix and its import in nixos-modules/default.nix", + "dataMigration" : "Full d2b 3.0 reset; no v2 clipboard runtime state or option import", + "dependencyOwner" : "ADR046-clipboard-006; Nix/module removal owner", + "destination" : "nixos-modules/default.nix, docs/how-to/ migration guide, tests/static.sh example iteration expectations, CHANGELOG.md", + "detailedDesign" : "After the new Provider Nix module validates, delete nixos-modules/clipboard.nix, remove its default.nix import, update migration guide with the option mapping table, ensure tests/static.sh examples no longer rely on removed paths, and add an Unreleased changelog entry. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete superseded module and update migration docs.", + "integration" : "Nix module aggregator, docs migration flow, static example iteration, and changelog all reflect Provider/clipboard-wayland as the only clipboard surface.", + "removalProof" : "nixos-modules/clipboard.nix is deleted, default.nix import removed, and grep/static checks show no old d2b.clipboard option references in examples.", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-clipboard-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-clipboard-wayland.md", + "validation" : "New module eval tests pass and tests/static.sh example iteration has no references to removed option paths.", + "workItemId" : "ADR046-clipboard-012" + }, + { + "currentSource" : "`packages/d2bd/src/lib.rs`, `provider_registry.rs` if present on source ref, supervisor state, operations; `d2b-realm-core/src/{allocator_engine,identity_store}.rs`", + "dataMigration" : "Full reset", + "dependencyOwner" : "W0/W1a; core-controller owner", + "destination" : "`packages/d2b-core-controller/src/{main,configuration,api_catalog,authz,providers,controllers,ownership,watches,cleanup,zone_links,budgets,store}.rs`", + "detailedDesign" : "One fixed process, isolated handlers, async ResourceClient, health/startup/restart Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Zone runtime local bus/session; Provider/system-core resource identity", + "removalProof" : "Current daemon branches removed after handler/Provider parity", + "reuseAction" : "adapt", + "reuseSource" : "Useful pure handler/toolkit code from main named in implementation sub-items", + "specId" : "ADR-046-core-controllers", + "specPath" : "docs/specs/ADR-046-core-controllers.md", + "validation" : "Per-handler unit/property tests plus multi-process startup/restart", + "workItemId" : "ADR046-core-001" + }, + { + "currentSource" : "Canonical Host/User resource, bootstrap-order, and Provider-boundary work owned by ADR046-exec-003, ADR046-exec-004, ADR046-exec-005, and ADR046-system-core-001", + "dataMigration" : "New v3 resources from Nix", + "dependencyOwner" : "ADR046-core-001, ADR046-exec-003, ADR046-exec-004, ADR046-exec-005, ADR046-system-core-001; coordination-only owner", + "destination" : "`packages/d2b-core-controller/tests/system_core_coordination.rs`", + "detailedDesign" : "Coordination-only acceptance: prove the fixed core controller loads the manifest/audit boundary from ADR046-system-core-001 and routes Host/User reconciliation to the canonical ADR046-exec-003/004 handlers after ADR046-exec-005 bootstrap ordering. This item defines no schema, handler, status, capability, or `packages/d2b-provider-system-core/src/{host,user}.rs` destination. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt only the integration acceptance.", + "integration" : "After all four canonical owner items are complete, exercise bootstrap Provider/system-core and prove other controllers resolve Host/Guest/User refs without a duplicate core-controller handler", + "removalProof" : "No independent implementation removal; canonical exec/system-core work items own legacy Host/User removal proofs", + "reuseAction" : "adapt", + "reuseSource" : "The canonical owner work items named above; this item reuses their public contracts but owns no Host/User implementation", + "specId" : "ADR-046-core-controllers", + "specPath" : "docs/specs/ADR-046-core-controllers.md", + "validation" : "Coordination test asserts one Host handler owner, one User handler owner, manifest load after bootstrap ordering, and no duplicate Host/User destination or reconcile path in core-controller", + "workItemId" : "ADR046-core-002" + }, + { + "currentSource" : "`d2b-realm-provider/src/credential.rs:OpaqueAzureRef` remains a bounded opaque identifier reuse source only; no Host login/token implementation is retained", + "dataMigration" : "Full v3 reset of d2b Credential metadata; Entrablau Guest state is preserved unless explicitly destroyed by the sibling-owned reset flow", + "dependencyOwner" : "`ADR046-credential-001` (Credential base fields and status), `ADR046-credential-002` (`d2b.credential.v3` service), D092 Endpoint, D093 Entrablau identity Guest decision, credential-entra owner", + "destination" : "`packages/d2b-provider-credential-entra/src/{lib.rs,controller.rs,service.rs,controller_main.rs,agent_main.rs,audit.rs,telemetry.rs}` and corresponding tests/integration docs", + "detailedDesign" : "Implement secret-free controller/helper; require Credential base `identityGuestRef` and `loginEndpointRef`; resolve dependency alias `entra-login-token`; validate same-Zone Guest placement and exact `consumerRef`; implement typed `EntraTokenLeaseClient` whose production implementation is the Entrablau Endpoint; implement `BeginLogin`/`ObserveLogin`/`CancelLogin` projection to Credential base interaction status; admit service calls only when the same exact allowed operation (`acquire-token`, `refresh-token`, `revoke-token`, or `inspect-metadata`) is present in both `Credential.spec.allowedOperations` and the canonical `use-credential` Role `subresources`; require matching `create`, `update-spec`, or `delete` under `admin-credential` in addition to ordinary CRUD, with no Credential-specific Role fields or coarse aliases; emit the login Endpoint with canonical `visibility = provider` and an exact `consumerPolicy` for the orchestration Provider and configured consumer; route access-token leases end-to-end from Entrablau service to exact consumer over Noise_KK; keep all refresh/login/TPM state inside the Entrablau Guest; declare no Provider state Volume; reject Host placement and all ambient fallback chains; enforce Credential name/ResourceRef/UID/digest as audit-only observable identity, with only `resource_name_digest` retained in authorized bounded audit and no Credential identity in status/errors/logs/OTEL Resource or span attributes/metric labels. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt tests/types where non-secret; replace production token acquisition with Entrablau Endpoint client.", + "integration" : "Consumer composes `inputs.entrablau.nixosModules.default` into the identity Guest; sibling package declares login/token Process and Endpoint; d2b Credential resource binds `identityGuestRef`, `loginEndpointRef`, and `consumerRef`", + "removalProof" : "Old abstract Host credential paths and any direct Entra effect client are deleted only after the Entrablau Endpoint-backed provider passes the test matrix", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-credential-entra", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-entra.md", + "validation" : "See §13", + "workItemId" : "ADR046-cred-entra-001" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/credential.rs:ManagedIdentityRef` (reachable); `packages/d2b-provider-aca/src/lib.rs:managed_identity_client_id` line 112 (reachable ACA config); `packages/d2bd/src/lib.rs:managed_identity_client_id` lines 3960, 4173 (reachable)", + "dataMigration" : "Full v3 reset. `d2b-provider-aca:managed_identity_client_id` raw field migrated to a Credential resource reference in the v3 ACA Provider config; see removal precondition below.", + "dependencyOwner" : "`ADR046-credential-001` (contracts), `ADR046-credential-002` (service); `ADR046-reconcile-001`; `ADR046-mi-topology-001`; `credential-managed-identity` crate owner", + "destination" : "`packages/d2b-provider-credential-managed-identity/src/{lib.rs, controller.rs, agent.rs, service.rs, audit.rs, telemetry.rs}`; `packages/d2b-provider-credential-managed-identity/{controller/main.rs, agent/main.rs}`; `packages/d2b-provider-credential-managed-identity/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs, topology.rs}`; `packages/d2b-provider-credential-managed-identity/integration/{container-service.sh, host-guest-placement.nix, aca-credential-ref.sh, cleanup-rollback.sh}`; `packages/d2b-provider-credential-managed-identity/README.md`", + "detailedDesign" : "(1) Adapt `ManagedIdentityCredentialProvider` to `d2b.credential.v3` service interface; split controller and agent roles (see `ADR046-mi-topology-001`). (2) Enforce `ManagedIdentityCredentialOwner::ExactSdkConsumer` in agent via `AuthenticatedSubjectContext` from ComponentSession, independently of scope fields. (3) Reject `user-agent` placement: `scope.domainFilter=user` returns `credential-placement-mismatch` before agent spawn. (4) Validate `clientId` using `OpaqueAzureRef::parse` from v3 baseline; artifact IDs match `^[a-z][a-z0-9-]*$`. (5) Validate `imdsEndpointAlias` against closed enum `{azure-imds, azure-imds-aca}`; project into LaunchTicket at spawn time (never into Process spec config or env); co-located runtime Provider constructs `ManagedIdentityCredentialClient` from LaunchTicket projection and supplies via effect port; resolved URL never in any output surface. (6) Agent Process declares `networkUsage.allowEgress=false`; uses canonical Process template shape (see `ADR046-mi-topology-001` design item 6). (7) Reject `sign-challenge` with `credential-schema-invalid` immediately. (8) Map `ManagedIdentityClientState::Unavailable` to `credential-provider-unavailable`; no `InteractionRequired` state. (9) Implement `ManagedIdentityLeaseHandle` as opaque bounded newtype with redacted `Debug`. (10) All token bytes held by injected client in agent; delivered only via agent-terminated `Noise_KK` delivery session. (11) Integrate with Provider resource descriptor and controller toolkit. (12) Confirm `credential_canary` never appears in any service response, status field, delivery record outer header, or audit record. (13) Apply D087 status-first state: declare no Provider state Volume, keep ProviderStateSet empty, and write only bounded non-secret lease observation to `Credential.status` plus the Operation ledger. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Agent `Process` resource under `Host` or `Guest` executionRef; controller `Process` resource at Zone system host; d2b-bus routes `d2b.credential.v3` token-delivery calls to agent; Credential controller reconciles status; ACA `Provider/runtime-azure-container-apps` holds `credentialRef` pointing to a `credential-managed-identity`-backed Credential resource", + "removalProof" : "`d2b-provider-aca:managed_identity_client_id` raw field removed only after the `credential-managed-identity` controller and agent are integrated and the ACA Provider config uses `credentialRef` exclusively; `ProviderWorkloadIdentity::ManagedIdentity` bootstrap path superseded only after the ACA Provider controller uses the Credential resource for token acquisition", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `packages/d2b-provider-credential-managed-identity/src/lib.rs` (full implementation); `src/tests.rs` (full test suite)", + "specId" : "ADR-046-provider-credential-managed-identity", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-managed-identity.md", + "validation" : "See §Tests. Run `cargo test -p d2b-provider-credential-managed-identity --lib --test lifecycle --test conformance --test faults --test canary --test delivery --test placement --test topology`; run `integration/{container-service.sh,aca-credential-ref.sh,cleanup-rollback.sh}` through `make test-integration` and `integration/host-guest-placement.nix` through `make test-host-integration`.", + "workItemId" : "ADR046-cred-mi-001" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/provider.rs:CredentialProvider` status/enrollment-only trait; main `a1cc0b2d` managed-identity controller/test behavior listed in §Source reuse", + "dataMigration" : "None — controller lifecycle code only; no runtime state import", + "dependencyOwner" : "ADR046-credential-001, ADR046-credential-002; ADR046-reconcile-001, ADR046-reconcile-002; ADR046-mi-topology-001; owner: Credential controller toolkit and managed-identity controller", + "destination" : "packages/d2b-provider-credential-managed-identity/src/controller.rs; packages/d2b-contracts/src/v3/credential_controller.rs", + "detailedDesign" : "Managed-identity-specific controller design: implement async reconcile and agent spawn/teardown from §Async reconcile; enforce system-only domain; spawn agent on Credential admission plus dependency-ready, not on `phase=Ready`; implement `observeInterval=30s` health-check RPC to the agent, which calls `InspectMetadata` on the injected client; controller never calls IMDS; derive idempotency key as `SHA-256(UID || \":\" || rotationGeneration.to_le_bytes() || \":\" || operation_class_byte)`; enforce `MAX_LOCAL_LEASES=256` in the resource store; implement Deleted-phase closure by clearing `provider-revoke` only after agent Process deletion and revocation confirmation while core/audit own Deleted revision and deletion record. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt shared Credential controller lifecycle to managed-identity controller/agent spawn and teardown.", + "integration" : "Shared Credential controller contract produces reconcile events; managed-identity controller consumes them, manages agent Process resources, and writes Credential/agent status; generated controller contracts are consumed by all Credential Providers.", + "removalProof" : "V2 `CredentialProvider` status/enrollment trait lifecycle is superseded after shared controller reconcile and managed-identity agent lifecycle pass parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-credential-managed-identity", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-managed-identity.md", + "validation" : "Managed-identity reconcile/controller tests; shared Credential reconciliation tests; topology tests validating agent spawn/teardown and Deleted-phase cleanup", + "workItemId" : "ADR046-cred-mi-002" + }, + { + "currentSource" : "`packages/d2b-provider-aca/src/lib.rs:managed_identity_client_id` and `packages/d2bd/src/lib.rs:managed_identity_client_id` are the current raw ACA managed-identity config surfaces; v3 Provider/Credential resource authoring is net-new", + "dataMigration" : "Full d2b 3.0 reset; raw ACA `managed_identity_client_id` config is replaced by a newly authored Credential resource reference rather than imported in place", + "dependencyOwner" : "Depends on ADR046-credential-001, ADR046-credential-002, and ADR046-cred-mi-001; owner: Nix resource compiler and activation cleanup", + "destination" : "nixos-modules/options-resources.nix; nixos-modules/activation-nixos-cleanup.nix; integration/aca-credential-ref.sh", + "detailedDesign" : "Shared Nix and cleanup: implement Nix eval-time assertions 1–12 from §Eval-time assertions, closed enum schema for `imdsEndpointAlias`, `clientId` validation via `OpaqueAzureRef` charset, generation cleanup contract, and artifact catalog validation for `credential-managed-identity-bin`. Integration fixture asserts that the migrated ACA Provider config carries `credentialRef: \"Credential/aca-relay-mi\"` and the raw `managed_identity_client_id` string field is absent from rendered Provider config JSON and ACA runtime bundle. Primary reuse disposition: `replace`. Preserved source-plan detail: replace raw ACA managed identity client-id fields with Credential resource references and v3 Provider/Credential Nix emission.", + "integration" : "Nix compiler emits Provider/Credential/ACA resource config; ResourceAPI admission validates it; activation cleanup deletes old generation resources through finalizers; ACA Provider consumes `credentialRef` to obtain tokens from the managed-identity Credential Provider.", + "removalProof" : "`managed_identity_client_id` raw fields in ACA config and daemon plumbing are removed only after ACA Provider config uses `credentialRef` exclusively", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-credential-managed-identity", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-managed-identity.md", + "validation" : "Nix eval assertion tests; artifact catalog validation tests; `integration/aca-credential-ref.sh`", + "workItemId" : "ADR046-cred-mi-003" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/error.rs:contains_sensitive_shape`; `packages/d2b-core/src/realm_workloads_launcher.rs:LauncherMetadataInvariants.no_secrets_or_credentials`; main `a1cc0b2d` managed-identity canary tests listed in §Source reuse", + "dataMigration" : "None — audit/telemetry only; no runtime state import", + "dependencyOwner" : "Depends on ADR046-cred-mi-001 and ADR046-mi-topology-001; owner: credential-managed-identity audit/telemetry implementation", + "destination" : "packages/d2b-provider-credential-managed-identity/src/{audit.rs,telemetry.rs}; packages/d2b-contract-tests/tests/credential_audit.rs", + "detailedDesign" : "Shared audit/OTEL: emit audit records for all methods and controller events per §Audit, with Credential identity represented only by the authorized bounded `resource_name_digest`; emit OTEL spans and metrics per §OTEL and metrics with no Credential resource name, ResourceRef, UID, digest, derived identity token, Zone/Credential/resource-name-derived label, or non-allowlisted OTEL Resource attribute; retain applicable generic collector-allowlisted Resource attributes (`d2b.zone`, `d2b.provider`, `d2b.component`, and service fields); report expiry as the minimum for each provider/placement aggregate; add `d2b_credential_imds_calls_total` counter with bounded `alias` label; enforce `contains_sensitive_shape` on all string fields in audit records and metric labels; add canary tests for `managed-identity-canary`, `credential_canary`, `imds-endpoint-canary`, Credential name/ref/UID/digest, and Zone name in `canary.rs`. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing sensitive-shape guard and canary pattern to v3 audit, OTEL, and metric surfaces.", + "integration" : "Controller and agent service methods call audit/telemetry helpers; audit subsystem and OTEL exporters consume bounded redacted records; contract tests validate credential audit shape across providers.", + "removalProof" : "None — audit/telemetry helpers are additive; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-credential-managed-identity", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-managed-identity.md", + "validation" : "`packages/d2b-contract-tests/tests/credential_audit.rs` requires `resource_name_digest` in authorized audit records and rejects raw Credential name/ResourceRef/UID; managed-identity `canary.rs` and audit/OTEL unit tests structurally assert exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `credential_ref`, `credential_uid`, `credential_digest`, `resource_name_digest`, and every resource-name-derived label key; reject Credential name/ref/UID/digest canaries from all OTEL Resource attributes, span attributes, and metric labels; preserve generic collector-allowlisted Resource attributes including `d2b.zone`, `d2b.provider`, `d2b.component`, and service fields; reject Zone-name span/label canaries and sensitive shapes; pass complete managed-identity metric/span frames through the shared collector ingress validator and prove that adding `d2b.credential.name` or any Credential identity key/value rejects the whole frame", + "workItemId" : "ADR046-cred-mi-004" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/provider.rs:CredentialProvider`; `packages/d2b-realm-provider/src/credential.rs` opaque credential refs and `OpaqueAzureRef` helpers", + "dataMigration" : "Full d2b 3.0 reset; no v2 CredentialProvider status/config import", + "dependencyOwner" : "Dependency for ADR046-cred-ss-003; owner: `packages/d2b-contracts` Credential ResourceType contract", + "destination" : "packages/d2b-contracts/src/v3/credential.rs", + "detailedDesign" : "Contract types: define `CredentialSpec`, `CredentialStatus`, `CredentialLeaseHandle`, `OperationClass`, `PlacementBinding`, `CredentialConditionType`, and serde/validation/redaction helpers. Reuse `OpaqueAzureRef` from the v3 baseline. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt current credential/status concepts into v3 ResourceType DTOs; reuse `OpaqueAzureRef` directly where applicable.", + "integration" : "Nix compiler emits these DTOs; ResourceAPI stores them; credential-secret-service controller/service consumes them; CLI and conformance tests validate base Credential spec/status behavior.", + "removalProof" : "Old `d2b-realm-provider:CredentialProvider` trait and `CredentialStatus` enum are removed only after all three v3 Credential Provider controllers reach full reconcile parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-credential-secret-service", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-secret-service.md", + "validation" : "Credential ResourceType schema/serde/redaction tests from `ADR-046-resources-credential`; credential-secret-service conformance consumes the shared types", + "workItemId" : "ADR046-cred-ss-001" + }, + { + "currentSource" : "None — net-new v3 `d2b.credential.v3` service; no pre-ADR45 baseline service proto equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 service state import", + "dependencyOwner" : "Dependency for ADR046-cred-ss-003; owner: credential service contract/codegen", + "destination" : "packages/d2b-contracts/proto/v3/credential.proto; packages/d2b-credential-service/", + "detailedDesign" : "Service proto: define the `d2b.credential.v3` protobuf/ttrpc service and generate typed client/server code. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `create`. Preserved source-plan detail: net-new service contract replacing the v2 in-process `CredentialProvider` trait.", + "integration" : "d2b-bus routes Credential service calls to credential-secret-service Process instances; generated client/server types bind the controller/service implementation to ComponentSession delivery.", + "removalProof" : "V2 `CredentialProvider` trait calls are superseded by `d2b.credential.v3` only after all credential providers reach parity", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-credential-secret-service", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-secret-service.md", + "validation" : "Generated-code compile tests and credential service contract tests from `ADR-046-resources-credential`; credential-secret-service lifecycle/delivery tests consume the generated service", + "workItemId" : "ADR046-cred-ss-002" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/provider.rs:CredentialProvider` (minimal v3 baseline)", + "dataMigration" : "Full reset; no migration from old `CredentialProvider` trait", + "dependencyOwner" : "`ADR046-cred-ss-001` (contract types); `ADR046-cred-ss-002` (service proto); `ADR046-reconcile-001`; credential-secret-service owner", + "destination" : "`packages/d2b-provider-credential-secret-service/src/{lib.rs, controller.rs, service.rs, main.rs}`; `packages/d2b-provider-credential-secret-service/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}`; `packages/d2b-provider-credential-secret-service/integration/{container-service.sh, host-placement.nix, guest-placement.nix, cleanup-rollback.sh}`; `packages/d2b-provider-credential-secret-service/README.md`", + "detailedDesign" : "Adapt `SecretServiceCredentialProvider` and `SecretServiceCredentialProviderFactory` to v3 `d2b.credential.v3` service; replace v2 `CredentialProvider` trait with v3 controller/service handler; retain `Oo7SecretServicePort` trait methods unchanged; ensure `SecretServiceOwner::Userd` placement guard rejects system-domain and guest-agent construction; validate `collectionAlias` against provider-internal charset (not `OpaqueAzureRef`; collection aliases may include spaces); integrate with Provider resource descriptor and controller toolkit; test that `credential_canary` never appears in any service response; create a Process resource per `(Zone, User, executionRef)` triple with `template = \"secret-service-controller\"` (plain string), canonical `sandbox` fields (`namespaceClasses`, `capabilityClasses`, `seccompClass`, `noNewPrivileges`, `startRoot`, `environmentClass`, `readOnlyRoot`), `budget` with nested `cpu`/`memory`/`pids`/`fds` sub-fields, `networkUsage: null`, no inline endpoint fields, an owned credential-service `Endpoint` resource, and `readiness.class = \"provider-defined\"`; component descriptor declares the required authenticated `dbus-session` FD attachment carried privately by the LaunchTicket; D087 status-first state model: no Provider state Volume is declared, ProviderStateSet is optional/query-time and empty, no Volume mount or layout principal is required, and the storage-need test is not met; bounded non-secret lease/acquisition/retry observation lives in `Credential.status` plus the core Operation ledger; any opaque status handle is non-secret, non-authorizing, bounded, safe for authorized status readers, and independently revalidated; no token/object-path/lease bytes persist anywhere; finalize() emits revoke outcome audit but MUST NOT emit the resource-deleted closure audit (audit subsystem only); controller writes only scoped Credential/Process health (core aggregates Provider status) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Target: user-domain `Process` resource under `Host` or `Guest` (ADR-only ResourceType); d2b-bus routes `d2b.credential.v3` calls to this process; Credential controller reconciles status. Current v3 has no user-credential host process: v3 `d2b-userd` is a guest exec stub (exits 78 in service mode; no credential functionality; `test-only-or-preview`). This integration path is fully new (ADR-only) work.", + "removalProof" : "Old `d2b-realm-provider:CredentialProvider` trait removed only after all three v3 Credential Provider controllers reach full reconcile parity", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `packages/d2b-provider-credential-secret-service/src/lib.rs` (full implementation); `src/tests.rs` (full test suite including `FakeOo7Port`, lease lifecycle, locked state, canary enforcement, cardinality limits)", + "specId" : "ADR-046-provider-credential-secret-service", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-secret-service.md", + "validation" : "See §16", + "workItemId" : "ADR046-cred-ss-003" + }, + { + "currentSource" : "ADR-only controller pattern from `ADR-046-resource-reconciliation`; no concrete secret-service baseline controller to import", + "dataMigration" : "None — controller toolkit code only; no runtime state migration", + "dependencyOwner" : "Dependency for ADR046-cred-ss-003; owner: common Credential controller/reconciliation toolkit", + "destination" : "packages/d2b-provider-credential-/src/controller.rs", + "detailedDesign" : "Controller toolkit: implement the common Credential controller handler conforming to the `ADR-046-resource-reconciliation` async loop. Secret-service-specific controller code plugs into this handler while keeping provider bytes out of status/store/audit. Primary reuse disposition: `create`. Preserved source-plan detail: net-new shared controller handler pattern specialized by each Credential Provider.", + "integration" : "Resource watches and Operation ledger drive the controller loop; credential-secret-service handler uses the toolkit to reconcile Credential status, finalizers, Process health, and service lifecycle.", + "removalProof" : "None — shared toolkit is additive; v2 trait removal is tracked by ADR046-cred-ss-003/001 parity", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-credential-secret-service", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-secret-service.md", + "validation" : "Shared reconciliation tests from `ADR-046-resources-credential`; credential-secret-service lifecycle/fault tests verify the handler integration", + "workItemId" : "ADR046-cred-ss-004" + }, + { + "currentSource" : "None — net-new v3 `d2b.zones..resources.` Credential/Provider authoring surface; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no old credential config is imported into v3 resources", + "dependencyOwner" : "Dependency for ADR046-cred-ss-003; owner: Nix resource compiler and activation cleanup", + "destination" : "nixos-modules/options-resources.nix; nixos-modules/activation-nixos-cleanup.nix", + "detailedDesign" : "Nix compiler: implement `d2b.zones..resources.` authoring, eval-time assertions, canonical JSON emission, artifact catalog, bundle digest, and generation cleanup contract. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `create`. Preserved source-plan detail: net-new Nix resource emission and cleanup contract.", + "integration" : "Nix emits Provider/Credential resource JSON and artifact catalog entries; ResourceAPI admission and credential-secret-service controller consume the rendered resources; activation cleanup issues async Delete/finalizer flow on generation removal.", + "removalProof" : "None — new v3 Nix resource surface; old trait removal waits for controller parity", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-credential-secret-service", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-secret-service.md", + "validation" : "Nix eval/assertion/golden tests from `ADR-046-resources-credential`; credential-secret-service cleanup rollback integration fixture", + "workItemId" : "ADR046-cred-ss-005" + }, + { + "currentSource" : "`packages/d2b-core/src/realm_workloads_launcher.rs:LauncherMetadataInvariants.no_secrets_or_credentials`; secret-service main reuse canary tests listed in §14", + "dataMigration" : "None — audit/telemetry only; no runtime state migration", + "dependencyOwner" : "Dependency for ADR046-cred-ss-003; owner: credential-secret-service audit and telemetry implementation", + "destination" : "packages/d2b-provider-credential-secret-service/src/{audit.rs,telemetry.rs}", + "detailedDesign" : "Audit/OTEL: emit authorized bounded audit records with Credential identity represented only by `resource_name_digest`, and emit OTEL spans/metrics for all credential service methods and controller events with canary enforcement, expiry aggregated across user-agent leases, no Credential resource name, ResourceRef, UID, digest, derived identity token, Zone/Credential/resource-name-derived metric label, or non-allowlisted OTEL Resource attribute; retain applicable generic collector-allowlisted Resource attributes (`d2b.zone`, `d2b.provider`, `d2b.component`, and service fields); no token/object-path/lease bytes in status, delivery outer headers, audit, metrics, spans, or logs. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt zero-secret invariant and canary test pattern to credential-secret-service audit/OTEL surfaces.", + "integration" : "Controller and service methods call audit/telemetry helpers; audit subsystem and OTEL exporters consume bounded event/span/metric records; canary tests verify every public observable surface stays secret-free.", + "removalProof" : "None — audit/telemetry helpers are new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-credential-secret-service", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-secret-service.md", + "validation" : "Credential audit/OTEL tests from `ADR-046-resources-credential` require `resource_name_digest` in authorized audit records and reject raw Credential name/ResourceRef/UID; `tests/canary.rs` structurally asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `credential_ref`, `credential_uid`, `credential_digest`, `resource_name_digest`, and every resource-name-derived metric key; Credential name/ref/UID/digest canaries are absent from all OTEL Resource attributes, span attributes, and metric labels; Zone-name canaries are absent from spans and labels while generic collector-allowlisted Resource attributes remain; complete secret-service metric/span frames pass the shared collector ingress validator, while adding `d2b.credential.name` or any Credential identity key/value rejects the whole frame; `tests/delivery.rs` covers credential-secret-service delivery", + "workItemId" : "ADR046-cred-ss-006" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/credential.rs`: `OpaqueAzureRef`, parse/deserialize/charset validation, tests", + "dataMigration" : "Full d2b 3.0 reset; no v2 credential import", + "dependencyOwner" : "`ADR046-object-001` (resource envelope); `ADR046-identities-001` (types); W0 shared contract root; `d2b-contracts`", + "destination" : "`packages/d2b-contracts/src/v3/credential.rs`", + "detailedDesign" : "Define `CredentialSpec`, `CredentialStatus`, `CredentialLeaseHandle` (opaque bounded newtype), `CredentialRotationPolicy`, `CredentialRevocationPolicy`, `CredentialScope`, `OperationClass` enum, `CredentialLeaseState`, `PlacementBinding`, `CredentialConditionType`, and all serde/validation/redaction helpers; reuse `OpaqueAzureRef` from v3 baseline directly; enforce zero-secret-bytes charset validation on all string fields at construction; prevent Credential name/ResourceRef/UID or any identity-derived digest from serializing into any status layer or typed error Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Credential controller, Provider dossier schemas, Nix compiler, and resource store all consume one canonical contract", + "removalProof" : "Old `CredentialProvider` trait and `CredentialStatus` enum removed only after all v3 Credential Provider controllers consume this contract", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `d2b-provider-credential-secret-service/src/lib.rs` types: `SecretServiceLeaseRequest`, `SecretServiceLeaseRef`, `SecretServiceLeaseGrant`, `SecretServiceLeaseInspection`, `SecretServiceLeaseRenewal`, `SecretServiceLeaseRevocation`, `SecretServiceLeaseState`; parallel entra/managed-identity types; `CredentialLease`, `CredentialLeaseRequest`, `CredentialLeaseState` from `d2b-contracts/src/v2_provider.rs`", + "specId" : "ADR-046-resources-credential", + "specPath" : "docs/specs/ADR-046-resources-credential.md", + "validation" : "Schema golden vectors; charset/length tests; serde unknown-field rejection; `OpaqueAzureRef` round-trip and secret-shape rejection parity; `leaseHandle` and `sourceVersion` opaque newtype tests; status/error redaction tests with Credential name/ResourceRef/UID/digest canaries", + "workItemId" : "ADR046-credential-001" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/provider.rs:CredentialProvider` (status-only trait); `d2b-contracts/proto/v2/provider_credential.proto` (main: Health, Capabilities, Status, AcquireLease, RefreshLease, RevokeLease)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-credential-001`; `ADR046-api-001` (resource API); `ADR046-bus-001` (d2b-bus); Credential service owner", + "destination" : "`packages/d2b-contracts/proto/v3/credential.proto`; `packages/d2b-credential-service/src/{service.rs, client.rs, server.rs}`", + "detailedDesign" : "Define `d2b.credential.v3` protobuf service with exactly five operation methods: `AcquireToken`, `RefreshToken`, `RevokeToken`, `SignChallenge`, and `InspectMetadata`; map those methods one-to-one to the same exact operation class in `spec.allowedOperations` and the existing Role `subresources` field (`acquire-token`, `refresh-token`, `revoke-token`, `sign-challenge`, or `inspect-metadata`) under the canonical `use-credential` resource verb; derive the operation class from the method rather than accepting a caller-selected operation-class field; each request carries `credential_ref`, `operation_id`, `idempotency_key`, `requested_expiry_unix_ms`, `deadline_unix_ms`; map administrative lifecycle to matching `admin-credential` subresources `create`, `update-spec`, and `delete`, supplemental to ordinary CRUD, never to a new Role field or method-name alias; `RevokeToken` and `InspectMetadata` responses carry only non-secret metadata (leaseHandle digest, rotationGeneration, sourceVersion, expiresAtUnixMs, state, outcome code); `AcquireToken`, `RefreshToken`, and `SignChallenge` responses additionally include a `delivery_session_params` field carrying the binding contract fields required to establish the end-to-end credential-delivery ComponentSession (see §Credential-delivery endpoint contract); the token bytes themselves travel in the separate Noise-encrypted delivery session, never in the outer DTO; strict unknown-field rejection; bounded message sizes; all record wrappers for delivery sessions must be zeroizing types", + "integration" : "d2b-bus routes `d2b.credential.v3` service to the exact credential provider Process identified by `Credential.spec.providerRef`; before dispatch RBAC requires the exact operation class in both the `use-credential` Role `subresources` and `spec.allowedOperations`; for `AcquireToken`/`RefreshToken`/`SignChallenge`, bus additionally authorizes the credential-delivery endpoint route and forwards opaque Noise-encrypted delivery records without terminating or buffering them; bus never stores or inspects delivery record plaintext", + "removalProof" : "Old v2 `CredentialProviderService` proto removed only after all v3 callers migrate", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `packages/d2b-contracts/proto/v2/provider_credential.proto` method names", + "specId" : "ADR-046-resources-credential", + "specPath" : "docs/specs/ADR-046-resources-credential.md", + "validation" : "Protocol golden vectors for each method; Role matrix for the five exact allowed-operation subresources under `use-credential` and exact `create`, `update-spec`, and `delete` subresources under `admin-credential`; prove admin permission is supplemental to ordinary CRUD; deny empty/wildcard/unknown/mismatched subresources, alternate Credential-operation Role fields, and method-name aliases before Provider dispatch; malformed/oversize rejection; `leaseHandle` opacity tests (secret-canary must not appear in outer DTO or delivery routing metadata); locked/unavailable/denied/expired state tests; delivery session binding contract round-trip; zeroizing record type unit tests; delivery channel never materialized in non-delivery method tests", + "workItemId" : "ADR046-credential-002" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/provider.rs:CredentialProvider` (minimal v3 baseline)", + "dataMigration" : "Full reset; no migration from old `CredentialProvider` trait", + "dependencyOwner" : "`ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`; credential-secret-service owner", + "destination" : "`packages/d2b-provider-credential-secret-service/src/{lib.rs, controller.rs, service.rs, main.rs}` (implementation + binary); `packages/d2b-provider-credential-secret-service/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}` (hermetic Cargo integration); `packages/d2b-provider-credential-secret-service/integration/{container-service.sh, host-placement.nix, cleanup-rollback.sh}` (orchestration fixtures); `packages/d2b-provider-credential-secret-service/README.md` (all §Provider README required sections)", + "detailedDesign" : "Adapt `SecretServiceCredentialProvider` and `SecretServiceCredentialProviderFactory` to use v3 `d2b.credential.v3` service interface; replace v2 `CredentialProvider` trait impl with v3 controller/service handler; adapt `Oo7SecretServicePort` trait (retain all methods unchanged); ensure `SecretServiceOwner::Userd` placement restriction rejects system-domain and guest-agent construction; validate `collectionAlias` against provider-internal charset (not `OpaqueAzureRef`; collection aliases may include spaces); integrate with Provider resource descriptor and controller toolkit; test `credential_canary` never appears in any service response Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Target: user-domain Process (ADR-only `Process` ResourceType) under Host (ADR-only `Host` ResourceType); d2b-bus (ADR-only) routes `d2b.credential.v3` calls to this process; Credential controller reconciles status. Current v3 has no user-credential host process: v3 `d2b-userd` (`packages/d2b-userd/src/lib.rs`) is a guest exec stub only (exits 78 in service mode; `UserAttachRequest`/`UserExecSession` guest wire primitives only; evidence class: `test-only-or-preview`); no credential or keyring functionality. This integration path is fully new (ADR-only) work.", + "removalProof" : "Old `d2b-realm-provider:CredentialProvider` trait removed only after this controller and the other two Credential controllers reach full reconcile parity", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `packages/d2b-provider-credential-secret-service/src/lib.rs` (full implementation); `src/tests.rs` (full test suite including `FakeOo7Port`, lease lifecycle, locked state, canary enforcement, cardinality limits)", + "specId" : "ADR-046-resources-credential", + "specPath" : "docs/specs/ADR-046-resources-credential.md", + "validation" : "**`src/` unit** (`#[cfg(test)]` in `src/`): `Oo7SecretServicePort` trait API surface, `SecretServiceOwner` placement guard, `collectionAlias` charset, `lockPolicy` state transitions. **`tests/` Cargo integration** (`cargo test -p d2b-provider-credential-secret-service`): copied test suite from main with v3 type substitutions; add `lifecycle.rs` (acquire/refresh/revoke/inspect end-to-end with `FakeOo7Port`); `conformance.rs` (all 11 `check_provider_conformance` arms pass); `faults.rs` (locked state → `credential-provider-unavailable`, unavailable, cardinality limit); `canary.rs` (`credential_canary` and `object_path_canary` absent from every response, status field, and delivery record); `delivery.rs` (delivery-session binding contract, zeroizing buffer, replay-safe sequence); `placement.rs` (system-domain and guest-agent construction rejected). **`integration/` fixtures**: `container-service.sh` (container-backed Provider service start/stop/drain); `host-placement.nix` (user-domain Host/Process placement in runNixOSTest); `cleanup-rollback.sh` (Nix-generation removal triggers async Delete and Provider-revoke finalizer).", + "workItemId" : "ADR046-credential-003" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/credential.rs:AzureControlPlaneRef`, `OpaqueAzureRef` (v3 baseline, reachable)", + "dataMigration" : "Full reset", + "dependencyOwner" : "`ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`; credential-entra owner", + "destination" : "`packages/d2b-provider-credential-entra/src/{lib.rs, controller.rs, service.rs, main.rs}`; `packages/d2b-provider-credential-entra/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}`; `packages/d2b-provider-credential-entra/integration/{container-service.sh, guest-placement.nix, cleanup-rollback.sh}`; `packages/d2b-provider-credential-entra/README.md` (all §Provider README required sections)", + "detailedDesign" : "Adapt `EntraCredentialProvider` and `EntraCredentialProviderFactory` to v3 service; replace v2 `AgentPlacementBinding` with v3 `PlacementBinding` enum (user-agent, guest-agent only; reject host-system); validate `tenantId` config field using `OpaqueAzureRef::parse` from v3 baseline `d2b-realm-provider/src/credential.rs` (note: current v3 source field is named via `AzureControlPlaneRef`; target field name is `tenantId`); retain `EntraCredentialClient` trait unchanged; map `EntraClientError::InteractionRequired` to `credential-provider-unavailable` (not denied); enforce `EntraCredentialOwner::ExactConsumer` so only the declared `consumerRef` may acquire; enforce canonical Credential verb/subresource admission, provider-visible Endpoint plus exact consumer policy, and audit-only Credential identity as frozen in the dedicated Entra dossier Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "User-domain or system-domain Process under Guest; d2b-bus routing; Credential controller", + "removalProof" : "Same as ADR046-credential-003", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `packages/d2b-provider-credential-entra/src/lib.rs` (full implementation); `src/tests.rs` (full test suite including `FakeEntraClient`, `credential_canary`/`endpoint_canary`, interaction-required, colocated-consumer, generation-mismatch tests)", + "specId" : "ADR-046-resources-credential", + "specPath" : "docs/specs/ADR-046-resources-credential.md", + "validation" : "**`src/` unit**: `EntraCredentialClient` trait API, `OpaqueAzureRef::parse` on `tenantId`, `EntraCredentialOwner::ExactConsumer` guard, `EntraClientState` transitions. **`tests/` Cargo integration**: `lifecycle.rs` (acquire/refresh/revoke/inspect with `FakeEntraClient`); `conformance.rs` (all conformance arms, canonical Endpoint visibility/consumer policy, and exact Role verb/subresource mapping); `faults.rs` (interaction-required → unavailable, generation-mismatch, colocated-consumer and mismatched Role-subresource rejection); `canary.rs` (secret plus Credential name/ResourceRef/UID/digest canaries absent from status/errors/logs and every OTEL Resource/span attribute/metric label; authorized audit retains only `resource_name_digest`); `delivery.rs` (delivery-session binding, zeroizing, replay-safe); `placement.rs` (host-system placement rejected). **`integration/` fixtures**: `container-service.sh`; `guest-placement.nix` (user-domain and system-domain Process on Guest in runNixOSTest); `cleanup-rollback.sh`.", + "workItemId" : "ADR046-credential-004" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/credential.rs:ManagedIdentityRef` (v3 baseline, reachable); `d2bd/src/lib.rs:managed_identity_client_id` (reachable ACA config); `d2b-provider-aca/src/lib.rs:managed_identity_client_id` (reachable)", + "dataMigration" : "Full reset; `d2b-provider-aca` managed_identity_client_id config field migrated to a Credential resource reference in the v3 ACA Provider config", + "dependencyOwner" : "`ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`; credential-managed-identity owner", + "destination" : "`packages/d2b-provider-credential-managed-identity/src/{lib.rs, controller.rs, service.rs, main.rs}`; `packages/d2b-provider-credential-managed-identity/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}`; `packages/d2b-provider-credential-managed-identity/integration/{container-service.sh, host-guest-placement.nix, aca-credential-ref.sh, cleanup-rollback.sh}`; `packages/d2b-provider-credential-managed-identity/README.md` (all §Provider README required sections)", + "detailedDesign" : "Adapt `ManagedIdentityCredentialProvider` to v3 service; enforce `ManagedIdentityCredentialOwner::ExactSdkConsumer`; reject user-agent placement (IMDS is machine-local, not user-session); validate `clientId` config using `OpaqueAzureRef::parse` directly from v3 baseline (note: current v3 source field is named via `ManagedIdentityRef.client_id`; target field name is `clientId`); retain `ManagedIdentityCredentialClient` trait unchanged; map `ManagedIdentityClientState::Unavailable` to `credential-provider-unavailable`; `sign-challenge` operation class returns schema-invalid immediately Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "System-domain Process under Host or Guest; d2b-bus routing; Credential controller; ACA runtime-azure-container-apps Provider may hold a reference to this Credential resource", + "removalProof" : "`d2b-provider-aca:managed_identity_client_id` raw field removed only after `credential-managed-identity` Provider controller is integrated and the ACA Provider config uses `credentialRef`", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `packages/d2b-provider-credential-managed-identity/src/lib.rs` (full implementation); `src/tests.rs` (full test suite)", + "specId" : "ADR-046-resources-credential", + "specPath" : "docs/specs/ADR-046-resources-credential.md", + "validation" : "**`src/` unit**: `ManagedIdentityCredentialClient` trait, `OpaqueAzureRef::parse` on `clientId`, `ManagedIdentityCredentialOwner::ExactSdkConsumer` guard, `imdsEndpointAlias` validation, `sign-challenge` schema-invalid fast path. **`tests/` Cargo integration**: `lifecycle.rs` (acquire/refresh/revoke/inspect with `FakeClient`); `conformance.rs`; `faults.rs` (unavailable state, colocated-consumer rejection); `canary.rs` (canary absent from all responses and delivery records); `delivery.rs`; `placement.rs` (user-agent placement rejected). **`integration/` fixtures**: `container-service.sh`; `host-guest-placement.nix` (system-domain Host and Guest placement in runNixOSTest); `aca-credential-ref.sh` (ACA Provider config uses `credentialRef`; raw `managed_identity_client_id` absent); `cleanup-rollback.sh`.", + "workItemId" : "ADR046-credential-005" + }, + { + "currentSource" : "No direct v3 current source; controller is `ADR-only`", + "dataMigration" : "None; v3 reset", + "dependencyOwner" : "`ADR046-credential-001`, `ADR046-credential-002`; `ADR046-reconcile-001`, `ADR046-reconcile-002`; Credential controller owner", + "destination" : "`packages/d2b-provider-credential-/src/controller.rs`; `packages/d2b-contracts/src/v3/credential_controller.rs`", + "detailedDesign" : "Implement Credential controller handler conforming to `ADR-046-resource-reconciliation` async loop; implement `reconcile`, `observe`, `finalize`, `drain`, and `health` handlers; implement rotation state machine (proactive/on-expiry/on-demand policies); implement `provider-revoke` finalizer execution with `revocation.onOwnerDelete` policy; implement provider-generation-change detection and revocation; implement `CredentialReady`, `RotationDue`, `ProviderUnavailable`, `LeaseRevoked` condition logic; use controller Roles with the exact allowed-operation subresource matching each `use-credential` service call, while status/finalizer writes retain their separate structural ownership checks; never add a Credential-specific Role field or coarse alias; implement bounded idempotency key derivation (Credential UID + rotationGeneration + operation class, no secret material); implement `observeInterval=30s` health check calling `InspectMetadata`; bounded retry/backpressure with typed `credential-rotation-failed` outcome; enforce `MAX_LOCAL_LEASES=256` per controller provider instance", + "integration" : "Provider controller Process → d2b-bus → `d2b.credential.v3` service in provider process → injected client/port; status updates through resource API; watch subscription on Credential, Provider, Host/Guest dependency types", + "removalProof" : "Not applicable (new controller)", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `packages/d2b-provider-toolkit/src/conformance.rs` (provider conformance pattern); `packages/d2b-provider-toolkit/src/adapter.rs` (controller toolkit pattern)", + "specId" : "ADR-046-resources-credential", + "specPath" : "docs/specs/ADR-046-resources-credential.md", + "validation" : "Controller state-machine golden vectors; rotation-policy matrix (proactive/on-demand/on-expiry × success/locked/unavailable/expired); exact Role-subresource admission matrix; finalizer execution tests; provider-generation-change revocation tests; idempotency key derivation tests; observe-interval drift detection test; canary tests confirm zero secret bytes and no Credential name/ResourceRef/UID/digest in any controller-written status or error", + "workItemId" : "ADR046-credential-006" + }, + { + "currentSource" : "`nixos-modules/assertions.nix` (secret-shape assertions, `implemented-and-reachable`). Note: `nixos-modules/options-realms.nix` line 318 contains `d2b.realms..relay.credentialRef` — this uses the current v3 **Realm** terminology (current symbol for Zone) and is a relay credential **state-directory path** reference for gateway relay provisioning, not a Zone Credential resource declaration. It is not a source for this work item. No current v3 source exists for Zone Credential resource Nix declarations (ADR-only).", + "dataMigration" : "None; v3 reset", + "dependencyOwner" : "`ADR046-credential-001`; `ADR046-identities-002` (Nix resource compiler); `ADR046-api-001` (resource API, for create/update/delete); `ADR046-reconcile-001` (activation-nixos controller); Nix integrator", + "destination" : "`nixos-modules/options-resources.nix` (generic schema-derived resource options; not type-specific), `nixos-modules/activation-nixos-cleanup.nix`", + "detailedDesign" : "**(1) Schema-derived options and eval-time validation**: implement `d2b.zones..resources. = { type = \"...\"; spec = { ... }; }` as a generic attrset option; an optional `metadata` sub-attr may contain `ownerRef` and/or presentation `labels`/`annotations`. Nix option types, defaults, and inline docs for `spec` fields are generated from the committed `ResourceTypeSchema` JSON (`docs/reference/schemas/v3/credential.json`) and the signed Provider schema — no bespoke options module is maintained separately. `metadata.name` derives from the attr key; `metadata.zone` from the Zone attr key; `apiVersion` defaults to `resources.d2bus.org/v3`; `status`/`uid`/`generation`/`revision`/timestamps/`managedBy`/`configurationGeneration` are not authored. Core assigns `metadata.managedBy = \"configuration\"` and `metadata.configurationGeneration = ` to all configuration-managed resources at create/update time; these are never authored in Nix. Eval-time assertions (applied to all entries with `type = \"Credential\"`): `spec.providerRef` resolves a Provider in the same Zone whose `credentialDomains` includes `spec.scope.domainFilter` and whose `spec.artifactId` (a sibling of `spec.config` on the Provider resource, not inside `spec.config`) resolves an artifact catalog entry of `type = \"provider\"`; `spec.audience` charset (`^[A-Za-z0-9._:/@-]+$`, max 256); `spec.rotation.proactiveWindowMs < maxLeaseLifetimeMs / 2`; `spec.consumerRef`/`scope.executionRef`/`scope.userRef` resolve declared Zone resources; duplicate `(providerRef, executionRef, userRef, audience)` tuple rejected; `contains_sensitive_shape` on all string fields; Provider-specific placement constraints; `allowedOperations` ⊆ `providerRef.supportedOperations`. **(2) Canonical JSON and bundle emission**: render `spec` attr directly to `spec` object in canonical JSON (no field renames/re-nesting); `metadata` in output contains only derived `name`/`zone` and optionally Nix-authored `ownerRef`/`labels`/`annotations`; `apiVersion` is top-level, not inside `metadata`; `finalizers` is omitted from the Nix-rendered input (core manages finalizers, never accepts them from the bundle); no management labels are emitted by Nix; sort bundle by `(type, name)`; write to `/etc/d2b/zones//resource-bundle.json` with digest. **(2b) Artifact catalog emission**: derivation-valued inputs (`d2b.artifacts.`) are compiled separately into an integrity-pinned artifact catalog (`/etc/d2b/zones//artifact-catalog.json`) with its own digest header; each entry records `id`, `type`, `sha256`, and bounded closure metadata; store paths are private catalog implementation data absent from the resource bundle, status, audit, and logs; `activation-nixos` verifies both digests before any create/update; missing or wrong-type `artifactId` references fail the NixOS build. **(3) Build-time schema validation**: validate rendered JSON against `docs/reference/schemas/v3/credential.json` and Provider-specific schema; enforce `secretRef` fields use `Credential/` refs; enforce no store paths in any resource bundle or status output; drift gate (`make test-drift`) regenerates schemas with `cargo xtask gen-schemas` and asserts `git diff --exit-code`; Nix options module drift checked in the same gate. **(4) Generation transition and cleanup contract**: activation-nixos controller verifies SHA-256 digest of both resource bundle and artifact catalog, creates/updates desired-set resources, activates without blocking on cleanup, issues async Delete for absent configuration-managed resources (those with `metadata.managedBy = \"configuration\"`), sets Degraded/Cleanup=True on removed resources; retains up to `retainedConfigurationMax` (default 3, range 1..16) prior bundles; oldest prune when count exceeded; no time-based rollback window. **(5) Configuration-managed vs controller/API isolation**: `managedBy` and `ownerRef` are orthogonal; configuration-managed and API-created resources may each carry an optional same-Zone `ownerRef` and participate in owner cascade. Cleanup checks `metadata.managedBy = \"configuration\"` before issuing Delete; resources with `metadata.managedBy = \"controller\"` or `metadata.managedBy = \"api\"` are never deleted by this path; API-created resources persist until explicit Delete and are never generation-swept.", + "integration" : "`activation-nixos` Provider creates/updates/deletes Credential resources from emitted envelopes with both the ordinary CRUD permission and the matching `admin-credential/create`, `admin-credential/update-spec`, or `admin-credential/delete` permission; Credential controller `provider-revoke` finalizer handles cleanup Deletes; owner controller reconciles children of deleted configuration-managed Credentials", + "removalProof" : "Not applicable (new module)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-credential", + "specPath" : "docs/specs/ADR-046-resources-credential.md", + "validation" : "**(eval/build)**: nix-unit golden JSON envelope for each example (spec shape, no management labels in Nix output, sort, digest); assertion-failure tests for secret-shaped audience, mismatched providerRef/domainFilter, proactiveWindow > half maxLifetime, duplicate binding tuple, unresolved refs; generated activation Role contains `admin-credential` with exact `subresources = [ \"create\" \"update-spec\" \"delete\" ]`, no coarse alias, and no undeclared Role field; permission tests prove each lifecycle action also requires its ordinary CRUD verb; artifact catalog: assertion-failure for missing `artifactId`, wrong-type `artifactId`, duplicate catalog ID; bundle + artifact catalog digest round-trip; artifact catalog store-path absence from resource bundle and status; Provider-specific schema cross-check; `make test-drift` schema drift gate. **(runtime integration in `tests/host-integration/`)**: `credential-cleanup-basic` (removed resource reaches Deleted); `credential-cleanup-nonblocking` (activation Ready before cleanup finalizer finishes); `credential-cleanup-pending-status` (Cleanup=True on removed resource, PendingCleanup=True on Provider); `credential-cleanup-stalled` (Degraded stall detection and recovery); `credential-cleanup-controller-children-preserved` (ownerRef children cleaned by Credential controller); `credential-cleanup-no-dynamic-deletion` (controller-created Credential with `managedBy = \"controller\"` not deleted); `credential-retained-generation-count` (up to retainedConfigurationMax bundles retained; rollback re-creates from retained bundle; oldest pruned when count exceeded); `credential-bundle-digest-mismatch` (tampered bundle aborts activation).", + "workItemId" : "ADR046-credential-007" + }, + { + "currentSource" : "`packages/d2b-core/src/privileges.rs:SecretAccess` (implemented-and-reachable); `d2b-realm-provider/src/error.rs:ProviderDiagnostic`/`contains_sensitive_shape` (implemented-and-reachable); `packages/d2b-contract-tests/tests/policy_observability.rs` (reachable audit policy tests)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-credential-001`, `ADR046-credential-006`; audit/OTEL integrator", + "destination" : "`packages/d2b-provider-credential-/src/audit.rs`, `telemetry.rs`; `packages/d2b-contract-tests/tests/credential_audit.rs`", + "detailedDesign" : "Implement audit record emission for all credential service methods and controller events using the field set defined in §Audit, with Credential identity represented only by the authorized bounded `resource_name_digest` after authorization and no identity-bearing record elicited by a denied request; implement OTEL span/metric emission using the closed semantic label set in §OTEL and metrics, with expiry reported as a provider/placement aggregate and no Credential resource name, ResourceRef, UID, digest, derived identity token, Zone/resource-name-derived label, or non-allowlisted OTEL Resource attribute; retain applicable generic collector-allowlisted Resource attributes (`d2b.zone`, `d2b.provider`, `d2b.component`, and service fields); implement `contains_sensitive_shape` checks in all string fields of audit records and metric label values (adapted from `d2b-realm-provider/src/error.rs:contains_sensitive_shape`); add canary-enforcement tests that verify `\"secret-canary\"`, `\"entra-token-canary\"`, `\"managed-identity-canary\"`, Credential name/ref/UID/digest canaries, and Zone name values never appear in any metric label, span attribute, log line, status field, error, or collector diagnostic, and that Credential identity canaries are also absent from OTEL Resource attributes", + "integration" : "Credential controller and service handlers emit audit records and telemetry through Zone audit/OTEL paths", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-credential", + "specPath" : "docs/specs/ADR-046-resources-credential.md", + "validation" : "Canary tests across all three Provider crates; authorized audit record field-presence tests require `resource_name_digest` and reject raw Credential name/ResourceRef/UID, while denied-request tests emit no identity-bearing audit; structural metric descriptor tests assert exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `credential_ref`, `credential_uid`, `credential_digest`, `resource_name_digest`, and every resource-name-derived key; Credential name/ref/UID/digest canaries are absent from every status field, error, log/Debug line, collector diagnostic, OTEL Resource attribute, span attribute, and metric label; Zone-name canaries are absent from spans and labels while resource-attribute tests preserve the generic collector allowlist including `d2b.zone`, `d2b.provider`, `d2b.component`, and service fields; complete Credential metric/span frames pass the shared collector ingress validator, while adding `d2b.credential.name` or any Credential identity key/value rejects the whole frame", + "workItemId" : "ADR046-credential-008" + }, + { + "currentSource" : "v3 evidence baseline plus parent/spec contradictions", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR 0046 integrator", + "destination" : "`docs/specs/ADR-046-decision-register.md`", + "detailedDesign" : "Record every evidence-underdetermined choice before dependent spec work proceeds", + "integration" : "Parent decision summary and all affected specs link the decision ID", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-decision-register", + "specPath" : "docs/specs/ADR-046-decision-register.md", + "validation" : "Zero unresolved entries before pre-panel review; manifest/link consistency", + "workItemId" : "ADR046-decisions-001" + }, + { + "currentSource" : "none in this repository; `Makefile` heavy-lane targets do not yet exist", + "dataMigration" : "None — net-new tooling", + "dependencyOwner" : "`ADR046-W0`; delivery-tooling integrator", + "destination" : "`packages/xtask/src/heavy_gate.rs`; `Makefile` targets `heavy-check`, `heavy-test-integration`, `heavy-test-host-integration`, `heavy-test-hardware`, `heavy-cargo-test`, `heavy-flake-check`", + "detailedDesign" : "Two-slot per-UID OFD-locked semaphore, 250 ms nonblocking retry up to 30 minutes, fail-closed on unsupported locking, duplicated locked-FD handoff to child, wrapper-owned group-signal/reap, as specified in §11 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt paths/crate names to this repository's `packages/xtask` layout.", + "integration" : "Every heavy lane in §10.4/§10.10/§10.11 routes through this one binary; no wave adds a second lock mechanism", + "removalProof" : "Not applicable (net-new; nothing to remove)", + "reuseAction" : "adapt", + "reuseSource" : "sibling-lineage `cargo xtask heavy-gate` implementation (per D001/D041 unrestricted-reuse policy)", + "specId" : "ADR-046-validation-and-delivery", + "specPath" : "docs/specs/ADR-046-validation-and-delivery.md", + "validation" : "Unit tests for slot acquisition/timeout/fail-closed paths; integration test spawning two concurrent heavy-gate invocations and asserting the second blocks until the first releases", + "workItemId" : "ADR046-delivery-001" + }, + { + "currentSource" : "none in this repository", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-W0`; delivery-tooling integrator", + "destination" : "`packages/xtask/src/delivery/snapshot.rs`", + "detailedDesign" : "Binds base/head OIDs, dependency graph, repository set into `candidate_id`/`content_id`/`snapshot_sha256` per §12.1 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt.", + "integration" : "Called by the integrator immediately after PR opening (§13.1), before any validator/panel lane starts", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "sibling-lineage `cargo xtask delivery wave snapshot` implementation", + "specId" : "ADR-046-validation-and-delivery", + "specPath" : "docs/specs/ADR-046-validation-and-delivery.md", + "validation" : "Unit tests asserting identical inputs produce identical digests and any single-byte content change produces a different `content_id`", + "workItemId" : "ADR046-delivery-002" + }, + { + "currentSource" : "none in this repository", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-delivery-002`; delivery-tooling integrator", + "destination" : "`packages/xtask/src/delivery/validate_import.rs`; external candidate-ID-addressed evidence directory (never under Git)", + "detailedDesign" : "Imports CI/local/host validator command/result evidence, keyed by `candidate_id`, per §12.2 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt.", + "integration" : "Consumed by `wave seal` (§ADR046-delivery-005) as one of the seal's required inputs", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "sibling-lineage `cargo xtask delivery wave validate-import` implementation", + "specId" : "ADR-046-validation-and-delivery", + "specPath" : "docs/specs/ADR-046-validation-and-delivery.md", + "validation" : "Test asserting evidence for a stale `candidate_id` is rejected; test asserting raw command output never lands in a tracked file", + "workItemId" : "ADR046-delivery-003" + }, + { + "currentSource" : "`docs/specs/README.md`'s described-but-not-yet-generated `ADR-046-spec-set.json`/`ADR-046-work-items.json` contract", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-delivery-002`; spec-set integrator", + "destination" : "`packages/xtask/src/gen_spec_set.rs`; `docs/specs/ADR-046-spec-set.json`, `docs/specs/ADR-046-work-items.json`", + "detailedDesign" : "Enumerates every `docs/specs/ADR-046-*.md` and `docs/specs/providers/ADR-046-provider-*.md` file, its metadata table, bytewise-sorted `workItemPrefixes` registry, content digest, and every `### ADR046--` work item, per §8. Each prefix belongs globally to exactly one member, and generation resolves ownership only through the registry rather than by splitting IDs or filenames. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (new generator, following the existing `xtask gen-schemas`/`gen-nix-options` pattern already used for other generated artifacts).", + "integration" : "`make test-drift` gains a row running this generator and `git diff --exit-code`; every wave's exit criteria (§4) require it committed as the wave's last commit", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "none required — this generator is specific to the `docs/specs/ADR-046-*` manifest shape", + "specId" : "ADR-046-validation-and-delivery", + "specPath" : "docs/specs/ADR-046-validation-and-delivery.md", + "validation" : "Golden-fixture test against a small synthetic spec directory; drift test against the real `docs/specs/` tree", + "workItemId" : "ADR046-delivery-004" + }, + { + "currentSource" : "none in this repository; this repository's existing `AGENTS.md` panel-review process is host-local script tooling (`/etc/nixos/scripts/panel-review.{md,sh}`), not a candidate-bound `xtask` subcommand", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-delivery-002`, `ADR046-delivery-003`; panel-tooling integrator", + "destination" : "`packages/xtask/src/delivery/panel.rs`", + "detailedDesign" : "`panel-request` writes the candidate-bound request naming the exact ten roles and required model; `panel-attest` validates a directory of exactly ten strict 13-field records, rejecting wrong model/candidate binding, duplicate provider/run provenance, or inconsistent `signoff`/`recommendations`, per §12.3 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt to bind the fixed `gemini-3.1-pro-preview`/`github-copilot` model/provider pair and this repository's existing ten-role roster (§12.3).", + "integration" : "Every wave's exit criteria (§4) require ten unanimous attested records before `wave seal`", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "sibling-lineage `cargo xtask delivery wave panel-request`/`panel-attest` implementation", + "specId" : "ADR-046-validation-and-delivery", + "specPath" : "docs/specs/ADR-046-validation-and-delivery.md", + "validation" : "Unit tests for every rejection class (wrong model, missing role, duplicate run_id, `signoff:true` with non-empty `recommendations`); integration test with ten synthetic valid records passing", + "workItemId" : "ADR046-delivery-005" + }, + { + "currentSource" : "none in this repository", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-delivery-002`, `ADR046-delivery-004`, `ADR046-delivery-005`; delivery-tooling integrator", + "destination" : "`packages/xtask/src/delivery/{seal,eligibility,history_proof}.rs`", + "detailedDesign" : "`seal` requires all ten panel records unanimous and bound to the same candidate/content/snapshot digests plus every validator lane passing; `merge-eligibility` checks each stacked PR's current base/head against the sealed OIDs or a passing history-proof; `history_proof` verifies byte-identical integrated content/generated artifacts/dependency diff/repository set across a rebase, per §12.4/§12.6 Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged, then adapt.", + "integration" : "`make check` gains no new required step for ordinary contributors; this tooling is invoked only by the wave integrator per §4/§13", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "sibling-lineage `cargo xtask delivery wave seal`, `merge-eligibility`, and history/byte-identity proof implementation", + "specId" : "ADR-046-validation-and-delivery", + "specPath" : "docs/specs/ADR-046-validation-and-delivery.md", + "validation" : "Unit tests for seal rejection on any missing/mismatched record; integration test proving a history-only rebase with identical content passes `history_proof` and reuses panel evidence, while any content change fails it", + "workItemId" : "ADR046-delivery-006" + }, + { + "currentSource" : "none in this repository; this codebase's existing `tests/tools/` timing logs (`d2b-static-timing.$$/`) are ad hoc, not a candidate-bound ledger", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-W0`; delivery-tooling integrator", + "destination" : "`packages/xtask/src/test_runtime_ledger.rs`; a `make`-invokable timing gate reusing `make test-rust`/Layer-1 shard targets", + "detailedDesign" : "Measures execution-only time (after build, warm cache) per test/crate/shard against §10.16 budgets, records the reference runner/repetitions/p95, reports the top slow tests, applies a historical regression threshold, and emits a machine-readable CI artifact; the placement lint rejects a hermetic-tier test that sleeps, spawns a process, or touches network/containers/DBus/systemd/broker/Nix/KVM/hardware/live cloud, and the deterministic-clock/sleep lint rejects wall-clock sleep/retry in `src/`/`tests/`", + "integration" : "Every wave's entry/exit criteria (§4) consume the ledger artifact; `make test-rust` and Layer-1 shards run concurrently; no new top-level `tests/*.sh` gate is added", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "existing `xtask`/`libtest --format=json` timing output; no new test framework", + "specId" : "ADR-046-validation-and-delivery", + "specPath" : "docs/specs/ADR-046-validation-and-delivery.md", + "validation" : "Policy self-tests: an intentional slow/sleep/process/network hermetic test is rejected; a synthetic timing regression fails the gate; parallel isolation holds under shuffled/parallel execution; a retired legacy selector is absent from `tests/layer1-jobs.json`, closed gate manifests, and CI shards", + "workItemId" : "ADR046-delivery-007" + }, + { + "currentSource" : "none in this repository; launch order and parallelism were previously derived only from this spec's §3/§6 prose", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "`ADR046-streamline-001`, `ADR046-W0`; delivery-tooling integrator", + "destination" : "`docs/specs/ADR-046-implementation-graph.json`, `docs/specs/ADR-046-implementation-graph.md` (generated by `ADR046-streamline-001`'s `xtask implementation-graph`); the artifact contract, generation, validation, and ready-wave query are owned by §3.5 of this spec", + "detailedDesign" : "Owns the D095 implementation-graph contract: `artifactKind`/`schemaVersion`/`adr`/`status`; one node per member spec and per work item mapped exactly once to a `W0`–`W7` wave and a file-disjoint `parallelGroup`, with `owner`/`destinations`/`entryContracts`/`prerequisites`/`blockers`/`exitGate`/`topologicalRank`; work-item nodes additionally embed the manifest's exact `detailedDesign` and `validation` text byte-for-byte; typed `spec-depends-on`/`shared-contract`/`work-item-depends-on`/`implements-spec`/`file-overlap-order` edges; the §3.5.1 ready-wave query; and the anti-serialization invariant that every ready file-disjoint group launches concurrently while a same-wave dependency is a prep barrier, not whole-wave serialization. The graph is a generated non-member artifact and does not change the 55-member `ADR-046-spec-set.json` count. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (D095 artifact contract).", + "integration" : "Consumed by §4 wave entry/exit and §6 anti-serialization checks and by `ADR046-streamline-013`; a `tests/unit/gates/` drift gate regenerates and `git diff --exit-code`s the graph after any spec/work-item edit", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "`ADR-046-spec-set.json`, `ADR-046-work-items.json`, and §3.1–§3.4/§3.5 of this spec; no new framework", + "specId" : "ADR-046-validation-and-delivery", + "specPath" : "docs/specs/ADR-046-validation-and-delivery.md", + "validation" : "Every 55 spec node and every work item present exactly once; all edge endpoints resolve; graph acyclic; waves monotonic (dependencies earlier or explicit same-wave prep barrier); parallel groups claim no ordering absent a dependency/file-overlap edge; deterministic JSON with no timestamps/host paths; every Mermaid node ID valid; the ready-wave query returns the expected concurrently-launchable groups on a seeded fixture", + "workItemId" : "ADR046-delivery-008" + }, + { + "currentSource" : "The fail-closed completeness, identity, and closed-action contract in `docs/specs/README.md`; the Proposed tree has no checked-in generator or policy test", + "dataMigration" : "None — documentation/build-policy contract only", + "dependencyOwner" : "`ADR046-delivery-004`, `ADR046-delivery-008`; spec-set policy-test owner", + "destination" : "`packages/xtask/src/gen_spec_set.rs`; `packages/d2b-contract-tests/tests/policy_adr046_work_items.rs`; generated spec-set, work-item, and implementation-graph drift checks", + "detailedDesign" : "Parse every normative member's exact level-three `### ADR046--` headings and tables; `##` or `####` item declarations are invalid. Require an exact Markdown/manifest bijection; exact `specId` and `specPath`; a bytewise-sorted, nonempty `workItemPrefixes` list for each item-owning member; global one-member ownership for every registered prefix; registry-based ID ownership; three-digit nonzero ordinals; every mandatory field exactly once and nonempty; one closed scalar `reuseAction`; and `reuseSource: null` for `create`. Reject dropped, extra, malformed, wrong-level, duplicate, ambiguous, heuristic-split, unregistered-prefix, or unconsumed items before writing any artifact. Validate all dependency endpoints, DAG acyclicity, wave monotonicity, and single-wave parallel groups before atomically publishing all generated files.", + "integration" : "`make test-policy` runs negative fixtures; `make test-drift` regenerates all ADR 0046 artifacts and requires a clean diff; `ADR046-delivery-008` consumes only a manifest that passed this policy", + "removalProof" : "Not applicable; the policy remains the permanent generated-artifact closure gate", + "reuseAction" : "adapt", + "reuseSource" : "`ADR046-delivery-004` generator shape and the existing `d2b-contract-tests` fixture-driven policy-test pattern", + "specId" : "ADR-046-validation-and-delivery", + "specPath" : "docs/specs/ADR-046-validation-and-delivery.md", + "validation" : "Fixtures fail for a dropped heading, `##`/`####` item heading, extra manifest row, duplicate ID, duplicate cross-member prefix, unsorted/empty required prefix registry, wrong owner/path/prefix, heuristic-only prefix match, two-digit/zero ordinal, missing/duplicate mandatory field, free-form/compound action, `create` with a reuse source, dangling dependency, cyclic DAG, backward-wave dependency, and cross-wave parallel group; the exact 55-spec real tree passes with every item once", + "workItemId" : "ADR046-delivery-009" + }, + { + "currentSource" : "`packages/d2b-contracts/src/security_key.rs` (SecurityKeyStatusResponse, SecurityKeySession, SecurityKeyLeaseState, SecurityKeyVmSessionState DTOs; implemented-and-reachable), `usbip.rs`, `broker_wire.rs`; `packages/d2b-core/src/privileges_w3.rs` (W3BrokerOperation: SecurityKeyOpenDevice, SecurityKeyApplyUdevRules, UsbipBindFirewallRule — implemented-and-reachable); `packages/d2b-core/src/manifest_v04.rs` VmEntry device fields (tpm, usbip_yubikey, security_key, graphics — old Workload manifest, generated-or-eval-contract)", + "dataMigration" : "Full reset; no v2 device object import", + "dependencyOwner" : "W0 shared contract root; `d2b-contracts`", + "destination" : "`packages/d2b-contracts/src/v3/device.rs`", + "detailedDesign" : "Device ResourceType schema (spec/status/conditions/claims/inventory); closed-set error codes; Device RBAC verbs; broker operation effect-limit constants; shared Device telemetry contract requires fixed semantic metric labels with no Zone/resource-name-derived identity and retains `d2b.zone`/`d2b.provider` only as OTEL resource attributes. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Provider dossiers, resource API/store, CLI status surfaces", + "removalProof" : "Old ProcessRole/DTO branches retained until Provider integrations are live", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-device", + "specPath" : "docs/specs/ADR-046-resources-device.md", + "validation" : "Schema golden vectors; unknown-field denial; exclusive/shared conflict rejection; arbitration/maxClaims invariant; cross-Provider structural descriptor test asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus Device/Zone-name canary absence while preserving `d2b.zone` resource attributes", + "workItemId" : "ADR046-device-001" + }, + { + "currentSource" : "`packages/d2b-host/src/swtpm_argv.rs`; `packages/d2b-priv-broker/src/ops/swtpm_dir.rs`; `nixos-modules/components/tpm.nix`", + "dataMigration" : "State dir and tamper markers preserved across reset", + "dependencyOwner" : "ADR046-device-001; device-tpm provider owner", + "destination" : "`packages/d2b-provider-device-tpm/src/` (controller, swtpm runner, state-dir logic); `packages/d2b-provider-device-tpm/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-tpm/integration/` (container/Host scenarios); `packages/d2b-provider-device-tpm/README.md`", + "detailedDesign" : "Device spec/status; flush EphemeralProcess → swtpm Process sequencing; state-dir hardening; tamper-marker; finalizer non-deletion of Volume; Nix emitter; all four required crate paths present (see \"Provider crate layout\") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Zone resource store; Process controller; Volume lifecycle", + "removalProof" : "ProcessRole::Swtpm and SwtpmPreStartFlush removed after parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-device", + "specPath" : "docs/specs/ADR-046-resources-device.md", + "validation" : "`src/`: swtpm argv golden, state-dir, flush sequencing, finalizer no-delete; `tests/`: `controller_state_machine.rs`, `conformance.rs`, `fault_swtpm_missing.rs`; `integration/`: `provision_and_reboot/`, `tamper_marker_survives/`, `finalizer_no_delete/`; workspace policy check: `make test-policy` passes with all four paths present", + "workItemId" : "ADR046-device-002" + }, + { + "currentSource" : "`packages/d2b-contracts/src/usbip.rs` (USBIP DTOs, SYSFS_BUS_ID_MAX, bus-ID validation — implemented-and-reachable); `packages/d2b-core/src/bundle_resolver.rs` USBIP intents; `packages/d2b-core/src/privileges.rs` authz rows; `packages/d2bd/src/usbip_state_machine.rs` (typed per-busid bring-up state machine, step order: modprobe→lock→withhold→firewall→backend→bind→proxy — implemented-and-reachable); `packages/d2bd/src/usbipd_perenv_autostart.rs` (per-env usbipd daemon autostart — implemented-and-reachable); `packages/d2bd/src/usbip_reconcile_state.rs` (restart-safe reconciler state model — implemented-but-unwired); old Workload Nix option: `nixos-modules/options-realms-workloads.nix` `d2b.vms..usbip.*` (generated-or-eval-contract); `nixos-modules/components/usbip.nix`", + "dataMigration" : "None; full reset", + "dependencyOwner" : "ADR046-device-001; device-usbip provider owner", + "destination" : "`packages/d2b-provider-device-usbip/src/` (controller, daemon Process, bind/unbind EphemeralProcess, firewall); `packages/d2b-provider-device-usbip/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-usbip/integration/` (container/Host scenarios); `packages/d2b-provider-device-usbip/README.md`", + "detailedDesign" : "Device spec/status; bus ID validation; firewall rule ownership-marker; bind/unbind EphemeralProcess; per-Device daemon Process (owned by device-usbip; Network supplies dependency/firewall interface); Nix emitter; all four required crate paths present (see \"Provider crate layout\") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Zone resource store; broker `UsbipBindFirewallRule { action: Ensure | Remove }`; nftables marker", + "removalProof" : "ProcessRole::Usbip removed after parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-device", + "specPath" : "docs/specs/ADR-046-resources-device.md", + "validation" : "`src/`: bus ID corpus, firewall marker format, EphemeralProcess creation; `tests/`: `arbitration_conflict.rs`, `conformance.rs`, `firewall_marker.rs`, `explicit_attach_split.rs`; `integration/`: `arbitration_conflict/`, `busid_bind_cycle/`, `network_firewall_coexistence/`; workspace policy check: `make test-policy` passes with all four paths present", + "workItemId" : "ADR046-device-003" + }, + { + "currentSource" : "`packages/d2b-contracts/src/security_key.rs` (DTOs — implemented-and-reachable); `packages/d2b-core/src/privileges_w3.rs` (W3BrokerOperation — implemented-and-reachable); **KEY: relay is in d2bd** — `packages/d2bd/src/security_key.rs` (CTAPHID relay: CID translation, SO_PEERCRED, hidraw async fd, accept loop — implemented-and-reachable) and `packages/d2bd/src/lib.rs:start_sk_accept_loop` (ProcessRole::SecurityKeyFrontend dispatch — implemented-and-reachable); **guest binary**: `packages/d2b-sk-frontend/src/` (static UHID frontend — implemented-and-reachable); old Workload Nix option: `nixos-modules/options-realms-workloads.nix` `d2b.vms..security_key.*`; `nixos-modules/components/security-key-guest.nix`", + "dataMigration" : "None; full reset", + "dependencyOwner" : "ADR046-device-001; device-security-key provider owner", + "destination" : "`packages/d2b-provider-device-security-key/src/` (controller, relay Process, guest frontend Process, lease/session ring); `packages/d2b-provider-device-security-key/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-security-key/integration/` (container/Host/Guest scenarios); `packages/d2b-provider-device-security-key/README.md`", + "detailedDesign" : "Device spec/status; unprivileged relay Process (`device--sk-relay`); guest frontend Process (`device--sk-frontend`, `executionRef: Guest/`); ceremony/CID/lease/session ring (max 1 session per Device); broker hidraw-only access; mandatory Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` claim shared with every USB Provider before effects; Nix emitter; all four required crate paths present (see \"Provider crate layout\") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Zone resource store; broker `SecurityKeyOpenDevice`/`SecurityKeyApplyUdevRules`; Guest frontend module", + "removalProof" : "ProcessRole::SecurityKeyFrontend removed after parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-device", + "specPath" : "docs/specs/ADR-046-resources-device.md", + "validation" : "`src/`: lease transitions, session ring eviction, broker op path-free, CID round-trip; `tests/`: `lease_state_machine.rs`, `session_ring.rs`, `mutual_exclusion.rs` proves security-key and USB implementations resolve one token to a byte-identical physical backing key and the loser receives `physical-usb-backing-conflict` before any effect, `conformance.rs`, `guest_frontend_process.rs`; `integration/`: `lease_acquire_cancel/`, `session_ring_capacity/`, `guest_frontend_connect/`; workspace policy check: `make test-policy` passes with all four paths present", + "workItemId" : "ADR046-device-004" + }, + { + "currentSource" : "`packages/d2b-host/src/gpu_argv.rs`, `video_argv.rs`; `packages/d2b-core/src/bundle_resolver.rs` Gpu/GpuRenderNode/Video; `nixos-modules/components/graphics.nix`, `video/guest.nix`", + "dataMigration" : "None; full reset", + "dependencyOwner" : "ADR046-device-001; device-gpu provider owner", + "destination" : "`packages/d2b-provider-device-gpu/src/` (controller, GPU/render-node/video worker Processes, broker token set); `packages/d2b-provider-device-gpu/tests/` (hermetic Cargo integration); `packages/d2b-provider-device-gpu/integration/` (container/Host/Guest scenarios); `packages/d2b-provider-device-gpu/README.md`", + "detailedDesign" : "Combined Device spec/status; GPU worker Process (`device--gpu`, exclusive); render-node Process (`device--render-node`, exclusive default, shared when explicit); video-decoder Process (`device--video`); broker token set; wire-contract constants; shared render-node arbitration enforcement; Nix emitter; all four required crate paths present (see \"Provider crate layout\") Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Zone resource store; broker `SpawnRunner`/`OpenDevice`; Display Provider device consumption", + "removalProof" : "ProcessRole::Gpu, GpuRenderNode, Video removed after parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-device", + "specPath" : "docs/specs/ADR-046-resources-device.md", + "validation" : "`src/`: process role selection, wire-constant snapshot, render-node vs full-GPU branching; `tests/`: `combined_reconcile.rs`, `render_node_enforcement.rs`, `wire_constant_snapshot.rs`, `conformance.rs`; `integration/`: `gpu_worker_start/`, `render_node_shared/`, `video_dependency/`; workspace policy check: `make test-policy` passes with all four paths present", + "workItemId" : "ADR046-device-005" + }, + { + "currentSource" : "`nixos-modules/components/tpm.nix`, `usbip.nix`, `security-key-guest.nix`, `video/guest.nix`, `graphics.nix`; `nixos-modules/assertions.nix`; **old Workload Nix namespace**: `nixos-modules/options-realms-workloads.nix` (`d2b.vms..tpm.enable`, `d2b.vms..graphics.enable`, `d2b.vms..usbip.*` — generated-or-eval-contract; v3 replaces `d2b.vms.*` with `d2b.zones.*`)", + "dataMigration" : "Consumers migrate from per-VM options to Zone Device declarations; data migration guide references \"Nix configuration\" section migration table", + "dependencyOwner" : "ADR046-device-001 through ADR046-device-005; Nix integrator", + "destination" : "`nixos-modules/resources-device.nix`; `nixos-modules/bundle-artifacts.nix` (bundle emission for resource store); `nixos-modules/assertions.nix` (six eval-time device assertions)", + "detailedDesign" : "Nix authoring shape `d2b.zones..resources. = { type = \"Device\"; metadata.ownerRef = ...; spec = { ...exact ResourceSpec fields... }; };` as specified in \"Nix configuration\" section; `metadata.name`/`metadata.zone`/`apiVersion` derived automatically; `status` and Core management fields (`managedBy`, `configurationGeneration`, uid, generation, revision, timestamps, finalizers) omitted from emitted JSON; `spec` field names/types/defaults identical to ResourceTypeSchema with no renaming; per-Provider `spec.provider.settings` validated against signed Provider schema; no `artifactId` or store-path fields in Device `spec.provider.settings` — binary paths are resolved from Provider package closures; Credential-ref enforcement; artifact catalog emitted as a separate private integrity-pinned map (ID→type/digest/closure) by its own emitter; six eval-time validation assertions; canonical sorted-key full resource-envelope JSON emission (`apiVersion`, `type`, `metadata`, `spec` only); Zone resource bundle with `contentDigest` as specified in \"Zone resource bundle/generation\" section; Core sets `metadata.managedBy=configuration` and `metadata.configurationGeneration` at activation", + "integration" : "Resource store Nix emitter; artifact catalog emitted separately by Provider/system resource emitter (not by this emitter); device resource JSON output; Zone generation object including `priorGeneration`, `pendingDeletion`, `cleanupStatus` fields; cleanup contract logic belongs in the Core configuration handler (`packages/d2b-core-controller/src/configuration.rs`) consuming the generation from this emitter", + "removalProof" : "Nix option `d2b.vms..tpm.enable` etc. retained until v3 reset", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-device", + "specPath" : "docs/specs/ADR-046-resources-device.md", + "validation" : "nix-unit: `device-tpm-eval.nix`, `device-usbip-eval.nix`, `device-security-key-eval.nix`, `device-gpu-eval.nix`, `device-schema-validation.nix`, `device-gen-cleanup-eval.nix`, `device-bundle-canonical.nix`, `device-inline-secret-rejected.nix`, `device-artifact-catalog.nix`; contract tests: `device_resource_schema.rs`, `device_bundle_canonical.rs`, `device_gen_cleanup.rs`", + "workItemId" : "ADR046-device-006" + }, + { + "currentSource" : "None (new work; no equivalent in v3 baseline or main a1cc0b2d)", + "dataMigration" : "Full d2b 3.0 reset; no prior generation cleanup state or v2 Device resource import", + "dependencyOwner" : "ADR046-device-006; Zone runtime implementer", + "destination" : "`packages/d2b-core-controller/src/configuration.rs`; `packages/d2b-contract-tests/tests/device_gen_cleanup.rs`", + "detailedDesign" : "Implement the cleanup contract described in \"Zone generation and cleanup\": (1) on new generation activation, diff `resources` against prior generation's `resources` by (type, name) — resources absent from new generation that have `managedBy=configuration` go into `pendingDeletion`; (2) Zone phase transitions to `Degraded/pending-cleanup` until all items in `pendingDeletion` reach terminal Delete; (3) items in `pendingDeletion` with `managedBy=controller` or `managedBy=api` are rejected with `cleanup-config-ownership-mismatch`; (4) `managedBy=controller` and `managedBy=api` resources are never touched by generation cleanup — `cleanup-controller-resource-protected` emitted if attempted; `managedBy=api` resources persist until explicit Delete; (5) prior generations retained by count: default 3, range 1..16, configured via `d2b.zones..retainedGenerations`; (6) each deletion is non-blocking; (7) finalizer-stuck timeout emits `cleanup-finalizer-stuck` and leaves Zone in `Degraded/pending-cleanup`; (8) all deletions emit `config-resource-deletion-requested` audit record with digested resource identity", + "integration" : "Consumes Zone resource bundle from ADR046-device-006 emitter; drives Device Provider finalizers via normal resource-Delete protocol; feeds Zone status conditions `GenerationCleanPending` and `GenerationCleanError`", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-device", + "specPath" : "docs/specs/ADR-046-resources-device.md", + "validation" : "Fast hermetic (D094): in-process Rust selectors under the owning crate `tests/` — `packages/d2b-core-controller/tests/gen_cleanup.rs`, `packages/d2b-core-controller/tests/gen_cleanup_controller_protected.rs`, `packages/d2b-core-controller/tests/gen_cleanup_audit.rs` — driven by a fake Zone store/clock with deterministic ordering and no wall-clock sleep; slower coverage reuses the existing integration directory scenarios `tests/integration/containers/device-gen-cleanup.sh`, `device-controller-resource-protected.sh`, and `device-gen-cleanup-audit.sh`; feasibility proof: generation cleanup with fake Zone runtime. No new top-level shell gate is added.", + "workItemId" : "ADR046-device-007" + }, + { + "currentSource" : "`packages/xtask/src/main.rs` (existing `gen-*` and `check-*` commands — implemented-and-reachable); `packages/d2b-contract-tests/tests/workspace_policy.rs` (existing workspace policy tests — implemented-and-reachable)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-device-002 through ADR046-device-005; workspace/tooling maintainer", + "destination" : "`packages/xtask/src/main.rs` (`check-provider-layout` subcommand); `packages/d2b-contract-tests/tests/workspace_policy.rs` (provider-layout policy assertions)", + "detailedDesign" : "Add `cargo xtask check-provider-layout`: enumerate workspace members matching `d2b-provider-*`; for each, assert `src/`, `tests/`, `integration/`, and `README.md` all exist relative to the crate root; report all missing paths before failing; no opt-out flag. Add companion test in `workspace_policy.rs` that asserts the same invariant against the static crate list in `Cargo.toml`. Wire `cargo xtask check-provider-layout` into `make test-policy` alongside existing workspace naming and sort checks. The check must also run in CI as part of the policy gate. Primary reuse disposition: `adapt`. Preserved source-plan detail: extend.", + "integration" : "`make test-policy`; `make check`; GitHub CI policy job", + "removalProof" : "N/A (new tooling; not removed)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-device", + "specPath" : "docs/specs/ADR-046-resources-device.md", + "validation" : "Policy gate passes once all four Device Provider crates exist with required paths; gate fails predictably when any path is removed from a Provider crate fixture; test fixture crate with one missing path must produce a named error identifying the exact missing path", + "workItemId" : "ADR046-device-008" + }, + { + "currentSource" : "No existing provider crate; baseline TPM behavior is in `nixos-modules/components/tpm.nix`, `packages/d2b-host/src/swtpm_argv.rs`, and broker/daemon swtpm paths listed in §18", + "dataMigration" : "None — scaffold only; TPM state migration is covered by later Volume/Nix work per §17.3", + "dependencyOwner" : "P0; unblocked; owner: `d2b-provider-device-tpm` crate", + "destination" : "packages/d2b-provider-device-tpm/{src/,tests/,integration/README.md,README.md}; Cargo workspace membership", + "detailedDesign" : "Crate scaffold: create `packages/d2b-provider-device-tpm/` with `src/`, `tests/`, `integration/README.md`, and `README.md`; add it to the Cargo workspace; workspace policy test must pass. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new crate scaffold; later items adapt baseline TPM behavior into the new Provider boundary.", + "integration" : "Workspace policy and Cargo consume the new crate; all controller, effect-port, resource-builder, status, Nix, and integration work lands under this scaffold.", + "removalProof" : "None — net-new crate scaffold; legacy TPM owners are removed only by later parity/removal items", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "Workspace policy test for required crate paths and Cargo workspace membership", + "workItemId" : "ADR046-device-tpm-001" + }, + { + "currentSource" : "`PrepareSwtpmDir` in `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` and `SpawnRunner { role: Swtpm }` in `packages/d2b-priv-broker/src/ops/spawn_runner.rs` remain privileged executors, but the controller must not import broker crates", + "dataMigration" : "Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state.", + "dependencyOwner" : "P0; blocked by ADR046-device-tpm-001; owner: device-tpm effect boundary", + "destination" : "packages/d2b-provider-device-tpm/src/{effect_port.rs,effect_impl.rs}; packages/d2b-provider-device-tpm/tests/effect_fake.rs", + "detailedDesign" : "TpmEffectPort and FakeTpmEffectPort: implement the effect trait, typed TPM EndpointRef handoff, and fake test port. Prove non-test files contain no `use d2b_priv_broker::` and the controller sees only opaque resource IDs and EndpointRefs. Primary reuse disposition: `wrap`. Preserved source-plan detail: wrap privileged effects behind an injected async `TpmEffectPort`; keep broker operations only behind `volume-local` and `system-minijail`.", + "integration" : "Device controller calls `TpmEffectPort`; ResourceClient-backed implementation talks to ResourceAPI/ComponentSession; `volume-local` and `system-minijail` translate resource operations into broker effects.", + "removalProof" : "Direct broker references in controller/daemon TPM paths are superseded by the effect-port/resource-provider boundary; final deletion is ADR046-device-tpm-013", + "reuseAction" : "wrap", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "`tests/effect_fake.rs`; static proof that non-test files do not import `d2b_priv_broker`", + "workItemId" : "ADR046-device-tpm-002" + }, + { + "currentSource" : "Current direct daemon/broker swtpm lifecycle call sites in `packages/d2bd/src/*` are superseded; controller algorithm is specified in §11.1", + "dataMigration" : "Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state.", + "dependencyOwner" : "P0; blocked by ADR046-device-tpm-002; owner: device-tpm controller FSM", + "destination" : "packages/d2b-provider-device-tpm/src/controller.rs; packages/d2b-provider-device-tpm/tests/controller_fsm.rs", + "detailedDesign" : "Controller reconcile state machine: implement the Device reconcile algorithm from §11.1 against `FakeTpmEffectPort`, covering happy path, Volume not-ready, marker fail-closed, flush failure, swtpm maxRestarts, and finalizer behavior where Process is deleted and Volume retained; emit §14 metrics with only closed `phase`/`outcome`/`status` semantics and no Zone/resource-name-derived label. Primary reuse disposition: `replace`. Preserved source-plan detail: replace direct daemon lifecycle with Provider reconcile against `FakeTpmEffectPort` and resource status.", + "integration" : "Resource watches drive the controller; controller creates/observes Volume, Process, EphemeralProcess, and Endpoint resources through `TpmEffectPort`; Device status/finalizers expose outcomes to the ResourceAPI.", + "removalProof" : "Direct daemon swtpm lifecycle logic is removable after this Provider reconcile FSM reaches parity and ADR046-device-tpm-013 removes the old call sites", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "`tests/controller_fsm.rs` covering happy path, Volume not-ready, marker fail-closed, flush failed, swtpm maxRestarts, and finalizer behavior; `tests/metrics_labels.rs` structurally asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys and Device/Zone-name canary absence", + "workItemId" : "ADR046-device-tpm-003" + }, + { + "currentSource" : "`nixos-modules/components/tpm.nix` declares TPM enablement today; §17.3 defines migration from the existing swtpm directory and marker", + "dataMigration" : "Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state.", + "dependencyOwner" : "P0; blocked by ADR046-device-tpm-001; owner: device-tpm resource builders", + "destination" : "packages/d2b-provider-device-tpm/src/resources.rs; packages/d2b-provider-device-tpm/tests/volume_create.rs", + "detailedDesign" : "Controller-created Volume spec: implement `build_tpm_state_volume_spec` with `cleanupPolicy: never`, `repairPolicy: fail-closed`, `adoptionPolicy: quarantine-on-ambiguity`, `sensitivity: secret-adjacent`, required invariants, `source.sourceId`, no `hostPath`, no top-level identityMarker/persistenceClass/quotaBytes/stateSchema, `ownerRef: Device/`, `managedBy: controller`, empty attachments, and `quota: null`. Primary reuse disposition: `replace`. Preserved source-plan detail: replace VM-level TPM option/state path with controller-created Device-owned Volume spec.", + "integration" : "Device controller creates the TPM data Volume; `volume-local` materializes/protects state and marker; swtpm Process mounts the Volume; Guest runtime receives only EndpointRefs.", + "removalProof" : "`nixos-modules/components/tpm.nix` state-path ownership is superseded by the Device-owned Volume once migration and Nix roundtrip are complete", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "`tests/volume_create.rs` proving every canonical Volume field and forbidden field listed in this item", + "workItemId" : "ADR046-device-tpm-004" + }, + { + "currentSource" : "`SwtpmArgvInput` in `packages/d2b-host/src/swtpm_argv.rs`; `ProcessRole::Swtpm` in `packages/d2b-core/src/processes.rs`; `minijail_swtpm_video.rs` contract tests", + "dataMigration" : "Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state.", + "dependencyOwner" : "P0; blocked by ADR046-device-tpm-004; owner: device-tpm Process resource builder", + "destination" : "packages/d2b-provider-device-tpm/src/resources.rs; Process spec tests under packages/d2b-provider-device-tpm/tests/", + "detailedDesign" : "Canonical swtpm Process spec: implement `build_swtpm_process_spec` with `readOnlyRoot: true`, `userNamespace.mappingClass: process-principal-root`, namespace classes `[pid, mount, user]`, empty capability classes, `seccompClass: w1-swtpm`, two Device-owned Endpoint resources (`tpm` and `ctrl`), `mounts[0].required: true`, and no socket path, binary path, UID integer, or GID integer in any spec field. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract swtpm argv/sandbox intent into canonical Process resources; remove caller-supplied binary path, UID, GID, and socket path fields.", + "integration" : "Controller emits the Process spec; `system-minijail` consumes it and invokes broker `SpawnRunner`; Endpoint resources publish TPM and control sockets for downstream consumers.", + "removalProof" : "`ProcessRole::Swtpm` and swtpm argv builder call sites can be retired after the canonical Process resource covers runner launch", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "Process spec golden tests proving all required and forbidden fields; preserved `minijail_swtpm_video.rs` contract tests", + "workItemId" : "ADR046-device-tpm-005" + }, + { + "currentSource" : "`SwtpmIoctlFlushInput` in `packages/d2b-host/src/swtpm_argv.rs`; `ProcessRole::SwtpmPreStartFlush` in `packages/d2b-core/src/processes.rs`", + "dataMigration" : "Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state.", + "dependencyOwner" : "P0; blocked by ADR046-device-tpm-003; owner: device-tpm EphemeralProcess resource builder", + "destination" : "packages/d2b-provider-device-tpm/src/resources.rs; packages/d2b-provider-device-tpm/tests/flush_mandatory.rs", + "detailedDesign" : "Mandatory flush EphemeralProcess spec: implement `build_flush_ephemeral_process_spec`; no `startupClear` field exists; flush is always created before swtpm Process start with no skip path; TTLs are `successfulTtl: \"1h\"` and `failedTtl: \"24h\"`; no userNamespace on flush Process; deadlines are `startDeadline: \"30s\"` and `runtimeDeadline: \"60s\"`. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract flush intent into mandatory EphemeralProcess resource; remove configurable `startupClear` path and caller-supplied binary path fields.", + "integration" : "Controller inserts the EphemeralProcess before every swtpm activation cycle; Process provider runs the flush against the control Endpoint fd before the long-lived swtpm Process becomes Ready.", + "removalProof" : "`ProcessRole::SwtpmPreStartFlush` and any optional startup-clear wiring are retired after mandatory EphemeralProcess coverage is complete", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "`tests/flush_mandatory.rs` plus contract proof that user-NS is long-lived only", + "workItemId" : "ADR046-device-tpm-006" + }, + { + "currentSource" : "Existing status/path observations from direct daemon TPM paths are superseded; status fields are defined in §10", + "dataMigration" : "None — status is re-derived during v3 reconcile; TPM state migration remains the Volume/marker migration in §17.3", + "dependencyOwner" : "P1; blocked by ADR046-device-tpm-003; owner: device-tpm status builder", + "destination" : "packages/d2b-provider-device-tpm/src/status.rs; packages/d2b-provider-device-tpm/tests/{endpoint_ref.rs,redaction.rs}", + "detailedDesign" : "Device status builder: implement `build_device_status`; `tpmEndpointRef` is an `Endpoint/` ResourceRef with no opaque endpoint ID compatibility alias and never a filesystem path; `stateVolumeRef` and `swtpmProcessRef` are canonical ResourceRef strings; no path, socket name, UID, GID, PID, or pidfd appears in status; `markerStatus` is one of `verified`, `missing`, `replaced`, or `unknown`. Primary reuse disposition: `create`. Preserved source-plan detail: net-new bounded Device status projection; do not reuse path/socket/UID/GID/PID observations.", + "integration" : "Controller writes Device status; Guest runtime Provider reads EndpointRef from Device status; CLI/support tooling reads bounded non-secret status.", + "removalProof" : "Legacy path/socket status compatibility aliases are absent; removal is proven by redaction/status tests", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "`tests/endpoint_ref.rs`; `tests/redaction.rs`; status builder tests for allowed `markerStatus` values", + "workItemId" : "ADR046-device-tpm-007" + }, + { + "currentSource" : "Baseline guest wiring consumed socket paths from TPM sidecar state; v3 handoff is the Endpoint resource contract in §8.4 and §10.5", + "dataMigration" : "None — endpoint handoff has no state migration; TPM data migration remains §17.3", + "dependencyOwner" : "P1; blocked by ADR046-device-tpm-007; owner: device-tpm endpoint handoff integration", + "destination" : "packages/d2b-provider-device-tpm/src/{effect_port.rs,status.rs}; packages/d2b-provider-device-tpm/integration/guest_endpoint.rs", + "detailedDesign" : "EndpointRef handoff: hermetic tests prove `tpmEndpointRef` is an EndpointRef and never a path; integration proves Guest runtime Provider reads `tpmEndpointRef` and obtains the socket fd from the Zone runtime endpoint resolver with no path string in Guest spec or LaunchTicket API surface. Primary reuse disposition: `replace`. Preserved source-plan detail: replace path handoff with EndpointRef and Zone runtime endpoint resolver fd acquisition.", + "integration" : "Device status publishes EndpointRef; Guest runtime Provider resolves the Endpoint through the Zone endpoint resolver; LaunchTicket receives an fd, not a socket path.", + "removalProof" : "Socket-path handoff is superseded when all Guest runtime TPM attachment uses EndpointRef resolution", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "`tests/endpoint_ref.rs`; `integration/guest_endpoint.rs`", + "workItemId" : "ADR046-device-tpm-008" + }, + { + "currentSource" : "Existing provisioning marker behavior in `/var/lib/d2b/swtpm-markers/` is preserved and re-keyed by volume-local per §17.3", + "dataMigration" : "Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state.", + "dependencyOwner" : "P0; blocked by ADR046-device-tpm-004; owner: device-tpm marker/fail-closed tests", + "destination" : "packages/d2b-provider-device-tpm/tests/marker_fail_closed.rs; packages/d2b-provider-device-tpm/integration/marker_tamper.rs", + "detailedDesign" : "Marker fail-closed test: FakeTpmEffectPort returning `markerStatus: replaced` makes Device Failed, prevents a second `ensure_state_volume` call, and prevents swtpm Process creation. Integration physically replaces `swtpm/`; volume-local sets Volume Failed; Device fails with no auto-recovery. Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve fail-closed marker semantics while moving ownership to Volume/Device resources.", + "integration" : "volume-local observes marker state and reports Volume/marker status; controller maps that to Device failure and blocks Process creation; integration exercises the actual filesystem marker path through volume-local.", + "removalProof" : "Any silent marker recreation path is removed/proven absent by the fail-closed tests", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "`tests/marker_fail_closed.rs`; `integration/marker_tamper.rs`", + "workItemId" : "ADR046-device-tpm-009" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — controller has no Provider state Volume to migrate; Device data migration remains §17.3", + "dependencyOwner" : "P1; blocked by ADR046-device-tpm-001; owner: device-tpm controller Process descriptor", + "destination" : "packages/d2b-provider-device-tpm/src/resources.rs; packages/d2b-provider-device-tpm/tests/controller_process.rs", + "detailedDesign" : "Controller Process (status-first; no Provider state Volume): implement the controller Process spec from §4.1 with `processClass: controller`, `readOnlyRoot: true`, empty mounts, no controller-scratch namespace, no scratch mount, no `User/device-tpm-controller-system` state-layout principal, no permission to create Provider-owned Volumes, and restart re-derivation from resource store plus external marker/process observations while treating status as observation. Primary reuse disposition: `create`. Preserved source-plan detail: net-new status-first controller Process spec; no Provider state Volume is reused or created.", + "integration" : "ProviderDeployment creates the controller Process from the descriptor; controller status and Operation ledger carry bounded non-secret observations; Device-owned TPM data Volume remains separate from ProviderStateSet.", + "removalProof" : "None — this item prevents introduction of a Provider state Volume and has no prior state owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "Controller Process spec tests proving the bullets in this item", + "workItemId" : "ADR046-device-tpm-010" + }, + { + "currentSource" : "`nixos-modules/components/tpm.nix` current `d2b.vms..tpm.enable` option is replaced by the Device declaration in §17.1", + "dataMigration" : "Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state.", + "dependencyOwner" : "P1; blocked by ADR046-device-tpm-001; owner: Nix Resource compiler for Device declarations", + "destination" : "nixos-modules/options-resources.nix and Nix eval/golden tests for §17.1 Device JSON", + "detailedDesign" : "Nix roundtrip test: Device Nix spec from §17.1 round-trips through the Nix emitter to expected resource JSON; emitted bundle contains no Volume, Process, or EphemeralProcess resources because controller-managed resources are not in the Nix bundle. Primary reuse disposition: `replace`. Preserved source-plan detail: replace VM-level TPM enable option with v3 Device resource emission and assertions.", + "integration" : "Nix authoring emits only the Device and Provider resources; ResourceAPI admission hands the Device to the controller; controller creates managed resources at runtime.", + "removalProof" : "`d2b.vms..tpm.enable` in `nixos-modules/components/tpm.nix` is superseded after Device Nix declaration roundtrip and migration are in place", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "Nix roundtrip/golden test for §17.1 and emitted-bundle absence of controller-managed resources", + "workItemId" : "ADR046-device-tpm-011" + }, + { + "currentSource" : "Current TPM state retention behavior is tied to the swtpm directory and marker; v3 retention is `cleanupPolicy: never` on the Device-owned Volume", + "dataMigration" : "Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state.", + "dependencyOwner" : "P0; blocked by ADR046-device-tpm-003; owner: device-tpm finalizer lifecycle", + "destination" : "packages/d2b-provider-device-tpm/src/controller.rs; packages/d2b-provider-device-tpm/tests/finalizer.rs", + "detailedDesign" : "Finalizer: Volume retained on Device deletion. Hermetic tests cover Device deletion finalizer leading to swtpm Process deletion, TPM state Volume not deleted because `cleanupPolicy: never`, Volume persists, Core emits `phase=Deleted` for Device after finalizer clears, and audit carries no path/UID. Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve TPM identity retention while moving deletion sequencing to Resource finalizers.", + "integration" : "Resource deletion sets finalizer; controller deletes Process and retains Volume; core completes Deleted revision after finalizer clears; audit subsystem records redacted deletion outcome.", + "removalProof" : "Any cleanup path that deletes TPM state on Device deletion is removed/proven absent by the finalizer test", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "`tests/finalizer.rs` proving Process deletion, retained Volume, core Deleted phase, and redacted audit", + "workItemId" : "ADR046-device-tpm-012" + }, + { + "currentSource" : "`packages/d2bd/src/*` direct broker/swtpm call sites; `packages/d2b-core/src/processes.rs` `ProcessRole::Swtpm` and `ProcessRole::SwtpmPreStartFlush`; `packages/d2b-host/src/swtpm_argv.rs` argv builders", + "dataMigration" : "Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state.", + "dependencyOwner" : "P0; blocked by ADR046-device-tpm-002; owner: device-tpm migration/removal cleanup", + "destination" : "packages/d2bd/src/*; packages/d2b-core/src/processes.rs; packages/d2b-provider-device-tpm/src/; packages/d2b-host/src/swtpm_argv.rs", + "detailedDesign" : "Remove direct broker references: remove pre-ADR-0046 daemon swtpm broker call sites, retire `ProcessRole::Swtpm` and `ProcessRole::SwtpmPreStartFlush`, move argv builders from `d2b-host/src/swtpm_argv.rs` to `d2b-provider-device-tpm/src/` with binary path fields removed, while retaining `d2b-priv-broker/src/ops/swtpm_dir.rs` for `volume-local` and `spawn_runner.rs` for `system-minijail`. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: remove direct broker references from daemon and move argv builders into the Provider with binary path fields removed; retain broker ops only behind resource providers.", + "integration" : "After controller/effect-port parity, daemon no longer calls TPM broker ops; Resource providers invoke broker effects from Volume and Process reconciliation; contract tests ensure swtpm sandbox/readiness still hold.", + "removalProof" : "Direct daemon swtpm call sites removed; `ProcessRole::Swtpm` and `ProcessRole::SwtpmPreStartFlush` retired; `d2b-host/src/swtpm_argv.rs` no longer owns Provider argv builders", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-tpm", + "specPath" : "docs/specs/providers/ADR-046-provider-device-tpm.md", + "validation" : "Static search/proof for no direct broker swtpm references in daemon/controller plus preserved swtpm contract tests", + "workItemId" : "ADR046-device-tpm-013" + }, + { + "currentSource" : "`packages/d2b-wayland-proxy/`, `packages/d2b-host/src/wayland_proxy_argv.rs`, `packages/d2b-host-providers/src/lib.rs`, `packages/d2b-realm-provider/src/{conformance,mock}.rs`", + "dataMigration" : "Full reset; no v2 session compatibility", + "dependencyOwner" : "`ADR046-provider-001`, `ADR046-process-001`; display Provider owner", + "destination" : "`packages/d2b-provider-display-wayland/src/`", + "detailedDesign" : "Create Provider crate layout (`src/`, `tests/`, `integration/`, `README.md`); extract `FilterPolicy`, `PolicyInput`, `DecorationManager`, `BridgeConfig`, `ProxyReadinessEvent`, `ProxyIdentity`, `ClipboardGlobalDisposition` from `d2b-wayland-proxy`; implement single `display-controller` using toolkit `ResourceClient`/`Reconciler` to manage both `WaylandSession` and `WaylandPolicy` resources; implement `display-user-portal` as a separately sandboxed user-domain service that receives pre-opened compositor connections from the fixed user session supervisor (never reads `WAYLAND_DISPLAY`), validates same-user via `SO_PEERCRED`, and issues bounded per-session compositor connection attachment grants to `ProviderSupervisor`; implement LaunchTicket composition with opaque attachment grant handles (compositor, GPU endpoint) so no fd transits through the controller; implement pool-slot acquisition using opaque hash-derived account names (`d2b-wlp-` for bundle sessions, `d2b-wlp-p` for pool) that fails closed with `NoPrincipalAvailable` when all pool slots are occupied; implement `wl-cross-domain-proxy` guest frontend binary at `src/bin/wl-cross-domain-proxy.rs` within the Provider package; implement provider-neutral `display_fails_closed_when_unsupported` conformance; assert D087 status-first state: ProviderStateSet is empty and bounded operational state is in resource status plus the core Operation ledger Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Zone Provider resource/catalog → `WaylandSession` controller (in `display-controller`) → Process resources → supervisor ticket; framework enrollment creates no Provider state Volume for display components", + "removalProof" : "`packages/d2b-host-providers/src/lib.rs` `LocalCrossDomainWaylandProvider` removed only after `display-controller` passes conformance; `packages/d2b-host/src/wayland_proxy_argv.rs` removed only after Process template sealing verified", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-display-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-display-wayland.md", + "validation" : "conformance vectors, fake-bus tests, filter policy golden tests (migrate from `packages/d2b-wayland-proxy/`), redaction/audit contract tests, no-fallback test, `controller_unknown_interface_fails_closed`, `controller_finalizer_ambiguous_retained`, `user_portal_unavailable_blocks_pending`, `provider_state_set_empty_status_first`", + "workItemId" : "ADR046-display-001" + }, + { + "currentSource" : "`nixos-modules/components/graphics.nix` `graphics.waylandProxy.*`; `nixos-modules/ui-colors.nix` VM border color resolution; `nixos-modules/options-vms.nix` `graphics.*` options", + "dataMigration" : "Legacy `graphics.waylandProxy.*` Nix options accepted with deprecation warning during migration window; removed after parity", + "dependencyOwner" : "`ADR046-display-001`; Nix integrator", + "destination" : "Zone bundle emitter for `WaylandSession` / `WaylandPolicy` ResourceSpecs under `d2b.zones..resources.*`; `WaylandSession` color resolution in Nix bundle emitter", + "detailedDesign" : "Emit `WaylandSession` and `WaylandPolicy` ResourceSpecs from Nix; derive colors from `d2b-niri-border` palette (§16.2); enforce `crossDomainTrusted = true` at eval time; emit v3 `display-wayland-provider` artifact catalog entry with `spec.config.principalPoolSize` (default 4, bounds 1..32) and `spec.config.runtimeVolumePolicyId`; provision opaque hash-derived OS accounts (`d2b-wlp-` for bundle-declared sessions, `d2b-wlp-p` for pool; `isSystemUser = true`, no raw UID/GID) for all sessions and pool slots; validate that account names are unique and do not expose guest/session identity; validate signed Provider config schema; enforce eval-time bound that bundle session count + pool size does not exceed provisioned principal count; configure `display-controller` and `display-user-portal` in the signed ProviderDeployment manifest; nix-unit tests for color derivation, opaque principal name derivation, spec shape, JSON round-trip, and principal provisioning uniqueness", + "integration" : "Zone NixOS module system → bundle emitter → `/etc/d2b/zones//bundle/generation-N.json`", + "removalProof" : "`nixos-modules/components/graphics.nix` `graphics.waylandProxy.*` and `nixos-modules/ui-colors.nix` VM color resolution removed only after Zone bundle emitter parity verified by a full nix-unit pass", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-display-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-display-wayland.md", + "validation" : "nix-unit spec-shape tests, eval-time guard tests (crossDomainTrusted=false rejected), color derivation golden tests, principal provisioning count bound test", + "workItemId" : "ADR046-display-002" + }, + { + "currentSource" : "`packages/d2b-wayland-proxy/src/diag.rs` (rate-limited bounded diagnostics)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-display-001`; telemetry/audit owner", + "destination" : "`packages/d2b-provider-display-wayland/src/audit.rs`, `packages/d2b-provider-display-wayland/src/metrics.rs`", + "detailedDesign" : "Implement audit record types for all events in §14.1; implement OTEL metric counters/gauges in §14.2; adapt `DiagRateLimiter` to use closed semantic label sets with no Zone UID/name or resource-name-derived key; retain Zone identity in `d2b.zone` resource attributes; validate that no socket path, user identity, window title, or app-id appears in any log/audit/metric surface. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Providers emit via Zone telemetry emitter; audit records committed before operation completion", + "removalProof" : "N/A (new code)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-display-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-display-wayland.md", + "validation" : "Redaction contract tests (`policy_observability.rs` pattern), audit record schema tests, structural label-policy tests asserting exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus WaylandSession/Zone-name canary absence", + "workItemId" : "ADR046-display-003" + }, + { + "currentSource" : "`tests/integration/` test orchestration structure", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-display-001`; integration test owner", + "destination" : "`packages/d2b-provider-display-wayland/integration/`", + "detailedDesign" : "Container/Host/Guest/cross-process integration fixtures for: (a) end-to-end WaylandSession create → proxy Process ready → guest frontend ready; (b) GPU endpoint unavailable → Pending; (c) proxy crash → Failed backoff; (d) policy policy warning production; (e) clipboard boundary denial; (f) crossDomainTrusted=false admission rejection. Follows `ADR-046-provider-model-and-packaging` integration/ convention.", + "integration" : "Invoked by existing repository test orchestration (`make test-integration` / container lane)", + "removalProof" : "N/A (new code)", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-display-wayland", + "specPath" : "docs/specs/providers/ADR-046-provider-display-wayland.md", + "validation" : "All six scenarios above pass; no socket paths in test output", + "workItemId" : "ADR046-display-004" + }, + { + "currentSource" : "`packages/d2b/tests/host_doctor_contract.rs` (env-redirect sandbox: `D2B_BROKER_SOCKET`, `D2B_PUBLIC_SOCKET`, `D2B_DAEMON_STATE_DIR`, `D2B_METRICS_URL`, `D2B_MANIFEST_PATH`; `doctor::render_summary` JSON envelope fields: `command`, `mode`, `broker_ready`, per-check `status`+`data`, `summary`, `exitCode`); `packages/d2bd/src/audit_check.rs` (`defects` array audit-chain validation pattern); `packages/d2bd/src/lib.rs` (doctor read-only path: `host doctor --read-only` reads from `D2B_DAEMON_STATE_DIR`, pidfd_table file, kernel-module check file, metrics URL)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-core-001 + ADR046-audit-001; CLI/doctor owner", + "destination" : "`packages/d2b/src/zone_doctor.rs`, `packages/d2b/tests/zone_doctor_contract.rs`", + "detailedDesign" : "`d2b zone doctor [--zone ] [--json]` reads resource status from Zone API (read-only verb), OTEL self-metrics from `observability-otel` Provider endpoint (optional), and audit segment inventory from `d2b-audit` segment reader. Named check set from this spec. Exit 0 on all-ready; 1 on any warn/error. Env-redirect sandbox for all test fixtures. Current `MANIFEST_JSON` fixture pattern adapted: `\"_observability\": {\"enabled\": false}` test ensures OTEL probe short-circuits cleanly. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt env-redirect sandbox test harness; adapt `defects` array pattern for `audit-hash-chain-clean` check; adapt `broker_ready` → `zone_phase` field.", + "integration" : "`d2b` CLI → Zone resource API status reads + audit segment reader", + "removalProof" : "`d2b host doctor` retained until `d2b zone doctor` covers all check parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "`zone_doctor_contract.rs`: all-ready/degraded/quarantine/otel-absent/audit-absent fixtures; no resource names/paths/argv/PIDs; `zone_phase` field present; no legacy `broker_ready` field", + "workItemId" : "ADR046-doctor-001" + }, + { + "currentSource" : "`packages/d2b/tests/host_doctor_contract.rs` (env-redirect sandbox scaffold — no current `support-bundle` equivalent exists)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-doctor-001; CLI/doctor owner", + "destination" : "`packages/d2b/src/zone_support_bundle.rs`, `packages/d2b/tests/zone_support_bundle_contract.rs`", + "detailedDesign" : "`d2b zone support-bundle [--zone ]` invokes only `d2b.support.v3.SupportService/GenerateBundle` under the admin-only `support-bundle` session verb; the caller receives no resource read authority. The service reads bounded resource status snapshots (32 per type, 512 total; metadata + status only; no spec bytes; no `metadata.name`), controller queue depths, schema catalog (names+versions only), audit segment inventory, OTEL collector metrics summary, and a bounded structured log ring (2000 entries). NDJSON output. On quarantine: `bundle_completeness: \"partial\"`, exit 1. Primary reuse disposition: `adapt`. Preserved source-plan detail: reuse env-redirect sandbox scaffold.", + "integration" : "`d2b` CLI → ComponentSession `d2b.support.v3.SupportService/GenerateBundle` with `sessionVerbs=[support-bundle]` → bounded internal resource/controller/audit/OTEL readers", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "`zone_support_bundle_contract.rs`: complete/partial bundles; no spec/name/path/argv; field completeness", + "workItemId" : "ADR046-doctor-002" + }, + { + "currentSource" : "`packages/d2b-core/src/processes.rs`: `ProcessRole` (18 variants), `ProcessNode`, `RoleProfile`, `NamespaceSet`, `MountPolicy`, `CgroupPlacement`, `ReadinessPredicate`; `packages/d2b-core/src/minijail_profile.rs`: `MinijailProfile`, `UserNamespaceProfile`, `NamespaceSet`, `MountPolicy`, `BindMount`, `CgroupPlacement`; `packages/d2b-core/src/storage.rs`: `StoragePathSpec`, `AclGrant`, `CleanupPolicy`, `RepairPolicy`; `packages/d2b-realm-core/src/ids.rs`: `RealmId`, `WorkloadId` (→ GuestRef), `NodeId` (→ HostRef), `ProviderId` (→ Provider ResourceRef), `ExecutionId` (→ EphemeralProcess exec identity), `PrincipalId` (→ User ResourceRef), `AllocatorLeaseId`, `ControllerGenerationId`; `packages/d2b-realm-core/src/workload.rs`: `WorkloadProviderKind` (`LocalVm`→runtime-cloud-hypervisor Provider, `QemuMedia`→runtime-qemu-media Provider, `ProviderManaged`→ACA/relay Providers, `UnsafeLocal`→user-only Host `isolationPosture=\"none\"`), `IsolationPosture` (`VirtualMachine`→Guest, `ProviderManaged`→Guest, `UnsafeLocal`→Host `isolationPosture=\"none\"`), `WorkloadExecutionPosture`, `WorkloadSummary`, `WorkloadState`; `packages/d2b-realm-core/src/target.rs`: `RealmTarget`, `TargetName`, `RealmTargetParser` (current analog for `/` ResourceRef parsing); `packages/d2b-realm-core/src/realm.rs`: `RealmPath`, `RealmControllerPlacement`, `EntrypointMode` (current Zone hierarchy analog); `packages/d2b-core/src/workload_identity.rs`: `WorkloadIdentity`, `WorkloadTarget` (= `RealmTarget`), `WorkloadBackend`, `WorkloadRuntimeIntent` (identity/backend separation reuse model for Host/Guest ResourceType split)", + "dataMigration" : "Full reset; no v2 resource import", + "dependencyOwner" : "W0 shared contract root; `d2b-contracts`", + "destination" : "`packages/d2b-contracts/src/v3/host.rs`, `packages/d2b-contracts/src/v3/guest.rs`, `packages/d2b-contracts/src/v3/execution_policy.rs`, `packages/d2b-contracts/src/v3/process.rs`, `packages/d2b-contracts/src/v3/ephemeral_process.rs`, `packages/d2b-contracts/src/v3/user.rs`, `packages/d2b-contracts/src/v3/endpoint.rs`", + "detailedDesign" : "Implement strict typed Rust structs for HostSpec, GuestSpec, ExecutionPolicy, ExecutionSpec, SandboxSpec, BudgetSpec, MountSpec, NetworkUsageSpec, DeviceUsageSpec, EndpointSpec, EndpointConsumerPolicy, TelemetrySpec, ProcessSpec, EphemeralProcessSpec, UserSpec; EndpointSpec accepts exactly `owner|provider|zone`, and EndpointConsumerPolicy owns the only finer gates (`allowedSubjects`, `allowedProviderComponents`, `allowedOperations`) with no schema aliases; strict serde deny_unknown_fields; bounds/redaction on all string fields; stable error types; `UserSpec.osUsername` validated as OS username (1..255 bytes, no NUL/control/path-separator), not ResourceName grammar Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Provider dossiers, controller descriptors, Zone resource API, Nix resource compiler", + "removalProof" : "Old DTO types removed only after owning Resource/Provider integrations are live", + "reuseAction" : "adapt", + "reuseSource" : "`packages/d2b-contracts/src/v3/` as destination; no equivalent main source for Host/Guest/Process ResourceType contracts", + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Golden JSON vectors for each ResourceType; Endpoint vectors accept only `owner|provider|zone`, reject every legacy/private visibility alias, reject scalar/array `consumerPolicy` aliases, and cover each canonical consumer allowlist; a docs drift test parses every `type: Endpoint` YAML/Nix example and fails unless visibility is canonical and finer gates occur only under `consumerPolicy`; serde unknown-field rejection; bounds enforcement; `UserSpec.osUsername` OS-username validation (underscore allowed, NUL rejected)", + "workItemId" : "ADR046-exec-001" + }, + { + "currentSource" : "`packages/d2b-core/src/processes.rs`: `ReadinessPredicate`, `VmProcessInvariants`; `packages/d2b-core/src/processes.rs`: `SpawnRunnerPlanOp`; `packages/d2b-priv-broker/src/ops/` SpawnRunner", + "dataMigration" : "Full reset", + "dependencyOwner" : "ADR046-exec-001; `d2b-contracts`", + "destination" : "`packages/d2b-contracts/src/v3/process_provider.rs`: LaunchTicket, ProcessIdentityDigest, AdoptionCandidate, PidfdEvidence, WaitReapOwner, BrokerTerminalResult, ProcessOutcome, ExitClass", + "detailedDesign" : "LaunchTicket (Process/EphemeralProcess ref/UID/revision/generation, owner Provider/component/template, executionRef/domain/userRef, providerRef, compiled sandbox/budget/mount/device/network/endpoint digest, inherited FD table, operation/deadline/cancellation, expected identity/readiness); ProcessIdentityDigest (opaque bounded hex string); AdoptionCandidate (cgroup leaf path relative to controller root, start-time token, executable hash); BrokerTerminalResult binds process identity/operation to the clone3 parent's exact-once wait/reap status and cannot be constructed from pidfd readability; all types zeroizing where credential-adjacent Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "ProviderSupervisor adapter; system-systemd and system-minijail Process Providers", + "removalProof" : "None — net-new types; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Golden LaunchTicket and BrokerTerminalResult vectors; field redaction test; digest-binding test; duplicate/mismatched/non-parent terminal relay rejection", + "workItemId" : "ADR046-exec-002" + }, + { + "currentSource" : "`packages/d2b-core/src/host_check.rs`: `HostCheckReport`, `HostCheckSummary`, `HostCheckFinding`, `HostCheckSeverity`; `packages/d2bd/src/pidfs_probe.rs`; `packages/d2bd/src/kernel_module_check.rs`; `packages/d2b-core/src/provider_capabilities.rs`; `packages/d2b-realm-core/src/ids.rs`: `HostResourceId` (current host-identity handle), `NodeId` (execution node identity); `packages/d2b-realm-core/src/node.rs`: `NodeKind::FullHost`, `NodeSummary` (host node's capability advertisement — direct reuse model for Host status `capabilities[]`)", + "dataMigration" : "New Host resources from Nix; no v2 state import", + "dependencyOwner" : "ADR046-exec-001; system-core Provider owner", + "destination" : "`packages/d2b-provider-system-core/src/host.rs`: HostReconciler; status/conditions/capability probe implementation; `packages/d2b-provider-system-core/tests/`: hermetic reconcile/conformance/fault tests; `packages/d2b-provider-system-core/integration/`: Host probe and lifecycle integration scenarios; `packages/d2b-provider-system-core/README.md`: Provider identity, `spec.provider.settings` schema, ResourceTypes, placement, RBAC, security posture, telemetry labels, build/test commands (provider crate standard layout — see §Provider crate standard layout)", + "detailedDesign" : "Async Host reconcile loop per this spec's Reconcile section; HostCapabilityClass probe set (kvm/pidfd/cgroup-v2/user-namespace/wayland/audio-pipewire/gpu-render/tpm2/usbip); bounded OS probes with timeout; mandatory system-minijail placement gate for Linux ≥5.14 plus writable delegated-leaf `cgroup.kill` independent of optional `kernelVersionMin`; `isolationPosture` validation and status; aggregate budget reservation tracking via List; status write with expected revision Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Provider/system-core fixed bootstrap process; ResourceClient Get/List/UpdateStatus", + "removalProof" : "Current host capability checks in `d2bd` removed after Host reconcile parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Multiple Hosts per Zone; system-only and user-only Hosts; capability probe mocks; Linux <5.14 and missing/unwritable `cgroup.kill` reject system-minijail before spawn; Linux ≥5.14 positive probe; `isolationPosture=\"none\"` rejection of system processes; budget overcommit rejection; `tests/` all pass under `cargo test`; `integration/` scenario passes in container fixture; `README.md` present and covers all required sections (provider crate standard layout acceptance)", + "workItemId" : "ADR046-exec-003" + }, + { + "currentSource" : "`packages/d2b-userd/src/lib.rs`: `UserdConfig`, `UserSessionIdentity`; `packages/d2bd/src/admission.rs` (`SO_PEERCRED` UID/GID lookup); `packages/d2b-realm-core/src/ids.rs`: `PrincipalId` (current host-local principal identity); `packages/d2b-realm-core/src/access.rs`: `HostLocalPeerCredentialSemantics`, `HostLocalPeerCredentialSource`, `RealmAccessClientContract` (current host-local credential/uid resolution model; direct reuse precedent for v3 User → Process userRef resolution)", + "dataMigration" : "New User resources from Nix; no v2 state import", + "dependencyOwner" : "ADR046-exec-001; system-core Provider owner", + "destination" : "`packages/d2b-provider-system-core/src/user.rs`: UserReconciler; NSS lookup implementation", + "detailedDesign" : "Async User reconcile loop per this spec's Reconcile section; NSS `getpwnam(spec.osUsername)` with bounded timeout (default 5 s); `spec.osUsername` validated on admission (1..255 bytes, no NUL/control/path-separator); `metadata.name` used only as Zone-local resource identity and `User/` ref; uid/gid/home/shell/group discovery written to status; session manager availability check via fixed user supervisor; status write; phase Degraded on SessionManagerReady=False Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Provider/system-core fixed bootstrap process; ResourceClient Get/List/UpdateStatus; other controllers resolve User status via ResourceClient", + "removalProof" : "Current local uid/group lookup in `d2bd/src/admission.rs` removed after User resource parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "User found/not-found; group membership; sessionManagerAvailable; multiple Users; deletion blocked by Process userRef; `spec.osUsername` with underscore succeeds NSS lookup where ResourceName grammar would reject it; `metadata.name` and `spec.osUsername` differ (e.g. `alice-admin` / `alice_admin`); `spec.osUsername` containing NUL/control/path-separator rejected at admission", + "workItemId" : "ADR046-exec-004" + }, + { + "currentSource" : "`packages/d2bd/src/lib.rs` (daemon startup); `packages/d2b-core/src/host_check.rs`; `nixos-modules/host.nix` (host activation); `nixos-modules/options-host.nix` (host options)", + "dataMigration" : "Full v3 reset", + "dependencyOwner" : "ADR046-exec-001 + ADR046-exec-004; system-core Provider owner", + "destination" : "`packages/d2b-provider-system-core/src/host.rs` (continued); bootstrap startup sequence", + "detailedDesign" : "ProviderSystem-core fixed bootstrap: start with embedded Zone runtime and compiled bootstrap authorization; run Host reconcile before any Process Provider launches; create initial User resources from Nix; publish initial Role/RoleBinding from Nix config; hand off to stored RBAC", + "integration" : "Zone runtime startup sequence; system-minijail bootstrap process launch", + "removalProof" : "Current d2bd initialization sequence removed after bootstrap parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Bootstrap without prior Host; User-before-Process ordering; compilation bootstrap authorization closed-set tests", + "workItemId" : "ADR046-exec-005" + }, + { + "currentSource" : "`packages/d2b-unsafe-local-helper/src/systemd.rs`; `packages/d2b-guestd/src/exec.rs`: `SystemdRunUnitManager`, `ManagedUnit`, `ExecPolicy`; `packages/d2b-guestd/src/exec_linux.rs`; `packages/d2bd/src/supervisor/` (transient unit management)", + "dataMigration" : "Current ProcessRole/systemd unit roles converted by ProcessRole disposition table after parity", + "dependencyOwner" : "ADR046-exec-001 + ADR046-exec-002; system-systemd Process Provider owner", + "destination" : "`packages/d2b-provider-system-systemd/src/`: launch.rs, adoption.rs, pidfd.rs, wait.rs, user_supervisor.rs; `packages/d2b-provider-system-systemd/tests/`: hermetic lifecycle/conformance/fault tests; `packages/d2b-provider-system-systemd/integration/`: transient-unit and user-supervisor integration scenarios; `packages/d2b-provider-system-systemd/README.md`: Provider identity, `spec.provider.settings` schema, ResourceTypes, placement, RBAC, security posture, telemetry labels, build/test commands (provider crate standard layout — see §Provider crate standard layout)", + "detailedDesign" : "system-systemd Process/EphemeralProcess provider conformance per this spec's system-systemd conformance section; transient system unit (Type=exec); InvocationID+cgroup+MainPID+start-time binding before pidfd_open; systemd-owned wait/reap; user domain via fixed user supervisor; adoption re-verification; sandboxSpec compilation to systemd hardening directives; runtimeDeadline enforcement; drainTimeout SIGTERM/SIGKILL sequence Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Zone-installed Provider/system-systemd; ProviderSupervisor LaunchTicket interface; ResourceClient UpdateStatus", + "removalProof" : "ProcessRole roles using systemd (Audio, WaylandProxy, VsockRelay, etc.) removed per disposition table after system-systemd parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Shared process conformance test matrix (lifecycle/readiness/crash/drain/adoption/user-domain/sandboxSpec/pidfd); system-specific InvocationID binding; no-static-unit test; `tests/` all pass under `cargo test`; `integration/` scenario passes in container fixture; `README.md` present and covers all required sections (provider crate standard layout acceptance)", + "workItemId" : "ADR046-exec-006" + }, + { + "currentSource" : "`packages/d2b-core/src/processes.rs`: `ProcessNode`, `RoleProfile`, `NamespaceSet`, `MountPolicy`, `CgroupPlacement`; `packages/d2b-core/src/minijail_profile.rs`: full; `packages/d2b-priv-broker/src/ops/spawn_runner.rs` (if present at baseline); `packages/d2bd/src/supervisor/` pidfd/wait; `packages/d2b-core/src/process_builder.rs`", + "dataMigration" : "Current RoleProfile/NamespaceSet/MountPolicy/CgroupPlacement adapted to SandboxSpec/BudgetSpec", + "dependencyOwner" : "ADR046-exec-001 + ADR046-exec-002; system-minijail Process Provider owner", + "destination" : "`packages/d2b-provider-system-minijail/src/`: sandbox_compiler.rs, launch.rs, adoption.rs, pidfd.rs, wait.rs, user_ns.rs; `packages/d2b-provider-system-minijail/tests/`: hermetic sandbox-compilation/lifecycle/conformance/fault tests; `packages/d2b-provider-system-minijail/integration/`: clone3/user-namespace and broker-spawn integration scenarios; `packages/d2b-provider-system-minijail/README.md`: Provider identity, `spec.provider.settings` schema, ResourceTypes, placement, RBAC, security posture (capabilities, namespaces, seccomp), telemetry labels, build/test commands (provider crate standard layout — see §Provider crate standard layout)", + "detailedDesign" : "system-minijail Process/EphemeralProcess provider conformance per this spec's system-minijail conformance section; SandboxSpec-to-minijail plan compilation; Linux ≥5.14/cgroup.kill placement gate; broker clone3(CLONE_PIDFD|CLONE_INTO_CGROUP) parent retains sole waitid(P_PIDFD)/reap/exit-status ownership and relays a typed terminal result; ProviderSupervisor polls a verified duplicate and retains exact-main pidfd_send_signal semantics but never waits/reaps; adoption verifies original broker parent; runtimeDeadline/drainTimeout use graceful main signal then anchored leaf cgroup.kill, broker wait/reap, and empty-leaf proof; no PID/PGID fallback; EphemeralProcess one-shot launch Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Zone-installed Provider/system-minijail fixed bootstrap process; ProviderSupervisor LaunchTicket; privileged broker effect adapter", + "removalProof" : "ProcessRole roles using minijail (Virtiofsd, Swtpm, SecurityKeyFrontend, etc.) removed per disposition table after system-minijail parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Shared process conformance test matrix; minijail-specific sandbox compilation tests; user namespace tests; clone3 parent-only wait/reap and broker-relay tests; poll-readability-not-status test; pidfd_send_signal duplicate-holder test; setsid descendant/recycled-PGID cgroup.kill teardown test; Linux 5.14/cgroup.kill platform-gate tests; adoption quarantine asserts no signal/cgroup.kill; `tests/` all pass under `cargo test`; `integration/` scenario passes; `README.md` present and covers all required sections (provider crate standard layout acceptance)", + "workItemId" : "ADR046-exec-007" + }, + { + "currentSource" : "`packages/d2b-core/src/processes.rs` test coverage; `packages/d2bd/src/supervisor/` tests; minijail/seccomp test vectors", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-exec-006 + ADR046-exec-007; conformance test owner", + "destination" : "`packages/d2b-process-conformance/src/`: shared conformance test matrix run against both system-systemd and system-minijail providers", + "detailedDesign" : "Full process lifecycle tests: start/ready/crash/restart/maxRestarts/drain/stop/delete; EphemeralProcess start/succeed/fail/ttl/cleanup; adoption after controller restart (fresh/quarantine); sandboxSpec compilation contract; pidfd rules (never-serialized/never-exported/re-verified-after-supervisor-restart; clone3 broker parent alone waits/reaps; non-parent readability is not status; verified duplicate holder may pidfd_send_signal exact main); system-minijail intentional teardown uses anchored cgroup.kill against setsid descendant and recycled-PGID fixtures, with no kill on ambiguity; Linux ≥5.14/cgroup.kill platform gate; user domain (system-systemd only); desiredLifecycle=stopped; fast path latency gate (<=5ms/<=20ms p95); 1/10/100 concurrent Process start", + "integration" : "system-systemd and system-minijail providers must both pass all shared tests", + "removalProof" : "None — permanent conformance tests; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Hard pass/fail per-test; latency gates enforced; no exception for partial conformance", + "workItemId" : "ADR046-exec-008" + }, + { + "currentSource" : "`packages/d2b-unsafe-local-helper/src/lib.rs`: `UserdConfig`, protocol traits; `packages/d2b-unsafe-local-helper/src/runtime.rs` (`ScopeRuntime`, `SupervisorSpec`); `packages/d2b-unsafe-local-helper/src/systemd.rs`; `packages/d2bd/src/unsafe_local_helper.rs`; `packages/d2b-realm-core/src/workload.rs`: `WorkloadProviderKind::UnsafeLocal`, `IsolationPosture::UnsafeLocal` (current evidence that the no-isolation posture exists and is classified separately from VM isolation — the exact semantics this spec's `Host.spec.isolationPosture=\"none\"` preserves); `packages/d2b-core/src/workload_identity.rs`: `WorkloadBackend::UnsafeLocal`; `nixos-modules/options-realms-workloads.nix` (`d2b.realms..workloads..kind = \"unsafe-local\"`)", + "dataMigration" : "Full reset; no unsafe-local session state migration", + "dependencyOwner" : "ADR046-exec-001; unsafe-local migration owner", + "destination" : "`packages/d2b-provider-system-core/src/host.rs` (user-only no-isolation Host); `nixos-modules/options-zones.nix` (Nix unsafe-local Host declaration)", + "detailedDesign" : "v3 unsafe-local migration: `kind = \"unsafe-local\"` in the current Nix Realm workload model becomes a Host resource with `providerRef: Provider/system-core`, `spec.isolationPosture: \"none\"`, `defaultDomain: user`, `allowedDomains: [user]`, `defaultUserRef: User/`. This is a Host ResourceType, not a Guest and not a v3 Provider. Child Process and EphemeralProcess resources on this Host use the normal Process Providers (Provider/system-systemd for user-domain transient user scope; Provider/system-minijail is also valid for callers that explicitly request namespace isolation within the user session). No special unsafe-local-specific Provider is introduced. The explicit no-isolation posture and its warnings are preserved: Host status reflects `isolationPosture=\"none\"` and this is surfaced in every operator CLI/UI view as an explicit \"no isolation boundary\" warning; `ProcessEffect` audit records (launch, stop, adopt, quarantine) for child Processes and EphemeralProcesses carry the stable `no_isolation=true` attribute; operator CLI/UI always shows the warning and may not suppress it. The `no_isolation=true` attribute belongs on ProcessEffect records only — it must NOT appear on OTEL metric labels, span attributes, log fields, or audit records for other event kinds. The legacy helper protocol (`d2b-unsafe-local-helper`) is not exposed as a v3 ComponentSession service.", + "integration" : "Host resource reconcile; User resource; system-systemd user-domain Process launch", + "removalProof" : "`d2b-unsafe-local-helper` helper binary and protocol removed after user-only Host + shell-terminal Provider parity; `options-realms-workloads.nix` unsafe-local kind removed in Nix reset", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "User-only no-isolation Host rejected for system processes; `isolationPosture=\"none\"` Host rejected for `allowedDomains` containing `system`; `allowedDomains=[\"user\"]`+`defaultDomain=user`+`defaultUserRef` set with `isolationPosture=null` rejected at eval time (bidirectional evasion test); posture warning visible in CLI/UI status; `no_isolation=true` attribute present on ProcessEffect launch/stop/adopt/quarantine audit records for child Processes/EphemeralProcesses; `no_isolation=true` absent from OTEL span attributes, metric labels, log fields, and non-ProcessEffect audit records; user-domain Process under user-only Host starts correctly with normal Process Provider", + "workItemId" : "ADR046-exec-009" + }, + { + "currentSource" : "`packages/d2b-guestd/src/exec.rs`: `ExecPolicy`, `ExecError`, `ExecState`, `ExitOutcome`, `ValidatedCommand`, `SpawnedProcess`, `RingChunk`, `ExecSnapshot`, `ExecCreateInput`; `packages/d2b-guestd/src/exec_linux.rs`; `packages/d2b-guestd/src/exec_pty.rs`; `packages/d2b-guestd/src/detached.rs`: `ManagedUnit`, `UnitError`, `UnitIdentity`; `packages/d2b-guestd/src/detached_registry.rs`; `packages/d2b-guestd/src/service.rs`; `packages/d2b-guestd/src/auth.rs`; `packages/d2b-guestd/src/login_session.rs`", + "dataMigration" : "Full reset; no guestd session state migration", + "dependencyOwner" : "ADR046-exec-001 + ADR046-exec-007; guestd migration owner", + "destination" : "`packages/d2b-provider-system-systemd/src/guest_exec.rs` (guest-domain EphemeralProcess launch via systemd-run inside guest); `packages/d2b-session/` (ComponentSession replacing ad-hoc guest ttrpc); runtime Provider guest bootstrap Process", + "detailedDesign" : "Guest-side execution transitions to EphemeralProcess/Process resources under the owning Guest; guestd service becomes a fixed bootstrap Process owned by the runtime Provider controller; exec operations become EphemeralProcess creates via the Zone ResourceClient; detached sessions become EphemeralProcess with failedTtl=24h; shell sessions become shell-terminal Provider Processes; guestd auth.rs becomes ComponentSession via d2b-session (copy/adapt from ADR046-session-001 per ComponentSession spec) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "runtime-cloud-hypervisor Guest controller creates guest-bootstrap Process; Zone ResourceClient in guest-bootstrap creates EphemeralProcess/Process on behalf of guest workloads", + "removalProof" : "`d2b-guestd` binary removed after all guest-side behaviors have Process/EphemeralProcess/ComponentSession successors and tests pass", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Guest EphemeralProcess exec lifecycle; detached exec TTL; guestd auth replaced by ComponentSession; no SSH fallback", + "workItemId" : "ADR046-exec-010" + }, + { + "currentSource" : "`packages/d2b-userd/src/lib.rs`: `UserdConfig`, `UserSessionIdentity`, `UserAttachRequest`, `UserOutputCursor`, `UserExecSession` trait, `UserdError`, `UserdTransport`, `UnixSocketOnly`, `validate_attach_request`; `packages/d2b-userd/src/main.rs`", + "dataMigration" : "Full reset; no session state import", + "dependencyOwner" : "ADR046-exec-004 + ADR046-exec-010; userd migration owner", + "destination" : "guest-domain process attachment becomes a ComponentSession named stream to the EphemeralProcess running in the guest; `UserExecSession` trait reimplemented as a typed ResourceClient+ComponentSession attachment", + "detailedDesign" : "`UserdConfig.socket_name` replaced by Zone-local EphemeralProcess ResourceRef and ComponentSession attach verb; `UserSessionIdentity.uid`/`gid` become Process `userRef` resolved against User resource status; `UserAttachRequest.exec_id`/`tty`/`initial_size` become ComponentSession method parameters on the EphemeralProcess attachment service; `UserdError` becomes stable v3 error codes; `validate_attach_request` tty/size validation retained in the ComponentSession session schema Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (logic retained; protocol replaced).", + "integration" : "CLI/client uses ResourceClient to Get/Watch EphemeralProcess; attaches via d2b-bus ComponentSession attach verb", + "removalProof" : "`d2b-userd` binary removed after ComponentSession attach parity on EphemeralProcess; all d2b exec CLI paths must use new attach mechanism", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Attach/detach; TTY size; stream close; error path parity with UserdError variants", + "workItemId" : "ADR046-exec-011" + }, + { + "currentSource" : "`nixos-modules/options-realms-workloads.nix`: `d2b.realms..workloads..kind` = `local-vm`/`qemu-media` → `d2b.zones..resources.` (flat, `type=\"Guest\"`); `unsafe-local` → `d2b.zones..resources.` (flat, `type=\"Host\"`, user-only; see unsafe-local anchor); `nixos-modules/options-realms.nix`: top-level `d2b.realms` option shape including `providerKind` regex `^[a-z][a-z0-9-]*$` and `realmPath` regex `^[a-z][a-z0-9-]*(\\\\.[a-z][a-z0-9-]*)*$` (Zone hierarchy path encoding); `nixos-modules/processes-json.nix`; `nixos-modules/options-host.nix`; `nixos-modules/host.nix`; `packages/d2b-realm-core/src/realm.rs`: `RealmPath`, `RealmControllerPlacement` (current Zone declaration structure analog)", + "dataMigration" : "Full reset; Realm/Workload options removed in the Nix reset wave", + "dependencyOwner" : "ADR046-exec-001; Nix resource compiler owner", + "destination" : "`nixos-modules/options-zones.nix`: `d2b.zones..resources` option as `types.attrsOf (types.submodule resourceModule)` where each resource module has `type` (required enum), optional `metadata` submodule (`ownerRef`, `labels`, `annotations`), and `spec` (type-dependent, auto-generated submodule); `nixos-modules/zone-bundle.nix`: zone resource bundle emitter (see ADR046-exec-014); `nixos-modules/resource-schemas/`: generated per-type Nix option modules imported by `options-zones.nix`", + "detailedDesign" : "`d2b.zones..resources` is a flat attrset; each entry has `type`, optional `metadata` (`ownerRef`, `labels`, `annotations`), and `spec`. The attrset key is the resource name. `spec` submodule fields and their Nix types/defaults/docs are auto-generated from the committed `packages/d2b-contracts/src/v3/schemas/.json` via `xtask gen-resource-nix-options`; no second vocabulary and no renaming of `spec.*` fields. `spec.provider.settings` sub-fields are constrained to the specific Provider's `providerNixSettingsSchema` attribute if present. All 17 eval-time validation rules from the \"Eval-time validation rules\" section are enforced by `lib.assertMsg` on the flat resource attrset. The `spec` object in the emitted JSON is the direct 1:1 serialization of the `spec` submodule. `metadata.ownerRef`, `metadata.labels`, `metadata.annotations` are serialized into the `metadata` object of the ResourceEnvelope JSON. `metadata.managedBy` and `metadata.configurationGeneration` are NOT in the bundle; they are set by the activation controller at runtime. `metadata.name` is the attrset key; `metadata.zone` is the enclosing zone key. Eval errors carry stable rule codes (1–17). Status is never present in the Nix option. `Guest.spec.systemArtifactId` (top-level spec field, not in `spec.provider.settings`) is validated against `d2b.artifacts` by rule 17; the `spec` submodule never contains derivation values or store paths.", + "integration" : "Zone Nix configuration → eval-time validation → Nix-to-ResourceEnvelope compilers → zone bundle emitter (ADR046-exec-014) → configuration publication controller (ADR046-exec-015)", + "removalProof" : "Realm/Workload Nix options removed only after Zone resource Nix option parity and successful eval tests", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "nix-unit eval/build tests 1–18 from the \"Tests\" section; eval validation rule tests 1–17 with expected error messages and stable codes; `spec` fields in emitted JSON match `spec` submodule values exactly (1:1 invariant test); `type` field in JSON matches `type` option value; `metadata.name` = attrset key; `metadata.zone` = enclosing zone key; `Guest.spec.systemArtifactId` plain string in resource bundle JSON at top-level spec (no store path, not in `spec.provider.settings`); missing/wrong-type artifact ID raises rule 17 eval error", + "workItemId" : "ADR046-exec-012" + }, + { + "currentSource" : "No direct current equivalent; current processes.json has no TTL/cleanup concept", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-exec-008; EphemeralProcess cleanup controller owner", + "destination" : "`packages/d2b-core-controller/src/cleanup.rs`: EphemeralProcess TTL cleanup controller handler", + "detailedDesign" : "Cleanup controller handler as specified in core-controllers spec; watches EphemeralProcess resources for terminal phase; computes cleanupEligibleAt from successfulTtl/failedTtl + completedAt; handles incidentHold; respects finalizers; issues normal Delete via ResourceClient; does not remove rows directly; bounded requeue-at for TTL expiry", + "integration" : "core-controller process; ResourceClient Watch(EphemeralProcess); ResourceClient Delete", + "removalProof" : "None — net-new controller; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Succeeded TTL default 1h; Failed TTL default 24h; incidentHold blocking; finalizer blocking; Delete with expected revision; cleanup controller restart recovery", + "workItemId" : "ADR046-exec-013" + }, + { + "currentSource" : "`nixos-modules/processes-json.nix` (current Nix-to-JSON serialization pattern); `nixos-modules/manifest.nix` (current bundle manifest emitter with `bundleVersion`, `manifestVersion`, integrity-tracking pattern); `packages/xtask/src/main.rs` (`gen-schemas`) (current schema-fingerprint generation method); `packages/d2b-core/src/bundle.rs`: `Bundle`, `BundleVersion`, `BundleManifest`, `BundleArtifact` (current bundle integrity model)", + "dataMigration" : "Full d2b 3.0 reset; new artifact replaces current `processes.json` and `manifest.json` pattern for Zone resources without importing v2 state", + "dependencyOwner" : "ADR046-exec-001 + ADR046-exec-012; Nix resource compiler owner", + "destination" : "`nixos-modules/zone-bundle.nix`: Zone resource bundle emitter; `nixos-modules/resource-schemas/`: generated per-type Nix option submodules; `packages/d2b-contracts/src/v3/resource_bundle.rs`: `ResourceBundle`, `ResourceEnvelope`, `BundleManifest`, `BundleIntegrityPin` Rust types; `packages/xtask/src/gen_resource_schemas.rs`: `xtask gen-resource-schemas` (generates schema JSON and Nix option modules)", + "detailedDesign" : "`zone-bundle.nix` iterates the flat `d2b.zones..resources` attrset (all types share the same attrset; each entry has `type`, optional `metadata`, and `spec`). For each entry it serializes the `spec` submodule to canonical JSON 1:1 (field names unchanged; keys sorted at every level; order-significant arrays preserve declaration order; semantically unordered arrays sorted). `metadata.ownerRef`, `metadata.labels`, `metadata.annotations` are serialized from the author-supplied `metadata` submodule into the envelope's `metadata` object. `metadata.managedBy` and `metadata.configurationGeneration` are NOT in the bundle envelope; they are set by the activation controller at runtime. `metadata.name` is the attrset key; `metadata.zone` is the zone key. `xtask gen-resource-schemas` generates both the schema JSON files AND the per-type Nix option submodule files under `nixos-modules/resource-schemas/` from the Rust DTO definitions; both must be regenerated when any ResourceType spec or Rust struct changes (same drift-gate pattern as current `make test-drift` / `xtask gen-schemas`). The bundle sorts all envelopes by `type` then `metadata.name` alphabetically, computes `bundleSha256` from the canonical JSON of the `resources` array, and emits the bundle manifest as a NixOS store artifact. No secret values, credentials, or OS paths not already declared in `spec` or `metadata` fields may appear in any envelope JSON. The bundle file includes private integrity metadata (resource type schema fingerprints, per-Provider schema fingerprints) alongside the `resources` array; these are bundle-level fields, never per-envelope fields. Alongside the resource bundle, the emitter installs the global private artifact catalog at `/etc/d2b/artifact-catalog.json` (root:d2bd 0640) mapping each `d2b.artifacts.` to `{ \"sha256\", \"size\", \"storePath\", \"type\" }`; this catalog is never included in public resource bundle envelopes. The bundle outer wrapper includes a `catalogSha256` binding the catalog to the bundle for activation-time integrity verification.", + "integration" : "Zone Nix configuration → ADR046-exec-012 compilers → ADR046-exec-014 bundle emitter → `/etc/d2b/zones//resource-bundle.json` symlink → ADR046-exec-015 activation controller", + "removalProof" : "`nixos-modules/processes-json.nix` and current `manifest.nix` bundle artifact for Host/Guest/Process retained in parallel until Zone resource bundle replaces all roles; then removed", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "nix-unit test: bundle sort order (Host < Guest < Process; within type, names alphabetical); nix-unit test: `bundleSha256` recomputed from resources array matches recorded value; nix-unit test: schema fingerprints appear in private bundle file fields, not in any individual ResourceEnvelope `metadata` or `spec` object; nix-unit test: `catalogSha256` changes when any artifact derivation changes; nix-unit test: `schemaFingerprint` changes when ResourceTypeSchema JSON changes; nix-unit test: `providerSchemaFingerprint` changes when Provider settings schema changes and is null when Provider declares no schema; nix-unit test: no inline secret value passes through to bundle JSON; nix-unit test: identical configuration produces byte-identical bundle JSON across two builds; nix-unit test: artifact catalog JSON contains `storePath` field for each entry; nix-unit test: no envelope in `resources` array contains `storePath`, `nixSystem`, `schemaFingerprint`, or `providerSchemaFingerprint`", + "workItemId" : "ADR046-exec-014" + }, + { + "currentSource" : "No direct current equivalent; current realm/workload configuration is applied at NixOS activation time directly, not through a Resource API generation controller", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-exec-014 + ADR046-exec-001 + ADR046-exec-022; configuration generation controller owner", + "destination" : "`packages/d2b-core-controller/src/configuration.rs`: `ZoneConfigController`, `GenerationState`, `PendingCleanup`, `BundleActivation`, `ActivationResult`, `ActivationError`, `GenerationDiff`, `DiffEntry`, `DiffKind`, `CleanupRecord`, `CleanupPhase`, `CleanupOutcome`, `RetentionPolicy`, `RetentionState`; `packages/d2b-core-controller/src/audit.rs`: audit event emission per the \"Audit events\" section", + "detailedDesign" : "The Zone configuration controller (`packages/d2b-core-controller/src/configuration.rs`) runs as a fixed process in the `d2b-core-controller` crate. It watches `/etc/d2b/zones//resource-bundle.json` (inotify or polling). On change: (1) read and verify `bundleSha256` integrity; fail closed on mismatch, emit `d2b.zone.config.activate.error` with `config-bundle-integrity-failed`, make no changes. (2) Compare candidate `bundleSha256` against the currently active bundle record; if identical, this is a no-op re-activation (return immediately). (3) Verify resource type schema fingerprints (from bundle private fields) against committed schemas at `packages/d2b-contracts/src/v3/schemas/.json`; any mismatch fails the entire bundle closed with `config-schema-mismatch`, emits error, makes no changes. (4) Verify Provider schema fingerprints against installed Provider schemas; any mismatch fails the entire bundle closed with `provider-schema-mismatch`, emits error, makes no changes. (5) Verify `catalogSha256` binding against `/etc/d2b/artifact-catalog.json`; mismatch fails bundle closed. (6) Fetch current `metadata.managedBy=\"configuration\"` resources via ResourceClient List (one call per type). (7) Compute `GenerationDiff` (new/changed/unchanged/removed) by type+name key. (8) Submit Create, UpdateSpec, and Delete intents concurrently with bounded async concurrency (default max 32 in-flight); activation returns after all intents are durably queued by the resource store, without waiting for reconcile loops to complete. For unchanged specs: issue UpdateConfigGeneration to refresh `metadata.configurationGeneration` to the new generation number; no controller reconcile triggered. For new resources: core sets `metadata.managedBy=\"configuration\"` and `metadata.configurationGeneration=` on Create; if a same-name resource already exists with `metadata.managedBy=\"controller\"` or `metadata.managedBy=\"api\"`, record a per-item `config-collision` error for that resource without seizing it, emit error, continue other intents. For changed specs: submit UpdateSpec with `expectedRevision`; retry on optimistic lock conflict. For removed resources: submit Delete; set `metadata.deletionRequestedAt`. (9) Set Zone `phase=Pending` while create/update intents are outstanding. (10) Return after durable queue commit; do not block on reconcile completion. (11) The cleanup controller watches ResourceClient Watch streams for `Deleted` revision events (not polling GET) for each pending-cleanup resource by type+name+expectedRevision. When all finalizers release, the resource store commits the `Deleted` revision event atomically with row and index removal in a single transaction; following this commit, the audit subsystem appends the deletion audit record using a dedup/exactly-once recovery key (audit is NOT part of the atomic store transaction). Zone transitions to `phase=Degraded` immediately whenever any pending-cleanup item is outstanding. When all pending-cleanup items receive `Deleted` Watch events, `pendingCleanup` empties and Zone transitions to `phase=Ready`. Cleanup-stuck threshold: 10 min default; configurable; stuck resources remain Degraded without blocking later activations. Prior generation retention: controller retains the N most recently activated, cleanup-complete bundle records (default N=3; range 1..16; no time-based TTL).", + "integration" : "Fixed process in `packages/d2b-core-controller`; ResourceClient (Create/UpdateSpec/UpdateConfigGeneration/UpdateStatus/Delete/List/Watch) per ADR046-exec-022; Zone resource status UpdateStatus (configGeneration, pendingCleanup, lastActivatedAt, lastActivationError); Zone `phase` transitions (Pending while outstanding intents; Degraded immediately when cleanup remains; Ready when create/update complete and cleanup empty); audit segment per ADR046-exec-014 audit events table", + "removalProof" : "None — net-new controller; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Runtime/integration tests 19–23 from the \"Tests for Nix configuration and ResourceType-specific lifecycle\" section in this spec; additionally: `GenerationDiff` hermetic unit tests (new/changed/unchanged/removed classification); `bundleSha256` integrity failure aborts and emits correct audit event; `catalogSha256` mismatch aborts bundle; UpdateSpec optimistic lock conflict retried correctly; Watch `Deleted` revision events consumed (not polling GET) to track cleanup completion; Zone `phase=Pending` while intents outstanding; Zone `phase=Degraded` immediately when any cleanup outstanding (no grace window); Zone `phase=Ready` when complete; activation returns after durable queue commit, not after reconcile; same-name `managedBy=controller` OR `managedBy=api` collision emits per-item `config-collision` error without seizing resource, other intents continue; unchanged spec refreshes `configurationGeneration` without triggering controller reconcile; final deletion: atomic tx commits `Deleted` revision event + row/index removal only; audit append follows committed revision via dedup/exactly-once recovery (NOT part of atomic tx); recovery retry produces no duplicate audit record; prior bundle record released after cleanup-complete and retention count exceeded; activation with zero diff and identical bundleSha256 is a no-op", + "workItemId" : "ADR046-exec-015" + }, + { + "currentSource" : "No pre-ADR45 v3 baseline equivalent; ADR-only", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-exec-001; `d2b-session` v3 crate owner", + "destination" : "`packages/d2b-bus-session/src/`: all above modules verbatim; `packages/d2b-bus-session/tests/`: all above tests verbatim", + "detailedDesign" : "The entire `d2b-session` portable ComponentSession runtime is transport-agnostic and contains no ADR 0045 realm-specific types. All Noise handshake parameters, record framing, fragmentation, named stream mux, fair scheduler, cancellation, deadlines, bootstrap PSK, and attachment bindings are directly reusable. `ComponentSessionDriver` trait and `serve_ttrpc_services` are the primary integration surface for every v3 bus service. The `sessions/lib.rs` re-export boundary (`pub use d2b_contracts::v2_component_session as contract`) must be updated to point at the v3 wire contract module. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy verbatim; rename crate from `d2b-session` to `d2b-bus-session` or retain name.", + "integration" : "All v3 bus service implementations (`d2b-zone-service`, `d2b-provider-agent`, `d2b-bus-client`) depend on this crate; EphemeralProcess attach service; Process Provider launch ticket channel", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Copy tests verbatim; all tests must pass on v3 baseline without modification; re-run `tests/noise_vectors.rs` golden vectors; add one v3-specific test: endpoint policy identity uses v3 `ZoneId`/zone-name binding rather than ADR45 `RealmId`", + "workItemId" : "ADR046-exec-016" + }, + { + "currentSource" : "`packages/d2b-session-unix/src/` at pre-ADR45 baseline `b5ddbed6` (partially equivalent: `d2b-session-unix` existed with `d2b-session-unix/src/adapter.rs` `UnixSeqpacketTransport`, `d2b-session-unix/src/descriptor.rs`, `d2b-session-unix/src/pidfd.rs`); main commit extends with credit pools, full pidfd identity verifier, and `host-socket` feature gate", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-exec-016; `d2b-bus-session-unix` crate owner", + "destination" : "`packages/d2b-bus-session-unix/src/`: all above modules verbatim; `packages/d2b-bus-session-unix/tests/`: all above tests verbatim", + "detailedDesign" : "Provides the Linux-specific `OwnedTransport` implementation for Unix seqpacket and stream sockets. `CreditPool`/`ProcessCreditLimit` enforces per-scope FD attachment budget (ADR45 constants: `MAX_PROCESS_ATTACHMENT_CREDITS = 2048`, `MAX_HOST_ATTACHMENT_CREDITS = 8192`, `RESERVED_CONTROL_FDS = 64`). `PidfdIdentityVerifier` provides the `/proc//fdinfo/` parse path that Process controllers use to verify process identity before `pidfd_open(2)` — this is a direct dependency of system-systemd and system-minijail Process Providers (ADR046-exec-006, ADR046-exec-007). Primary reuse disposition: `adapt`. Preserved source-plan detail: copy verbatim; rename crate from `d2b-session-unix` to `d2b-bus-session-unix` or retain name.", + "integration" : "v3 Zone runtime public socket listener; system-minijail/system-systemd pidfd identity verification; EphemeralProcess attach named stream", + "removalProof" : "If the crate is renamed, the superseded `packages/d2b-session-unix/` owner is removed or reduced to a compatibility wrapper after `packages/d2b-bus-session-unix/` passes copied and v3-specific tests; if the name is retained, no prior owner is removed.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Copy all 20+ tests verbatim; all must pass; add v3-specific test: `PathnamePeerVerifier` verifies against `d2b-zonert` daemon uid; credit pool per-Zone-runtime limits match v3 constants", + "workItemId" : "ADR046-exec-017" + }, + { + "currentSource" : "No pre-ADR45 v3 baseline equivalent for v3 bus wire types; ADR-only", + "dataMigration" : "Full d2b 3.0 reset; no existing constants module state to migrate", + "dependencyOwner" : "ADR046-exec-016; `d2b-bus-wire` contract owner", + "destination" : "`packages/d2b-bus-wire/src/session.rs`: v3 bus protocol constants and wire types; all numeric constants copied verbatim; `PREFACE_MAGIC` retained; `EndpointPolicy` and `EndpointPolicyIdentity` adapted to use v3 `ZoneId`/`ProviderId` instead of ADR45 `RealmId` in the policy identity fingerprint", + "detailedDesign" : "All numeric constants (frame sizes, credit limits, deadline values, reconnect limits) are directly reusable without change — they are derived from protocol analysis, not from realm semantics. `LimitProfile::local_default()` is the source for `serve_ttrpc_services` capacity; retain the value. `EndpointPolicy` carries the Noise static key and schema fingerprint; the fingerprint computation does not embed realm names and is reusable. The `EndpointPolicyIdentity` type carries the zone runtime's static public key — update from ADR45 `RealmId` to v3 `ZoneId` string encoding. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "`d2b-bus-session` imports constants from here; all v3 bus service and client crates import protocol constants from `d2b-bus-wire`", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Compile-time assertions on all copied numeric constants matching source values; `EndpointPolicyIdentity` golden-vector test with v3 zone name encoding; `LimitProfile::local_default()` round-trip test", + "workItemId" : "ADR046-exec-018" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/provider.rs` at baseline `b5ddbed6` (`HostSubstrateProvider`, `RuntimeProvider`, `WorkloadProvider` traits — baseline, unwired); ADR-only for the runtime registry and RPC proxy", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-exec-016 + ADR046-exec-018; `d2b-provider-runtime` crate owner", + "destination" : "`packages/d2b-provider-runtime/src/`: `registry.rs`, `rpc.rs`, `instance.rs`, `context.rs`, `error.rs`; provider trait objects moved to `d2b-bus-wire` or `d2b-provider-contracts`", + "detailedDesign" : "`ProviderRegistry` manages in-flight permits, draining, and provider lifecycle. `RegistryLimits { total_in_flight, per_provider_in_flight }` is directly reusable. `RpcProviderProxy` wraps a `ComponentSessionDriver` and dispatches typed `RpcCall` to an `AuthenticatedProviderRpc` implementation — the proxy pattern is fully reusable for v3 Provider resource controllers. `SessionIdentity { peer_role, service, provider_id, provider_type, provider_generation }` maps directly to a v3 Provider session credential. `InFlightPermit` RAII guard is directly reusable. The provider trait object set (`RuntimeProvider`, `StorageProvider`, etc.) adapts to v3 Provider resource typed methods; the trait hierarchy is preserved but `ProviderMethod` enum variant names may be renamed to drop ADR45 workload terminology. `ProviderRegistry::MAX_PROVIDER_REGISTRY_ENTRIES` bound from v2_provider is retained. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Zone runtime provider-agent ComponentSession; system-core Provider controller; every v3 Provider resource controller", + "removalProof" : "Supersedes the baseline `packages/d2b-realm-provider/src/provider.rs` runtime/provider trait owner after `packages/d2b-provider-runtime/` registry/RPC tests pass and no v3 registry path imports ADR45 workload terminology.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Copy `d2b-provider` tests; `RegistryLimits::validate` enforces non-zero and per≤total; `InFlightPermit` RAII release; drain/retire state machine; `RpcProviderProxy` round-trip over `FakeProvider` (from toolkit ADR046-exec-020)", + "workItemId" : "ADR046-exec-019" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/conformance.rs` at baseline `b5ddbed6` (`check_provider_conformance`); ADR-only for toolkit server and fixture", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-exec-019; `d2b-provider-toolkit` owner", + "destination" : "`packages/d2b-provider-toolkit/src/`: retain all modules verbatim; adapt `ProviderAgentAdapter` to use v3 `ProviderRegistry` (ADR046-exec-019) and v3 bus wire types; adapt `GeneratedProviderServiceServer` to use v3 generated service stubs (ADR046-exec-021); `packages/d2b-provider-agent/src/`: adapted from `gateway-runtime/src/provider_agent.rs`", + "detailedDesign" : "`ProviderAgentAdapter` is the core: it drives a `ComponentSessionDriver` receive loop, dispatches decoded ttrpc frames to `ProviderRegistry`, and forwards responses. `GeneratedProviderServiceServer` closes the loop by registering all generated service stubs with `serve_ttrpc_services`. `FakeProvider` implements every v2 Provider trait with deterministic outputs — adapt each trait method to v3 Provider resource semantics while retaining the fixture pattern. `Redacted` / `Secret` zero-copy wrappers are used in every audit log path; copy verbatim. Provider conformance check pattern is retained: descriptor validation, capability publication, fixture round-trip, observability query result. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Every v3 Provider resource controller uses `ProviderAgentAdapter` + `ProviderAgentProcess`; conformance tests gate Provider dossier acceptance; `FakeProvider` is used in all Provider controller hermetic tests", + "removalProof" : "Supersedes baseline `packages/d2b-realm-provider/src/conformance.rs` ownership only after `packages/d2b-provider-toolkit/` conformance coverage passes; ADR45 provider-agent registration behavior is not retained in the v3 provider-agent path.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Copy conformance tests verbatim; `check_descriptor_conformance` passes on a `FakeProvider` descriptor; `check_provider_conformance` covers all five `ConformanceError` variants; `ProviderAgentProcess` shutdown within deadline test; `MAX_DISPATCH_IN_FLIGHT` semaphore back-pressure test", + "workItemId" : "ADR046-exec-020" + }, + { + "currentSource" : "No pre-ADR45 v3 baseline equivalent; ADR-only", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-exec-016 + ADR046-exec-018; v3 Zone service contract owner", + "destination" : "`packages/d2b-bus-contracts/src/generated_v3_services/`: v3 generated ttrpc stubs for Zone service methods (Resource CRUD, Watch, ComponentSession service verbs); `packages/d2b-zone-service/src/`: Zone runtime service handler adapted from `DaemonServiceV2` pattern; `packages/d2b-zone-service/src/admission.rs`, `handler.rs`, `routing.rs`", + "detailedDesign" : "`StrictWireMessage` trait (decode_strict, encode_strict) is directly reusable — it enforces deny-unknown-fields decode and schema-pinned fingerprint validation. `ServiceInventoryDocument` / `service_schema_fingerprint` pattern provides the service schema publication mechanism that v3 Provider resources use to advertise their ComponentSession service interface. `DaemonServiceV2` / `DaemonOperationHandler` pattern becomes the v3 Zone service handler base: `DaemonCallContext` → v3 `ZoneCallContext` with `ZoneId`, principal `User/`, operation deadline; `DaemonMethod` enum → v3 `ZoneMethod` (ResourceGet, ResourceList, ResourceWatch, ResourceCreate, ResourceUpdateSpec, ResourceUpdateStatus, ResourceDelete, BusAttach); `daemon_endpoint_policy` → v3 zone endpoint policy with v3 `ZoneId`-bound static key and schema fingerprint. `server_stream_name`/`parse_server_stream_name` for Watch stream naming is reusable verbatim. `TerminalStreamValidator` / `ServerStreamLease` for terminal byte stream safety is reusable for EphemeralProcess attach. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt; v3 generates new protobuf definitions and new ttrpc stubs; reuse message shaping, method dispatch patterns, and service inventory pattern.", + "integration" : "Zone runtime ttrpc service over public socket ComponentSession; Provider resource controller attaches via `RpcProviderProxy`; ResourceClient Watch uses server stream naming convention", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "`StrictWireMessage` decode rejects unknown fields on all v3 message types; `service_schema_fingerprint` is stable across builds; `ZoneCallContext` deadline enforcement test; `DaemonSeqpacketTransport` → `ZoneSeqpacketTransport` end-to-end roundtrip; generated v3 stub compile check", + "workItemId" : "ADR046-exec-021" + }, + { + "currentSource" : "No pre-ADR45 v3 baseline equivalent; ADR-only", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-exec-016 + ADR046-exec-021; `d2b-bus-client` crate owner", + "destination" : "`packages/d2b-bus-client/src/`: all above modules; `DaemonClient` → `ZoneClient` (v3 Resource CRUD/Watch verbs); `GuestClient` → `ProcessAttachClient`; `HostSocketConnector` → `ZoneSocketConnector`; `LocalDaemonSession` → `LocalZoneSession`", + "detailedDesign" : "`Client` is the core typed async client with bounded retry, wall-clock injection, and cancellation. `ConnectedSession` wraps a `ComponentSessionDriver` and provides `call()`, `open_stream()`, and `close()`. `ComponentSessionConnector` trait decouples connection establishment from the client — v3 `ZoneSocketConnector` implements this for the local Zone runtime public socket. `TargetResolver`/`RouteTable` provides request routing to local vs remote Zone runtimes. `HostSocketConnector::local_daemon_endpoint_identity` provides the peer identity pinning that prevents MITM on the local socket — this is a security-critical invariant; copy verbatim, rename from `d2bd` to `d2b-zonert` uid. `DaemonClient` method table adapts to v3 Resource verbs: `ResourceGet`, `ResourceList`, `ResourceWatch` (streaming), `ResourceCreate`, `ResourceUpdateSpec`, `ResourceUpdateStatus`, `ResourceDelete`. `ServiceHandle`/`MethodHandle`/`GeneratedClient` provide the typed client stub generation pattern. `RetryPolicy`/`RetryClass`/`RemoteErrorKind` error classification is directly reusable. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "CLI (`d2b` binary), external Zone API callers, Process/EphemeralProcess controller ResourceClient, all consumer-facing API paths", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Copy `tests/client.rs` verbatim; add v3-specific tests: `ResourceWatch` streaming teardown; `ZoneSocketConnector` peer-uid mismatch rejection; retry policy respects `RetryClass::Transient`/`Permanent`; `TargetInput`→`ResolvedTarget` for local-only v3 Zone; `local_daemon_endpoint_identity` returns correct v3 zone-rt uid", + "workItemId" : "ADR046-exec-022" + }, + { + "currentSource" : "`packages/d2b-realm-router/src/` at baseline `b5ddbed6` (`OperationRouter`, `RouteDecision`, `OperationRoutePlan`, `ReconcilableLease` — unwired in daemon); ADR-only for Zone service routing with idempotency", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-exec-021 + ADR046-exec-019; Zone bus routing owner", + "destination" : "`packages/d2b-zone-router/src/`: `router.rs` (v3 `ZoneOperationRouter` — idempotency semantics copied verbatim; dedup key namespace adapted from `(realm, principal, node, kind, key)` to `(zone, resource-type, resource-name, verb, idempotency-key)`); `service.rs` (v3 `ZoneServiceLimits`, `ZoneServiceServer`, `ZoneAuditEvent`); `resolver.rs` (v3 `ZoneTargetResolver`, `ZoneEntrypointTable`)", + "detailedDesign" : "The idempotency/dedup semantics from `OperationRouter` are security-critical and must be copied exactly: (1) dedup key is the full 5-tuple namespace — reusing a key under a different principal is a conflict, not a replay; (2) expired keys leave tombstones for a no-reuse horizon; (3) same-key/same-request returns the original `operation_id` and recorded result; (4) same-key/different-request returns conflict error fail-closed. These semantics apply to all v3 Resource mutation verbs (Create, UpdateSpec, UpdateStatus, Delete) that carry an idempotency key. `RealmServiceLimits` numeric bounds are copied verbatim: `MAX_DISPATCH_IN_FLIGHT=64` gates concurrent resource mutations per Zone session. `RealmSessionAuthority` principal-binding model (session principal MUST match request principal field, derived in trusted code from authenticated session) maps directly to v3 Zone RBAC: `ZoneCallContext` carries the authenticated `User/` principal from `SO_PEERCRED`; no caller-supplied principal field is accepted. `DurableExecTable`/`DEFAULT_MAX_EXECUTIONS` from `execution.rs` provides the EphemeralProcess in-flight table bound for the Zone router.", + "integration" : "Zone runtime ttrpc service (ADR046-exec-021); Zone ResourceClient (ADR046-exec-022); every Resource mutation verb", + "removalProof" : "Supersedes the baseline `packages/d2b-realm-router/src/` routing owner after `packages/d2b-zone-router/` passes idempotency, dispatch-limit, and principal-binding tests; ADR45 realm/workload route tables are not imported into v3.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Idempotency replay returns original result; conflict returns error; expired tombstone fails closed; `MAX_DISPATCH_IN_FLIGHT` semaphore back-pressure; principal-binding enforcement (mismatched principal returns auth-denied); `DurableExecTable` capacity limit; v3 5-tuple dedup key golden vector test", + "workItemId" : "ADR046-exec-023" + }, + { + "currentSource" : "`packages/d2b-core/src/{storage,sync}.rs` (atomic/idempotency reference shape, E4); no redb usage exists anywhere in this repository at any inspected commit", + "dataMigration" : "None (disposable fixture data only)", + "dependencyOwner" : "W0 shared contract root; store/reconciliation integrator", + "destination" : "`proofs/redb-resource-store-spike/`", + "detailedDesign" : "Implements SPIKE-01 and SPIKE-02: the eight-table schema, fair write queue, blocking store-actor, watch registrar, and hint bus described in those two spike entries Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (the atomic-write/idempotency discipline in `storage.rs`/`sync.rs` is adapted into the spike's write-transaction algorithm; redb itself is used unmodified).", + "integration" : "None (standalone; no d2b-bus/ComponentSession/broker dependency)", + "removalProof" : "Per SPIKE-01/SPIKE-02 Cleanup rows: deleted once `packages/d2b-resource-store-redb` and `packages/d2b-controller-toolkit/benches/reaction.rs` reproduce equal-or-stricter coverage", + "reuseAction" : "adapt", + "reuseSource" : "None (redb is a new external dependency; no main or v3 code implements it)", + "specId" : "ADR-046-feasibility-and-spikes", + "specPath" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "validation" : "SPIKE-01 metrics (1)-(5) and SPIKE-02 metrics (1) across all 3 concurrency profiles, per those entries' exact pass/fail thresholds", + "workItemId" : "ADR046-feasibility-001" + }, + { + "currentSource" : "`packages/d2bd/src/supervisor/{dag,pidfd}.rs` (current DAG/pidfd reference shape, E4)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-feasibility-001`; reconciliation/process integrator", + "destination" : "`proofs/process-fastlaunch-spike/`", + "detailedDesign" : "Implements SPIKE-03: the fake Process controller loop, fake `ProcessLaunchEffectPort`, and the 1/10/100-concurrency commit-to-launch-attempt and next-dispatch-independence benchmarks Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (current DAG ordering/readiness concepts are adapted into the spike's per-resource single-flight/parallel-semaphore loop).", + "integration" : "Consumes `ADR046-feasibility-001`'s hint-bus shape as its watch-receiver input", + "removalProof" : "Deleted once `packages/d2b-controller-toolkit/benches/reaction.rs` and the Process Provider integration tests named by `ADR046-reconcile-003` reproduce equal-or-stricter coverage", + "reuseAction" : "adapt", + "reuseSource" : "None (the generic async controller loop is ADR-only per `ADR-046-resource-reconciliation`'s own current-code-fit row)", + "specId" : "ADR-046-feasibility-and-spikes", + "specPath" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "validation" : "SPIKE-03 metrics (1)-(3) and thresholds", + "workItemId" : "ADR046-feasibility-002" + }, + { + "currentSource" : "`packages/d2b-priv-broker/src/ops/spawn_runner.rs` and `tests/pidfd_real_spawner.rs` (current blocking-call reference shape, E4)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Independent of `-001`/`-002`; EffectPort/ProviderSupervisor integrator", + "destination" : "`proofs/effectport-async-spike/`", + "detailedDesign" : "Implements SPIKE-04: the four fake EffectPort traits, the deliberately slow blocking-primitive backends, and the current-thread-runtime heartbeat-jitter detector", + "integration" : "None (standalone)", + "removalProof" : "Deleted once `packages/d2b-provider-supervisor` and the volume-domain effect adapter each carry an equal-or-stricter in-tree blocking-adapter regression test", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-feasibility-and-spikes", + "specPath" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "validation" : "SPIKE-04 heartbeat-jitter metric and threshold", + "workItemId" : "ADR046-feasibility-003" + }, + { + "currentSource" : "None in v3 at `b5ddbed6` (no generic Provider registry exists; per parent ADR context, this is explicitly listed as missing)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Independent of `-001`/`-002`/`-003`; Provider packaging/toolkit integrator", + "destination" : "`proofs/provider-packaging-spike/`", + "detailedDesign" : "Implements SPIKE-05: the two-binary crate, hand-authored manifest, fake `ProviderDeployment`, and the `cargo metadata` dependency-edge check Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (the crate-layout policy check reuses the same `src/`/`tests/`/`integration/`/`README.md` structure already enforced elsewhere in this repository's workspace policy tests).", + "integration" : "None (standalone)", + "removalProof" : "Deleted once the real Provider-toolkit crate (`ADR046-provider-001` destination) ships equal-or-stricter manifest-parsing/enumeration/workspace-policy coverage", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-feasibility-and-spikes", + "specPath" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "validation" : "SPIKE-05 metrics (1)-(4) across 20 repeated randomized-order manifest loads", + "workItemId" : "ADR046-feasibility-004" + }, + { + "currentSource" : "`packages/d2b-realm-router/src/{session,secure_session,mux_session}.rs` (current routing reference shape, E4); main `a1cc0b2d` `packages/d2b-session/**`, `packages/d2b-session-unix/**` (re-verified green in E3)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Independent of `-001` through `-004`; bus/session/transport/credential integrator", + "destination" : "`proofs/bus-routing-noise-spike/`, `proofs/transport-opaque-streams-spike/`, `proofs/credential-kk-e2e-spike/`", + "detailedDesign" : "Implements SPIKE-06 (exact-addressed routing + per-recipient Noise isolation), SPIKE-07 (Unix/vsock/relay-shaped opaque byte-stream conformance across 3 backends), and SPIKE-08 (Credential Provider → consumer Provider KK delivery with the 13-field binding contract) Primary reuse disposition: `adapt`. Preserved source-plan detail: `copy-unchanged` for the Noise/record/transport machinery (path-dependency on a pinned local checkout of `a1cc0b2d`); `adapt` for the fake router/relay/credential-delivery wrapper code that SPIKE-06/07/08 add on top.", + "integration" : "SPIKE-07's Unix backend and SPIKE-08's session machinery both depend on the same pinned `a1cc0b2d` path-dependency established for SPIKE-06", + "removalProof" : "Deleted per each spike's own Cleanup row: `packages/d2b-bus/src/router.rs` for SPIKE-06; the three real transport Provider crates for SPIKE-07; the real Credential Provider crates for SPIKE-08", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `packages/d2b-session/src/{handshake,bootstrap,record,engine,scheduler,streams,lifecycle,transport}.rs`, `packages/d2b-session-unix/src/{adapter,vsock,pidfd,socket,systemd,credit,descriptor}.rs`, and the exact test files listed in E3's table", + "specId" : "ADR-046-feasibility-and-spikes", + "specPath" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "validation" : "SPIKE-06 metrics (1)-(3), SPIKE-07 metrics (1)-(4) against the exact numeric gates already committed in the transport-unix/vsock dossiers, SPIKE-08 metrics (1)-(6)", + "workItemId" : "ADR046-feasibility-005" + }, + { + "currentSource" : "None in v3 at `b5ddbed6` (ProviderStateSet and the generalized Volume ResourceType are both ADR-only)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Independent of `-001` through `-005`; Provider-state/Volume integrator", + "destination" : "`proofs/provider-state-export-spike/`, `proofs/volume-policy-spike/`", + "detailedDesign" : "Implements SPIKE-09 (optional declared state-Volume creation order, guest-local/host-backed-guest placement, virtiofs Export child ownership) and SPIKE-10 (Volume ACL/`sourcePolicyId`/quota/lifecycle-marker policy conformance)", + "integration" : "None between the two spikes beyond sharing the same fake resource-store oracle shape", + "removalProof" : "Deleted per each spike's Cleanup row: the real `ADR-046-provider-state` work-item destination for SPIKE-09; `d2b-provider-volume-local`'s own `tests/`/`integration/` suite for SPIKE-10", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-feasibility-and-spikes", + "specPath" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "validation" : "SPIKE-09 metrics (1)-(4); SPIKE-10 metrics (1)-(5), zero-tolerance on path leakage", + "workItemId" : "ADR046-feasibility-006" + }, + { + "currentSource" : "`packages/d2b-priv-broker/src/ops/spawn_runner.rs` (real-spawn reference shape, E4); current unsafe-local helper runtime/systemd invocation shape", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Independent of `-001` through `-006`; Process Provider integrator", + "destination" : "`proofs/process-provider-conformance-spike/`", + "detailedDesign" : "Implements SPIKE-11: the shared `ProcessProviderHarness` trait, the minijail-shaped `clone3(CLONE_PIDFD)` launcher, and the systemd transient-user-scope launcher, plus the identity-drift/quarantine and clean-exit cases", + "integration" : "None (standalone; requires a Linux host with `clone3`/`pidfd_open`, and optionally a running `systemd --user` instance behind the `systemd-user` feature)", + "removalProof" : "Deleted once `packages/d2b-provider-system-systemd` and `packages/d2b-provider-system-minijail` each carry this exact shared conformance suite in their own `tests/`", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-feasibility-and-spikes", + "specPath" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "validation" : "SPIKE-11 metrics (1)-(4), zero-tolerance on false adoption", + "workItemId" : "ADR046-feasibility-007" + }, + { + "currentSource" : "`nixos-modules/assertions.nix` pattern, `packages/xtask` `gen-schemas` pattern, `make test-drift` gate (existing generated-or-eval-contract precedent)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Independent of `-001` through `-007`; Nix/xtask integrator", + "destination" : "`proofs/nix-authoring-spike/`", + "detailedDesign" : "Implements SPIKE-12: the minimal flake, the two synthetic ResourceTypes, the hand-written committed schemas, the standalone `gen-schemas`-shaped drift check, and the two-generation removed-resource cleanup simulation", + "integration" : "None (standalone flake; no dependency on the main `flake.nix`)", + "removalProof" : "Deleted once the real `nixos-modules/resources.nix` and `packages/xtask` `gen-schemas` implementation reproduce these metrics as part of `make test-drift`/`make test-flake`", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-feasibility-and-spikes", + "specPath" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "validation" : "SPIKE-12 metrics (1)-(5), byte-for-byte reproducibility across 3 hermetic builds", + "workItemId" : "ADR046-feasibility-008" + }, + { + "currentSource" : "`packages/d2b/src/lib.rs` `cmd_audio`/`cmd_clipboard_arm` and the current command-table shape (current CLI reference shape)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Independent of `-001` through `-008`; CLI integrator", + "destination" : "`proofs/cli-discovery-spike/`, `proofs/clean-cutover-spike/`", + "detailedDesign" : "Implements SPIKE-13 (dynamic Provider-projection discovery, bounds, latency isolation) and SPIKE-14 (zero v2 dispatch, fresh Zone bootstrap ignoring legacy state)", + "integration" : "None between the two spikes beyond sharing the same fixture command-table shape", + "removalProof" : "Deleted per each spike's Cleanup row: the real `d2b` CLI crate's own discovery conformance test for SPIKE-13; the real CLI crate's workspace-policy/lint gate plus the real bootstrap sequence for SPIKE-14", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-feasibility-and-spikes", + "specPath" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "validation" : "SPIKE-13 metrics (1)-(7); SPIKE-14 metrics (1)-(3), zero-tolerance on legacy-file access", + "workItemId" : "ADR046-feasibility-009" + }, + { + "currentSource" : "None (this is a pure integration of the other nine work items' fixtures)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-feasibility-001` through `ADR046-feasibility-009` (integrates their fakes; must run last)", + "destination" : "`proofs/e2e-composition-spike/`", + "detailedDesign" : "Implements SPIKE-15: the three representative compositions (local/cloud-hypervisor, cloud/azure, interaction/shell-terminal-or-wayland), wired from the fakes built by `-001` through `-009`, plus the combined 3-Zone aggregate RSS measurement", + "integration" : "Depends on and imports the fake shapes from `proofs/redb-resource-store-spike/`, `proofs/process-fastlaunch-spike/`, `proofs/effectport-async-spike/`, `proofs/provider-packaging-spike/`, `proofs/bus-routing-noise-spike/`, `proofs/transport-opaque-streams-spike/`, `proofs/credential-kk-e2e-spike/`, `proofs/provider-state-export-spike/`, `proofs/volume-policy-spike/`, and `proofs/process-provider-conformance-spike/`", + "removalProof" : "Deleted once the real integration test suites named by the individual Provider dossiers (`integration/` per D059) collectively reproduce all three compositions against real, non-fake Zone/store/bus/broker code", + "reuseAction" : "adapt", + "reuseSource" : "None beyond what `-001` through `-009` already reuse", + "specId" : "ADR-046-feasibility-and-spikes", + "specPath" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "validation" : "SPIKE-15 metrics (1)-(4) across all three compositions", + "workItemId" : "ADR046-feasibility-010" + }, + { + "currentSource" : "this codebase's ad hoc `tests/tools/` timing logs (`d2b-static-timing.$$/`), which are not a candidate-bound, reference-runner-recorded ledger", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-delivery-007`; delivery/test-tooling integrator", + "destination" : "`proofs/test-runtime-budget-spike/`; the committed baseline ledger consumed by `ADR046-delivery-007`", + "detailedDesign" : "Establishes the D094 measurement baseline: records the reference runner class, repetition count, and per-test/crate/shard p95 for a representative hermetic crate; proves the §10.16 budgets (individual normal test p95 ≤50 ms, per-crate `--lib --tests` ≤2 s, Layer-1 hermetic shard ≤60 s) are met on the reference runner and that an injected slow/sleeping test is detected as a regression", + "integration" : "Output ledger shape is consumed by the runtime ledger/timing gate; establishes the historical threshold seed", + "removalProof" : "Deleted once `ADR046-delivery-007`'s in-tree ledger/timing gate reproduces the baseline against the real crate set", + "reuseAction" : "adapt", + "reuseSource" : "existing `libtest --format=json` timing output and `xtask` (no new test framework)", + "specId" : "ADR-046-feasibility-and-spikes", + "specPath" : "docs/specs/ADR-046-feasibility-and-spikes.md", + "validation" : "The representative crate meets every budget; a synthetic slow/sleep/process/network test is flagged; cold compile time is recorded on a separate line and excluded from the execution budgets", + "workItemId" : "ADR046-feasibility-011" + }, + { + "currentSource" : "No v3 source; `packages/d2b-host/src/gpu_argv.rs` and `packages/d2b-host/src/video_argv.rs` (implemented-and-reachable) provide argv generators", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-resources-device` accepted; `ADR046-provider-model-and-packaging` accepted; workspace root must add crate", + "destination" : "`packages/d2b-provider-device-gpu/` with `src/`, `tests/`, `integration/`, `README.md`; add to workspace `Cargo.toml` members list (alphanumerically sorted)", + "detailedDesign" : "Crate scaffold: `Cargo.toml` with `d2b-host`, `d2b-contracts`, `d2b-provider-toolkit`, `d2b-core` dependencies; `lib.rs` exporting controller binary entry points; `error.rs` with `DeviceGpuError` closed-set enum; placeholder `controller.rs` Primary reuse disposition: `extract`. Preserved source-plan detail: `extract` both argv files into `d2b-provider-device-gpu/src/argv.rs` as re-exports; do not copy logic.", + "integration" : "Workspace policy test must pass; crate must build; `src/`, `tests/`, `integration/`, `README.md` must exist", + "removalProof" : "N/A (new crate)", + "reuseAction" : "extract", + "reuseSource" : "`packages/d2b-host/src/gpu_argv.rs` (baseline `b5ddbed`), `packages/d2b-host/src/video_argv.rs` (baseline `b5ddbed`)", + "specId" : "ADR-046-provider-device-gpu", + "specPath" : "docs/specs/providers/ADR-046-provider-device-gpu.md", + "validation" : "`cargo build -p d2b-provider-device-gpu`; workspace policy crate-layout check passes", + "workItemId" : "ADR046-gpu-001" + }, + { + "currentSource" : "`packages/d2bd/src/usbip_state_machine.rs` (implemented-and-reachable) as reconcile loop pattern reference. GPU/video reconcile state is `ADR-only`.", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-gpu-001; `ADR-046-resource-reconciliation` implementation present; Provider toolkit `ResourceClient` available", + "destination" : "`packages/d2b-provider-device-gpu/src/{controller.rs,telemetry.rs}`", + "detailedDesign" : "Five triggers: `spec-generation-changed`, `deletion-requested`, `dependency-changed`, `scheduled-observe`, `owned-resource-changed`. Each trigger handler writes optimistic `ResourceMutationBatch`. Status writer in `status.rs`. Async watch task + per-resource reconcile tasks. Independent resources in parallel. Telemetry uses only closed `mode`/`video_sidecar`/`arbitration` semantics; `d2b.zone` and `d2b.provider` remain resource attributes and no Zone/resource-name-derived key is a metric label. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — implement the five-trigger reconcile loop using Provider toolkit async reconciler.", + "integration" : "Resource API (ADR046 store) must be present; fake ResourceClient available from Provider toolkit; `tests/combined_reconcile.rs` validates trigger dispatch", + "removalProof" : "Current ProcessRole::Gpu/Video/GpuRenderNode retained until this test passes; see ProcessRole disposition table", + "reuseAction" : "adapt", + "reuseSource" : "Pattern only: `packages/d2bd/src/usbip_state_machine.rs` (baseline). No code copy.", + "specId" : "ADR-046-provider-device-gpu", + "specPath" : "docs/specs/providers/ADR-046-provider-device-gpu.md", + "validation" : "`cargo test -p d2b-provider-device-gpu --test combined_reconcile`; all five trigger handlers must reach their expected output state; structural metric descriptor test asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus GPU Device/Zone-name canary absence while preserving `d2b.zone` resource attributes", + "workItemId" : "ADR046-gpu-002" + }, + { + "currentSource" : "`nixos-modules/assertions.nix` x86_64-linux guard; `packages/d2b-core/src/processes.rs` ProcessRole::Gpu/GpuRenderNode; no existing sysfs probe module", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-gpu-002", + "destination" : "`packages/d2b-provider-device-gpu/src/probe.rs`", + "detailedDesign" : "Call `GpuEffectPort::probe_drm_device(selector)` on each `scheduled-observe` trigger; the effect port resolves device presence against the trusted device table and returns a presence/health result without exposing raw sysfs or device paths to the controller. Three-strike failure counter; `observe_interval_secs` (10–60, default 30); emit `DevicePresent` condition and update `lastProbedAt`.", + "integration" : "`scheduled-observe` trigger from reconcile loop calls `probe::check_drm_device`", + "removalProof" : "N/A (new module)", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-gpu", + "specPath" : "docs/specs/providers/ADR-046-provider-device-gpu.md", + "validation" : "`tests/conformance.rs` contains probe-mock path; `cargo test` passes", + "workItemId" : "ADR046-gpu-003" + }, + { + "currentSource" : "`packages/d2b-core/src/bundle_resolver.rs` `validate_graphics_vm_invariants` (assertion guard) — `ADR-only` for resource-level arbitration", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-gpu-002", + "destination" : "`packages/d2b-provider-device-gpu/src/arbitration.rs`", + "detailedDesign" : "On `spec-generation-changed` and each new claim: check `arbitration` vs `maxConcurrentClaims` vs current `holderRefs` length. Exclusive: reject any second claim with `ClaimConflict` condition, set requesting Device phase `Degraded`. Shared render-node: accept up to `maxConcurrentClaims`. Admission: `shared + renderNodeOnly=false` fails with `shared-arbitration-requires-render-node-only`.", + "integration" : "Tested by `tests/arbitration_conflict.rs`; integration fixture `render_node_shared/`", + "removalProof" : "N/A (new module)", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-gpu", + "specPath" : "docs/specs/providers/ADR-046-provider-device-gpu.md", + "validation" : "`cargo test -p d2b-provider-device-gpu --test arbitration_conflict`; `cargo test -p d2b-provider-device-gpu --test render_node_enforcement`", + "workItemId" : "ADR046-gpu-004" + }, + { + "currentSource" : "`packages/d2b-host/src/gpu_argv.rs` (implemented-and-reachable); `packages/d2b-core/src/bundle_resolver.rs` lines 1888–1894 (device token set); `packages/d2b-core/src/processes.rs` `ProcessRole::Gpu`, `ProcessRole::GpuRenderNode` (implemented-and-reachable); `nixos-modules/minijail-profiles.nix` gpu/gpu-render-node profiles (implemented-and-reachable)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-gpu-002; `ADR046-components-processes-and-sandbox` (Provider/system-minijail present and able to handle Process resources with `gpu-worker`/`render-node-worker` templates)", + "destination" : "`packages/d2b-provider-device-gpu/src/worker_gpu.rs`", + "detailedDesign" : "Build and commit `Process` resource record with `template: gpu-worker` or `template: render-node-worker`; set `sandbox.seccompClass` (`w1-gpu` or `w1-gpu-render-node`), `sandbox.userNamespace: {mappingClass: process-principal-root}` (uid/gid resolved privately by core from signed worker template — controller does NOT write numeric values), `sandbox.namespaceClasses`, `sandbox.capabilityClasses=[]`, `sandbox.startRoot=false`; set `deviceUsage[{deviceRef,access,purpose}]`, `networkUsage: null`, `endpoints[{name,transport,purpose}]`, `budget` (including `pids` and `fds` bounded limits), `readiness` (with `class`, `initialDelay`, `timeout`, `failureThreshold`, `successThreshold`), and `restartPolicy` (with `class`, `backoffBase`, `backoffMax`, `backoffMultiplier`, `maxRestarts`, `resetAfter`). Provider/system-minijail validates and resolves the LaunchTicket and sends effect requests via `MinijailProcessEffectPort`; the core EffectPort adapter routes them to the **privileged broker** which performs `SpawnRunner`, `OpenDevice`, `clone3`, `uid_map`/`gid_map` writes, and fd transfer — the device-gpu controller does not have execution authority or fd access. `crossDomainTrusted` gating: the signed descriptor is static; `crossDomainTrusted` is projected from the Device setting into the LaunchTicket by Provider/system-minijail, which omits `GpuContextType::CrossDomain` from runtime argv when false. Primary reuse disposition: `adapt`. Preserved source-plan detail: `extract` argv builder logic into `argv.rs` as re-export from `d2b-host` (used by Provider/system-minijail at LaunchTicket resolution time; the signed component descriptor is static and is not rewritten per Device); `adapt` device allowlist token set from `bundle_resolver.rs` into `worker_gpu.rs` `GPU_DEVICE_ALLOWLIST` constant for `deviceUsage` population.", + "integration" : "`integration/gpu_worker_start/`; `integration/render_node_shared/`; `packages/d2b-contract-tests/tests/minijail_gpu.rs` (reused existing test)", + "removalProof" : "`ProcessRole::Gpu` and `ProcessRole::GpuRenderNode` removed from `processes.rs` only after both integration tests pass and the ProcessRole disposition contract test confirms zero remaining references", + "reuseAction" : "adapt", + "reuseSource" : "`packages/d2b-host/src/gpu_argv.rs` (baseline `b5ddbed`): `GpuArgvInput`, `GpuParams`, `GpuContextType`, `GpuDisplayConfig`; `packages/d2b-core/src/bundle_resolver.rs` device token constant comment", + "specId" : "ADR-046-provider-device-gpu", + "specPath" : "docs/specs/providers/ADR-046-provider-device-gpu.md", + "validation" : "`cargo test -p d2b-provider-device-gpu`; `cargo test -p d2b-contract-tests --test minijail_gpu` continues to pass", + "workItemId" : "ADR046-gpu-005" + }, + { + "currentSource" : "`packages/d2b-host/src/video_argv.rs` (implemented-and-reachable): `VideoArgvInput`, `VideoBackend`, `wire_contract_snapshot()`; `packages/d2b-contract-tests/tests/video_binary_contract.rs` (implemented-and-reachable); `packages/d2b-contract-tests/tests/minijail_swtpm_video.rs` video section (implemented-and-reachable); `nixos-modules/minijail-profiles.nix` video profile (implemented-and-reachable)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-gpu-005 (video depends on GPU Process being Ready)", + "destination" : "`packages/d2b-provider-device-gpu/src/worker_video.rs`, `tests/wire_constant_snapshot.rs`", + "detailedDesign" : "Controller creates `Process/device--video` only after `GpuWorkerReady=True`. `worker_video.rs` builds `VideoArgvInput` from resolved device spec and signed descriptor binary path. Validates `wire_contract_snapshot()` matches committed golden at startup; fails closed if mismatch (error `device-wire-contract-mismatch`). NVIDIA device gating: include `nvidia-ctl`, `nvidia-device`, `nvidia-uvm` tokens in `deviceUsage[]` entries only when `videoNvidiaDecode=true`; the **privileged broker** opens the fds when executing the effect request from the core EffectPort adapter. Distinct allocator-assigned principal enforced by LaunchTicket (internal invariant; not expressed in the resource spec); `template: video-worker` descriptor declares no Wayland/audio endpoint capability. `sandbox.seccompClass: w1-video`; `sandbox.namespaceClasses` includes `pid`; `userNamespace: null` (explicit, tested invariant). Primary reuse disposition: `adapt`. Preserved source-plan detail: `extract` argv generator (re-export from `argv.rs`); `copy-unchanged` wire-contract constants into `tests/wire_constant_snapshot.rs` golden comparison.", + "integration" : "`integration/video_dependency/`; `packages/d2b-contract-tests/tests/video_binary_contract.rs` (reused); `packages/d2b-contract-tests/tests/minijail_swtpm_video.rs` video section (reused)", + "removalProof" : "`ProcessRole::Video` removed from `processes.rs` only after `integration/video_dependency/` passes and the video Process reaches `Ready` in a live Zone", + "reuseAction" : "adapt", + "reuseSource" : "`packages/d2b-host/src/video_argv.rs` (baseline `b5ddbed`): argv generator, wire-contract constants, `wire_contract_snapshot()`", + "specId" : "ADR-046-provider-device-gpu", + "specPath" : "docs/specs/providers/ADR-046-provider-device-gpu.md", + "validation" : "`cargo test -p d2b-provider-device-gpu --test video_dependency`; `cargo test -p d2b-provider-device-gpu --test wire_constant_snapshot`; `cargo test -p d2b-contract-tests --test video_binary_contract` continues to pass", + "workItemId" : "ADR046-gpu-006" + }, + { + "currentSource" : "`nixos-modules/options-realms-workloads.nix` `d2b.vms..graphics.*` options (generated-or-eval-contract); `nixos-modules/assertions.nix` graphics assertions; `nixos-modules/components/graphics.nix` (host-side); `nixos-modules/components/video/guest.nix` (guest-side)", + "dataMigration" : "Consumer config migration guide: replace `d2b.vms..graphics.enable = true` with a Device resource declaration. Old options emit deprecation warnings, not hard failures, during the transition window.", + "dependencyOwner" : "ADR046-gpu-001; ADR 0046 Zone Nix emitter wired; `ADR-046-nix-configuration` Nix emitter present", + "destination" : "`nixos-modules/assertions.nix` (new GPU Device eval assertions); `tests/unit/nix/cases/device-gpu-eval.nix` (new Nix eval case); committed settings schema `docs/reference/schemas/v3/providers/device-gpu.settings.json`", + "detailedDesign" : "Eval assertions as documented in § Nix configuration / Eval-time assertions. Canonical JSON golden as documented. Settings schema drift gate via `make test-drift`. `d2b.vms..graphics.*` options are deprecated (emit deprecation warning) until a transition generation removes them; they are not removed in the same commit that adds the Device spec option. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — map old `d2b.vms..graphics.*` fields to `d2b.zones..resources.` Device spec settings fields; add eval assertions.", + "integration" : "`nix flake check`; `tests/unit/nix/cases/device-gpu-eval.nix`; `make test-drift`", + "removalProof" : "`d2b.vms..graphics.*` options removed only after migration guide ships and the deprecation warning has been live for one minor release", + "reuseAction" : "adapt", + "reuseSource" : "Settings schema field names/defaults/bounds from `nixos-modules/options-realms-workloads.nix` options documentation", + "specId" : "ADR-046-provider-device-gpu", + "specPath" : "docs/specs/providers/ADR-046-provider-device-gpu.md", + "validation" : "`nix-unit tests/unit/nix/cases/device-gpu-eval.nix`; `make test-drift`; `make test-flake`", + "workItemId" : "ADR046-gpu-007" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — no Provider state Volume exists to migrate.", + "dependencyOwner" : "ADR046-gpu-001; D087 status-first state model present in the foundational ADR-046 specs", + "destination" : "`packages/d2b-provider-device-gpu/` component descriptor; controller/status tests", + "detailedDesign" : "Do **not** declare a controller Provider state Volume. The device-gpu component descriptor declares an empty ProviderStateSet; controller and worker Process templates contain no `/state` mount. Bounded non-secret operational state is published to Device/Provider status and the core Operation ledger. GPU has no Device-payload Volume; render-node access remains a Device attachment resolved by LaunchTicket and broker policy. Primary reuse disposition: `create`. Preserved source-plan detail: `new` — status-first state assertions in the component descriptor and controller tests.", + "integration" : "`tests/status_state.rs`; `integration/gpu_worker_start/` verifies controller startup is gated by resource dependencies and status writer authority, not by a Provider state Volume", + "removalProof" : "`StorageRoot`/`StoragePathSpec` lifecycle tracking entries for GPU/video roles in `d2b-core/src/storage.rs` removed after Device/Process status-first lifecycle and restart-adoption integration tests pass in a live Zone", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-gpu", + "specPath" : "docs/specs/providers/ADR-046-provider-device-gpu.md", + "validation" : "`cargo test -p d2b-provider-device-gpu --test status_state`; component descriptor golden has no Provider state Volume declaration; controller Process template has no `/state` mount; ProviderStateSet query is empty; status/core-ledger fields carry bounded operational observations", + "workItemId" : "ADR046-gpu-008" + }, + { + "currentSource" : "None; new file", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-gpu-001", + "destination" : "`packages/d2b-provider-device-gpu/README.md`", + "detailedDesign" : "Must include: Provider identity, supported ResourceTypes, controller/service/worker binary descriptions, placement (Host, system domain), dependencies (system-minijail, volume-local, observability-otel), RBAC roles, security model summary, state/telemetry contract, build command (`cargo build -p d2b-provider-device-gpu`), test commands (`cargo test -p d2b-provider-device-gpu`), integration command (`make test-integration`), hardware test note (see `integration/README.md`), standalone-repository consumption stub.", + "integration" : "Workspace policy checks for `README.md` presence", + "removalProof" : "N/A (new file)", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-gpu", + "specPath" : "docs/specs/providers/ADR-046-provider-device-gpu.md", + "validation" : "`make test-policy` (workspace crate layout policy check)", + "workItemId" : "ADR046-gpu-009" + }, + { + "currentSource" : "`packages/d2b-core/src/unsafe_local_workloads.rs` (`UnsafeLocalWorkloadsJson`, `UnsafeLocalWorkload`, `UnsafeLocalLauncherItem`, `UNSAFE_LOCAL_WORKLOADS_SCHEMA_VERSION = \"v2\"`, `MAX_UNSAFE_LOCAL_WORKLOADS = 256`); `packages/d2b-contracts/src/unsafe_local_wire.rs` (`HelperHello.uid: u32`, `HelperLaunchRequest`, `HelperShellRequest`, `HelperScopeKind::{Exec,Shell}`, `DaemonToUnsafeLocalHelper`, `UnsafeLocalHelperToDaemon`); `packages/d2bd/src/unsafe_local_helper.rs` (`HelperRegistry::new(daemon_uid, allowed_uids)`, `dispatch_launch`, `bind_helper_socket`); `packages/d2b-unsafe-local-helper/src/{main,protocol,runtime,systemd}.rs` (`HelperClient`, `ScopeRuntime`, `run_scope_supervisor`, `SystemdUserScopeManager`); `nixos-modules/options-realms-workloads.nix` (lines 221, 233–235 `kind = \"unsafe-local\"` description; lines 264–275 null `stateDir`/`runDir`); `nixos-modules/unsafe-local-workloads-json.nix` (`runtimeKind = \"unsafe-local\"`, `providerId = \"unsafe-local\"`); `nixos-modules/unsafe-local-helper.nix` (service unit)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-audit-001 + ADR046-core-001; `Provider/system-core` owner", + "destination" : "`packages/d2b-provider-system-core/src/{host_reconciler.rs,host_status.rs,host_process_audit.rs}`; adapted `nixos-modules/unsafe-local-workloads-json.nix`; `packages/d2b-provider-system-core/tests/host_posture_contract.rs`", + "detailedDesign" : "`Provider/system-core` reconciler: (1) On user-only `Host` resource creation (`defaultDomain=user`, `allowedDomains=[user]`), set `status.isolationPosture = \"none\"` and `status.isolationPostureMessage = \"...\"` unconditionally; reject any operator-supplied value for these fields. Host resources with other execution policies do not receive `isolationPosture`. (2) On every user-only Host process launch: emit `ProcessEffect{event:\"launch\", provider:\"system-core-user\", domain:\"user\", no_isolation:true, ...}` audit record. (3) On every user-only Host process stop: emit `ProcessEffect{event:\"stop\", ...}`. (4) `d2b zone list`/`inspect` CLI renders `⚠ no isolation boundary (user domain)` annotation only for `Host` resources with `isolationPosture: \"none\"`; annotation is not suppressible. (5) `isolation-posture-declared` doctor check: passes when user-only `Host` resource status has `isolationPosture: \"none\"`; omitted when Zone has no user-only `Host` resources. (6) `no_isolation=true` is emitted in `ProcessEffect` records only; it does not appear in any OTEL span attribute, log field, or metric label. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `UnsafeLocalWorkload` private-bundle contract for the `Host` resource spec payload; adapt `HelperRegistry::allowed_uids` constraint as `defaultUserRef=User/` validation; adapt Nix `unsafe-local-workloads-json.nix` emitter for the new Host resource shape; gap-fill: add `ProcessEffect{no_isolation:true}` at `dispatch_launch` / stop call sites.", + "integration" : "`Provider/system-core` reconciler → `d2b-audit` sink; `d2b zone doctor` → resource status check; `d2b zone list`/`inspect` → CLI output renderer", + "removalProof" : "`d2b-unsafe-local-helper` binary and `DaemonToUnsafeLocalHelper`/`UnsafeLocalHelperToDaemon` wire types retired after `Provider/system-core` Process Provider supervisor ticket migration; `nixos-modules/unsafe-local-helper.nix` Nix unit retired after migration; `nixos-modules/unsafe-local-workloads-json.nix` adapted (not deleted) to emit Host resource spec format", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "`host_posture_contract.rs` tests from the Host posture tests section of this spec; `d2b-contract-tests/tests/policy_telemetry_redaction.rs` asserts `no_isolation` key absent from all span/metric/log surfaces", + "workItemId" : "ADR046-host-posture-001" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/ids.rs`, `realm.rs`, `target.rs`, `workload.rs`", + "dataMigration" : "Destructive d2b 3.0 reset; no RealmRef parser compatibility", + "dependencyOwner" : "W0 shared contract root; `d2b-contracts`", + "destination" : "`packages/d2b-contracts/src/v3/identity.rs`, `packages/d2b-contracts/src/v3/resource_ref.rs`", + "detailedDesign" : "Add ZoneId, ResourceTypeName, ResourceName, ResourceUid, ResourceRef, generation/revision newtypes, exact parsing/serde/Debug/redaction, and golden vectors Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Resource API/store/controllers/SDK/Nix import only these canonical types", + "removalProof" : "Old public Realm target types removed only after all v3 callers consume Zone/ResourceRef", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-terminology-and-identities", + "specPath" : "docs/specs/ADR-046-terminology-and-identities.md", + "validation" : "Rust property/vector tests; pure-Nix vector parity; malformed/collision/UID-recreate tests", + "workItemId" : "ADR046-identities-001" + }, + { + "currentSource" : "`nixos-modules/options-realms.nix`, `options-realms-workloads.nix`, `index.nix`", + "dataMigration" : "Full reset and new Zone declarations", + "dependencyOwner" : "ADR046-identities-001; Nix integrator", + "destination" : "`nixos-modules/options-zones.nix`, `nixos-modules/resources.nix`, `nixos-modules/index.nix`", + "detailedDesign" : "Validate Zone names, ResourceTypes/names/refs, shared Host/Guest ExecutionPolicy, and canonical sorted resource identities", + "integration" : "Nix resource objects serialize exactly the Rust contract", + "removalProof" : "Realm-facing declarations removed only in the reset/purge wave", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-terminology-and-identities", + "specPath" : "docs/specs/ADR-046-terminology-and-identities.md", + "validation" : "nix-unit vectors and rendered contract tests", + "workItemId" : "ADR046-identities-002" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/provider.rs:CredentialProvider` status/enrollment-only trait; main `a1cc0b2d` managed-identity Provider implementation and tests listed in §Source reuse", + "dataMigration" : "Full d2b 3.0 reset; no v2 managed-identity process/session state import", + "dependencyOwner" : "ADR046-credential-001 and ADR046-credential-002; owner: credential-managed-identity controller/agent topology. This topology contract is consumed by the later managed-identity controller work.", + "destination" : "packages/d2b-provider-credential-managed-identity/src/{controller.rs,agent.rs}; packages/d2b-provider-credential-managed-identity/{controller/main.rs,agent/main.rs}; packages/d2b-provider-credential-managed-identity/tests/topology.rs", + "detailedDesign" : "Implement the controller/agent process split: separate `d2b-managed-identity-controller` binary with no IMDS client and no KK delivery, and `d2b-managed-identity-agent` binary with injected IMDS client via effect port and KK delivery. Controller manages Credential resources, spawns/monitors agent Processes, uses canonical Process templates, attaches LaunchTickets projecting `imdsEndpointAlias` and `credentialRef`, monitors agent Process health with bounded backoff, performs Deleted-phase cleanup without emitting Deleted closure audit, and applies D087 status-first state with no Provider state Volume. Agent validates `ExactSdkConsumer` via `AuthenticatedSubjectContext`, serves token-delivery methods, terminates Noise_KK delivery sessions, reports lease state, declares no direct IMDS egress, and keeps token bytes transient. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt the main managed-identity Provider; replace v2 provider registry/session assumptions with v3 controller/agent Process topology and `d2b.credential.v3` service.", + "integration" : "ProviderDeployment starts the controller Process; controller reconciles Credential resources and creates agent Process/Endpoint resources at the declared executionRef; d2b-bus routes `d2b.credential.v3` calls to the agent; co-located runtime Provider injects the IMDS client through the LaunchTicket/effect port; core aggregates Provider status and audit subsystem appends deletion records.", + "removalProof" : "V2 single-process/trait topology is superseded once controller and agent Process split is integrated and all token delivery terminates in the agent", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-credential-managed-identity", + "specPath" : "docs/specs/providers/ADR-046-provider-credential-managed-identity.md", + "validation" : "`tests/topology.rs`; `integration/host-guest-placement.nix`; `make test-rust`; `make test-integration`; `make test-host-integration`", + "workItemId" : "ADR046-mi-topology-001" + }, + { + "currentSource" : "`d2b-core/src/minijail_profile.rs`; `d2b-core/src/processes.rs` (NamespaceSet, MountPolicy, CgroupPlacement); `d2b-priv-broker/src/ops/spawn_runner.rs`", + "dataMigration" : "Full reset; current `MinijailProfile` not import-compatible with v3 SandboxSpec", + "dependencyOwner" : "`ADR046-process-001` (common spec/status types); `ADR046-provider-001` (toolkit/contracts); system-minijail Provider owner", + "destination" : "`packages/d2b-provider-system-minijail/src/sandbox_compiler.rs`", + "detailedDesign" : "Accept `SandboxSpec` from common contracts; compile NamespaceClass/CapabilityClass/SeccompClass/UserNamespaceSpec/mount/environment/rlimit/umask into a versioned `CompiledSandboxPlan`; compute `sandboxRevisionDigest`; all rejection conditions from §12.1; no raw bitmask/BPF/argv/path in any output type; golden round-trip test vectors Primary reuse disposition: `adapt`. Preserved source-plan detail: EXTRACT/ADAPT.", + "integration" : "LaunchTicket builder (ADR046-minijail-002); effect port integration (ADR046-minijail-003)", + "removalProof" : "Current `MinijailProfile`/`NamespaceSet` types in `d2b-core` removed after all callers migrate to SandboxSpec", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-system-minijail", + "specPath" : "docs/specs/providers/ADR-046-provider-system-minijail.md", + "validation" : "`tests/sandbox_compilation.rs`; `tests/schema.rs`; golden vectors", + "workItemId" : "ADR046-minijail-001" + }, + { + "currentSource" : "`d2b-core/src/process_builder.rs`; `d2bd/src/supervisor/*.rs` (ticket generation)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-minijail-001; common `LaunchTicket` contract", + "destination" : "`packages/d2b-provider-system-minijail/src/launch.rs`", + "detailedDesign" : "LaunchTicket construction with compiled sandbox/budget/mount digests; ticket verification on ProviderSupervisor receipt; `d2b.supervisor.v3/IssueLaunchTicket` service call; expired/revoked/malformed ticket rejection", + "integration" : "`ProviderSupervisor` local adapter; minijail controller (ADR046-minijail-005)", + "removalProof" : "Current `process_builder.rs` removed after parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-system-minijail", + "specPath" : "docs/specs/providers/ADR-046-provider-system-minijail.md", + "validation" : "`tests/lifecycle.rs`; `tests/fault_injection.rs`; `tests/fast_path.rs`", + "workItemId" : "ADR046-minijail-002" + }, + { + "currentSource" : "`d2b-priv-broker/src/ops/spawn_runner.rs`; `d2b-priv-broker/src/sys.rs` (`clone3_spawn_runner`, user namespace setup)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-minijail-001; broker integration owner", + "destination" : "Broker-side: `d2b-priv-broker` retains `SpawnRunner` op, invoked by the `MinijailProcessEffectPort` implementation owned by core/ProviderSupervisor; Provider-side: `packages/d2b-provider-system-minijail/src/launch.rs` calls `MinijailProcessEffectPort` with opaque Process/LaunchTicket/profile IDs; `user_ns.rs` implements the user namespace pre-establishment protocol", + "detailedDesign" : "Linux ≥5.14 and delegated-leaf `cgroup.kill` platform gate; `clone3(CLONE_PIDFD | CLONE_INTO_CGROUP)` with pre-declared cgroup leaf FD; broker retained as child parent and sole `waitid(P_PIDFD)`/reap/exit-status owner; verified duplicate returned privately to ProviderSupervisor for poll/readiness and exact-main `pidfd_send_signal`; anchored `cgroup.kill` write for unambiguous intentional teardown; user namespace pre-establishment sequence (§7.7) when `userNamespace` set; host UID 0 rejection; parent name-to-inode re-validation; zero-host-capability invariant (ADR 0021); `MinijailProcessEffectPort` privately maps opaque IDs to SpawnRunner/OpenDevice/clone3/uid-map/FD effects; Provider crate imports no broker service/client/DTO", + "integration" : "ADR046-minijail-002 (LaunchTicket); real cgroup/broker fixture in `integration/clone3_pidfd/` and `integration/user_namespace/`", + "removalProof" : "Old broker `SpawnRunner` direct-caller paths in `d2bd` removed after system-minijail Provider integration", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-system-minijail", + "specPath" : "docs/specs/providers/ADR-046-provider-system-minijail.md", + "validation" : "`tests/fault_injection.rs`; `tests/platform_gate.rs`; `tests/broker_wait_contract.rs`; `tests/cgroup_kill_finalize.rs`; `integration/clone3_pidfd/`; `integration/user_namespace/`; `integration/broker_parent_reap/`; `integration/cgroup_kill_subtree/`; `integration/kernel_platform_gate/`", + "workItemId" : "ADR046-minijail-003" + }, + { + "currentSource" : "`d2bd/src/supervisor/pidfd_table.rs` (PidfdTable, WaitTermination, BrokerReapLog)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-minijail-003; wait/pidfd owner", + "destination" : "Broker-side parent wait/reap and typed terminal relay in `packages/d2b-priv-broker/src/`; non-parent observation/status consumption in `packages/d2b-provider-system-minijail/src/{pidfd,wait}.rs`", + "detailedDesign" : "Broker that called `clone3` alone calls `waitid(P_PIDFD)`, collects exit status, and reaps exactly once; ProviderSupervisor `AsyncFd` readability is a hint only and never a wait/status source; controller consumes the identity-bound broker relay and holds no raw pidfd; ProviderSupervisor duplicate reacquisition is dispatched through a bounded blocking adapter with explicit timeout; pidfd never serialized; verified broker/ProviderSupervisor holder retains exact-main `pidfd_send_signal`; no PID/PGID fallback; graceful deadline followed by mandatory anchored leaf `cgroup.kill`; empty-leaf proof before rmdir; exit class classification (clean-exit/crash/signal/timeout/unknown) Primary reuse disposition: `adapt`. Preserved source-plan detail: EXTRACT/ADAPT.", + "integration" : "Controller restart → adoption (ADR046-minijail-005); finalize (§8.6)", + "removalProof" : "Old `PidfdTable` in `d2bd` supervisor removed after Provider integration", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-system-minijail", + "specPath" : "docs/specs/providers/ADR-046-provider-system-minijail.md", + "validation" : "`tests/lifecycle.rs`; `tests/broker_wait_contract.rs` (only clone3 parent calls waitid/reaps; poll readability cannot supply status); `tests/cgroup_kill_finalize.rs` (setsid descendant and PGID reuse); `tests/redaction.rs` (PID never in log/status/audit); `tests/blocking_adapter.rs` (duplicate/status relay via adapter; timeout → error)", + "workItemId" : "ADR046-minijail-004" + }, + { + "currentSource" : "`d2bd/src/supervisor/*.rs` (DagExecutor, NodeOutcome); `d2bd/src/supervisor/pidfd_table.rs`; `d2b-realm-core/src/allocator_engine.rs` (adoption/identity concepts)", + "dataMigration" : "Full reset; current DAG/role snapshot import not required", + "dependencyOwner" : "All of ADR046-minijail-001 through ADR046-minijail-004; ComponentSession/d2b-bus (ADR046-session-001, ADR046-bus-001); bootstrap authz", + "destination" : "`packages/d2b-provider-system-minijail/src/` — controller binary entry point; reconcile loop; adoption; quarantine; bootstrap authz; health/status; restart; finalize", + "detailedDesign" : "Full Process/EphemeralProcess reconcile algorithm (§8); fast path ≤5/≤20 ms gates; spawn via `MinijailProcessEffectPort` (opaque IDs; no broker DTO imported); adoption algorithm (§8.5) with `/proc` reads, cgroup enumeration, and original-broker-parent verification via bounded blocking adapters; quarantine on ambiguity; quarantine reuse blocked until externally established process-absence proof or full Zone reset; no signal or cgroup.kill write to quarantined/ambiguous identity; restart/backoff driven only by broker-relayed terminal status; finalize (§8.6) with exact-main SIGTERM, bounded grace, mandatory cgroup.kill, broker wait/reap, empty-leaf proof, and no PGID ownership; EphemeralProcess continuation recovery (§9); bootstrap authz scope (§3); post-bootstrap RBAC; metric label closed-set enforcement (no `zone` label); controller writes status only on Process/EphemeralProcess resources; Provider resource status aggregated by core; the controller declares no Provider state Volume and mounts none — its bounded non-secret operational state lives in `status`/the core Operation ledger (§5.1, D087) and running units are re-adopted from cgroup leaves + fresh pidfds on restart", + "integration" : "Zone runtime startup (bootstrap); all v3 ResourceClient/bus/session paths", + "removalProof" : "Current `d2bd` DAG executor and direct spawn paths removed only after all ProcessRoles in the role-disposition table (ADR-046-components-processes-and-sandbox, §Representative baseline mapping) reach parity under system-minijail or system-systemd", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-system-minijail", + "specPath" : "docs/specs/providers/ADR-046-provider-system-minijail.md", + "validation" : "`tests/lifecycle.rs`; `tests/ephemeral_lifecycle.rs`; `tests/conformance.rs`; `tests/adoption_quarantine.rs`; `tests/broker_wait_contract.rs`; `tests/cgroup_kill_finalize.rs`; `tests/platform_gate.rs`; `tests/bootstrap_authz.rs`; `tests/fast_path.rs`; `tests/blocking_adapter.rs`; `integration/adoption_restart/`; `integration/quarantine_scenario/`; `integration/broker_parent_reap/`; `integration/cgroup_kill_subtree/`; `integration/kernel_platform_gate/`; `integration/latency_gate/`; shared conformance suite in `d2b-process-conformance`", + "workItemId" : "ADR046-minijail-005" + }, + { + "currentSource" : "`nixos-modules/processes-json.nix`; `nixos-modules/minijail-profiles.nix`; `packages/d2b-contract-tests/tests/policy_observability.rs`", + "dataMigration" : "Current `nixos-modules/processes-json.nix` and minijail profile Nix removed at cutover", + "dependencyOwner" : "ADR046-minijail-005; Nix integrator; test infrastructure owner", + "destination" : "`nixos-modules/` — v3 Nix `Process`/`EphemeralProcess` resource authoring; Provider catalog entry; `docs/reference/schemas/v3/Process.json`; `docs/reference/schemas/v3/EphemeralProcess.json`; `make test-drift` schema drift gate", + "detailedDesign" : "Nix module accepts `d2b.zones..resources.` with `type = \"Process\"` or `\"EphemeralProcess\"`; eval-time validation rules (§16.4); build-time JSON validation (§16.5); artifact catalog integration; cleanup contract tests (§16.5)", + "integration" : "`d2b.artifacts` catalog; Zone bundle emission; `make test-drift`", + "removalProof" : "`processes-json.nix`, `minijail-profiles.nix`, and `programs-json.nix` removed after v3 Nix parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-system-minijail", + "specPath" : "docs/specs/providers/ADR-046-provider-system-minijail.md", + "validation" : "`nix-unit` eval cases for every validation rule; schema drift gate; `tests/schema.rs`", + "workItemId" : "ADR046-minijail-006" + }, + { + "currentSource" : "`packages/d2b-core/src/host.rs` lines 290–520 (`NetEnv`, `IfName`, `ExternalNetworkPolicy`, `NftablesModel`, `BridgePortFlags`, `TapRole`, `Ipv6SysctlEntry`, `IfNameMapping` lines 242–256; **also** `VmRuntimeRow` lines 155–167 with `tap`/`bridge`/`net_vm`/`env` fields — attachment status precursors); `packages/d2b-core/src/processes.rs` lines 98–141 (`ProcessNetworkInterface`, `ProcessNetworkInterfaceType`, `ProcessMacvtapInterface` — current VMM runner network interface DTOs; these are per-Guest VMM fields, not Network-level fields, and migrate to Guest spec under `Provider/runtime-cloud-hypervisor`); `packages/d2b-contracts/src/broker_wire.rs` (authoritative broker op list; network-relevant: `ApplyNftables`, `ApplyNmUnmanaged`, `ApplyRoute`, `ApplySysctl`, `SetBridgePortFlags`, `UpdateHostsFile`, `SeedDnsmasqLease`, `CreatePersistentTap`, `CreateTapFd`); `nixos-modules/lib.nix` lines 396–460 (`subnetIp`, `subnetMask`, `mkMac`, `cidrOverlaps`)", + "dataMigration" : "Full d2b 3.0 reset; no env→Network import", + "dependencyOwner" : "W0 shared contract root; `d2b-contracts`", + "destination" : "`packages/d2b-contracts/src/v3/network.rs`: NetworkSpec, NetworkStatus, AttachmentSpec, AttachmentStatus, ExternalAttachmentSpec, ExternalAttachmentStatus, PortForwardSpec, NetworkConditionType, opaque AttachmentHandle, and AttachmentGenerationFence; `packages/d2b-contracts/src/v3/ifname.rs`: IfName newtype, derivation, collision detection (extracted from `d2b-host/src/ifname.rs`). Also defines `User/net-local-controller` as a proper Resource with explicit lifecycle: `Provider/network-local`'s Nix package/module provisions the reserved `net-local-controller` OS account with a private fixed UID/GID in Host prerequisites and in the generic net-VM nixos-system artifact (same account, same UID/GID inside the Guest); the network-local controller creates and owns the User Resource (`spec.osUsername: net-local-controller`, `ownerRef: Provider/network-local`, `managedBy: controller`); `Provider/system-core` verifies the account via NSS lookup and reconciles the User Resource to Ready — it does not provision the OS account. No numeric UID/GID enters any ResourceSpec field, authz check, or audit record; `User.status` MAY carry diagnostic `uid`/`gid` values discovered by NSS lookup, but those are informational only and are never authorization inputs. The network-local controller waits for `User/net-local-controller` to reach `Ready` before creating any config Volume (reconcile precondition, not a bootstrap side effect).", + "detailedDesign" : "Strict ResourceEnvelope with Network-specific spec/status. IfName newtype: IFNAMSIZ-1 validated, FNV-1a 64-bit derivation, base32 Crockford, 8-char suffix, bridge/tap role prefixes, detect_collisions over IfNameMapping slice. cidrOverlaps: pure Rust IPv4 arithmetic, same algorithm as lib.nix. NetworkSpec validators: /24 lanCidr with .0 base, /30 uplinkCidr, unique attachment indices 2–250, default hostBlocklist enforcement. The opaque attachment realization binds Network UID/generation and attachment UID/generation so deletion can supply a non-printable ID plus explicit expected generation fence without an IfName/path. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Provider dossiers, Nix resource compiler, resource store/API bind these canonical types", + "removalProof" : "Old `d2b_core::host::NetEnv` and related types removed only after v3 resource API consumers use `d2b_contracts::v3::network` types", + "reuseAction" : "adapt", + "reuseSource" : "None from main; all from v3 baseline", + "specId" : "ADR-046-resources-network", + "specPath" : "docs/specs/ADR-046-resources-network.md", + "validation" : "Golden JSON/CBOR vectors; CIDR overlap property tests; IfName collision and derivation determinism tests; default hostBlocklist enforcement; attachment index uniqueness; `User/net-local-controller` User resource lifecycle/readiness test: controller creates User Resource with `spec.osUsername = \"net-local-controller\"` (`ownerRef: Provider/network-local`); controller waits for User resource to reach `Ready` before proceeding; controller aborts with `ConfigVolumeReady=False/user-not-ready` if User resource is not Ready; verifies no numeric UID/GID appears in the Resource spec, authz check, or audit record; verifies that any diagnostic `uid`/`gid` in `User.status` is never used as an authorization input", + "workItemId" : "ADR046-network-001" + }, + { + "currentSource" : "`packages/d2b-host/src/ifname.rs` (FNV-1a derivation, detect_collisions, DEFAULT_PREFIX, BRIDGE_TAG, TAP_TAG); `packages/d2b-host/src/bridge_port.rs` (BridgePortReadback, east-west policy, TapRole defaults); `packages/d2b-host/src/nftables.rs` (NftBatch, hash_inet_d2b_table, coexistence policy); `packages/d2b-host/src/routes.rs` (route/dnsmasq-bound/IPv6 preflight); `packages/d2b-host/src/netlink.rs` (IPv6 sysctl sequence)", + "dataMigration" : "None (behavior preserved; host bridge names change from `br--*` to `d2b-b` after cutover)", + "dependencyOwner" : "ADR046-network-001; d2b-host network modules owner", + "destination" : "`packages/d2b-provider-network-local/src/ifname.rs`, `bridge_port.rs`, `nftables.rs`, `routes.rs`, `netlink.rs`", + "detailedDesign" : "Move IfName derivation to `d2b-contracts` (ADR046-network-001); keep bridge_port, nftables, routes, netlink in Provider crate. nftables: retain the Network-owned `inet d2b` chain layout, ownership markers, and coexistence matrix; emit no USBIP/TCP-3240 rule and compute drift over only the Network UID ownership projection. routes: adapt dnsmasq-bound check to use Network status instead of `HostJson.environments`. netlink: keep IPv6-off sequence; add defense-in-depth re-application path. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract (ifname, bridge_port, nftables) into shared network-local Provider library; adapt (routes, netlink) into controller observe loop.", + "integration" : "Controller observe loop uses nftables digest drift, bridge_port readback, and IPv6 sysctl to drive `FirewallReady`, `FabricReady` conditions", + "removalProof" : "`packages/d2b-host/src/{ifname,bridge_port,nftables,routes,netlink}.rs` removed only after Provider conformance tests pass", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-network", + "specPath" : "docs/specs/ADR-046-resources-network.md", + "validation" : "Existing `bridge_port::tests::readback_matches_defaults`, `ops::tap::tests::set_bridge_port_flags_readback_drift_fails_closed`, `netlink::tests::ipv6_off_sequence_runs_in_order`, nftables coexistence matrix tests; all pinned in `tests/golden/pinned/host-prepare-network.txt` and `tests/golden/pinned/net-canaries.txt`", + "workItemId" : "ADR046-network-002" + }, + { + "currentSource" : "`nixos-modules/net.nix` (full file, 450 lines); `nixos-modules/net-mdns.nix`; `nixos-modules/lib.nix` subnetIp/mkMac/cidrOverlaps", + "dataMigration" : "Destructive v3 reset; existing net VMs are re-created under new IfNames", + "dependencyOwner" : "ADR046-network-001, ADR046-network-002; Provider/runtime-cloud-hypervisor dossier owner", + "destination" : "`packages/d2b-provider-network-local/` — artifact catalog integration for net-VM nixos-system artifact resolution; `packages/d2b-provider-network-local/nix/` — default net-VM NixOS module (parameterized successor to net.nix), built and registered as a nixos-system artifact in `d2b.artifacts`", + "detailedDesign" : "`Network.spec.netVmSystemArtifactId` is REQUIRED. It must reference a declared `d2b.artifacts` entry with `type = \"nixos-system\"`; verified at Nix build time (Stage 2 check, hard build error if absent or wrong type). No implicit default exists; Provider artifacts cannot silently provide a separately typed system artifact. The controller sets `Guest.spec.systemArtifactId` to the artifact ID value at reconcile time (the value is already validated by the build; the controller fails closed if absent at runtime). The net-VM nixos-system artifact is **generic** (INV-NET-008): it contains the guest-agent binary and runtime, kernel, base NixOS services, systemd-networkd NIC bootstrap, and the `net-local-controller` **OS account** provisioned by `Provider/network-local`'s Nix module (same private fixed UID/GID as on the Host, so that virtiofs view ACLs on config Volume layout entries are enforced consistently inside the Guest; `Provider/system-core` performs NSS lookup reconciliation, not OS account provisioning; no numeric UID/GID appears in any ResourceSpec field, authz check, or audit record; `User.status` MAY carry diagnostic `uid`/`gid` from NSS lookup but those are informational only and never authorization inputs). It does NOT encode per-Network desired data; per-Network config (dnsmasq, nftables, routing, attachments) is delivered via the controller-owned config Volume and applied by the guest-agent Process. The artifact preserves compile-time-fixed content: `lib.mkForce` on 10-eth-dhcp (INV-NET-001); two systemd-networkd interface units matched by MAC; IPv6 suppression sysctls on NIC interfaces; ip6 filter table drop-all policy. **mDNS reflector and local dnsmasq DNS bridge are separate owned Process resources** (D-NETWORK-001); they are not inline services in the artifact.", + "integration" : "Network controller resolves artifact ID → sets `Guest.spec.systemArtifactId`. Controller separately creates `Volume/net--config` with per-Network config and `Process/net--agent` (guest-agent). `Provider/runtime-cloud-hypervisor` reads `systemArtifactId` to produce the net-VM bundle and mounts the Volume view into the Guest.", + "removalProof" : "`nixos-modules/net.nix` and `nixos-modules/net-mdns.nix` removed only after net-VM artifact parity tests pass", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-network", + "specPath" : "docs/specs/ADR-046-resources-network.md", + "validation" : "nix-unit: `tests/unit/nix/cases/net-vm-network.nix` (adapted to v3 resource API); INV-NET-001 assertion in new nix-unit case; no mDNS inline service appears in the generated artifact; no per-Network dnsmasq or nftables data in artifact (INV-NET-008); integration test: mDNS Process resources are created when `spec.mdns.enable = true`; Stage 2 build test: absent `netVmSystemArtifactId` fails with required-field build error; wrong artifact type fails with `artifact-type-mismatch` error; `packages/d2b-provider-network-local/tests/net_vm_artifact_is_generic.rs` — two Networks with different CIDRs produce same `systemArtifactId` and different config Volume content", + "workItemId" : "ADR046-network-003" + }, + { + "currentSource" : "`nixos-modules/network.nix` (full file, 500+ lines; bridge/netdev/sysctl/NM-unmanaged/route sections); `nixos-modules/host-json.nix` (emits `host.json` `environments[].nftables`, `environments[].ifNameMappings`, `environments[].usbipBusidLocks` — superseded by Network status API); `nixos-modules/processes-json.nix` (emits `processes.json` `ProcessNetworkInterface`/`ProcessMacvtapInterface` per runner — superseded by Guest spec network fields); `nixos-modules/index.nix` envMeta/netMeta sections; `nixos-modules/options-envs.nix`; `nixos-modules/options-realms-network.nix`; `nixos-modules/options-vms.nix` (`d2b.vms..env` line 944, `d2b.vms..index` line 962 — current attachment semantics source; maps to `Network.spec.attachments[].executionRef` + `index`); `nixos-modules/options-site.nix` (`d2b.site.allowUnsafeEastWest` line 48 — maps to per-Network `Network.spec.isolation.allowEastWest`; Zone.spec is empty in v3; `d2b.hostLanCidrs` line 382 — maps to Host resource network inventory at runtime; not a Zone.spec field)", + "dataMigration" : "Full v3 reset; `d2b.envs.*` declarations must be rewritten as Network resources", + "dependencyOwner" : "ADR046-network-001, ADR046-network-002, ADR046-network-003; Nix integrator", + "destination" : "`nixos-modules/resources-network.nix`: Nix resource object emitter for Network ResourceType; `nixos-modules/index.nix`: network resource compilation section", + "detailedDesign" : "The emitter replaces `d2b.envs.` with `d2b.zones..resources. = { type = \"Network\"; spec = { ... }; }` (attr key = resource name; `type` explicit field; `spec` fields identical to the canonical ResourceSpec JSON — no bespoke Nix vocabulary). It validates CIDR shape, attachment index uniqueness, external attachment constraints, and CIDR overlap (reusing `cidrOverlaps` from `lib.nix`). **Bridges are NOT emitted as `systemd.network.netdevs` entries** (D-NETWORK-003 resolved; bridges are created dynamically by the broker `CreateBridge` op at reconcile time). The Nix emitter provisions only bootstrap/static prerequisites that do not require runtime bridge IfNames: `networking.networkmanager.unmanaged` pattern for the `d2b-*` prefix (covers all dynamically-created bridges and taps regardless of specific IfNames; emitted to `00-d2b-unmanaged.conf`); schema validation and controller binary deployment artifacts. Current `d2b.vms..env` + `d2b.vms..index` attachment semantics (`options-vms.nix` lines 944/962) become `Network.spec.attachments[].executionRef` + `index`. Current `d2b.site.allowUnsafeEastWest` (`options-site.nix` line 48) moves to the per-Network `isolation.allowEastWest` field; Zone.spec is empty in v3. Current `d2b.hostLanCidrs` (`options-site.nix` line 382) becomes the Host resource's network inventory, queried at runtime; at Nix build time the eval may validate CIDRs against declared host configuration input. The emitter does not emit `boot.kernel.sysctl` entries per bridge IfName (bridges do not exist at activation time; IPv6 suppression is applied by `CreateBridge` and `ApplySysctl` per INV-NET-002). **Nix option types** for `spec.*` fields are generated from `Network.schema.json`; they are not hand-written. **Bundle generation**: the emitter collects all declared `Network` resource objects, sorts them lexicographically by `(type, name)`, serializes each as canonical JSON **omitting `managedBy` and `configurationGeneration`** (core sets these at activation), and assembles the Zone resource bundle at `$out/bundle.json` (see [Nix configuration contract — Stage 3](#stage-3--build-output-zone-resource-bundle)). The emitter records a `providerSchemaDigest` entry for `Provider/network-local` in the bundle resolved from the artifact catalog. The bundle's `contentHash` is a SHA-256 of the sorted canonical resource array; the derivation is fixed-output so that identical configuration always produces the same store path. The `managedBy` field is NOT set by the emitter; core sets `managedBy = \"configuration\"` and assigns the `configurationGeneration` counter when applying the bundle. The core controller retains prior bundle copies under `/var/lib/d2b/zones//configuration/prior/` per [Generation lifecycle and cleanup contract](#generation-lifecycle-and-cleanup-contract).", + "integration" : "Nix resource objects serialize exactly the Rust NetworkSpec contract (ADR046-network-001). The provider install declares the schema digest. Zone runtime generation-transition logic (ADR046-network-008) reads the bundle at activation.", + "removalProof" : "`nixos-modules/network.nix`, `nixos-modules/options-envs.nix`, and `nixos-modules/options-realms-network.nix` removed only after `resources-network.nix` and controller reach parity; `d2b.envs` consumer migration guide updated", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-network", + "specPath" : "docs/specs/ADR-046-resources-network.md", + "validation" : "nix-unit CIDR overlap, assertion eval, and bridge-sysctl cases; `make test-flake` with updated examples; `make test-drift` for schema/emitter parity; `packages/d2b-contracts/tests/generation_bundle.rs` for bundle format and `contentHash` stability; nix-unit `tests/unit/nix/cases/generation-cleanup-absent-network.nix` for removed-resource scheduling (added by ADR046-network-008)", + "workItemId" : "ADR046-network-004" + }, + { + "currentSource" : "`nixos-modules/network.nix` (tap/sysctl sections); `packages/d2b-host/src/{bridge_port,nftables,netlink,routes}.rs`; broker ops in `packages/d2b-contracts/src/broker_wire.rs`: **real runtime ops** `ApplyNftables`, `ApplyNmUnmanaged`, `ApplyRoute`, `ApplySysctl`, `CreatePersistentTap`, `SetBridgePortFlags`, `UpdateHostsFile`, `SeedDnsmasqLease` (all `implemented-and-reachable`); **new ops to author**: canonical `DeletePersistentTap` paired with `CreatePersistentTap`, plus `CreateBridge` and `DeleteBridge` (do not exist in v3 baseline; must be added to `broker_wire.rs` and implemented as `RealBrokerRequest` handlers in `packages/d2b-priv-broker/src/runtime.rs`); **NOT current ops**: no unsuffixed tap-deletion alias is valid, and `CreateMacvtap` does not exist — macvtap is created inside broker's `SpawnRunner` dispatch (`packages/d2b-priv-broker/src/runtime.rs` line 5097 `live_create_macvtap_fd`)", + "dataMigration" : "None after full reset", + "dependencyOwner" : "ADR046-network-001–004; network-local controller owner; D-NETWORK-001, D-NETWORK-002, and D-NETWORK-003 resolved", + "destination" : "`packages/d2b-provider-network-local/src/controller.rs`: async NetworkReconciler; `packages/d2b-provider-network-local/src/plan.rs`: ReconcilePlan computation; `packages/d2b-provider-network-local/src/observe.rs`: drift-detection observe loop. Full crate layout required (see [Package and crate boundary](#package-and-crate-boundary)): `src/` (controller/plan/observe + colocated unit tests), `tests/` (hermetic conformance and state-machine tests), `integration/` (provider-system reconcile fixtures), `README.md` (Network ResourceType, controller binary, placement, RBAC, security invariants, build/test/integration commands).", + "detailedDesign" : "Implements full async reconcile interface from `ADR-046-resource-reconciliation`. `plan()` computes desired vs. actual bridge-presence, sysctl, host-side nftables, hosts-file, NM-unmanaged, config Volume content, Guest, guest-agent Process, and mDNS-Process states. `reconcile()` dispatches in order: `CreateBridge` for each bridge not present (broker applies IPv6 sysctls atomically; `CreateBridge` failure sets `FabricReady=False/bridge-create-error` and aborts) → `ApplySysctl` (defense-in-depth IPv6) → `ApplyNftables` (host-side `inet d2b` table) → `ApplyNmUnmanaged` → `ApplyRoute` → `UpdateHostsFile` → `SeedDnsmasqLease` for new reservations → **Volume upsert** (two-phase): Phase 1 — create `Volume/net--config` with `kind: ephemeral`, `source.executionRef: Host/`, `source.settings.kind: tmpfs`, `quota: {maxBytes: 4194304, maxInodes: 128, enforcement: hard}` (tmpfs quota charged to Host memory budget), `layout` entries (root directory with `type: directory` and four config files each with `type: file`, `ownerRef: User/net-local-controller`, `groupRef: User/net-local-controller`, `mode: \"0640\"`, `accessAcl: []`, `defaultAcl: []`, `noFollow: true`, conservative create/repair/cleanup policies), `views: {guest-readonly: {path: \"\", rights: [read, traverse]}}`, `attachments: []` (no Guest attachment); abort on terminal error with `ConfigVolumeReady=False/config-volume-error`. Wait for Volume backing to reach `Ready`; requeue on `Degraded`/`Failed` with `ConfigVolumeReady=False/volume-not-ready`. Write rendered config content through Volume write service (no raw host paths). Phase 2 — create Guest upsert with `systemArtifactId` from REQUIRED `Network.spec.netVmSystemArtifactId`. Wait for Guest `Ready`. Then update Volume with Guest attachment: `attachments: [{executionRef: Guest/, transport: virtiofs, view: guest-readonly, access: read-only, mountPath: \"/run/d2b/net-config\", settings: {posixAcl: false, xattr: false, cache: auto, inodeFileHandles: never, threadPoolSize: null, socketGroup: null}}]`; wait for attachment `Ready`; requeue on `Degraded` with `ConfigVolumeReady=False/attachment-not-ready`. Create or update guest-agent Process `Process/net--agent` with `processClass: worker`, `sandbox: {namespaceClasses: [], capabilityClasses: [network-admin, network-bind, network-raw]}` (inherits Guest network namespace; `network-admin`→`CAP_NET_ADMIN`, `network-bind`→`CAP_NET_BIND_SERVICE`, `network-raw`→`CAP_NET_RAW`, all effective in Guest network namespace only; INV-NET-009), `mounts: [{volumeRef: Volume/net--config, view: guest-readonly, mountPath: \"/run/d2b/net-config\", access: read-only, required: true}]`. mDNS Process upsert when `spec.mdns.enable = true` (D-NETWORK-001) → `SetBridgePortFlags` per tap. Removed attachments first wait for Guest/VMM FD ownership to close, then issue `DeletePersistentTap` with the retained opaque attachment ID and current expected Network/attachment generations; the handle remains retained until confirmed effect or validated absence. Stale generation refreshes/requeues, transient kernel error retries, and foreign marker fails closed. Each broker op returns typed audit evidence. `observe()` re-reads `firewallDigest` (host-side), bridge isolation flags, IPv6 sysctls, and guest-agent Process status (`dnsmasq-bound`, `firewall-applied` predicates); queues reconcile on drift. Metrics use only the fixed semantic labels in §OTEL spans and metrics; Zone/Network identity remains in OTEL resource attributes and permitted audit fields and never enters metric labels or span attributes. **Finalizer** (strictly child-first): `NetworkDraining` → stop workload Guests and await VMM FD closure → generation-fenced `DeletePersistentTap` for each retained attachment, awaiting confirmation → delete guest-agent Process and mDNS Processes; wait for each Deleted watch event → update Volume to remove Guest attachment (`attachments: []`); wait for attachment removal confirmed → delete `Guest/`; wait for Deleted watch event → delete `Volume/net--config`; wait for Deleted watch event → `ApplyNftables` (empty) → `ApplyNmUnmanaged` (empty) → `UpdateHostsFile` (empty) → `DeleteBridge` for each bridge (idempotent) → clear finalizer. No USBIP rules installed by Network; device-usbip issues the existing `UsbipBindFirewallRule` request with action `Ensure` for apply or `Remove` for release (D-NETWORK-002).", + "integration" : "Controller process registers descriptor, watches `Network` resources via d2b-bus/ComponentSession/ResourceClient. Owned Guest and Process mutations trigger owner reconciliation. Device-usbip watches only Network identity/readiness/generation; its Core adapter privately resolves relay/firewall effects (D-NETWORK-002).", + "removalProof" : "Daemon-orchestrated network/bridge lifecycle removed only after controller passes conformance and parity tests", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-network", + "specPath" : "docs/specs/ADR-046-resources-network.md", + "validation" : "`ADR046-reconcile-001` toolkit conformance; latency gates (p95 ≤5 ms hint-to-handler); Network-specific: CIDR conflict blocks reconcile, `CreateBridge` failure sets `FabricReady=False`, Volume creation failure sets `ConfigVolumeReady=False/config-volume-error`, `User/net-local-controller` not Ready aborts with `ConfigVolumeReady=False/user-not-ready`, Volume schema round-trip (kind=ephemeral, source.settings.kind=tmpfs, quota.enforcement=hard, layout type=file entries, views.guest-readonly.rights=[read,traverse]), tmpfs quota charged to Host memory budget (test Volume creation fails when Host memory budget exceeded), Guest not created before Volume backing `Ready`, Guest attachment not added before Guest `Ready`, guest-agent Process created after attachment `Ready` (`processClass: worker`, `namespaceClasses: []`, `capabilityClasses: [network-admin, network-bind, network-raw]`, `access: read-only`, `required: true`), host-capability leakage test: no host-netns process gains `CAP_NET_ADMIN`/`CAP_NET_BIND_SERVICE`/`CAP_NET_RAW` as result of guest-agent launch (INV-NET-009; `tests/host-integration/guest-agent-cap-confinement.nix`), removed attachment and finalizer call `DeletePersistentTap` only after Guest/VMM FD closure with opaque ID/current generations, validated absence succeeds, transient failure retains handle/retries, stale generation refreshes, foreign marker blocks without deletion, request/audit contain no IfName/path, `DeleteBridge` called only after tap confirmations, Volume attachment removed before net-VM Guest deletion in finalizer (test order: workload FD closure → persistent taps deleted → agent Deleted → Volume attachment removed → net-VM Guest Deleted → Volume Deleted → bridges), east-west invariant (INV-NET-003), hostBlocklist enforcement (INV-NET-004), macvtap attachment status (delegated to runtime-ch), mDNS Process created/deleted with `spec.mdns.enable` toggle, broker INV-NET-002 tests, config-only spec change updates Volume content and triggers agent reload without Guest restart (INV-NET-008); golden tests updated for v3 IfNames; structural metric descriptor test asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `network`, and every resource-name-derived label and verifies a Network-name canary is absent from emitted label values", + "workItemId" : "ADR046-network-005" + }, + { + "currentSource" : "`tests/unit/nix/cases/net-vm-network.nix`; `tests/golden/pinned/net-vm-bundle-gate.txt`; `tests/golden/pinned/net-canaries.txt`; `tests/golden/pinned/host-prepare-network.txt`; `tests/host-integration/bridge-isolation.nix`; `tests/integration/live/network-isolation.sh`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-network-001, ADR046-network-005; test owner", + "destination" : "`tests/unit/nix/cases/net-vm-network.nix` (adapted to v3 resource API); updated golden pins; `tests/host-integration/bridge-isolation.nix` (adapted); `packages/d2b-priv-broker/tests/{bridge_lifecycle,persistent_tap_lifecycle}.rs` (new hermetic broker tests). Provider crate test directories: `packages/d2b-provider-network-local/tests/` — hermetic Cargo integration tests (conformance suite, controller state machine, CIDR validation vectors, IfName determinism, invariant tests INV-NET-001–007, reconcile/observe/finalize with deterministic clock, fault injection); `packages/d2b-provider-network-local/integration/` — container/Host/Guest lifecycle fixtures invoked by `make test-integration` (bridge isolation, east-west double opt-in, nftables drift detection, persistent-tap and macvtap lifecycle). Both directories required by package policy.", + "detailedDesign" : "Rust integration tests: NetworkSpec CIDR validation golden vectors; AttachmentSpec index uniqueness; ExternalAttachmentSpec mutual-exclusion validation; IfName derivation determinism; CIDR overlap arithmetic; INV-NET-001 through INV-NET-009 invariant tests; reconcile/observe/finalize state machine (deterministic clock). Broker tests: `create_bridge_applies_ipv6_sysctl` (INV-NET-002 layer 1); `delete_bridge_is_idempotent`; `delete_bridge_never_cascades_attached_tap`; `create_bridge_parameters_match_spec` (MTU, STP disabled, multicast snooping disabled); `delete_persistent_tap_pairs_with_create`; `delete_persistent_tap_absent_is_idempotent_after_ownership_validation`; `delete_persistent_tap_rejects_stale_network_generation`; `delete_persistent_tap_rejects_stale_attachment_generation`; `delete_persistent_tap_foreign_marker_fails_closed`; `delete_persistent_tap_request_and_audit_have_no_ifname_or_path`. Controller tests: `reconcile_applies_sysctl_defense_in_depth` (INV-NET-002 layer 2); `volume_created_before_guest`; `guest_not_created_until_volume_ready`; `agent_process_created_after_guest`; `removed_attachment_waits_for_vmm_then_delete_persistent_tap`; `finalizer_order_vmm_then_taps_then_agent_then_guest_then_volume_then_bridges`; `delete_persistent_tap_transient_retry_retains_handle`; `delete_persistent_tap_generation_mismatch_refreshes`; `delete_persistent_tap_foreign_marker_blocks_finalizer`; `config_only_spec_change_updates_volume_no_guest_restart` (INV-NET-008); `finalizer_calls_delete_bridge`; `mdns_process_created_on_enable`; `mdns_process_deleted_on_disable`; `host_capability_leakage` (INV-NET-009). nix-unit: INV-NET-001 lib.mkForce assertion; net-VM artifact has no inline mDNS service and no per-Network dnsmasq/nftables data (INV-NET-008); Network emitter CIDR constraint assertions; no `systemd.network.netdevs` bridge entries emitted. Host integration: bridge isolation with east-west opt-in; nftables drift detection; persistent-tap and macvtap create/delete lifecycle; config Volume update propagates to guest-agent without Guest restart; `tests/host-integration/guest-agent-cap-confinement.nix` (INV-NET-009 zero leakage to host netns).", + "integration" : "Pinned tests registered in `tests/golden/pinned/`; nix-unit cases in `tests/unit/nix/cases/`; host integration in `tests/host-integration/`", + "removalProof" : "Not applicable (this work item IS the test successor)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-network", + "specPath" : "docs/specs/ADR-046-resources-network.md", + "validation" : "All listed tests must pass before `nixos-modules/network.nix` removal is eligible", + "workItemId" : "ADR046-network-006" + }, + { + "currentSource" : "`nixos-modules/network.nix` lines 444–461 (USBIP host firewall); `packages/d2b-core/src/host.rs` lines 324–328 (usbip_backend_port, usbip_busid_locks in NetEnv); `packages/d2b-host/src/` usbip_argv.rs", + "dataMigration" : "Current network.nix USBIP carve-out replaced by UsbipBindFirewallRule broker op", + "dependencyOwner" : "ADR046-network-005; device-usbip Provider dossier; D-NETWORK-002 resolved", + "destination" : "`Provider/device-usbip` owns one relay Process/Endpoint authority per Network plus the typed EffectPort adapter for the existing closed `UsbipBindFirewallRule` request with closed action enum `Ensure|Remove`. The controller watches only the `networkRef` resource's identity/readiness/generation; Core privately resolves Network UID to relay attachment and firewall intent. Network spec/status is not mutated with USBIP fields. Full crate layout required for `packages/d2b-provider-device-usbip/` (see [Package and crate boundary](#package-and-crate-boundary)): `src/` (controller and usbip runner + unit tests), `tests/` (hermetic conformance, dependency-watch state machine, `UsbipBindFirewallRule` `Ensure|Remove` round-trip), `integration/` (Host/Guest USBIP attach/detach lifecycle fixtures), `README.md` (Provider identity, provider-neutral USB Service/Binding types, USBIP Processes/Endpoints, Network least-privilege dependency contract, RBAC, security invariants, standalone-repo path).", + "detailedDesign" : "Device-usbip's typed EffectPort is the sole semantic owner of every USBIP TCP/3240 rule. Its Core adapter resolves the opaque per-Network/per-busid intent and issues the same `UsbipBindFirewallRule` request with action `Ensure` for apply and `Remove` for release; no separate release op exists. `Remove` is generation-bound, ownership-scoped, idempotent after validated absence, and foreign-marker fail-closed. The controller retains firewall token/status and the relay authority reference until the broker confirms `Remove`; its strict provider status owns firewall digest/drift. Network-local emits no generic host or net-VM TCP/3240 allow and ignores device-usbip ownership markers in Network drift. The device Provider owns exactly one multiplexed relay Endpoint authority per Network and supplies Binding proxies only authorized connected streams through LaunchTickets. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "device-usbip watches Network readiness → Core adapter resolves opaque Network attachment → `UsbipBindFirewallRule { action: Ensure, ... }` for apply or `{ action: Remove, ... }` for release + one relay Endpoint authority → Binding proxy LaunchTicket; release clears status/authority only after confirmed `Remove`", + "removalProof" : "Network.nix USBIP sections removed only after UsbipBindFirewallRule mechanism passes conformance", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-network", + "specPath" : "docs/specs/ADR-046-resources-network.md", + "validation" : "device-usbip conformance tests cover the exact closed `Ensure|Remove` enum (unknown actions rejected), same-request broker mapping for apply/release, expected Network/Service generation binding, exact per-Network/per-busid scoping, idempotent validated-absence `Remove`, one relay Endpoint authority, ownership-scoped drift/status, foreign-marker rejection, transient retry, and retention of status/token/authority until effect confirmation; network-local nftables tests assert no TCP/3240/USBIP rule on host or net VM and prove USBIP rule churn does not change Network `FirewallReady`; the pinned USBIP firewall golden moves to device-usbip ownership", + "workItemId" : "ADR046-network-007" + }, + { + "currentSource" : "No current v3 source: generation lifecycle and `managedBy`/`configurationGeneration` classification do not exist on the pre-ADR45 v3 baseline. The v3 baseline uses NixOS activation scripts that atomically replace all host JSON artifacts; there is no per-resource generation tracking, no async cleanup queue, and no `managedBy` field.", + "dataMigration" : "None on v3 initial install (no prior generation state). Host upgrades from the pre-ADR45 v3 baseline perform a reset: core starts with `configurationGeneration = 1` and no prior bundle. All declared resources are treated as new Creates.", + "dependencyOwner" : "ADR046-network-004, ADR046-network-005; Zone runtime integrator", + "destination" : "`packages/d2b-core-controller/src/configuration.rs`: bundle application, diff, generation-transition logic (including per-item name-conflict handling), prior-bundle retention under `/var/lib/d2b/zones//configuration/prior/`; `packages/d2b-core-controller/src/cleanup.rs`: removal scheduling and `PendingCleanup` condition tracking; `packages/d2b-contracts/src/generation_bundle.rs`: `ZoneBundle`/`BundleResource`/`BundleMetadata` **input** DTOs — MUST NOT include `managedBy` or `configurationGeneration` (both are persisted resource metadata set by core at activation, not bundle input fields); `ManagedBy` closed enum `{ Configuration, Controller, Api }` and `configurationGeneration: u64` live in `packages/d2b-core-controller/src/resource_store.rs` as persisted resource metadata fields; `nixos-modules/resources-network.nix` (emits bundle with `managedBy`/`configurationGeneration` absent; core sets both at activation per ADR046-network-004); `d2b.zones..retainedGenerations` Nix/compiler-level Zone option (outside `Zone.spec`; default `3`, range `1..16`); `tests/unit/nix/cases/generation-cleanup-absent-network.nix`; `packages/d2b-contracts/tests/generation_bundle.rs`; `tests/host-integration/nix-generation-cleanup.nix`", + "detailedDesign" : "**Core generation tracking** (`packages/d2b-core-controller/src/configuration.rs`): core maintains a monotone `configurationGeneration` counter per Zone in its durable state. On each bundle application it compares the incoming `contentHash` against the prior applied hash. If different, it increments `configurationGeneration`, sets `managedBy = \"configuration\"` and the new counter value on each resource in the bundle, and performs the resource diff (create/update/delete scheduling). The `managedBy` and `configurationGeneration` fields are absent from the Nix-emitted bundle and are set exclusively by core at activation time. **`managedBy` field and per-item name-conflict handling**: `ManagedBy` is a closed enum (`Configuration`, `Controller`, `Api`) persisted in resource metadata at `packages/d2b-core-controller/src/resource_store.rs`. It is NOT a field in `ZoneBundle`/`BundleResource` input DTOs; core sets it at activation. Controllers set `ManagedBy::Controller` when creating owned children (net-VM Guest, config Volume, guest-agent Process, mDNS Processes); exact controller identity/UID/generation are tracked in separate internal metadata, not embedded in the `managedBy` value. API-created resources carry `ManagedBy::Api` and persist until explicit delete with no bundle-driven lifecycle. Core's generation-transition logic only schedules bundle-driven Delete for `ManagedBy::Configuration` resources. **Per-item name-conflict handling**: when a bundle item's `(zone, name)` already exists with `managedBy ≠ \"configuration\"`, core skips that item and records it with `phase = Degraded, reason: name-conflict`; a `ResourceConflictSkipped` audit record is emitted for that item. All non-conflicting items in the bundle proceed normally (Provider-state contract). The existing resource is left completely untouched. The operator deletes the conflicting resource via the resource API; the next bundle application applies the item. **Removal scheduling**: on generation N+1 activation, core performs a set difference: `prev_configuration_managed - new_configuration_managed` = resources to delete. For each, it sets `metadata.deletionRequestedAt` in the resource store and emits a `ResourceDeletionScheduled` audit record. Normal finalizer-path Delete proceeds asynchronously. **`PendingCleanup` condition**: the Zone self resource carries a `PendingCleanup = True` condition while any `managedBy = Configuration` resource has `deletionRequestedAt` set and has not yet been atomically removed. Aggregate Zone `phase = Degraded` applies. The condition transitions to `False` and Zone phase returns to `Ready` when all scheduled deletions complete. **Prior generation bundle retention** (`cleanup.rs`): count-based (`d2b.zones..retainedGenerations`, outside `Zone.spec`, default 3, range 1..16); no TTL. Core copies prior bundles to `/var/lib/d2b/zones//configuration/prior/.json`. A generation is eligible for pruning when all configuration-managed resources from it have either been atomically removed or are present unchanged in a newer generation, AND the count would be exceeded. **`BundleActivated` audit record**: emitted at each generation transition with `contentHash`, `configurationGeneration`, `resourceCount`, and `providerSchemaDigests` map (digests from `type=provider` artifacts via `Provider.spec.artifactId`); no spec contents, CIDRs, or resource names appear in the record. Provider schema digests in the bundle are re-verified against installed Provider artifact digests at application time; a mismatch aborts application with a `BundleRejected` audit record.", + "integration" : "ADR046-network-004 (emitter writes bundle format; core sets `managedBy`/`configurationGeneration` at activation) → ADR046-network-008 (runtime reads and applies). ADR046-network-005 (controller Delete path) is invoked by ADR046-network-008 removal scheduling for Network resources. Zone `PendingCleanup` condition and `Degraded` phase are read by CLI `d2b zone status`.", + "removalProof" : "Not applicable (this is a new capability). The `PendingCleanup` condition and zone cleanup audit path have no prior equivalent to remove.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-network", + "specPath" : "docs/specs/ADR-046-resources-network.md", + "validation" : "**nix-unit**: `tests/unit/nix/cases/generation-cleanup-absent-network.nix` — verifies that a Network resource present in generation N and absent from generation N+1 receives `deletionRequestedAt` and appears in the `PendingCleanup` condition; verifies that a controller-owned `Guest` (`managedBy = \"controller\"`) does NOT receive a direct bundle-driven Delete; verifies that a re-declared (identical spec) Network is NOT scheduled for Delete; verifies `retainedGenerations` default is 3. **Rust contract tests**: Two separate test files — (1) `packages/d2b-contracts/tests/generation_bundle.rs`: tests the **input** bundle DTO only: `ZoneBundle`/`BundleResource`/`BundleMetadata` JSON round-trip, `contentHash` stability across serialization, `providerSchemaDigests` presence, `managedBy` and `configurationGeneration` fields ABSENT from `BundleResource` input struct (verified by both compile-time type check: the fields must not exist on the `BundleResource` type, and runtime JSON serialization: the serialized object must not contain those keys). (2) `packages/d2b-core-controller/tests/resource_metadata.rs`: `ManagedBy` closed enum round-trip with `\"configuration\"`/`\"controller\"`/`\"api\"` values tested separately here since `ManagedBy` is persisted resource metadata in `resource_store.rs`, not a field of the input bundle DTO. **Controller integration tests**: async Delete triggered through finalizers for Network; mDNS Process child deleted before Network finalizer clears; bridge `DeleteBridge` broker call made exactly once during finalizer; controller waits for Deleted watch event (not a persistent phase=Deleted row) before proceeding. **Host integration**: `tests/host-integration/nix-generation-cleanup.nix` — runNixOSTest scenario: apply generation 1 with Network resource, then apply generation 2 with that Network absent; assert Zone enters `Degraded/PendingCleanup`; assert Network `phase = Degraded` with `NetworkDraining = True` and `deletionRequestedAt` set and `reason: configuration-generation-removed`; assert cleanup completes (single store transaction: Deleted REVISION event + row/index removal; dedup-guarded audit append follows committed transaction) and Zone returns to `Ready`; assert no controller-owned children deleted directly by core; assert prior bundle copied to `/var/lib/d2b/zones//configuration/prior/` and retained until cleanup complete; assert bundle pruned when `retainedGenerations` exceeded and generation eligible. **INV-NET-LIFECYCLE-001**: core never schedules bundle-driven Delete for `managedBy ≠ \"configuration\"` resources; verified by static analysis of core's generation-transition diff function, which is bounded at compile time to iterate only the `configuration_managed_resources` set. **INV-NET-LIFECYCLE-002**: per-item name-conflict — when a bundle item collides with `managedBy = \"controller\"` or `\"api\"`, that item is recorded as `Degraded/name-conflict`; the existing resource is left untouched; non-conflicting items continue to activate; tested by `packages/d2b-core-controller/tests/configuration_name_conflict.rs` (three cases: collision with a controller-owned child, an API-created resource, and a same-name configuration resource from a prior generation that completed deletion; each case asserts non-conflicting items still activate).", + "workItemId" : "ADR046-network-008" + }, + { + "currentSource" : "Existing macvtap spawn path resolves `parentInterface` but has no cross-Zone authority admission or compatible-sharing contract", + "dataMigration" : "Full d2b 3.0 reset; no authority ledger import", + "dependencyOwner" : "D097 Host-global authority index; ADR046-network-001, ADR046-network-005; Provider/network-local and Core authority owners", + "destination" : "`packages/d2b-contracts/src/v3/network.rs` external-attachment sharing schema/status; `packages/d2b-core-controller/src/authority.rs` Core-derived physical-NIC identity and Host-global claim; Provider/network-local descriptor/reconcile/finalizer", + "detailedDesign" : "Resolve operator-declared `parentInterface` against trusted Host inventory and derive an opaque `external-physical-nic/v1` digest; index `(Host, external-physical-nic, opaqueKeyDigest)` before any macvtap/VMM effect. `passthru`, `private`, and `vepa` are exclusive. `bridge` defaults exclusive and is multiplexed only under explicitly authored compatible policy. Use typed `external-physical-nic-conflict`; expose only bounded authority availability/holder-count/queue/arbitration/update-currency and conditions; keep digest, interface identity, and owner proof private. Parent/mode/policy update drains and releases the old claim before replacement; deletion closes macvtap/VMM ownership before releasing the claim; restart adopts exact owner proof and quarantines ambiguity. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt the existing private macvtap-FD spawn path; add authority admission before it.", + "integration" : "Network validation and Core authority preflight gate runtime-cloud-hypervisor LaunchTicket/`SpawnRunner`; the finalizer and D091 update planner release in dependency order", + "removalProof" : "None — authority admission is new; existing direct macvtap spawn becomes unreachable without a claim.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-network", + "specPath" : "docs/specs/ADR-046-resources-network.md", + "validation" : "Hermetic authority tests cover same-Zone and cross-Zone exclusive collisions, mixed-policy conflicts, non-bridge multiplex rejection, explicit compatible bridge multiplex admission, Core-derived key equality for two selectors resolving to one fake NIC, caller-supplied digest rejection, no-effect conflict, owner-proof adoption/ambiguity, disruptive update, and release-after-close ordering. Nix eval covers schema and declared cross-Zone conflicts; host integration covers create/update/delete with a fake macvtap parent and status/condition transitions without raw identity exposure.", + "workItemId" : "ADR046-network-009" + }, + { + "currentSource" : "`nixos-modules/options-realms.nix` (`RealmId`/`RealmPath`/`RealmControllerPlacement`/`EntrypointMode` labels); `options-realms-workloads.nix` (`WorkloadId`/`WorkloadProviderKind`/`IsolationPosture`/`WorkloadExecutionPosture`); `options-realms-network.nix`; `d2b-realm-core/src/realm.rs`, `workload.rs`, `capability.rs`, `ids.rs` (symbols to adapt)", + "dataMigration" : "Operator configs migrate `d2b.realms.*` → `d2b.zones.*`; `d2b.vms.*` → `d2b.zones..resources.*` with `type = \"Guest\"`", + "dependencyOwner" : "W0; `d2b-contracts` identities (ADR046-identities-001, ADR046-identities-002)", + "destination" : "`nixos-modules/options-zones.nix` (Zone-level options: `label`, `parentZone`, `retainedGenerations`, `trustedPublishers` — compiler settings, not Zone spec fields); `nixos-modules/options-zones-resources.nix` (unified `resources` attrset)", + "detailedDesign" : "`d2b.zones..resources. = { type = \"\"; spec = { ... }; }` — single attrset covering all ResourceTypes; `type` discriminates dispatch; `spec` fields mirror exact ResourceTypeSchema field names and nesting; Nix option types/defaults/docs generated from `docs/reference/schemas/v3/.json`; no Nix-only fields inside resource declarations; `metadata.name` derives from attr key; `metadata.zone` derives from enclosing zone attr key; `apiVersion` defaulted; `uid`/`generation`/`revision`/`status`/`managedBy` never in Nix; `resource_name` regex `^[a-z][a-z0-9-]*$`; ref validation assertions; `WorkloadProviderKind` → Guest/Host mapping per disposition table above; `Capability` → Role verb mapping per resource-api/authz foundation spec; Zone self-resource spec is `{}`; `parentZone` is a required non-root/forbidden-root compiler-only plain Zone name compiled into sealed allocator topology; `retainedGenerations`/`trustedPublishers` are likewise Zone-level compiler settings not emitted in Zone spec", + "integration" : "`nixos-modules/default.nix` imports new options files; old realms options coexist until ADR046-nix-002", + "removalProof" : "`options-realms*.nix` removed after `options-zones*.nix` achieves parity and parity drift test passes", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "nix-unit vectors for each ResourceType; ref-validation rejection vectors; malformed ref error shape; resource/session verb closed-enum vectors accept `relay` only in `sessionVerbs`; relay wildcard/unbounded/Provider-self-asserted fixtures fail before activation while an exact core-generated ZoneLink fixture passes; Endpoint visibility accepts exactly `owner|provider|zone` and uses `consumerPolicy` for finer bounds; `parentZone` is missing on non-root/forbidden on local-root/unknown/self/cyclic/over-depth rejection; scalar module conflicts prove one parent; child-local ZoneLink `childZoneName` must equal its enclosing Zone key; a second uplink resource (even disabled) and a local-root uplink fail eval; missing/local-unresolved `transportProviderRef` fails eval; `managedBy` in spec rejected at eval; Zone spec is `{}` (no `parentZone`, `parentRef`, `retainedGenerations`, etc.)", + "workItemId" : "ADR046-nix-001" + }, + { + "currentSource" : "`nixos-modules/options-envs.nix` (`lanSubnet`/`uplinkSubnet`/`mtu`/`mssClamp`/`externalNetwork.*`); `nixos-modules/options-vms.nix` (`d2b.vms..*`; `ProcessRole` toggle options for components)", + "dataMigration" : "Operator configs migrate env/VM component options into Zone Network/Guest/Device resources; full d2b 3.0 reset means no v2 runtime state/config import beyond rewritten Nix declarations.", + "dependencyOwner" : "ADR046-nix-001; env/VM migration", + "destination" : "`Network` resource fields in `nixos-modules/options-zones-resources.nix`; `Guest` resource fields", + "detailedDesign" : "`d2b.envs.work.lanSubnet` → `d2b.zones.work.resources.work-lan = { type = \"Network\"; spec = { lanSubnet = \"...\"; ... }; }`; CIDR overlap assertion migrated; `sys-` reserved prefix and VM-name regex retained; `d2b.vms..tpm.enable` → `d2b.zones..resources.vm-tpm = { type = \"Device\"; spec = { providerRef = \"Provider/device-tpm\"; ... }; }`", + "integration" : "`options-zones-resources.nix` emits Network/Guest resources consumed by the rewritten index/bundle and Provider/network-local/device Providers; assertions run during eval before bundle emission.", + "removalProof" : "`options-envs.nix`, `options-vms.nix` removed after migration parity test passes", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "nix-unit CIDR rejection; eval assertion for `sys-` prefix; VM-name regex", + "workItemId" : "ADR046-nix-002" + }, + { + "currentSource" : "`nixos-modules/options-daemon.nix`, `options-site.nix`, `options-host.nix`", + "dataMigration" : "Operator site options stay in place; new per-Zone `retainedGenerations` defaults apply on first v3 activation; no v2 runtime state import.", + "dependencyOwner" : "ADR046-nix-001; site options", + "destination" : "`nixos-modules/options-site.nix` (retained); per-Zone options in `options-zones.nix`", + "detailedDesign" : "`d2b.zones..retainedGenerations` (default 3, range 1..16, compiler setting — not emitted in Zone spec); `d2b.site.stateDir` maps to Zone storage roots; `d2b.site.usePrebuiltHostTools` retained Primary reuse disposition: `adapt`. Preserved source-plan detail: retain and extend.", + "integration" : "`options-site.nix` retains site defaults consumed by `options-zones.nix` and `bundle-zones.nix`; activation-helper and Zone storage roots read `stateDir`/`usePrebuiltHostTools` settings.", + "removalProof" : "No removal; file extended only", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "nix-unit case verifies `retainedGenerations` default/range, `site.stateDir` mapping, and `usePrebuiltHostTools` retention.", + "workItemId" : "ADR046-nix-003" + }, + { + "currentSource" : "`nixos-modules/index.nix` — `cfg._index.*`: `enabledEnvs`, `enabledVms`, `netMeta` (derives bridge names/IPs from `lanSubnet`/`uplinkSubnet`), `declaredRealms`, `enabledRealms`, `workloadsInEnv`, `runtimeRows`", + "dataMigration" : "Index is rebuilt from v3 Zone resources at activation; full d2b 3.0 reset; no v2 index state import.", + "dependencyOwner" : "ADR046-nix-001, ADR046-nix-002", + "destination" : "`nixos-modules/index.nix` (rewritten); emits `/etc/d2b/index.json`", + "detailedDesign" : "Cross-Zone normalized index: zone/host/guest/network/closure entries; executionIndex; networkIndex; closureIndex; sorted output; `cfg._index` attribute tree retained as internal helper during migration Primary reuse disposition: `adapt`. Preserved source-plan detail: rewrite.", + "integration" : "`options-zones-resources.nix` feeds rewritten `index.nix`; `bundle-zones.nix`, resource emitters, and activation-helper consume `/etc/d2b/index.json`.", + "removalProof" : "`cfg._index.envMeta`, `cfg._index.realms.*` sub-trees removed after all callers migrate to Zone resource lookups", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "nix-unit golden vectors for index shape; drift gate: `xtask gen-index` round-trip", + "workItemId" : "ADR046-nix-004" + }, + { + "currentSource" : "`nixos-modules/bundle-artifacts.nix` (`artifactModule` submodule, `installFileName`, `mode 0640` ownership); `nixos-modules/bundle.nix` (bundle derivation, SHA256SUMS, `d2b._bundle` integrity chain)", + "dataMigration" : "Per-Zone bundles are generated from v3 resource declarations; full d2b 3.0 reset; no v2 monolithic bundle state import.", + "dependencyOwner" : "ADR046-nix-004", + "destination" : "`nixos-modules/bundle-zones.nix` (per-Zone bundle derivation); common helpers retained in `bundle-artifacts.nix`", + "detailedDesign" : "Per-Zone `bundle.json` with `candidateId`/`contentId` binding; SHA256 digest chain; `generationIndex`; atomic activation pointer; `manifestVersion` → `schemaVersion` rename Primary reuse disposition: `adapt`. Preserved source-plan detail: extend and rewrite.", + "integration" : "`d2b-activation-helper` reads `bundle.json` per Zone; validates digest chain before staging", + "removalProof" : "Monolithic `bundle.json` and `d2b._bundle` artifact table retired after all Zone bundle tests pass", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Artifact-shape contract tests in `packages/d2b-contract-tests/tests/`; determinism test (build twice, diff outputs)", + "workItemId" : "ADR046-nix-005" + }, + { + "currentSource" : "`nixos-modules/processes-json.nix` (`VmProcessDag`, `ProcessRole`, `binaryPath`, `argv`, `share.source == \"/nix/store\"` sentinel); `packages/d2b-core/src/processes.rs` (`ProcessRole` enum — all variants in disposition table above)", + "dataMigration" : "Processes are emitted from v3 Process/EphemeralProcess resources; full d2b 3.0 reset; no v2 process artifact import.", + "dependencyOwner" : "ADR046-nix-005; Process Provider work items (ADR046-primitives-002)", + "destination" : "`nixos-modules/resources-zones-processes.nix`; emits `zones//processes.json`", + "detailedDesign" : "Process/EphemeralProcess resource serialization per disposition table and the frozen `ExecutionSpec`: exact `providerRef`/`executionRef`/domain/user/processClass/template/config/credential/mount/sandbox/budget/`networkUsage`/`deviceUsage`/telemetry names; semantic owner from `metadata.ownerRef` with template fallback through the Process Provider; no `packageRef`, free-form `binaryPath`, `argv`, renamed network/device fields, or inline endpoints; stable produced endpoints are separate owned `Endpoint` resources with `producerRef`; Process uses canonical `restartPolicy` fields; EphemeralProcess uses `runtimeDeadline` and has no restart policy; VsockRelay → `Process` under `Provider/transport-vsock`; GuestSshReadiness retired at v3 cutover; Usbip long-lived backend/proxy → `Process`, Usbip per-busid attach/detach → `EphemeralProcess`, all owned by `Provider/device-usbip` Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "`processes.json` replaces `cfg._bundle.processesJson`; Process Providers read the new format", + "removalProof" : "`processes-json.nix` and current `processes.json` schema removed after all Process Providers consume `zones//processes.json`", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Process and EphemeralProcess exact-schema vectors; reject `packageRef`, `network`, `devices`, `endpoints`, `restart`, and `runDeadline`; accept canonical `networkUsage`, `deviceUsage`, `restartPolicy`, `runtimeDeadline`, and separately owned Endpoint with `producerRef`; no-raw-path assertion; ProcessRole parity test (every variant has a test case)", + "workItemId" : "ADR046-nix-006" + }, + { + "currentSource" : "`nixos-modules/storage-json.nix` (all `mkPath` calls, `PrincipalRef` `uid`/`gid`/`user` kinds, `StorageRoot`/`StoragePathSpec`/`repairPolicy`/`cleanupPolicy`); `packages/d2b-core/src/storage.rs` (`StorageJson`, `StoragePathSpec`); `nixos-modules/sync-json.nix` (`SyncJson` OFD lock rows); `packages/d2b-core/src/sync.rs`", + "dataMigration" : "Volume resources are regenerated from v3 config; full d2b 3.0 reset; no v2 storage/sync state import.", + "dependencyOwner" : "ADR046-nix-005; Volume Provider work items (ADR046-primitives-003)", + "destination" : "`nixos-modules/resources-zones-volumes.nix`; emits `zones//volumes.json`; OFD lock rows move to `d2b-contracts` internals", + "detailedDesign" : "Volume layout/views/ACL/no-follow/repair preserving current policy; `PrincipalRef { kind: \"uid\" }` → `User/` typed ref only; OFD rows removed from Nix artifacts Primary reuse disposition: `adapt`. Preserved source-plan detail: extract storage policy → adapt; retire sync rows.", + "integration" : "Volume resources feed `zones//volumes.json`; the Volume Provider consumes it for path/view/ACL reconciliation; `d2b-contracts` internals own OFD lock rows.", + "removalProof" : "`storage-json.nix`, `sync-json.nix`, and `/etc/d2b/storage.json`/`sync.json` removed after Volume controller parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Volume schema vectors; ACL/no-follow/view policy tests", + "workItemId" : "ADR046-nix-007" + }, + { + "currentSource" : "`nixos-modules/allocator-json.nix` (realm/env bridge assignments, `allocatorStateDir`, socket paths, `providerPlacement`); `nixos-modules/realm-controller-config-json.nix` (emits `realm-controllers.json`); `packages/d2b-core/src/realm_controller_config.rs` (`RealmControllersJson`, `RealmControllerMetadataSummary`; read live by `d2bd/src/realm_access_resolver.rs` from `/etc/d2b/realm-controllers.json`)", + "dataMigration" : "Topology is regenerated from `d2b.zones..parentZone`; full d2b 3.0 reset; no allocator/realm-controller state import.", + "dependencyOwner" : "ADR046-nix-004; Zone/Network migration", + "destination" : "Compiler-only `parentZone` map in `nixos-modules/options-zones.nix`; local-root allocator bootstrap compiler/sealer; runtime-created Zone self-resource with `spec = {}`; child-local ZoneLink resource; `realm-controllers.json` RETAINED during migration (live d2bd reads it)", + "detailedDesign" : "Resolve each non-root `parentZone` to one declared Zone; reject a root value, missing/unknown/self/conflicting/cyclic parent, and ancestry deeper than 16 names. Canonicalize sorted child→parent rows and feed them to the local-root allocator's private bootstrap sealer. Parent allocators derive/pre-bind privileged socket/listener state from that topology; no socket path or `parentZone` enters `Zone.spec` or a ResourceRef. Child-local ZoneLinks provide transport/local route state. `realm-controllers.json` remains published until `realm_access_resolver` is replaced. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt and retire.", + "integration" : "Nix Zone options → validated topology compiler → local-root allocator bootstrap sealer; ZoneLink controller consumes only its sealed selected-parent binding. During migration, `realm-controllers.json` remains available to the legacy resolver until the Zone topology/ZoneLink path takes over.", + "removalProof" : "Legacy `allocator-json.nix`, `realm-controller-config-json.nix`, and `/etc/d2b/allocator.json`/`realm-controllers.json` removed ONLY after the sealed `parentZone` topology and ZoneLink resolver are live; the allocator bootstrap mechanism itself remains", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Zone self-resource round-trip with exact empty spec; `parentZone` required/root-forbidden/existence/one-parent/self/cycle/16-name-depth vectors; topology seal excludes `parentZone` from Zone/resource bundles; bootstrap socket/listener regression tests", + "workItemId" : "ADR046-nix-008" + }, + { + "currentSource" : "`nixos-modules/realm-workloads-launcher-v2-json.nix` (`RealmWorkloadsLauncherV2Json`, `LauncherWorkloadSummary`; live); `nixos-modules/realm-identity-config-json.nix` (`RealmIdentityConfigJson`; live, read by d2bd from `/etc/d2b/realm-identity.json`); `packages/d2b-core/src/realm_workloads_launcher.rs`; `packages/d2b-realm-core/src/identity_config.rs`", + "dataMigration" : "Launcher and identity config are re-emitted as v3 resources; full d2b 3.0 reset; no v2 launcher/identity state import.", + "dependencyOwner" : "ADR046-nix-006; display/credential Provider work items", + "destination" : "Provider/display-wayland and Provider/shell-terminal Process configs in `zones//processes.json`; `Provider/credential-entra` Credential resource; `realm-identity.json` RETAINED during migration", + "detailedDesign" : "Launcher metadata folded into Process resource annotations; identity config → Credential resource fields (`providerRef`, `identityGuestRef`, `loginEndpointRef`, `scope`, `audience`, `consumerRef`, `allowedOperations`, canonical `spec.provider` extension where Provider schema declares it; no secret bytes); `realm-identity.json` must remain until d2bd `RealmIdentityConfigJson` loading is replaced by Credential resource reader", + "integration" : "Provider/display-wayland, Provider/shell-terminal, and Provider/credential-entra consume Process/Credential resources; `d2bd` continues reading `realm-identity.json` until the Credential reader lands.", + "removalProof" : "`realm-workloads-launcher-v2-json.nix`/`realm-identity-config-json.nix` and `/etc/d2b/realm-workloads-launcher-v2.json`/`realm-identity.json` removed ONLY after display/credential Providers read resource configs", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Launcher metadata shape regression; Entra identity-Guest fixture requires login Endpoint purpose `credential-entra.d2bus.org/entra-login-token`, `visibility = \"provider\"` (and rejects `\"zone\"`), exact `consumerPolicy.allowedSubjects` containing both `Provider/credential-entra` and the Credential's configured `consumerRef`, and exact canonical `resolve` operation; no-secret assertion vectors", + "workItemId" : "ADR046-nix-009" + }, + { + "currentSource" : "`nixos-modules/unsafe-local-workloads-json.nix` (`WorkloadProviderKind::UnsafeLocal`/`IsolationPosture::UnsafeLocal`; current `unsafe-local-workloads.json` artifact); `nixos-modules/unsafe-local-helper.nix` (user-domain process/helper definitions); `packages/d2b-core/src/unsafe_local_workloads.rs`", + "dataMigration" : "unsafe-local declarations migrate to Host/Process resources in Nix; full d2b 3.0 reset; no helper runtime state import.", + "dependencyOwner" : "ADR046-nix-001; unsafe-local migration", + "destination" : "User-only `Host` resource in `zones//hosts.json` (`spec.isolationPosture: \"none\"`, `defaultDomain: user`, `allowedDomains: [user]`, `defaultUserRef: User/`); child `Process` resources in `zones//processes.json` using normal Process Providers; shell session supervisor → `Process` under `Provider/shell-terminal`; never a `Guest`; not a v3 Provider", + "detailedDesign" : "`isolationPosture: \"none\"` is a promoted Host base field declared at top-level `spec.isolationPosture` in the Host schema; enforced at eval time; user-only Host rejects system-domain Process refs; `NoIsolation` condition in Host status; `status.isolationPosture: none`; every `ProcessEffect` audit event under this Host carries `no_isolation=true`; OTEL telemetry never carries an isolation label; CLI/UI warning non-suppressible", + "integration" : "The unsafe-local Nix emitter produces a user-only Host plus Process resources; Provider/system-core and shell-terminal consume them; CLI/UI and audit/telemetry surfaces read resulting status/events.", + "removalProof" : "`unsafe-local-workloads-json.nix` and unsafe-local-specific Nix code removed after user-only Host/Process resources pass all `tests/host-integration/unsafe-local-helper.nix` tests", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "User-only Host rejection of system-domain Process refs; `spec.isolationPosture != \"none\"` assertion rejection for user-only system-core Hosts; `NoIsolation` condition present in status; `no_isolation=true` in `ProcessEffect` audit event; OTEL attribute absent; no Guest emitted for unsafe-local declaration", + "workItemId" : "ADR046-nix-010" + }, + { + "currentSource" : "`nixos-modules/privileges-json.nix`", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "Broker privileges owner", + "destination" : "`nixos-modules/privileges-json.nix` (retained); `/etc/d2b/privileges.json` (retained, site-wide)", + "detailedDesign" : "Broker op catalog is not Zone-scoped; no structural change required Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: retain.", + "integration" : "`privileges-json.nix` remains a site-wide bundle artifact consumed by broker authorization and drift gates; Zone resource emission references it without Zone scoping.", + "removalProof" : "Not removed in this spec", + "reuseAction" : "copy-unchanged", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Existing `tests/unit/gates/drift-check.sh`", + "workItemId" : "ADR046-nix-011" + }, + { + "currentSource" : "`nixos-modules/closures-json.nix` (keyed by `d2b.vms.`); `nixos-modules/minijail-profiles.nix`", + "dataMigration" : "Closure entries are regenerated by artifact ID from `d2b.artifacts`; full d2b 3.0 reset; no v2 VM-keyed closure state import.", + "dependencyOwner" : "ADR046-nix-005; ADR046-nix-022 (artifact catalog emitter); Provider/volume-virtiofs", + "destination" : "`nixos-modules/closures-json.nix` (rewritten, keyed by artifact ID from `d2b.artifacts` with `type = \"nixos-system\"`); `nixos-modules/minijail-profiles.nix` (retained, adapted to reference Zone Guests)", + "detailedDesign" : "Closure emitter iterates `d2b.artifacts` entries with `type = \"nixos-system\"`, computes `pkgs.closureInfo`, records `storePath`/digest/size in artifact catalog (private root:d2bd 0640 field; absent from all public ResourceSpecs/status/audit/OTEL); `Guest.spec.systemArtifactId` links Guest to artifact; `Volume.source.systemArtifactId` links Volume to artifact; minijail profile emitter structurally unchanged; old `d2b.vms.` keying retired", + "integration" : "Artifact catalog and Guest/Volume resources reference closure entries; activation-helper and Provider/volume-virtiofs consume private `storePath` metadata while public surfaces remain path-free.", + "removalProof" : "Old `d2b.vms.*`-keyed closure entries removed after all Guests use `zones//guests.json` and artifact catalog", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Closure map round-trip; per-VM store hardlink integrity; `storePath` present in private catalog; `storePath` absent from all emitted public ResourceSpecs and status/audit/OTEL surfaces", + "workItemId" : "ADR046-nix-012" + }, + { + "currentSource" : "`nixos-modules/manifest.nix` (`manifestVersion` pinned contract); `nixos-modules/host-json.nix`", + "dataMigration" : "Manifest/host artifacts are regenerated as Zone bundle/Host resources; full d2b 3.0 reset; no v2 manifest state import.", + "dependencyOwner" : "ADR046-nix-005; manifest contract", + "destination" : "Per-Zone `zones//bundle.json` (`schemaVersion`); Host resource in `zones//hosts.json`", + "detailedDesign" : "`manifestVersion` → `schemaVersion`; `host.json` host config folded into Host resource spec; CHANGELOG entry for rename required", + "integration" : "`bundle-zones.nix` writes `schemaVersion` into per-Zone `bundle.json`; activation-helper validates it and Host resource consumers replace `host.json` readers.", + "removalProof" : "`manifest.nix` and `host.json` emitters removed after Zone bundle activation path passes", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Schema drift gate; CHANGELOG enforcement", + "workItemId" : "ADR046-nix-013" + }, + { + "currentSource" : "`nixos-modules/assertions.nix` (CIDR overlap, `sys-` prefix, VM-name regex, platform gate)", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-nix-001, ADR046-nix-002", + "destination" : "`nixos-modules/assertions.nix`", + "detailedDesign" : "Migrate existing assertions to Zone/Resource terminology; add ref validation, owner cycles, CIDR overlap (Zones), provider resolution, RoleBinding verb set assertions Primary reuse disposition: `adapt`. Preserved source-plan detail: extend in place.", + "integration" : "`assertions.nix` validates Zone/Resource declarations before index, bundle, and schema validation derivations run; failing configs block activation artifacts.", + "removalProof" : "No removal; extended only", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Each new assertion has a failing-config test vector", + "workItemId" : "ADR046-nix-014" + }, + { + "currentSource" : "`nixos-modules/host.nix`, `host-daemon.nix` (fixed local-root endpoint set), `host-activation.nix`, `host-users.nix`", + "dataMigration" : "Host activation reads newly staged Zone bundles on first v3 switch; full d2b 3.0 reset; no v2 daemon runtime state import.", + "dependencyOwner" : "ADR046-nix-001; host activation", + "destination" : "Same files; updated to use Zone bundle activation path and Zone resource state dirs", + "detailedDesign" : "`d2b-activation-helper` updated to validate/stage per-Zone bundles; `d2bd.service` updated to read Zone bundle; `d2b` group retained for `SO_PEERCRED` Primary reuse disposition: `adapt`. Preserved source-plan detail: retain and adapt.", + "integration" : "Host activation units call `d2b-activation-helper` against per-Zone bundles, then `d2bd.service` reads the staged Zone bundle; `SO_PEERCRED` authorization remains via the `d2b` group.", + "removalProof" : "No removal; adapted in place", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Host-integration test with Zone bundle activation and daemon readiness", + "workItemId" : "ADR046-nix-015" + }, + { + "currentSource" : "`nixos-modules/network.nix`, `nixos-modules/net.nix` (including `lib.mkForce` DHCP neutralizer and per-env MTU/MSS/east-west wiring)", + "dataMigration" : "Network resource specs are generated from v3 Nix declarations; full d2b 3.0 reset; no v2 bridge/NAT runtime state import.", + "dependencyOwner" : "ADR046-nix-002; Provider/network-local dossier", + "destination" : "Network reconciliation by `Provider/network-local` Process resources", + "detailedDesign" : "Current bridge/NAT/DHCP/firewall Nix units retained; `Provider/network-local` controller emits equivalent configuration from Network resources; `lib.mkForce` neutralization preserved Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: retain until Provider successor.", + "integration" : "Network resources feed Provider/network-local parity implementation while `net.nix`/`network.nix` remain the fallback; network eval tests compare both paths.", + "removalProof" : "`network.nix`/`net.nix` removed after `Provider/network-local` parity and `tests/net-vm-network-eval.sh` passes", + "reuseAction" : "copy-unchanged", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "`tests/net-vm-network-eval.sh` passes against Network resource spec", + "workItemId" : "ADR046-nix-016" + }, + { + "currentSource" : "`nixos-modules/store.nix` (`ProcessRole::Virtiofsd` and `share.source == \"/nix/store\"` sentinel; per-VM hardlink farm)", + "dataMigration" : "Full d2b 3.0 reset; no v2 store metadata import", + "dependencyOwner" : "ADR046-nix-012; Provider/volume-virtiofs", + "destination" : "Per-VM store reconciliation by `Provider/volume-virtiofs` EphemeralProcess/Process resources", + "detailedDesign" : "`store.nix` retained; `Provider/volume-virtiofs` controller creates equivalent EphemeralProcess; per-VM store path derived from Zone stateDir + Guest name via Provider, not raw path in spec Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: retain until Provider successor.", + "integration" : "Guest/Volume resources drive Provider/volume-virtiofs, which stages hardlink farm processes consumed by Guest process resources; `store.nix` remains until parity.", + "removalProof" : "`store.nix` removed after `Provider/volume-virtiofs` manages farm lifecycle and existing store tests pass", + "reuseAction" : "copy-unchanged", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Store hardlink integrity; no direct `/nix/store` export", + "workItemId" : "ADR046-nix-017" + }, + { + "currentSource" : "`nixos-modules/components/graphics.nix` (`ProcessRole::Gpu`, `GpuRenderNode`, `Video`); `components/tpm.nix` (`ProcessRole::Swtpm`, `SwtpmPreStartFlush`); `components/usbip.nix` (`ProcessRole::Usbip`); `components/audio/` (`ProcessRole::Audio`)", + "dataMigration" : "Component toggles migrate to v3 Device/Guest resource declarations; full d2b 3.0 reset; no v2 component runtime state import.", + "dependencyOwner" : "ADR046-nix-002; Provider dossiers for graphics, tpm, usbip, audio", + "destination" : "`Provider/device-tpm`, `Provider/device-usbip`, `Provider/device-gpu`, `Provider/audio-pipewire` resource install declarations in `options-zones-resources.nix`", + "detailedDesign" : "`d2b.vms..tpm.enable = true` → `d2b.zones..resources.vm-tpm = { type = \"Device\"; spec = { providerRef = \"Provider/device-tpm\"; ... }; }`; all component eval assertions migrated to `assertions.nix`; GuestSshReadiness retired at v3 cutover; Usbip long-lived backend/proxy → `Process`, per-busid attach/detach → `EphemeralProcess`, both owned by `Provider/device-usbip` Primary reuse disposition: `replace`. Preserved source-plan detail: each component becomes a Provider install resource + Device/Guest spec field.", + "integration" : "Device/Guest specs feed Provider/device-* and Provider/audio-pipewire install declarations; migrated assertions/tests consume Zone resource configs instead of component toggles.", + "removalProof" : "`components/` Nix units removed after Provider resource install achieves parity and all component eval tests pass against Zone resource configs", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Existing component eval tests; `tests/usbip-gating-eval.sh`; `tests/video-contract-eval.sh`", + "workItemId" : "ADR046-nix-018" + }, + { + "currentSource" : "`nixos-modules/bundle-artifacts.nix` (`artifactModule` submodule, mode/ownership); `nixos-modules/bundle.nix` (digest chain, SHA256SUMS); `packages/xtask/src/main.rs` (`gen-schemas`); no current per-ResourceType JSON Schema under `docs/reference/schemas/v3/`", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-nix-005; ADR046-nix-001; ADR046-cli-011 assertions/resource-option shape; `d2b-contracts` schema generation (ADR046-nix-027)", + "destination" : "`docs/reference/schemas/v3/.json` for each ResourceType; `nixos-modules/resource-schema-validation.nix` (validates emitted spec against committed JSON Schema at build time); `nixos-modules/provider-settings-validation.nix` (validates `spec.provider.settings` where declared in schema, and Provider `config`, against Provider-embedded schema at build time); `nixos-modules/assertions.nix` (Credential ref enforcement, secret-pattern rejection)", + "detailedDesign" : "`cargo xtask gen-schemas` emits one JSON Schema per ResourceType under `docs/reference/schemas/v3/`; Nix derivation reads these schemas from `pkgs.d2b-resource-schemas` and validates every emitted `spec` JSON before producing the Zone bundle; Provider-settings validation reads `settingsSchemaDigest` from `provider-catalog.json` and resolves the schema from the Provider package closure; Credential ref enforcement: eval assertion rejects any `spec` string field matching `-----BEGIN`, `eyJ`, or a hex string ≥ 32 bytes in a secret-typed field; `managedBy` in any input spec rejected at eval (core-set runtime field, never in Nix input); bundle integrity: `candidateId`/`contentId` computed over canonical sorted output Primary reuse disposition: `adapt`. Preserved source-plan detail: extend xtask schema generation; new Nix eval/build validation hooks.", + "integration" : "Validation hooks wired into `bundle-zones.nix` derivation; `d2b-activation-helper` re-verifies digest chain at staging", + "removalProof" : "Not removed; extended as new ResourceTypes are added", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Schema round-trip: emit spec, validate against schema, verify byte-identical re-emit; `spec.provider.settings` rejection test (unknown field, out-of-bounds value, raw store path, secret bytes) where schema declares the field; Provider `config` rejection test (unknown field); Credential ref enforcement: PEM-in-spec rejected; secret-pattern-in-spec rejected; valid `Credential/` ref accepted; `managedBy` in spec input rejected at eval", + "workItemId" : "ADR046-nix-019" + }, + { + "currentSource" : "No current equivalent; current `bundle.nix` replaces all artifacts atomically with no per-resource cleanup tracking", + "dataMigration" : "Full d2b 3.0 reset; existing resources gain `managedBy`/`configurationGeneration` only when first v3 activation creates them; no v2 state/config import.", + "dependencyOwner" : "ADR046-nix-005; ADR046-nix-001; configuration-publication controller (ADR-046-core-controllers)", + "destination" : "Configuration-publication controller handler in `packages/d2bd/src/config_publication.rs`; `ConfigurationOwnedClassifier`; `AbsentResourceReaper`; `Zone` status conditions in `d2b-contracts/src/v3/zone_status.rs`; cleanup audit emitter in `d2b-state/src/audit_segments.rs`", + "detailedDesign" : "`ConfigurationOwnedClassifier`: classify resources by core-set `managedBy` field only — `managedBy=configuration` resources are owned by config publication; `managedBy=controller` and `managedBy=api` resources are never touched. At activation, diff new-generation bundle name+type set against all resources with `managedBy=configuration` in the Zone store; resources absent from the new bundle are enqueued for Delete. Never infer ownership from `ownerRef`, labels, or absence from emitted files. `AbsentResourceReaper`: processes the Absent queue asynchronously; does not block pointer swap (step 4); sets `status.phase=Pending` + `PendingDeletion` condition (`reason: AbsentFromConfiguration`); waits for all finalizers to clear; commits a single store transaction that appends the `Deleted` revision and removes the resource row and its indexes; subsequent `Get` returns not-found; the `ResourceDelete` audit event is appended afterward with dedup/exactly-once recovery. Zone phase: `Pending` during pointer-swap-to-first-reconcile window; `Degraded` while any `managedBy=configuration` resource carries `PendingDeletion` or a ZoneLink lags; `Ready` when all reconciled. Generation pruning: prune when `generationIndex ≤ activeIndex - retainedGenerations` AND all enqueued resources from that generation have reached `Deleted`. Rollback: re-adopt `managedBy=configuration` resources in `Pending/PendingDeletion` back to the rollback target generation's owned set", + "integration" : "`d2b-activation-helper` sets `managedBy=configuration` + `configurationGeneration` on every resource it activates; controller reads these fields to determine owned set — never `ownerRef` or bundle membership alone", + "removalProof" : "Not removed; extended as new ResourceTypes are added", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Classification: `managedBy=controller` resource never enqueued (even if absent from bundle). `managedBy=api` resource never enqueued. `managedBy=configuration` resource absent from new bundle always enqueued. Finalizer safety: resource with active finalizer enters DeletionBlocked; not force-deleted; stays in `Pending`. Final deletion: a single store transaction appends the `Deleted` revision and removes the resource row and its indexes; subsequent `Get` returns not-found; `ResourceDelete` audit event is appended afterward with dedup/exactly-once recovery. Zone status: `Pending` during activation; `Degraded` while PendingDeletion outstanding; `Ready` when clean. Audit: `ResourceDelete` event includes `configurationGeneration` field.", + "workItemId" : "ADR046-nix-020" + }, + { + "currentSource" : "`packages/d2b-contract-tests/tests/` (existing workspace policy lints: `tests/workspace-member-sort.rs`, `tests/crate-naming.rs`); no current Provider crate layout gate exists", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-nix-001; `d2b-contracts` workspace policy tests", + "destination" : "`packages/d2b-contract-tests/tests/provider-crate-layout.rs`; workspace scan in `packages/xtask/src/main.rs` (extend `check-workspace` or add `check-provider-layout` subcommand)", + "detailedDesign" : "Parse the root `packages/Cargo.toml` workspace member list; for every member path matching `packages/d2b-provider-*-*`: assert (1) `src/` directory exists and contains at least one `.rs` file; (2) `tests/` directory exists and contains at least one `.rs` file; (3) `integration/` directory exists and contains at least one `.rs` or fixture file; (4) `README.md` exists and is ≥ 200 bytes. All four conditions required; any single failure fails the test with a structured message naming the crate and missing path. Test runs as `cargo test -p d2b-contract-tests provider_crate_layout`; wired into `make test-policy`.", + "integration" : "Wired into `make test-policy` (same gate family as existing workspace policy tests); no new `Makefile` target needed unless `test-policy` does not yet exist", + "removalProof" : "Not removed; extended as new Provider crates are added to the workspace", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Fixture: add a stub `packages/d2b-provider-test-missing-integration/` with `src/lib.rs` and `tests/smoke.rs` but no `integration/` and no `README.md`; assert test fails naming both missing paths. Add complete stub with all four paths; assert test passes.", + "workItemId" : "ADR046-nix-021" + }, + { + "currentSource" : "No current equivalent for a separate artifact catalog. Current `nixos-modules/bundle.nix` embeds package derivation references inline. Current `nixos-modules/closures-json.nix` uses `pkgs.closureInfo` keyed by `d2b.vms.`.", + "dataMigration" : "Artifact catalog entries are generated from new `d2b.artifacts` declarations; full d2b 3.0 reset; no v2 `closureRef`/`nixosSystem` metadata import.", + "dependencyOwner" : "ADR046-nix-005 (bundle derivation); `d2b-contracts` schema generation (ADR046-nix-027)", + "destination" : "`nixos-modules/artifact-catalog.nix` (new emitter); `nixos-modules/options-artifacts.nix` (new option: `d2b.artifacts. = { package; type; }`); `/etc/d2b/artifact-catalog.json` (output artifact, `root:d2bd` 0640); `nixos-modules/bundle-zones.nix` (extend to include artifact catalog digest in `bundle.json`); `nixos-modules/options-zones-resources.nix` (replace `closureRef` / `nixosSystem` helpers with `systemArtifactId` validation)", + "detailedDesign" : "`d2b.artifacts.` attrset option: `id` matches `^[a-z][a-z0-9-]*$`; `type ∈ { \"provider\", \"nixos-system\", \"nixos-module-set\", \"config-bundle\" }`; no other fields. Emitter computes `pkgs.closureInfo` for each entry and writes `artifact-catalog.json` with sorted entries (by `artifactId`) containing `artifactId`, `type`, `storePath` (private, for activation-helper staging), `packageDigest`, `closureDigest`, `closureSize`. `storePath` is a private field of the root:d2bd 0640 file; it is never emitted in public ResourceSpecs, status fields, audit records, or OTEL telemetry. The `bundle.json` manifest includes the artifact catalog file entry and its SHA256 digest. `d2b-activation-helper` reads `storePath` from the catalog to resolve and stage each artifact; verifies catalog digest before staging. Build-time validation: `artifactId` / `systemArtifactId` / `source.systemArtifactId` fields in resource specs resolve against `d2b.artifacts`; type-mismatch fails with a structured error. `d2b.providerCatalog..package` option is removed; replaced by `d2b.providerCatalog..artifactId`. `Guest.spec.systemArtifactId` replaces the former `nixosSystem` Nix-only helper. `Volume.source.systemArtifactId` replaces `source.closureRef`.", + "integration" : "`nixos-modules/closures-json.nix` rewritten (ADR046-nix-012) to key by artifact ID; `nixos-modules/bundle-zones.nix` includes artifact catalog in integrity chain", + "removalProof" : "Not removed; extended as new artifact types are added", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Artifact catalog round-trip: declare artifact, build, verify JSON entry present with correct type/digests/storePath; missing artifact ID fails build with structured error; wrong-type artifact fails build; `storePath` absent from all public ResourceSpecs and status/audit/OTEL surfaces", + "workItemId" : "ADR046-nix-022" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-session/` at `a1cc0b2d`, including session engine, handshake, lifecycle, scheduler, record protection, fragmentation, transport, driver, server, and metrics modules.", + "dataMigration" : "Full d2b 3.0 reset; no v2 session runtime state/config import", + "dependencyOwner" : "ADR-046-componentsession-and-bus spec; ADR046-nix-027 (wire contract) must land first", + "destination" : "`packages/d2b-bus/src/session/` (new crate `d2b-bus`); `ComponentSessionDriver` becomes the central abstraction for all Zone bus sessions.", + "detailedDesign" : "Complete session protocol: preface negotiation, Noise handshake profiles, record protection, replay cache, fair scheduler, fragmentation/reassembly, keepalive, ttrpc bridge, generation discovery, and `ComponentSessionDriver` application surface. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; adapt ADR45 EndpointPurpose/ServicePackage names listed in `ADR45 exclusions` while preserving wire tag values..", + "integration" : "`d2b-bus` session runtime is used by Zone bus sessions for local-root controller to broker, controller to guest agent, and controller to provider agent; `serve_ttrpc_services` exposes service handlers.", + "removalProof" : "ADR45 EndpointPurpose/ServicePackage realm names listed in `ADR45 exclusions` are adapted during copy; no prior v3 session owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port `component_session.rs` and `noise_vectors.rs`; run `make test-rust`; Noise KAT vectors must pass after copy.", + "workItemId" : "ADR046-nix-023" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-session/src/attachment.rs` and `packages/d2b-session/src/streams.rs` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 attachment or stream state/config import", + "dependencyOwner" : "ADR046-nix-023; ADR046-nix-027", + "destination" : "`packages/d2b-bus/src/session/` (same crate as ADR046-nix-023).", + "detailedDesign" : "Attachment ownership validates descriptors only after authenticated decrypt; unconsumed payloads close on drop. NamedStreamMux preserves half-close, reset, per-stream limits, aggregate limits, and credit-based flow control. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; keep attachment/stream semantics and descriptor validation, with no ADR45 realm adaptation required for this item..", + "integration" : "`d2b-bus` session transport validates attachments after authenticated decrypt and exposes named streams to ttrpc/services; Unix/vsock transports provide payloads.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port `component_session.rs` attachment ownership, stream mux, and credit-accounting tests.", + "workItemId" : "ADR046-nix-024" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-session/src/cancellation.rs`, `deadline.rs`, and `bootstrap.rs` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 cancellation/deadline/bootstrap state import", + "dependencyOwner" : "ADR046-nix-023; ADR046-nix-027", + "destination" : "`packages/d2b-bus/src/session/`.", + "detailedDesign" : "Per-generation `RequestRegistry`, request-wide cancellation, `DeadlineBudget::admit_metadata()` as the inbound metadata gate, and single-use `BootstrapAdmission::consume()` replay protection. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; adapt `CancelRequest`/`CancelResponse` module paths if v3 services move the common proto types..", + "integration" : "`RequestRegistry`, `DeadlineBudget`, and `BootstrapAdmission` feed all `d2b-bus` inbound ttrpc calls and resource API cancellation paths.", + "removalProof" : "ADR45 common proto type paths are adapted to the v3 service module; no prior v3 owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port cancel-before-dispatch, cancel-after-dispatch, generation-mismatch cancel, and deadline admit tests; add v3 common-proto path adapter test.", + "workItemId" : "ADR046-nix-025" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-session-unix/src/{adapter,credit,descriptor,pidfd,socket,systemd,vsock}.rs` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 Unix transport state/config import", + "dependencyOwner" : "ADR046-nix-023", + "destination" : "`packages/d2b-bus/src/transport/unix/`.", + "detailedDesign" : "Unix seqpacket/stream transports, pidfd identity verification, six-scope credit pool with host/process bounds, systemd-activated seqpacket listener support, and length-prefixed vsock framing. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse from main commit `a1cc0b2d`; retain host-socket/native-vsock feature gates and replace only ADR45 socket-name strings with Zone bootstrap config..", + "integration" : "`d2b-bus` listeners and Provider/guest sessions instantiate UnixSeqpacket/UnixStream/vsock transports; credit pool enforces attachment FD budgets before handing payloads to session runtime.", + "removalProof" : "ADR45 socket-name strings listed in exclusions are replaced by Zone bootstrap config; no prior v3 transport owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port `unix_session.rs` seqpacket, stream, pidfd, FD attachment, and credit pool tests; add Zone bootstrap socket-name fixture.", + "workItemId" : "ADR046-nix-026" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-contracts/src/v2_component_session.rs` at `a1cc0b2d` plus committed session/noise tests.", + "dataMigration" : "Full d2b 3.0 reset; no v2 ComponentSession contract state import", + "dependencyOwner" : "ADR-046-componentsession-and-bus; naming and wire enumeration decisions per ADR-046-componentsession-and-bus owning spec before final adoption", + "destination" : "`packages/d2b-contracts/src/v3/component_session.rs`.", + "detailedDesign" : "Canonical wire values, bounded vectors, binary size constants, closed-enum tag codecs, and fail-closed validation for the ComponentSession protocol. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse wire constants/types from main commit `a1cc0b2d`; rename ADR45 realm-oriented variants only as directed by the bus owning spec and preserve numeric tags..", + "integration" : "`d2b-contracts` v3 component_session module is consumed by `d2b-bus`, `d2b-session`, `d2b-session-unix`, `d2b-client`, and provider toolkit for shared wire constants/enums.", + "removalProof" : "ADR45 realm-named enum variants are renamed only if the owning spec requires while preserving wire tags; no prior v3 contract owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port component session and Noise vector tests; add wire-tag stability gates for renamed role/service/purpose variants.", + "workItemId" : "ADR046-nix-027" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-contracts/src/generated_v2_services/`, `v2_guest_services.rs`, and `v2_component_session.rs` service inventory at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 generated service state import", + "dependencyOwner" : "ADR046-nix-027; Zone service naming per ADR-046-componentsession-and-bus owning spec", + "destination" : "`packages/d2b-contracts/src/v3/services/`.", + "detailedDesign" : "Versioned ttrpc service modules preserve server dispatch tables, shared request/cancel/outcome/error types, and service fingerprint inventory. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse generated ttrpc service tables from main commit `a1cc0b2d`; adapt ADR45 Realm/Workload fields and service names to v3 Zone/Resource naming per owning bus spec..", + "integration" : "Generated v3 services feed `d2b-bus` server/client dispatch, provider-toolkit conformance, and `SERVICE_INVENTORY` fingerprint validation.", + "removalProof" : "ADR45 realm/user WorkloadId fields listed in exclusions are adapted to Zone/ResourceName; `generated_v2_services` remains only as reuse source.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port provider-toolkit conformance and service fingerprint assertions; add v3 service-name gate for Zone service and ResourceName field adaptation.", + "workItemId" : "ADR046-nix-028" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-provider/src/{registry,rpc,instance,context}.rs` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 ProviderRegistry runtime state import", + "dependencyOwner" : "ADR046-nix-027; ADR046-nix-028; EndpointRole naming per ADR-046-componentsession-and-bus owning spec", + "destination" : "`packages/d2b-provider/src/` (adapt in place).", + "detailedDesign" : "ProviderRegistry snapshot admission/drain, bounded in-flight caps, typed RpcProviderProxy dispatch, and build-time RegistryLimits validation. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse ProviderRegistry/RPC runtime from main commit `a1cc0b2d`; adapt `SessionIdentity` to Zone naming and update EndpointRole/ServicePackage names per owning bus spec..", + "integration" : "Zone controller publishes active ProviderRegistry snapshots; provider-toolkit RPC proxy admits sessions through `d2b-bus` and dispatches typed Provider operations.", + "removalProof" : "ADR45 role/service names are adapted per owning bus spec; no prior v3 ProviderRegistry owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port `d2b-provider` `runtime.rs` tests; add v3 AdmissionOptions peer-role and SessionIdentity zone-name gates.", + "workItemId" : "ADR046-nix-029" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-provider-toolkit/src/{adapter,server,conformance,registration}.rs` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 provider-toolkit runtime state import", + "dependencyOwner" : "ADR046-nix-029; ADR046-nix-028; EndpointRole and ServicePackage naming per ADR-046-componentsession-and-bus owning spec", + "destination" : "`packages/d2b-provider-toolkit/src/` (adapt in place).", + "detailedDesign" : "ProviderAgentAdapter validates ComponentSession identity, GeneratedProviderServiceServer dispatches all ProviderType method families with bounded stores/in-flight state, and conformance kit gates every Provider implementation. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse provider toolkit from main commit `a1cc0b2d`; adapt endpoint role/service variant names and v2_identity imports while preserving wire tags..", + "integration" : "Provider processes embed `GeneratedProviderServiceServer`; `ProviderAgentAdapter` validates ComponentSession identity before invoking registry instances; conformance kit gates every Provider crate.", + "removalProof" : "ADR45 `v2_identity` imports are adapted to ZoneId/ResourceName; no prior v3 toolkit owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port `conformance.rs`; assert endpoint role/service wire tags remain 7/4 after any variant rename.", + "workItemId" : "ADR046-nix-030" + }, + { + "currentSource" : "None — D096/D098 semantic Service/Binding authoring and projection factories are net-new in v3", + "dataMigration" : "Full d2b 3.0 reset; old audio/USB/security-key/observability options lower to new authority Service plus authored Binding only where the owning dossier explicitly preserves migration, and no old public type alias survives", + "dependencyOwner" : "ADR046-provider-004, ADR046-zone-control-019, ADR046-zone-control-020, ADR046-zone-control-024; Nix resource compiler owner", + "destination" : "`nixos-modules/resources-sharing.nix`; `nixos-modules/assertions.nix`; `tests/unit/nix/cases/resource-sharing.nix`", + "detailedDesign" : "Compile the eight exact D098 semantic Service/Binding types plus `ResourceExport`/`ResourceImport` without a second vocabulary. Nix authors authority Services and consumer Bindings, never projection Services; Binding requires same-Zone `serviceRef` and allowed target, while all observed realization remains absent from spec. Core receives a stable projection name/factory fingerprint and creates exactly one same-type Service with ResourceImport ownership, `providerRef`, semantic base/import fields, and no `spec.provider`. Reject Device/Endpoint/Binding export/projection, implementation-qualified/former `*State` aliases, implementation detail in base schemas, cross-Zone refs, USB export without every policy opt-in, and any USB/security-key physical resource that does not use the exact Core-derived `physical-usb-backing` tuple class.", + "integration" : "The per-Zone generation uses ADR046-provider-004 common schema fingerprints and ADR046-zone-control-019/020 factory metadata; the four initial Provider modules contribute only strict Provider extensions and implementation config.", + "removalProof" : "Provider-local duplicate Nix base schemas and old direct Device/Endpoint/`*State` projection emitters are removed after the shared emitter and all four dossier migration tests pass.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Fast Nix eval/build tests cover all four exact pairs, common base schema discovery, canonical minimal base without `spec.provider`, stable lowering across repeated evaluations, same-Zone refs/targets, strict Provider extension placement, Service-only export, exactly-one Core projection metadata with no `spec.provider`, no auto-Binding, status-field rejection in spec, USB/security-key resolution to one byte-identical Host-global `(Host, physical-usb-backing, opaqueKeyDigest)` collision before effects, Provider-private-class bypass rejection, and rejection of all forbidden names/aliases.", + "workItemId" : "ADR046-nix-031" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-client/src/{client,session,target,service,daemon_service,guest_service,host_socket}.rs` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 client state/config import", + "dependencyOwner" : "ADR046-nix-029; ADR046-nix-027; TargetInput v3 shape per ADR-046-componentsession-and-bus owning spec", + "destination" : "`packages/d2b-client/src/` (adapt in place).", + "detailedDesign" : "Transport-neutral typed async client, ComponentSessionConnector abstraction, precise SessionFailure classification, signed metadata envelopes, named-stream client abstraction, and host-socket connector. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse async client from main commit `a1cc0b2d`; adapt TargetInput, ServiceOwner, service packages, and local socket identity to Zone/ResourceRef semantics..", + "integration" : "CLI/controllers construct TargetInput/MetadataInput; `d2b-client` resolves Zone/Resource routes via `d2b-bus` and reuses ComponentSessionConnector for calls/streams.", + "removalProof" : "ADR45 TargetInput Realm/Workload forms are replaced by Zone/ResourceRef forms; no prior v3 client owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port `d2b-client` tests; add ResourceRef TargetInput and Zone bootstrap socket identity gates.", + "workItemId" : "ADR046-nix-032" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-realm-router/src/service_v2.rs` and tests at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 routing state import", + "dependencyOwner" : "ADR046-nix-023; ADR046-nix-027; naming and service name per ADR-046-componentsession-and-bus owning spec", + "destination" : "`packages/d2b-bus/src/routing/zone_service.rs`.", + "detailedDesign" : "ZoneSessionAuthority preserves no-host-credential custody, gateway/ZoneLink custody behavior, bounded concurrent dispatch, and graceful shutdown semantics. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse routing service behavior from main commit `a1cc0b2d`; rename RealmSessionAuthority/service identifiers to Zone terms per owning bus spec while preserving custody behavior..", + "integration" : "`d2b-bus` Zone service handles route admission and shortcut lifecycle; ZoneSessionAuthority feeds session/audit/metric labels with stable custody state.", + "removalProof" : "ADR45 RealmService name and RealmId fields are adapted to Zone service; no prior v3 router owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port `realm_service_v2.rs` and topology harness tests with Zone naming; add service-name wire gate.", + "workItemId" : "ADR046-nix-033" + }, + { + "currentSource" : "Main commit source row below: `packages/d2bd/src/provider_registry.rs` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; provider bundle format bumps to v3 and no v2 provider registry state import.", + "dependencyOwner" : "ADR046-nix-029; ADR046-nix-028; ADR046-nix-027; PROVIDER_BUNDLE_VERSION bump required on any bundle artifact format change", + "destination" : "`packages/d2bd/src/provider_registry.rs` (adapt in place).", + "detailedDesign" : "Named ProviderCompositionError variants, first-party factory instantiation, bundle resolver validation, version/schema constants, and monotone lifecycle operation IDs. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse fail-closed provider composition from main commit `a1cc0b2d`; adapt v2 identity/bundle artifact types to v3 provider registry contracts and bump bundle versions..", + "integration" : "`provider_registry.rs` composes the v3 provider catalog into active ProviderRegistry snapshots consumed by `d2b-bus`/provider runtime and bundle validation.", + "removalProof" : "ADR45 `provider_registry_v2` bundle artifact types are replaced by `d2b_contracts::v3::provider_registry` after v3 bundle schema lands.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port provider_registry composition tests; add PROVIDER_BUNDLE_VERSION/SCHEMA_VERSION v3 bump gate and ZoneId/ResourceName binding test.", + "workItemId" : "ADR046-nix-034" + }, + { + "currentSource" : "Main commit source row below: `packages/d2bd/src/provider_effects.rs` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 lifecycle mutation state import", + "dependencyOwner" : "ADR046-nix-034; ADR046-nix-029; GuestLifecycleRequest ResourceRef addressing per ADR-046-componentsession-and-bus owning spec", + "destination" : "`packages/d2bd/src/provider_effects.rs` (adapt in place).", + "detailedDesign" : "DaemonEffectAdapters bind semantic effect ports; ProviderLifecycleDispatch bounds lifecycle mutations and routes broker start/stop through blocking adapters with idempotency-key deduplication. Primary reuse disposition: `adapt`. Preserved source-plan detail: Reuse descriptor-bound effect dispatch from main commit `a1cc0b2d`; adapt lifecycle request wire type from VM name to Zone-scoped Guest ResourceRef..", + "integration" : "`ProviderLifecycleDispatch` receives v3 Guest lifecycle operations from `d2b-bus`/core controller and routes broker start/stop through descriptor-bound effect ports.", + "removalProof" : "`VmLifecycleRequest`/`vm_name` path is replaced by a v3 ResourceRef Guest lifecycle op after broker/public wire contract versioning.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-nix-configuration", + "specPath" : "docs/specs/ADR-046-nix-configuration.md", + "validation" : "Port `provider_effects.rs` lifecycle dispatch tests; add ResourceRef Guest lifecycle request and BrokerCallerRole compatibility gates.", + "workItemId" : "ADR046-nix-035" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent for a provider-neutral `NetworkEffectPort` core adapter.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Core; owns `NetworkEffectPort` contract/versioning in `d2b-contracts` and adapter implementation in `d2b-core`.", + "destination" : "`d2b-contracts` trait plus `d2b-core` core adapter; maps to broker wire operations and audit emission.", + "detailedDesign" : "Implement `NetworkEffectPort` core adapter in `d2b-core`; map to broker wire ops; emit audit records. `revoke_attachment_tap` accepts only an opaque `AttachmentHandle` plus `AttachmentGenerationFence { expected_network_generation, expected_attachment_generation }` and maps to `DeletePersistentTap`; no IfName/path or caller-authored marker crosses the trait. Versioning: minor releases may add methods with default impls; major releases require Provider upgrade. The trait lives in `d2b-contracts`; the adapter in `d2b-core`.", + "integration" : "`Provider/network-local` reconcile calls injected `NetworkEffectPort`; the core adapter resolves opaque Network intents to closed broker wire ops and emits broker-level audit records.", + "removalProof" : "None — net-new; no prior owner to remove.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`packages/d2b-provider-network-local/tests/fault_injection.rs` verifies fake `NetworkEffectPort` behavior, error mapping, no broker socket in provider context, and audit-safe adapter boundaries.", + "workItemId" : "ADR046-nl-001" + }, + { + "currentSource" : "Existing broker wire has related ApplyNftables, ApplyRoute, ApplySysctl, ApplyNmUnmanaged, UpdateHostsFile, SeedDnsmasqLease, and `CreatePersistentTap` operations, but no paired `DeletePersistentTap`, `CreateBridge`, `DeleteBridge`, `ReadNftablesDigest`, `ReadSysctlState`, or `ReadBridgePortFlags` v3 ops.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Core; broker/core contract work consumed by ADR046-nl-001.", + "destination" : "Broker wire contract and broker/core adapter operation table for `DeletePersistentTap`, `CreateBridge`, `DeleteBridge`, `ReadNftablesDigest`, `ReadSysctlState`, and `ReadBridgePortFlags`.", + "detailedDesign" : "Add canonical closed `DeletePersistentTap` paired with `CreatePersistentTap`, plus `CreateBridge`, `DeleteBridge`, `ReadNftablesDigest`, `ReadSysctlState`, and `ReadBridgePortFlags`. `DeletePersistentTapRequest` contains only an opaque attachment ID and expected Network/attachment generations. The broker resolves trusted realization state, validates generations and ownership marker, treats validated absence as success, rejects foreign markers without deletion, and emits path-free post-effect audit. No request accepts an IfName or path. Primary reuse disposition: `adapt`. Preserved source-plan detail: extend broker wire with net-new operations and reuse existing closed broker-operation dispatch shape.", + "integration" : "`NetworkEffectPort` core adapter invokes these broker ops for attachment/fabric lifecycle and observe/drift checks; `Provider/network-local` receives only typed results and opaque digests/handles. Attachment removal and Network finalization retain the handle until `DeletePersistentTap` confirms deletion or validated absence.", + "removalProof" : "None — net-new broker ops; remove only if no Provider consumes them per the removal checklist.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "Broker tests cover `DeletePersistentTap` success, validated already-absent idempotency, stale Network/attachment generations, foreign-marker fail-closed behavior, path-free audit, and rejection of any IfName/path field; `integration/host_fabric.rs` covers persistent-tap deletion, bridge create/delete, nftables apply/digest, IPv6 suppression, NetworkManager unmanaged handling, and real `NetworkEffectPort` implementation.", + "workItemId" : "ADR046-nl-002" + }, + { + "currentSource" : "None — net-new v3 work; no public pre-ADR45 baseline equivalent for opaque `AttachmentHandle` or `FabricHandle`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Core; handle DTOs are owned by `d2b-contracts` and consumed by `d2b-core` plus `Provider/network-local`.", + "destination" : "`d2b-contracts` opaque byte-array newtypes; core-held HMAC key and provider-facing redacted handle types.", + "detailedDesign" : "Implement `AttachmentHandle` and `FabricHandle` as opaque byte-array newtypes (32 bytes of HMAC-SHA-256 over internal identity material; key held by core). Each attachment handle identifies one generation-fenced realization and is retained until explicit `DeletePersistentTap` confirmation during attachment removal or Network finalization. These types are declared in `d2b-contracts`, not in the provider crate.", + "integration" : "Core creates handles from Network and attachment identity, stores them only in internal state/status-resource attachment realization, and supplies resolved tap FDs through LaunchTicket without exposing IfNames or MACs.", + "removalProof" : "None — net-new; no prior owner to remove.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`tests/fault_injection.rs` and `tests/controller_state.rs` cover opaque-handle mismatch, generation-fenced `DeletePersistentTap`, retained handle until confirmation, and no raw IfName/IP/MAC/path public surface.", + "workItemId" : "ADR046-nl-003" + }, + { + "currentSource" : "Existing v1 runtime tap handling is broker/runtime-specific; no v3 LaunchTicket owner-graph FD resolution surface exists.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Core; depends on ADR046-nl-003 handles and runtime-cloud-hypervisor LaunchTicket consumption.", + "destination" : "Core LaunchTicket builder and dependency resolver that walks `Guest.ownerRef: Network/` to resolved tap FDs.", + "detailedDesign" : "Implement LaunchTicket FD resolution: when core builds the LaunchTicket for a Guest with `ownerRef: Network/`, it walks the owner graph, locates the Network, reads its internally-held `AttachmentHandle` set, and includes the corresponding tap FDs in the ticket. No API surface for the provider or runtime is required beyond the existing LaunchTicket mechanism. Primary reuse disposition: `create`. Preserved source-plan detail: net-new LaunchTicket integration; reuse existing LaunchTicket mechanism without adding provider/runtime API surface.", + "integration" : "Runtime-cloud-hypervisor starts Guests using tap FDs supplied in LaunchTickets; `Provider/network-local` declares attachments and core resolves handles privately.", + "removalProof" : "None — net-new; no prior owner to remove.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`integration/guest_lifecycle.rs` validates net-VM/workload Guest lifecycle, opaque attachment handle resolution, and `systemArtifactId` binding.", + "workItemId" : "ADR046-nl-004" + }, + { + "currentSource" : "Reuse modules listed in §25.1: `packages/d2b-host/src/ifname.rs:derive_ifname`, `packages/d2b-host/src/nftables.rs`, `packages/d2b-host/src/bridge_port.rs`, `packages/d2b-host/src/routes.rs`, and `packages/d2b-host/src/netlink.rs`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Provider plus Core; provider validates through `d2b_host::ifname::derive_ifname`, while core adapter consumes host networking modules.", + "destination" : "Core adapter imports `d2b-host` modules; `packages/d2b-provider-network-local/src/ifname.rs` re-exports `d2b_host::ifname::derive_ifname` only.", + "detailedDesign" : "The `d2b-host` IfName/nftables/bridge/route modules are consumed directly by the core adapter (not by the provider crate). The provider crate re-exports only `d2b_host::ifname::derive_ifname` for validation purposes. No additional extraction work is required beyond confirming the `d2b-host` API surface is stable. Primary reuse disposition: `adapt`. Preserved source-plan detail: reuse directly in core adapter; provider re-exports only `derive_ifname` for validation.", + "integration" : "Provider validateSpec uses deterministic IfName derivation for collision checks; core adapter applies bridge, nftables, route, and sysctl effects through reused `d2b-host` helpers.", + "removalProof" : "None — reused modules remain owned by `d2b-host`; no prior provider-local copy to remove.", + "reuseAction" : "adapt", + "reuseSource" : "`d2b-host` IfName, nftables, bridge-port, route preflight, and sysctl/netlink modules.", + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`tests/ifname_derive.rs`, `tests/fault_injection.rs`, and `integration/host_fabric.rs` prove derivation, adapter reuse, and real host-fabric behavior.", + "workItemId" : "ADR046-nl-005" + }, + { + "currentSource" : "None — net-new v3 provider controller; v1 behavior lived in `nixos-modules/network.nix` and `nixos-modules/net.nix` static NixOS module logic.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Provider; depends on ADR046-nl-001 through ADR046-nl-005 and owns the Network reconcile/observe/finalize handlers.", + "destination" : "`packages/d2b-provider-network-local/src/{controller.rs,metrics.rs}`.", + "detailedDesign" : "Implement `controller.rs` reconcile/observe/finalize handlers with `NetworkEffectPort` injection and the §21 metric descriptors with closed semantic labels. Attachment removal and finalization wait for Guest/VMM FD ownership to close, then call `revoke_attachment_tap` with the retained opaque handle and current Network/attachment generation fence; transient deletion retries retain the handle, stale generations refresh/requeue, and ownership conflict blocks finalizer clearing. No descriptor may carry `vm`, `zone`, `zone_id`, `zone_uid`, `network`, or another resource-name-derived key; Network/Zone identity stays only in OTEL resource attributes and permitted audit fields. Primary reuse disposition: `adapt`. Preserved source-plan detail: port semantics into provider reconcile state machine; do not reuse static per-env systemd/Nix ownership.", + "integration" : "Controller watches Network, Guest, Volume, Process, User, Host, and Zone resources; creates child resources, writes status, invokes `NetworkEffectPort`, and drives finalizers.", + "removalProof" : "Supersedes static per-env lifecycle in `nixos-modules/network.nix` and `nixos-modules/net.nix`; removal proof is successor controller coverage plus deletion of duplicate old gates when this provider lands.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`tests/controller_state.rs` covers normal reconcile, errors, finalizer ordering, adoption on restart, and observe/drift cycles with deterministic clock; `tests/metrics_labels.rs` structurally asserts exact identity-key absence and that a Network-name canary never enters metric label values.", + "workItemId" : "ADR046-nl-006" + }, + { + "currentSource" : "None — net-new v3 NetworkAgentService; v1 net-VM behavior was encoded in NixOS services and scripts under `nixos-modules/net.nix`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Provider; owns net-agent ComponentSession service and depends on ComponentSession/bus and net-VM Process lifecycle.", + "destination" : "`packages/d2b-provider-network-local/src/process_specs.rs` agent template plus agent service implementation in the net-VM artifact.", + "detailedDesign" : "Implement `NetworkAgentService` Noise-KK vsock ComponentSession (Reload + ReadinessQuery methods). Agent reconnect policy: if the controller cannot reach the agent vsock (Guest restart in progress), it retries with exponential backoff up to `drainTimeout` of the agent Process; after timeout it deletes and re-creates the agent Process resource. Primary reuse disposition: `create`. Preserved source-plan detail: net-new service; preserve semantic nftables/routes reload behavior from v1 net VM configuration.", + "integration" : "Controller writes config Volume content, resolves `Endpoint/net--agent-service`, calls `Reload(config_digest)`, and uses readiness predicates to set Network conditions.", + "removalProof" : "None — net-new ComponentSession service; no prior service endpoint to remove.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`integration/agent_reload.rs` validates Reload, `nft_applied` and `routes_applied` predicates, reconnect behavior, and config digest matching.", + "workItemId" : "ADR046-nl-007" + }, + { + "currentSource" : "Reuse semantics from `nixos-modules/net.nix` lines 168–296 for nftables and lines 302–441 for dnsmasq; runtime volume model is net-new.", + "dataMigration" : "Full d2b 3.0 reset; config Volume is runtime tmpfs content regenerated from Network spec.", + "dependencyOwner" : "Provider; config rendering owned by `Provider/network-local`, storage reconciliation owned by `Provider/volume-local`.", + "destination" : "`packages/d2b-provider-network-local/src/config_volume.rs`.", + "detailedDesign" : "Implement config Volume content rendering (dnsmasq.conf, nftables.rules, routing.conf, attachments.json). Primary reuse disposition: `adapt`. Preserved source-plan detail: port and render into bounded config Volume files.", + "integration" : "Controller creates `Volume/net--config`, writes four files through the Volume service, attaches the read-only view to the net VM, and triggers agent reload plus dnsmasq restart.", + "removalProof" : "Supersedes static per-env config generation in `nixos-modules/net.nix`; successor coverage retires duplicate old Nix/service assertions.", + "reuseAction" : "adapt", + "reuseSource" : "`nixos-modules/net.nix` dnsmasq, nftables, routing, and attachment configuration semantics.", + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`tests/controller_state.rs`, `integration/agent_reload.rs`, and `integration/delete_sequence.rs` validate rendering, write flow, reload, and cleanup ordering.", + "workItemId" : "ADR046-nl-008" + }, + { + "currentSource" : "v1 dnsmasq and mDNS process shape came from `nixos-modules/net.nix` and static NixOS services; no v3 Process builder exists.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Provider; Process resource builders owned by `d2b-provider-network-local`.", + "destination" : "`packages/d2b-provider-network-local/src/process_specs.rs`.", + "detailedDesign" : "Implement canonical Process spec builders for agent, dnsmasq, mdns-reflector, mdns-dnsbridge. Primary reuse disposition: `adapt`. Preserved source-plan detail: port service semantics into canonical Process resource specs.", + "integration" : "Controller creates agent service, dnsmasq worker, and optional mDNS workers as owned Process resources on the net VM; Process Provider reports readiness and lifecycle status.", + "removalProof" : "Supersedes static per-env systemd services; old duplicate service tests are retired after successor Process coverage passes.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`tests/controller_state.rs`, `integration/mdns_reflector.rs`, and eval case `process-sandbox-netns.nix` validate Process shape, optional mDNS, and guest-netns capability isolation.", + "workItemId" : "ADR046-nl-009" + }, + { + "currentSource" : "Reuse lib.mkForce NIC bootstrap from `nixos-modules/base.nix` `10-eth-dhcp` override and account reservation documented in `nixos-modules/host-users.nix`; v1 per-Network config does not carry forward.", + "dataMigration" : "Full d2b 3.0 reset; no per-Network v2 net-VM config import.", + "dependencyOwner" : "net-vm artifact; owns generic `net-vm-base` nixos-system artifact and shared net-local-controller UID/GID reservation.", + "destination" : "`net-vm-base` nixos-system artifact and artifact catalog entry `d2b.artifacts.net-vm-base`.", + "detailedDesign" : "Build generic `net-vm-base` nixos-system artifact with net-agent binary, agent-service endpoint, guest-agent binary, standard NIC bootstrap, lib.mkForce override; bake `net-local-controller` account with the UID/GID allocated from the host-users reservation table (documented in `nixos-modules/host-users.nix`). Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve generic boot/safety invariants; exclude per-Network static config.", + "integration" : "Network resource `spec.netVmSystemArtifactId` points to `net-vm-base`; runtime-cloud-hypervisor consumes the artifact ID, and the config Volume provides all per-Network DHCP/firewall/routing content.", + "removalProof" : "Supersedes implicit microvm/Nix path coupling; remove `net-vm-base` artifact catalog entry only after all Network resources and provider references are gone.", + "reuseAction" : "adapt", + "reuseSource" : "`nixos-modules/base.nix` `10-eth-dhcp` lib.mkForce override and host-users reservation table.", + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "Eval cases `net-vm-artifact-id-eval.nix` and `network-spec-eval.nix`, plus `tests/net-vm-network-eval.sh` for the lib.mkForce invariant.", + "workItemId" : "ADR046-nl-010" + }, + { + "currentSource" : "Existing NixOS modules `nixos-modules/network.nix`, `nixos-modules/net.nix`, and `nixos-modules/host-users.nix` provide v1 static declarations and user/account patterns.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Nix; owns resource declaration, User declaration, host account provisioning, and artifact catalog wiring.", + "destination" : "Nix module resource emission for `Provider/network-local`, `User/net-local-controller`, host OS account, `provider-network-local`, and `net-vm-base` artifacts.", + "detailedDesign" : "Nix module for `Provider/network-local` resource declaration; `User/net-local-controller` declaration; OS account provisioning; artifact catalog entries. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt Nix option/resource emission and account provisioning to v3 resources.", + "integration" : "Nix compiler emits Provider/User/Network resources and host prerequisites; core ProviderDeployment starts controller Process; system-core reconciles the User resource.", + "removalProof" : "Supersedes v1 NixOS module declarations; removal proof is deletion of old resource emission and account/artifact entries when provider is retired.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "Eval cases `network-spec-eval.nix`, `user-no-managed-by-eval.nix`, `net-vm-artifact-id-eval.nix`, and `make test-policy` for artifact/package paths.", + "workItemId" : "ADR046-nl-011" + }, + { + "currentSource" : "`nixos-modules/lib.nix:cidrOverlaps` lines 429–462 provides CIDR overlap logic in the v1 module layer.", + "dataMigration" : "None — docs/tooling only; no runtime state.", + "dependencyOwner" : "Nix; depends on Network resource schema and CIDR validation rules.", + "destination" : "Nix flake/resource schema checks for declared Networks and provider `validate.rs` parity.", + "detailedDesign" : "Build-time CIDR overlap check for declared Networks in flake check. Primary reuse disposition: `adapt`. Preserved source-plan detail: port/reuse overlap semantics in v3 eval checks and provider validation.", + "integration" : "Nix compiler rejects overlapping declared Network CIDRs before resource publication; runtime `validateSpec` re-checks full overlap matrix.", + "removalProof" : "None — validation net-new in v3 resource compiler; no prior owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : "`nixos-modules/lib.nix:cidrOverlaps`.", + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "Eval case `network-cidr-overlap-eval.nix` and `tests/cidr_overlap.rs` cover same-Network, cross-Network, external CIDR, and adjacency cases.", + "workItemId" : "ADR046-nl-012" + }, + { + "currentSource" : "Reusable semantic assertions come from §25.1 IfName/CIDR reuse inventory and Network schema defined in this spec.", + "dataMigration" : "None — docs/tooling only; no runtime state.", + "dependencyOwner" : "Tests; owned by `d2b-provider-network-local` hermetic test suite.", + "destination" : "`packages/d2b-provider-network-local/tests/schema_roundtrip.rs`, `tests/ifname_derive.rs`, and `tests/cidr_overlap.rs`.", + "detailedDesign" : "Conformance suite: NetworkSpec round-trip, IfName derivation, CIDR validation matrix. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing IfName/CIDR assertions into provider conformance tests.", + "integration" : "Test suite runs under `cargo test -p d2b-provider-network-local --lib --tests` and validates provider schema before integration gates.", + "removalProof" : "Retire replaced current-code tests only after successor hermetic tests cover the minimum reusable assertions and gate manifests/pins are updated.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "The listed conformance tests themselves are the validation, with workspace policy ensuring `tests/` exists.", + "workItemId" : "ADR046-nl-013" + }, + { + "currentSource" : "None — net-new v3 controller state-machine test; v1 shell/Nix gates are not a controller-reconcile equivalent.", + "dataMigration" : "None — docs/tooling only; no runtime state.", + "dependencyOwner" : "Tests; depends on ADR046-nl-006 controller and fake `NetworkEffectPort` from `d2b-contracts`.", + "destination" : "`packages/d2b-provider-network-local/tests/controller_state.rs`.", + "detailedDesign" : "Controller state-machine unit tests with fake `NetworkEffectPort` (from d2b-contracts mock) and deterministic clock, including attachment removal/finalizer calls to generation-fenced `DeletePersistentTap`.", + "integration" : "Hermetic fake effect port drives reconcile, observe, finalizer, and adoption transitions without real broker, systemd, container, or network dependencies.", + "removalProof" : "None — net-new; no prior owner to remove.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`tests/controller_state.rs` covers normal path, CIDR conflict, User not Ready, Volume error, Guest timeout, agent reload failure, finalizer sequence, `DeletePersistentTap` validated absence, transient retry with retained handle, stale-generation refresh, foreign-marker block, adoption, and drift.", + "workItemId" : "ADR046-nl-014" + }, + { + "currentSource" : "Existing Layer-1 eval and shell gates cover fragments; no v3 provider lifecycle integration test exists.", + "dataMigration" : "None — docs/tooling only; no runtime state.", + "dependencyOwner" : "Tests; integration coverage for the complete Network lifecycle.", + "destination" : "`packages/d2b-provider-network-local/integration/host_fabric.rs`, `guest_lifecycle.rs`, `agent_reload.rs`, and `delete_sequence.rs`.", + "detailedDesign" : "Integration tests: full Network lifecycle (create, config update, agent Reload, generation-fenced persistent-tap deletion, delete sequence) in container environment. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt reusable semantic assertions into v3 integration coverage.", + "integration" : "Integration tests exercise resource publication, host fabric effects, config Volume updates, ComponentSession reload, Process lifecycle, and finalizer cleanup through the provider stack.", + "removalProof" : "Old duplicate tests, shell gates, fixtures, static artifacts, CI jobs, manifests, and pins are deleted once successor coverage and removal proof pass.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`make test-integration` for container tests and `make test-host-integration` where guest lifecycle requires host/KVM coverage.", + "workItemId" : "ADR046-nl-015" + }, + { + "currentSource" : "The invariant is specified as INV-NET-008 and has eval coverage named `tests/unit/nix/cases/process-sandbox-netns.nix`.", + "dataMigration" : "None — docs/tooling only; no runtime state.", + "dependencyOwner" : "Security; depends on Process sandbox schema and `Provider/system-minijail` guest namespace inheritance.", + "destination" : "Process templates for agent and dnsmasq plus sandbox/eval tests.", + "detailedDesign" : "Verify INV-NET-008 (Guest-network-admin isolation): Process Provider correctly inherits Guest netns for agent/dnsmasq. Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve and verify existing guest-netns isolation invariant in v3 Process specs.", + "integration" : "`Provider/network-local` emits `namespaceClasses: []` and guest-only capability classes; Process Provider starts agent/dnsmasq inside the net VM network namespace only.", + "removalProof" : "None — security invariant preserved; no prior owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`tests/unit/nix/cases/process-sandbox-netns.nix` and provider Process-template tests assert no host capability or host network namespace grant.", + "workItemId" : "ADR046-nl-016" + }, + { + "currentSource" : "None — provider crate README is net-new for v3 packaging; this dossier supplies the required topics.", + "dataMigration" : "None — docs/tooling only; no runtime state.", + "dependencyOwner" : "Docs; owned by `d2b-provider-network-local` package documentation.", + "destination" : "`packages/d2b-provider-network-local/README.md`.", + "detailedDesign" : "`packages/d2b-provider-network-local/README.md` covering all 7 required topics. Primary reuse disposition: `create`. Preserved source-plan detail: net-new documentation.", + "integration" : "Workspace policy requires the README alongside `src/`, `tests/`, and `integration/`; operators and contributors use it for provider identity, build, test, integration, state, RBAC, and standalone-repo path.", + "removalProof" : "None — net-new documentation; no prior owner to remove.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`make test-policy` / `xtask workspace-policy` verifies required provider crate paths and README presence.", + "workItemId" : "ADR046-nl-017" + }, + { + "currentSource" : "Existing `UsbipBindFirewallRule` owns per-busid `inet d2b` exposure, while legacy `network.nix` and `net.nix` add broader TCP/3240 allows.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Broker plus device provider boundary; `UsbipBindFirewallRule` remains owned by `Provider/device-usbip`.", + "destination" : "Device-usbip EffectPort/adapter owns USBIP rules, drift, and strict provider status; network-local host/net-VM renderers and status cover only Network-owned policy.", + "detailedDesign" : "`UsbipBindFirewallRule` remains the sole broker mutation path for exact per-Network/per-busid TCP/3240 exposure. Network-local emits no TCP/3240 match, excludes device-usbip ownership markers from its digest, and never reports USBIP drift. Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve the typed closed broker op; remove both generic network-local USBIP allows.", + "integration" : "`Provider/device-usbip` watches only Network identity/readiness/generation; Core privately resolves the Network attachment for the one relay Endpoint authority and firewall op. Binding proxies receive authorized connected streams through LaunchTickets.", + "removalProof" : "Legacy generic `network.nix` and `net.nix` USBIP allow fragments and any golden expectation for them are removed after device-usbip host integration passes.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "Network-local host and net-VM firewall intent tests assert no TCP/3240/USBIP rule; USBIP rule churn leaves Network digest/`FirewallReady` unchanged; device-usbip tests own exact scoping, drift, status, and release.", + "workItemId" : "ADR046-nl-018" + }, + { + "currentSource" : "None — net-new v3 ProviderStateSet/status contract; v1 network module did not expose Provider state Volumes.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Provider; depends on D087 ProviderStateSet and status-first storage rules.", + "destination" : "Provider descriptor, controller-main deployment, `tests/state_schema_roundtrip.rs`, and eval case `provider-state-volume-eval.nix`.", + "detailedDesign" : "Confirm `controller-main` declares no stateNamespace and core ProviderDeployment creates no Provider state Volume or state mount; validate ProviderStateSet query returns empty for `Provider/network-local`; validate bounded operational state is written to revisioned/redacted status and the core Operation ledger with `status-oversize` conformance; confirm per-Network config Volumes remain `ownerRef: Network/` runtime/config operational Volumes outside the ProviderStateSet and `Volume` is not in `ResourceTypes implemented`. Primary reuse disposition: `create`. Preserved source-plan detail: net-new status-first provider-state conformance.", + "integration" : "Core ProviderDeployment starts controller without `/state`; controller uses Network status and Operation ledger for bounded observations; ProviderStateSet query excludes per-Network config Volumes.", + "removalProof" : "None — net-new; no Provider state Volume or prior owner to remove.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`tests/state_schema_roundtrip.rs` and `tests/unit/nix/cases/provider-state-volume-eval.nix` validate empty ProviderStateSet, status bounds/redaction, and config Volume exclusion.", + "workItemId" : "ADR046-nl-019" + }, + { + "currentSource" : "Current macvtap spawn resolves a raw parent interface but has no Host-global duplicate admission, sharing policy, authority status, or owner-proof lifecycle.", + "dataMigration" : "Full d2b 3.0 reset; no legacy authority import.", + "dependencyOwner" : "D097 Core authority index, Network contracts, runtime-cloud-hypervisor private attachment path", + "destination" : "Network schema/Provider descriptor, Core authority index, Network reconcile/update/finalizer, runtime LaunchTicket resolver, and authority tests", + "detailedDesign" : "Register the external physical-NIC `AuthorityDescriptor`: Host-global `external-physical-nic/v1` Core-derived identity, `zero-or-one` authority, exclusive `passthru`/`private`/`vepa`, exclusive-by-default `bridge`, explicitly compatible bounded multiplexing only for bridge, `external-physical-nic-conflict`, exact owner proof, drain-release-reacquire update, forbidden export, and bounded FIFO holder policy. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt the existing broker-internal macvtap-FD creation path behind mandatory Core authority admission.", + "integration" : "Core preflight gates every runtime LaunchTicket/`SpawnRunner`; status reports bounded authority state and conditions; D091 update and finalizer close macvtap ownership before release.", + "removalProof" : "The old direct macvtap spawn path is unreachable unless Core supplies an admitted authority claim in the LaunchTicket.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-network-local", + "specPath" : "docs/specs/providers/ADR-046-provider-network-local.md", + "validation" : "`external_nic_authority.rs` covers Core-derived identity, same-/cross-Zone conflicts, explicit bridge multiplexing, incompatible policy, non-bridge multiplex denial, no-effect rejection, adoption ambiguity, owner transfer, disruptive update, release ordering, and redaction; Nix eval and host integration cover declared configuration and lifecycle.", + "workItemId" : "ADR046-nl-020" + }, + { + "currentSource" : "`packages/d2b-notify/src/{events,state,notifications,nonce}.rs`", + "dataMigration" : "No v2 compatibility; reset", + "dependencyOwner" : "W0 shared contract root; session/bus owner", + "destination" : "`packages/d2b-provider-notification-desktop/src/{types,redact,action_nonce}.rs`", + "detailedDesign" : "`NotificationRequest`/`NotificationResult` DTOs and stream record types; bounded fields; closed category set; icon catalog contract; `ActionNonce`/`ActionNonceStore` adapted from main; no ResourceType DTO Primary reuse disposition: `adapt`. Preserved source-plan detail: copy/adapt.", + "integration" : "Zone bus service; host-sink stream consumer; guest-source stream producer", + "removalProof" : "v2 `d2b.notify.v2` generated stubs removed after v3 service established", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-notification-desktop", + "specPath" : "docs/specs/providers/ADR-046-provider-notification-desktop.md", + "validation" : "`tests/stream_record.rs` — DTO schema vectors; `tests/action_nonce.rs` — single-use/TTL/capacity/replay", + "workItemId" : "ADR046-notify-001" + }, + { + "currentSource" : "`packages/d2b-notify/src/services/`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-session-001, ADR046-bus-001; session/bus wiring", + "destination" : "`packages/d2b-provider-notification-desktop/src/stream_admission.rs`", + "detailedDesign" : "Session admission checks, Noise profile enforcement, transport class validation Primary reuse disposition: `adapt`. Preserved source-plan detail: copy/adapt.", + "integration" : "ComponentSession/d2b-bus", + "removalProof" : "Old `DesktopServices` session admitted under v2 contract removed when v3 session established", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-notification-desktop", + "specPath" : "docs/specs/providers/ADR-046-provider-notification-desktop.md", + "validation" : "`tests/stream_admission.rs` — all rejection vectors", + "workItemId" : "ADR046-notify-002" + }, + { + "currentSource" : "`packages/d2b-notify/src/services/observer.rs`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-notify-001, ADR046-reconcile-001; controller owner", + "destination" : "`packages/d2b-provider-notification-desktop/src/controller.rs`", + "detailedDesign" : "Async Process placement controller; watches `guestSources` Guest refs; creates/drains/deletes guest-source Processes; creates/stops host-sink Process on display-wayland readiness change; declares no Provider state Volume and does not own/add/create/delete Volumes; bounded non-secret operational state lives in `status`/the core Operation ledger (D087); notification delivery state (in-memory projection, action nonce store) is host-sink process memory only; no ResourceType reconcile loop", + "integration" : "Zone resource store (Process API); d2b-bus; display-wayland dependency watch", + "removalProof" : "Not applicable (new controller)", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-notification-desktop", + "specPath" : "docs/specs/providers/ADR-046-provider-notification-desktop.md", + "validation" : "Unit tests for placement FSM in `tests/stream_record.rs`; Volume creation/deletion lifecycle in `tests/volume_lifecycle.rs`; see also `integration/cross_zone_source.rs` end-to-end", + "workItemId" : "ADR046-notify-003" + }, + { + "currentSource" : "`packages/d2b-notify/src/services/actions.rs`, `packages/d2b-notify/src/bin/`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-notify-002, ADR046-notify-003; host-sink owner", + "destination" : "`packages/d2b-provider-notification-desktop/src/host_sink.rs`", + "detailedDesign" : "D-Bus client; `DesktopNotificationSink` stream consumer; action nonce issuance; `DesktopNotificationObserver` projection (in-memory, not persisted); display-wayland ComponentSession bootstrap for pre-opened D-Bus FD", + "integration" : "D-Bus session (pre-opened FD via ComponentSession bootstrap); ComponentSession named streams", + "removalProof" : "`nixos-modules/notifications.nix` state-dir tmpfiles rule retired; all notification state is in-memory per-session with no Volume replacement", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-notification-desktop", + "specPath" : "docs/specs/providers/ADR-046-provider-notification-desktop.md", + "validation" : "`integration/dbus_sink.rs`, `integration/observer_client.rs`, `integration/action_invoke.rs`", + "workItemId" : "ADR046-notify-004" + }, + { + "currentSource" : "v3 security-key guest vsock path (conceptual similarity only; not copied directly)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-notify-002; guest-source owner", + "destination" : "`packages/d2b-provider-notification-desktop/src/guest_source.rs`", + "detailedDesign" : "Guest-side vsock ComponentSession; `NotificationRequest` record validation and field bounding; category filter; `DesktopNotificationSink` stream forwarding; `NotificationResult` handling; no host-side resource creation", + "integration" : "Guest process vsock → host ComponentSession", + "removalProof" : "v3 baseline security-key notification path in `d2b-notify` is superseded; clipd direct `notify_rust` call superseded", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-notification-desktop", + "specPath" : "docs/specs/providers/ADR-046-provider-notification-desktop.md", + "validation" : "`integration/cross_zone_source.rs`", + "workItemId" : "ADR046-notify-005" + }, + { + "currentSource" : "`nixos-modules/notifications.nix`; `packages/d2bd/src/metrics.rs`", + "dataMigration" : "`d2b.notifications.*` Nix options retired; `d2b.zones..resources.notification-desktop` with `spec.config.guestSources` replaces", + "dependencyOwner" : "ADR046-notify-001; Nix/telemetry owner", + "destination" : "Nix: Zone resource authoring in `nixos-modules/`; metrics: `packages/d2b-provider-notification-desktop/src/`", + "detailedDesign" : "Zone Provider resource and RoleBinding Nix compiler output; `spec.config.guestSources` authoring and eval-time assertions; OTEL metric emitters with closed semantic labels and no Zone/resource-name-derived dimensions while retaining `d2b.zone` as a resource attribute; audit record emitters", + "integration" : "Nix configuration compiler; OTEL emitter ring; authoritative audit", + "removalProof" : "`nixos-modules/notifications.nix` removed after Zone resource equivalence confirmed by eval test", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-notification-desktop", + "specPath" : "docs/specs/providers/ADR-046-provider-notification-desktop.md", + "validation" : "Eval tests for category enforcement, displayWaylandRef assertion, guestRef resolution; `tests/stream_redaction.rs` for content-free telemetry; `tests/metrics_labels.rs` for structural exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus notification/Zone-name canary absence", + "workItemId" : "ADR046-notify-006" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/ids.rs`, `workload.rs`, `error.rs`; `packages/d2b-core/src/storage.rs`, `processes.rs`", + "dataMigration" : "Full d2b 3.0 reset; no v2 resource import", + "dependencyOwner" : "W0 shared contract root; `d2b-contracts`", + "destination" : "`packages/d2b-contracts/src/v3/resource.rs`, `resource_status.rs`, `resource_schema.rs`", + "detailedDesign" : "Implement strict ResourceEnvelope, metadata, the three-layer spec shape (universal envelope + ResourceType base `spec.*` incl. `spec.providerRef` + optional canonical `spec.provider` `{ schemaId, schemaVersion, settings }`), the three-layer status shape (universal base + `status.resource` + optional `status.provider` with `providerRef`/`schemaId`/`schemaVersion`/`observedProviderGeneration`/`details`), phase/condition/outcome, canonical JSON, per-layer bounds/redaction, ownerRef/UID fields Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Store/API/SDK/Nix/codegen consume one contract", + "removalProof" : "Old DTOs removed per owning ResourceType wave only after rendered/runtime consumers move", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resource-object-model", + "specPath" : "docs/specs/ADR-046-resource-object-model.md", + "validation" : "Golden JSON/protobuf vectors; serde unknown-field; three-layer spec shape round-trip; canonical minimal base-spec acceptance; base-schema version/fingerprint conformance; `spec.provider` deny-unknown/version-mismatch/shadow rejection and providerRef-binding; three-layer status shape round-trip; base-only projection (universal + `status.resource`) ignores/omits `status.provider`; `status.provider` unknown-field/version-mismatch rejection; status redaction/size/time/phase tests; `status.update` currency object round-trip (state/reasons/disruption/preserveState/owned+dependency refs bounded); `spec.updatePolicy` base round-trip", + "workItemId" : "ADR046-object-001" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/allocator_engine.rs`, `d2b-realm-router/src/lib.rs` shared ownership/idempotency precedents", + "dataMigration" : "None after reset", + "dependencyOwner" : "ADR046-object-001; native resource store", + "destination" : "`packages/d2b-resource-store-redb/src/ownership.rs`, `packages/d2b-controller-toolkit/src/owner_hints.rs`", + "detailedDesign" : "Singular ownerRef resolution/UID binding, cycle/depth property checks, reverse index, owner hints, child-first deletion", + "integration" : "Every store mutation updates owner index and hint dispatcher atomically", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resource-object-model", + "specPath" : "docs/specs/ADR-046-resource-object-model.md", + "validation" : "Property tests for cycles/reparent/name reuse; integration tests for child drift repair and owner cascades", + "workItemId" : "ADR046-object-002" + }, + { + "currentSource" : "`packages/d2b-host/src/otel_host_bridge_argv.rs` (`OtelHostBridgeArgvInputs`, `otel_host_bridge_argv`, `OtelHostBridgeArgvError`); `packages/d2bd/src/otel_host_bridge_readiness.rs` (`OtelHostBridgeReadiness`, `otel_host_bridge_read`); `packages/d2b-contracts/src/broker_wire.rs::RunnerRole::OtelHostBridge`; `packages/d2b-core/src/processes.rs::ProcessRole::OtelHostBridge`", + "dataMigration" : "Full reset; existing socat bridge retired after cutover", + "dependencyOwner" : "ADR046-telem-001 (BoundedEmitter crate); W0/W2; telemetry/observability owner", + "destination" : "`packages/d2b-provider-observability-otel/src/{forwarder_bin,controller,binding}.rs`", + "detailedDesign" : "Binding-owned forwarder: accept OTLP only from the exact Guest producer, relay through the Binding-private Endpoint/Volume to its edge collector, enforce bounded frames/quota/session timeout, and use no OTEL SDK. Map forwarder readiness to `status.provider`, then derive provider-neutral Binding ingest readiness; Process Provider owns launch/pidfd. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (`OtelHostBridgeArgvInputs` vsock logic → native Rust OTLP relay); adapt (`OtelHostBridgeReadiness` → `TelemetryBinding` conditions); delete-after-cutover (`RunnerRole::OtelHostBridge`, `ProcessRole::OtelHostBridge`).", + "integration" : "Controller creates vsock-forwarder long-lived `Process` → ProviderSupervisor → system-minijail/systemd launch → vsock socket bind → Guest side connects", + "removalProof" : "Legacy symbols removed only after Binding ownership/readiness and forwarding integration pass", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-observability-otel", + "specPath" : "docs/specs/providers/ADR-046-provider-observability-otel.md", + "validation" : "`integration/scenario_obs_zone_forwarding.rs`; adapted `minijail_relay_otel.rs` shape test for Provider-managed runner; assert `RunnerRole::OtelHostBridge` is absent from `d2b-contracts` after removal", + "workItemId" : "ADR046-otel-001" + }, + { + "currentSource" : "`nixos-modules/components/observability/host.nix` (`otelRuntimeDir`, `hostEgressSocket`, `setfacl` ACL pattern, `scrapeJournal` option, `identityName`); `nixos-modules/components/observability/stack.nix` (`ingressSources`, `vmName`, `receiverGrpcPort`, loopback binding, `signoz.listenPort`)", + "dataMigration" : "Existing SigNoz data not migrated; v3 starts fresh per Zone", + "dependencyOwner" : "ADR046-otel-001 + ADR046-telem-001 + ADR046-provider-001 (Provider toolkit) + ADR046-provider-004 (common telemetry Service/Binding base) + resource/Endpoint/Volume contracts; W2; observability owner", + "destination" : "`packages/d2b-provider-observability-otel/src/{collector_bin,emitter_socket,ingress_policy,exporter,controller,service,binding}.rs`; updated Nix observability modules", + "detailedDesign" : "Register the initial implementation of both provider-neutral qualified ResourceTypes by binding the exact ADR046-provider-004 base versions/fingerprints, then define only strict observability-otel Service/Binding spec and status extensions. Reconcile each Binding into an edge collector, private Endpoints, runtime Volume, and optional forwarder. Collector links the full OTEL SDK, resolves `serviceRef`, stamps trusted producer identity into allow-listed OTEL Resource attributes, and routes Unix-emitter, OTLP-Unix, OTLP/vsock, and import-stream metrics through one structural `METRIC_LABEL_POLICY` gate before aggregation, queueing, batching, retry, or export. The gate rejects exact/suffix identity keys and trusted `metadata.name`/resource-identity canary values, applies bounded non-echoing error classes and connection quarantine, and never consumes audit. Write generic Service/Binding observations to `status.resource` and SigNoz/OTLP/OTEL observations only to `status.provider`; no state file or Provider state Volume. Provider root config remains installation-only. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt Nix pipeline shape (replace per-VM `vmName` with per-Zone name; replace socat runner with vsock-forwarder long-lived Process; adapt `ingressSources` per-Zone entry).", + "integration" : "`BoundedEmitter` → Binding-private Endpoint → edge collector/OTEL SDK → same-Zone authority or projected Service → SigNoz", + "removalProof" : "`guest.nix` per-VM guest collector retired after `integration/scenario_obs_zone_forwarding.rs` passes", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-observability-otel", + "specPath" : "docs/specs/providers/ADR-046-provider-observability-otel.md", + "validation" : "Common-fixture/fingerprint and canonical-minimal-base conformance (including a fake alternate telemetry Provider); `tests/emitter_socket_receive.rs`; table-driven `tests/ingress_metric_policy.rs` across all four ingress adapters; `tests/exporter_outage.rs`; `tests/exporter_backpressure.rs`; `integration/scenario_full_pipeline.rs`; adapted `policy_observability.rs` (retain all existing assertions; add new `d2b.zone`, `d2b.provider` allowlist entries)", + "workItemId" : "ADR046-otel-002" + }, + { + "currentSource" : "`nixos-modules/components/observability/host.nix::journaldStorageDir`, `scrapeJournal` option; journald cgroup-path filtering pattern", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-otel-002 + ADR046-telem-001 + ADR046-volume-001; Nix/observability owner", + "destination" : "`packages/d2b-provider-observability-otel/src/nix/journald.nix`; `packages/d2b-provider-observability-otel/src/journald.rs`", + "detailedDesign" : "Per `ADR-046-telemetry-audit-and-support` §journald stdout/stderr ingestion: cgroup filter derived from trusted `producerRef`; redaction drops credential/secret/path fields, `_CMDLINE`, `_EXE`, and `INVOCATION_ID`; the strict `TelemetryBinding.spec.provider.settings.journald.enable` defaults false. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt journald receiver config for per-Zone cgroup filter.", + "integration" : "Collector binary journald receiver config path → cgroup filter expression → OTel Collector journald receiver → redaction filter → OTLP export", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-observability-otel", + "specPath" : "docs/specs/providers/ADR-046-provider-observability-otel.md", + "validation" : "Nix eval test: filter expression set when enabled; assert `_CMDLINE` and `INVOCATION_ID` in drop list; `tests/redaction.rs` for journald field redaction", + "workItemId" : "ADR046-otel-003" + }, + { + "currentSource" : "`packages/d2b-contract-tests/tests/policy_observability.rs` (`loki_native_otel_resource_attributes` allowlist; `tempo_stack_signoz_backend_and_collector`; `startup_tracing_avoids_host_path_fields`); `packages/d2b-contract-tests/tests/policy_metrics.rs` (`EXPECTED_METRICS` table); `packages/d2b-contract-tests/tests/minijail_relay_otel.rs`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-otel-002; policy/contract-tests owner", + "destination" : "`packages/d2b-contract-tests/tests/policy_observability.rs` (updated); `packages/d2b-contract-tests/tests/policy_telemetry_redaction.rs` (new, per ADR046-telem-008); `packages/d2b-provider-observability-otel/tests/{no_vm_label_in_metrics,ingress_metric_policy}.rs`", + "detailedDesign" : "(1) Extend `loki_native_otel_resource_attributes` allowlist with `d2b.zone`, `d2b.provider`, `d2b.component`, `service.version`. (2) Add gate: `no_isolation` must not appear in any Provider `MetricDescriptor` label or span attribute catalog. (3) Adapt `minijail_relay_otel.rs` shape test for Provider-managed runner (no broker `RunnerRole::OtelHostBridge`). (4) Add metric inventory gates for `d2b_otel_*` instruments from this spec. (5) Structurally prove that all four metrics ingress adapters call the shared pre-batch policy gate; run exact forbidden-key, suffix, `metadata.name`, ResourceRef, UID, and resource-identity canaries through each adapter; assert bounded non-echoing outcomes/quarantine/backpressure and valid OTEL Resource identity preservation. (6) Retain: `startup_tracing_avoids_host_path_fields`; SigNoz-only backend assertion; `tempo_guest_collector_shape`; `config_source = \"realm-controllers\"` absence gate. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt and extend existing tests; keep existing test assertions.", + "integration" : "Contract-tests run in workspace `make test-drift` and `make test-lint`", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-observability-otel", + "specPath" : "docs/specs/providers/ADR-046-provider-observability-otel.md", + "validation" : "All contract-tests pass after update; existing allowlist test does not regress", + "workItemId" : "ADR046-otel-004" + }, + { + "currentSource" : "None — net-new ADR 0046 cross-Zone sharing (D096)", + "dataMigration" : "None — full d2b 3.0 reset", + "dependencyOwner" : "ADR046-zone-control-019, ADR046-zone-control-020; observability Provider owner", + "destination" : "`packages/d2b-provider-observability-otel/src/share_adapter.rs`", + "detailedDesign" : "Implement the signed adapter: `sys-obs` exports only the authority `TelemetryService`; its local ingest Endpoint remains Service-owned implementation transport and is never an Export field. ResourceExport uses only canonical `serviceType`, `projectionSchemaFingerprint`, and `factoryFingerprint`; ResourceImport uses the corresponding `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`. Every producer import causes Core to create one local `TelemetryService` projection with ResourceImport ownership, `providerRef`, semantic base/import fields, and no `spec.provider`. Routing derives from the signed local descriptor and ResourceImport record. The semantic factory fingerprint binds factory metadata plus projection-protocol version, never Provider/adapter identity, which the signed descriptor authenticates separately. Binding `serviceRef` targets that projection. Enforce many-to-one quota/credit/backpressure/schema/source-stamp/redaction/cardinality over bounded encrypted streams; no FD/socket crosses a Zone and audit authority stays local. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (implement the signed observability export/import adapter).", + "integration" : "Core export/import controller (ADR046-zone-control-019); local projection lifecycle (ADR046-zone-control-020); ComponentSession bounded encrypted named streams", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "SigNoz ingest authority (this dossier); `packages/d2b-provider/src/share_adapter.rs` `ExportAdapter`/`ImportAdapter` traits", + "specId" : "ADR-046-provider-observability-otel", + "specPath" : "docs/specs/providers/ADR-046-provider-observability-otel.md", + "validation" : "Fast `projection_chain.rs` proves Service projection semantics with a fake stream, exact canonical Export/Import type and fingerprint fields, rejection of obsolete `endpointRef`/`exportedType`/`baseSchemaFingerprint`/`expectedType`/`expectedBaseSchemaFingerprint`/`projectionType`, rejection of projection `spec.provider`, and semantic-fingerprint stability under Provider/adapter identity mutation while signed descriptor authentication remains exact; integration alone runs real encrypted streams and SigNoz; revocation/reconnect, quotas, source stamp, redaction/cardinality, no FD crossing, and audit locality", + "workItemId" : "ADR046-otel-005" + }, + { + "currentSource" : "`nixos-modules/components/observability/{stack,host,guest}.nix` (SigNoz/ClickHouse/Keeper loopback authority, per-source vsock ingress, source-identity `upsert`, `sending_queue`/`retry_on_failure`, allow-list projection); `nixos-modules/{index,observability-vm}.nix`; `packages/d2b-host/src/{otel_host_bridge_argv,vsock_relay_argv}.rs` (pre-opened vsock fds; broker rejects source VM ≠ obs VM); `packages/d2bd/src/otel_host_bridge_readiness.rs`", + "dataMigration" : "None — full d2b 3.0 reset", + "dependencyOwner" : "ADR046-otel-005, ADR046-zone-control-019, ADR046-zone-control-020; observability Provider owner", + "destination" : "`packages/d2b-provider-observability-otel/src/{authority,service,binding,projection}.rs`; `AuthorityDescriptor` on the `sys-obs` `TelemetryService`", + "detailedDesign" : "Implement one provider-neutral D097 authority Service with generic telemetry-ingest Endpoint refs and common service/signals/quota/policy fields. Its strict observability-otel `spec.provider` alone selects the loopback SigNoz stack, backend Endpoints, and OTLP. Core rejects duplicates and adopts by owner proof. Implement core-owned imported Service projections with no `spec.provider` or backend ownership. Implement per-producer Bindings with common service/producer/signals/quota/policy fields and strict implementation extension; Bindings own/cause edge collector/forwarder/private Endpoints/runtime Volume. Keep generic observations in `status.resource` and SigNoz/OTLP/OTEL observations in `status.provider`. Preserve trusted source upsert, retry/queue, bounded cardinality/redaction, audit non-transfer, status-first state, and no OTEL SDK in core. Endpoint is transport only. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new universal provider-neutral `telemetry.d2bus.org.TelemetryService`/`telemetry.d2bus.org.TelemetryBinding` pair; adapt existing authority/edge behavior as the initial observability-otel implementation.", + "integration" : "Ingest authority + export (ADR046-otel-005); core export/import controller and projection lifecycle (ADR046-zone-control-019/020); ComponentSession per-import encrypted streams; d2b-telemetry closed-label metrics", + "removalProof" : "Legacy fixed per-source vsock ingress and old gates are removed only after the Service/Binding/ComponentSession successor passes; neither old provider-qualified ResourceType name nor any Endpoint-projection or ResourceType alias remains, and no duplicate suite remains.", + "reuseAction" : "adapt", + "reuseSource" : "Same baseline stack/bridge/readiness symbols; `packages/d2b-provider/src/share_adapter.rs` traits", + "specId" : "ADR-046-provider-observability-otel", + "specPath" : "docs/specs/providers/ADR-046-provider-observability-otel.md", + "validation" : "Fast `resource_service_binding.rs` and `projection_chain.rs` plus reused nix-unit/policy tests prove provider-neutral names, base/Provider field separation, projection `spec.provider` rejection, exact D088 placement of semantic observations under `status.resource` and implementation observations under `status.provider`, ownership, schemas, stamping, quotas, redaction, and projection chain. Real SigNoz and real stream scenarios are integration-only.", + "workItemId" : "ADR046-otel-006" + }, + { + "currentSource" : "`packages/d2b-contract-tests/tests/policy_contracts.rs` lines 5–6 (D2B_FIXTURES gate / workspace-checks integration pattern — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-contract-tests/tests/static_invariants.rs` (hermetic policy test structure — `implemented-and-reachable`); `tests/tools/rust-workspace-checks.sh` (D2B_FIXTURES step shell harness — `implemented-and-reachable`); AGENTS.md \"Naming conventions\" section (`-` workspace sort rules — `implemented-and-reachable`); `packages/d2b-realm-core/src/ids.rs` `LABEL_PATTERN` / `MAX_ID_LEN` (name regex reused for crate name token validation — `implemented-and-reachable`)", + "dataMigration" : "Additive; no existing `d2b-provider-*` crates in the pre-ADR45 baseline; first Provider crate created must comply from inception", + "dependencyOwner" : "ADR046-zone-control-003; workspace policy owner", + "destination" : "`packages/d2b-contract-tests/tests/policy_provider_crate_layout.rs` (new file; gated under D2B_FIXTURES in existing `tests/tools/rust-workspace-checks.sh`)", + "detailedDesign" : "Implement `policy_provider_crate_layout.rs` with the following test functions: (1) `every_provider_crate_has_src` — walk `packages/d2b-provider-*/` directories in the workspace, assert each contains `src/`; failure names crate and missing path; (2) `every_provider_crate_has_tests` — assert `tests/` present; (3) `every_provider_crate_has_integration` — assert `integration/` present; (4) `every_provider_crate_has_readme` — assert `README.md` present; (5) `every_provider_readme_has_required_sections` — read `README.md`, check for all nine section headings from §4.8.3 (case-insensitive, after stripping `#` and whitespace); failure names the missing heading(s); (6) `every_integration_file_has_target_declaration` — for each `integration/*.rs` file, scan first 20 lines for exactly one `//! integration-target: (container|host-integration)` declaration; failure names the file and the violation (missing/multiple/invalid value); (7) `non_provider_crates_exempt` — verify the check does not run on non-`d2b-provider-*` crates. All checks are filesystem-only (no compilation). Workspace member list is discovered by parsing `packages/Cargo.toml` `[workspace].members`. Gate: add the new test file to `tests/tools/rust-workspace-checks.sh` D2B_FIXTURES list alongside existing policy tests", + "integration" : "`make test-policy` and `make check` both fail if any provider crate violates §4.8; consistent with existing `no-bash-ast-walker` and workspace-sort gates; ADR046-zone-control-003 references §4.8 for Provider package conventions", + "removalProof" : "No existing code removed; additive policy test only", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "§15.3 layout conformance tests: `provider-crate-layout-src-required`, `provider-crate-layout-tests-required`, `provider-crate-layout-integration-required`, `provider-crate-layout-readme-required`, `provider-readme-sections-all-present`, `provider-readme-sections-partial-missing`, `provider-integration-target-declared`, `provider-integration-target-unique`, `provider-integration-target-valid-values`, `provider-crate-naming-convention`, `provider-crate-layout-non-provider-exempt`", + "workItemId" : "ADR046-pkg-001" + }, + { + "currentSource" : "`packages/d2b-core/src/processes.rs`, `minijail_profile.rs`, `storage.rs`; `d2b-contracts/src/broker_wire.rs`", + "dataMigration" : "Full reset", + "dependencyOwner" : "W0; resource contracts", + "destination" : "`packages/d2b-contracts/src/v3/host.rs`, `guest.rs`, `execution_policy.rs`, `process.rs`, `volume.rs`, `user.rs`, `network.rs`, `device.rs`, `credential.rs`", + "detailedDesign" : "Complete minimal ResourceType schemas and shared execution/Volume sub-schemas Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Provider dossiers/controller descriptors bind exact types", + "removalProof" : "Old DTOs removed only by owning future slices", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-primitive-resource-composition", + "specPath" : "docs/specs/ADR-046-primitive-resource-composition.md", + "validation" : "Schema vectors and folded-field/no-duplicate-type policy tests", + "workItemId" : "ADR046-primitives-001" + }, + { + "currentSource" : "broker SpawnRunner/pidfd; d2bd supervisor; unsafe-local helper; guest exec runner", + "dataMigration" : "Current ProcessRoles converted by exact disposition table", + "dependencyOwner" : "Process contracts; system Provider slices", + "destination" : "`packages/d2b-provider-system-systemd/`, `packages/d2b-provider-system-minijail/`, shared neutral process conformance library", + "detailedDesign" : "Common Process/EphemeralProcess, provider-specific launch/pidfd/wait/adoption/status Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Process controller registration under Host/Guest; d2b-bus ResourceClient/status", + "removalProof" : "Role branches removed only after successor Provider tests", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-primitive-resource-composition", + "specPath" : "docs/specs/ADR-046-primitive-resource-composition.md", + "validation" : "Shared conformance plus Host/Guest/user integration", + "workItemId" : "ADR046-primitives-002" + }, + { + "currentSource" : "`storage-json.nix`, `d2b-core/src/storage.rs`, store/TPM/runtime path owners, virtiofsd argv/ProcessRole", + "dataMigration" : "Full reset; Provider-specific state export only where separately specified", + "dependencyOwner" : "Volume contract; Volume Provider slices", + "destination" : "`packages/d2b-provider-volume-*/`; `nixos-modules/resources-volume.nix`", + "detailedDesign" : "Fine-grained Volume layout/views, host-path policy, virtiofs attachments/status/owned Process Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Host/Guest/Process refs and Volume controller", + "removalProof" : "storage.json rows removed only after Volume successor parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-primitive-resource-composition", + "specPath" : "docs/specs/ADR-046-primitive-resource-composition.md", + "validation" : "ACL/no-follow/marker, sharing/views, virtiofs host/guest mount tests", + "workItemId" : "ADR046-primitives-003" + }, + { + "currentSource" : "`d2b-core/src/{processes,process_builder,minijail_profile}.rs`; `d2bd/src/supervisor/*`; broker `spawn_runner.rs`", + "dataMigration" : "Full reset; no role snapshot import", + "dependencyOwner" : "W0/W2; Process contracts/supervisor", + "destination" : "`packages/d2b-process/src/`, `packages/d2b-provider-supervisor/src/`", + "detailedDesign" : "Common spec/status/tickets/pidfd/adoption/naming/cgroup/fast path Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Process Provider controllers → supervisor/broker/systemd → async status", + "removalProof" : "Role/DAG path removed only per role disposition", + "reuseAction" : "adapt", + "reuseSource" : "Useful main ProviderSupervisor/session/process code named by sub-items", + "specId" : "ADR-046-components-processes-and-sandbox", + "specPath" : "docs/specs/ADR-046-components-processes-and-sandbox.md", + "validation" : "Shared conformance/fault/latency tests", + "workItemId" : "ADR046-process-001" + }, + { + "currentSource" : "unsafe-local helper runtime/systemd; guest exec systemd-run; broker SpawnRunner/minijail", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-process-001; systemd/minijail Provider owners", + "destination" : "`packages/d2b-provider-system-systemd/`, `packages/d2b-provider-system-minijail/`", + "detailedDesign" : "Two Process/EphemeralProcess implementations, pidfd/wait ownership, system/user domains Primary reuse disposition: `adapt`. Preserved source-plan detail: extract/adapt.", + "integration" : "Zone-installed Providers/controller instances per Host/Guest", + "removalProof" : "Old helpers retained until host/user/guest parity", + "reuseAction" : "adapt", + "reuseSource" : "Main process/session helpers if selected by exact sub-items", + "specId" : "ADR-046-components-processes-and-sandbox", + "specPath" : "docs/specs/ADR-046-components-processes-and-sandbox.md", + "validation" : "Identical schema/status conformance plus provider-specific adoption", + "workItemId" : "ADR046-process-002" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/{provider,capabilities,error,credential,rate_limit,conformance}.rs`", + "dataMigration" : "Full reset", + "dependencyOwner" : "W0; Provider contract/catalog owner", + "destination" : "`packages/d2b-contracts/src/v3/provider.rs`, `packages/d2b-provider/src/lib.rs`, `packages/d2b-provider-toolkit/`", + "detailedDesign" : "Provider resource/manifest/components/dependencies/services/trust/compatibility/toolkit Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Zone config/catalog → Provider resource → Process components → bus/resource routes", + "removalProof" : "Old trait crate retired only after all Provider dossiers migrate", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-model-and-packaging", + "specPath" : "docs/specs/ADR-046-provider-model-and-packaging.md", + "validation" : "Contract vectors, fake/malicious Provider, one-crate/one-identity policy", + "workItemId" : "ADR046-provider-001" + }, + { + "currentSource" : "`packages/Cargo.toml`; `flake.nix`; `nixos-modules/host-daemon.nix`; current source package derivations", + "dataMigration" : "No package compatibility path", + "dependencyOwner" : "Provider contract; package/Nix integrator", + "destination" : "one `packages/d2b-provider--/` per Provider with mandatory src/, tests/, integration/, README.md; generic Nix Provider package/catalog emitter", + "detailedDesign" : "Split current combined/composition crates; exact outputs/manifests/conformance/layout/documentation", + "integration" : "Provider package installed/registered per Zone", + "removalProof" : "Combined crate removed only after every live implementation has a Provider successor", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-model-and-packaging", + "specPath" : "docs/specs/ADR-046-provider-model-and-packaging.md", + "validation" : "Workspace naming/dependency/output/dossier/catalog parity policy", + "workItemId" : "ADR046-provider-002" + }, + { + "currentSource" : "`d2bd` DAG/broker spawn; unsafe-local helper; guestd/exec runner; `d2b-host` runtime provider", + "dataMigration" : "Current roles converted under reset", + "dependencyOwner" : "Process contracts; system Provider owners", + "destination" : "`packages/d2b-provider-system-core/`, `d2b-provider-system-systemd/`, `d2b-provider-system-minijail/`", + "detailedDesign" : "Bootstrap system-core; common Process/EphemeralProcess providers and pidfd conformance Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Host/Guest providerRef/domain/userRef, local supervisors, resource status", + "removalProof" : "Current role launch paths removed after parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-model-and-packaging", + "specPath" : "docs/specs/ADR-046-provider-model-and-packaging.md", + "validation" : "Shared conformance and host/user/non-Host tests", + "workItemId" : "ADR046-provider-003" + }, + { + "currentSource" : "None — D098 common semantic Service/Binding bases are net-new ADR 0046 contracts", + "dataMigration" : "Full d2b 3.0 reset; no prior public Service/Binding names or aliases are imported", + "dependencyOwner" : "ADR046-provider-001; shared semantic Service/Binding contract owner", + "destination" : "`packages/d2b-contracts/src/v3/semantic_services/{mod,audio,security_key,telemetry,usb}.rs`; generated schema artifacts for the eight exact qualified ResourceTypes", + "detailedDesign" : "Define one shared strict base spec/status DTO and schema contract for each frozen D098 Service/Binding pair, including exact semantic type/schema IDs, versions, fingerprints, minimal valid base fixtures without `spec.provider`, authority/projection Service union, same-Zone Binding `serviceRef`/target rules, D088 `status.resource` layering, status-only observations, and projection-factory type binding. A Core-generated projection permits only `providerRef`, semantic base/import fields, and ResourceImport ownership; it rejects `spec.provider`. Register no implementation-qualified or former `*State` alias.", + "integration" : "Provider manifests and ResourceApiBindings consume the common catalog fingerprint; ADR046-zone-control-019/020 use the same factory metadata to admit an owner Service and core-create one same-type projection Service; the four initial Provider dossiers supply only strict implementation extensions/controllers.", + "removalProof" : "Any Provider-local duplicate base DTO/schema is removed before that Provider is registered; only strict Provider extension DTOs remain implementation-owned.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-model-and-packaging", + "specPath" : "docs/specs/ADR-046-provider-model-and-packaging.md", + "validation" : "Shared contract tests cover exact names, strict serde/schema round trips, common base discoverability without any Provider package, canonical minimal base acceptance without `spec.provider`, same-Zone refs/targets, owner/projection discrimination, Core projection rejection of `spec.provider`, common fields only under `status.resource`, implementation observation only under `status.provider`, status-only observations, no Device/Endpoint/Binding projection, implementation-detail rejection, semantic factory-fingerprint stability under Provider/adapter identity changes, and rejection of every implementation-qualified/former `*State` alias. Each initial and fake alternate Provider must pass the identical base conformance fixture.", + "workItemId" : "ADR046-provider-004" + }, + { + "currentSource" : "main `a1cc0b2d`: `packages/d2b-gateway-runtime/src/provider_agent.rs` (`ProviderAgentError::{UnregisteredAdapter,RegistryNotAccepting,RegistrationRejected,InvalidAuditCapacity,SessionClosed,ProtocolViolation}`, `ProviderAgentAuditOutcome`, `ProviderAgentAuditEvent`, `ProviderAgentProcess::from_registry/from_registry_with/provider_type/service_names/audit_snapshot/serve`, `run_registered`, bounded in-memory audit ring, frame dispatch loop: semaphore in-flight limit, service/method routing, negative-timeout guard, `SessionClosed` termination, `ProtocolViolation` audit + terminate — lines 31–452; tests `standalone_entrypoint_fails_without_registration`, `audit_capacity_is_bounded` — lines 454–486); `packages/d2b-contracts/src/provider_registry_v2.rs` (`ProviderRegistryV2` wire contract, `ProviderRegistryEntryV2::validate` with provider-id derivation rule, schema fingerprint, scope-digest, generation exactness, `TrustedFirstPartyInProcess` placement requirement, `ProviderIntentId` label rules `max 128 bytes`, `MAX_PROVIDER_REGISTRY_ENTRIES`, `MAX_PROVIDER_MAPPING_IDS=64`, `ProviderBindingV2` non-exhaustive + `UnsupportedProviderBindingV2` fallback, `ProviderRegistryV2::validate` sort/unique/count checks — lines 23–566; tests `validates_closed_local_runtime_mapping`, `validates_closed_local_observability_mapping`, `serializes_declared_mapping_axes_as_closed_variants`, `rejects_duplicate_or_unbounded_mapping_ids`, `local_storage_binding_realm_must_match_descriptor_placement`, `rejects_generation_and_exact_identity_mismatches`, `contradictory_binding_realm_json_is_unrepresentable`, `unknown_binding_axis_remains_rejected_on_the_wire`, `identity_mismatch_messages_name_the_failed_contract`, `accepts_explicit_empty_registry` — lines 722–1044)", + "dataMigration" : "Not applicable; new implementation", + "dependencyOwner" : "ADR046-zone-control-011, ADR046-zone-control-017, ADR046-zone-control-018", + "destination" : "`packages/d2b-provider/src/agent.rs` (v3 provider agent dispatch); `packages/d2b-contracts/src/v3/provider_registry.rs` (v3 provider registry wire contract)", + "detailedDesign" : "**Selected**: `ProviderAgentProcess::serve` dispatch loop with semaphore in-flight limit; unsupported-service/method → ttrpc error; negative-timeout rejection; `SessionClosed` clean termination; `ProtocolViolation` audit-and-terminate path; bounded audit ring; `GeneratedProviderServiceServer` single-service-per-agent requirement; `ProviderRegistryV2` entry validation: provider-id derivation, schema fingerprint, scope digest, generation exactness; `MAX_PROVIDER_MAPPING_IDS=64` → Provider component mapping bound; `ProviderBindingV2` non-exhaustive + explicit `UnsupportedProviderBindingV2` fallback (never panics on unknown axis); `ProviderIntentId` `max 128 bytes` label rules → Provider component `spec.intentRef`; all 10 `provider_registry_v2.rs` tests ported. **Excluded ADR45 assumptions**: `aca_workload.rs` (`AcaGatewayWorkload`): ADR45 ACA external provider adapter, excluded entirely. `waypipe_display.rs` (`WaypipeDisplayProvider`): ADR45 display provider, excluded. `ProviderRegistryV2.registry_generation` / `configuration_fingerprint` bind to ADR45 bundle generation; v3 Provider resource version is tracked in redb store, not a JSON bundle. `run()` binary entrypoint uses the fixed `d2b-provider-agent` command; v3 provider processes use normal Zone runtime Process launch. `TrustedFirstPartyInProcess` is the only placement in v2; v3 Provider resources extend to `HostLocal`/`GatewayVm` etc. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "integration" : "Zone runtime spawns each Provider component Process via normal Process launch; Process binary calls `ProviderAgentProcess::from_registry` then `serve()` on established ComponentSession; on `SessionClosed` the process exits and Zone runtime observes `status.phase` transition", + "removalProof" : "Not applicable; new implementation", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`provider-agent-dispatch-unsupported-service-returns-ttrpc-error`, `provider-agent-negative-timeout-rejected`, `provider-agent-session-closed-terminates-serve-loop`, `provider-agent-audit-ring-capacity-bounded` (ported from `audit_capacity_is_bounded`), `provider-registry-entry-fingerprint-generation-exact` (ported from `rejects_generation_and_exact_identity_mismatches`), `provider-registry-unknown-axis-fallback-non-exhaustive` (ported from `unknown_binding_axis_remains_rejected_on_the_wire`), `provider-registry-duplicate-ids-rejected` (ported from `rejects_duplicate_or_unbounded_mapping_ids`)", + "workItemId" : "ADR046-provider-agent-001" + }, + { + "currentSource" : "`packages/d2b-core/src/storage.rs` (`StoragePathSpec` with `scope: ContractId` currently keyed by `ProcessRole`/Workload; `SensitivityClass`; `StorageLifecycle`; `StorageRootClass`); `packages/d2b-state/src/atomic.rs` (main, 6faa5256; absent from v3 baseline)", + "dataMigration" : "Full v3 reset; no v2 state schema import", + "dependencyOwner" : "ADR046-primitives-001; v3 contracts owner", + "destination" : "`packages/d2b-contracts/src/v3/volume_state.rs`", + "detailedDesign" : "`VolumeStateSchema` struct (`schemaId`, `schemaVersion`, `schemaDigest`, `migrationPolicy`); `PersistenceClass` and `SensitivityClass` enums; `VolumeStateStatus` extension (`stateSchemaPhase`, `installedSchemaVersion`, `markerStatus`, `sealingStatus`, `quotaUsage`, `lastMigrationAt`); `StateEnvelope` (replaces v2 `StateEnvelope`); canonical JSON serde and digest helpers Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Volume spec and status structs embed these types; Provider descriptor component stateNamespace declaration uses the same types", + "removalProof" : "`d2b-core/src/storage.rs` StoragePathSpec/SensitivityClass removed only after all Provider descriptor consumers are on v3 Volume spec", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "Schema golden vectors; phase/reason round-trip; StateEnvelope digest tests", + "workItemId" : "ADR046-pstate-001" + }, + { + "currentSource" : "`packages/d2b-core/src/processes.rs` (`VmProcessDag`, `ProcessNode`, `ProcessRole`: each current `ProcessRole` variant maps to a `Process` or `EphemeralProcess` resource under its owning Provider; `ProcessRole::Swtpm`/`Virtiofsd`/`CloudHypervisorRunner` → Process resources under `Provider/device-tpm`/`Provider/volume-virtiofs`/`Provider/runtime-cloud-hypervisor`); Provider descriptor component model from ADR046-provider-001", + "dataMigration" : "Full reset", + "dependencyOwner" : "ADR046-pstate-001; Provider contracts owner", + "destination" : "`packages/d2b-contracts/src/v3/provider.rs` (component descriptor `stateNamespaces` field)", + "detailedDesign" : "Add `stateNamespaces: Vec` to the component descriptor (zero or more entries; a component declares an entry only when a payload passes the storage-need test; stateless components declare none); each entry includes `id`, `kind` (always `state`), `schemaId` (non-null), `schemaVersion`, `schemaDigest`, `persistenceClass` (must be `persistent`; `ephemeral`/`cache` rejected), `sensitivityClass`, `migrationPolicy`, `quotaBytes` (nonzero; minimum 4096), `storageNeed` (`secret` | `large-binary` | `private-unsafe-for-status` | `revision-unsuitable`), `sealingRequired`, `placementMode` (`guest-local` or `host-backed-guest` for Guest-targeted; omitted for Host-targeted), `hostCustodyPermitted` (required `true` for `host-backed-guest`; absent/false for `guest-local`), and `views`; there is no empty-payload (`schemaId: null`) namespace", + "integration" : "Provider package build emits component descriptors with state namespaces; Provider controller creates Volumes from descriptors at install time", + "removalProof" : "Not applicable (new)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "Descriptor schema golden vectors; descriptor-Volume consistency property test; stateless-component-declares-no-namespace round-trip; storage-need justification enforcement (namespace whose payload is derivable from spec/status/core ledger/external observation → `component-state-not-justified`); `kind != state` → `component-kind-invalid`; `persistenceClass: ephemeral` → `component-persistence-class-forbidden`; `quotaBytes: 0` or `1024` → `component-quota-too-small`; base `quota.maxBytes == quotaBytes` and `quota.maxInodes > 0`; Guest-targeted with `placementMode: guest-local` → source.executionRef=Guest; Guest-targeted with `host-backed-guest` + `hostCustodyPermitted: true` → source on Host, Export created; `host-backed-guest` without `hostCustodyPermitted: true` → `placement-host-custody-violation`; credential/audit schema with `host-backed-guest` → `guest-local-required`; `placementMode` change → descriptor version increment enforced", + "workItemId" : "ADR046-pstate-002" + }, + { + "currentSource" : "`packages/d2b-state/src/atomic.rs`, `path.rs`, `lock.rs` (main, 6faa5256); `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` (marker algorithm, v3 baseline)", + "dataMigration" : "New marker written for each Volume at v3 first-boot; TPM marker path adapted from current swtpm-markers root", + "dependencyOwner" : "ADR046-pstate-001; volume-local Provider owner", + "destination" : "`packages/d2b-provider-volume-local/` (new crate, full scaffold required): `src/{atomic.rs, path.rs, lock.rs, marker.rs, effect_port.rs}`; `tests/volume_local.rs` (marker missing/replaced/mismatch, domain-isolation rejection, quota enforcement); `integration/volume_local.rs` (real Host filesystem provision, broker-maintained marker check, domain-isolation rejection cross-process); `README.md`", + "detailedDesign" : "Anchored Volume root provision, identity marker write/check, quota soft-check on write, domain-isolation validation, fd-relative layout creation/repair/cleanup, broker-maintained marker root protocol; layout `ownerRef`/`groupRef` must reference a Nix-preprovisioned User principal or bounded system pool — Volume admission rejects runtime-created principals; `VolumeEffectPort` returns opaque IDs and named view dirfds only — no raw host path returned by any EffectPort operation; volume-local must support `source.executionRef: Guest/` for `guest-local` placement (controller running inside the Guest): when executing in a Guest domain, volume-local may not create, read, or hold dirfds/paths for Volumes sourced in another domain; `host-backed-guest` placement creates a `virtiofs.d2bus.org.Export` child per attachment entry and validates `hostCustodyPermitted: true` in the signed descriptor Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged (path.rs) / adapt (atomic.rs, lock.rs) / adapt (swtpm_dir.rs marker algorithm).", + "integration" : "`d2b-priv-broker` calls `volume_local::marker::provision_marker` at broker-maintained Volume creation; `d2b-provider-volume-local` controller calls `marker::verify_marker` on every daemon restart via reconcile startup relist", + "removalProof" : "`swtpm_dir.rs` marker implementation retired only after device-tpm Provider Volume is live and marker-check parity is confirmed", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "Marker missing/replaced/mismatch tests; domain-isolation rejection tests; quota enforcement tests; crash at every provision step", + "workItemId" : "ADR046-pstate-003" + }, + { + "currentSource" : "`packages/d2b-core/src/storage_lifecycle.rs` (`StorageLifecycleReport` issue detection); `packages/d2b-state/src/atomic.rs` (main, 6faa5256)", + "dataMigration" : "None (new protocol)", + "dependencyOwner" : "ADR046-reconcile-001, ADR046-pstate-003; volume-local Provider and controller-toolkit owners", + "destination" : "`packages/d2b-provider-volume-local/src/migration.rs`; `packages/d2b-provider-volume-local/tests/migration_unit.rs` (hermetic staging Volume and prepare/commit/rollback); `packages/d2b-provider-volume-local/integration/migration.rs` (real Host crash-injection at each migration step, N-Volume cross-component coordination); `packages/d2b-controller-toolkit/src/state_migration.rs`", + "detailedDesign" : "Pre-launch migration EphemeralProcess template, staging Volume create/destroy lifecycle, prepare/commit/rollback protocol implementation, roll-forward on restart detection, migration idempotency", + "integration" : "Provider controller's reconcile handler calls toolkit `state_migration::plan` and dispatches EphemeralProcess via ResourceClient; volume-local Provider reports `stateSchemaPhase` transitions", + "removalProof" : "`StorageLifecycleReport` and storage contract validation in `d2b-core` removed only after v3 Volume migration path is live", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "Migration with and without crash at each step; rollback after failed EphemeralProcess; roll-forward after interrupted commit; cross-component N-Volume coordination", + "workItemId" : "ADR046-pstate-004" + }, + { + "currentSource" : "`packages/d2b-state/src/lease.rs` (main, 6faa5256); Credential ResourceType from ADR046-primitives-001", + "dataMigration" : "Sealed Volumes are new (no existing sealing to migrate)", + "dependencyOwner" : "ADR046-pstate-003; volume-local Provider and Credential Provider owners", + "destination" : "`packages/d2b-provider-volume-local/src/sealing.rs`; `packages/d2b-provider-volume-local/tests/sealing_unit.rs` (hermetic seal/rotation state machine); `packages/d2b-provider-volume-local/integration/sealing.rs` (real Credential lease flow, interrupted key rotation with live Credential Provider)", + "detailedDesign" : "Envelope encryption on write using Credential lease key material; key rotation EphemeralProcess re-encrypt; no raw key on disk; `sealingStatus` transitions; `rotation-failed` fail-safe", + "integration" : "Volume controller reads Credential status/lease before provisioning; sealing wraps `StateEnvelope` writes in atomic.rs", + "removalProof" : "Not applicable (new)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "Seal/read/rotation tests; rotation interrupted at commit; credential revoked during runtime", + "workItemId" : "ADR046-pstate-005" + }, + { + "currentSource" : "`packages/d2b-state/src/atomic.rs` (main, 6faa5256): `AtomicFilesystem` read snapshot; no existing snapshot infrastructure in v3", + "dataMigration" : "No existing snapshots; new infrastructure only", + "dependencyOwner" : "ADR046-pstate-003, ADR046-pstate-004; volume-local Provider and snapshot toolkit owners", + "destination" : "`packages/d2b-provider-volume-local/src/snapshot.rs`; `packages/d2b-provider-volume-local/tests/snapshot_unit.rs` (hermetic `snapshotPolicy` enforcement, retention count and TTL logic); `packages/d2b-provider-volume-local/integration/snapshot.rs` (real Host filesystem snapshot creation, retention expiry, pre-migration auto-snapshot with interrupted migration)", + "detailedDesign" : "Snapshot EphemeralProcess; bounded `.snapshots/` sub-tree; `snapshotPolicy` enforcement; retention count and TTL cleanup; snapshot status tracking in Volume status", + "integration" : "Provider controller creates snapshot EphemeralProcess before migration and relocation based on `snapshotPolicy.triggerOnMigration` and `triggerOnRelocation`; status populated via Volume status update", + "removalProof" : "Not applicable (new)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "Create/read/expire/retention-limit tests; snapshot before interrupted migration; snapshot list in Volume status", + "workItemId" : "ADR046-pstate-006" + }, + { + "currentSource" : "`packages/d2b-priv-broker/src/ops/swtpm_dir.rs` (v3, marker pattern); `packages/d2b-state/src/path.rs` (main)", + "dataMigration" : "Not applicable", + "dependencyOwner" : "ADR046-pstate-003; volume-local Provider owner", + "destination" : "`packages/d2b-provider-volume-local/src/relocation.rs`; `packages/d2b-provider-volume-local/tests/relocation_unit.rs` (hermetic finalizer set/clear, commit/failure state machine); `packages/d2b-provider-volume-local/integration/relocation.rs` (real Host-to-Host anchored file copy, crash at copy midpoint, virtiofs source re-point after successful relocation)", + "detailedDesign" : "Relocation EphemeralProcess; source finalizer; anchored copy; source Volume (volume-local) relocation for components with Guest attachment (the attachment Volume backed by volume-virtiofs is re-pointed to the new source after copy; see `ADR-046-primitive-resource-composition` Volume attachment spec); commit/failure handling", + "integration" : "Provider controller adds `Relocating` finalizer before creating relocation EphemeralProcess; removes finalizer after successful destination Volume activation", + "removalProof" : "Not applicable (new)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "Relocation with crash at copy midpoint; failed relocation source preservation; virtiofs source relocation test", + "workItemId" : "ADR046-pstate-007" + }, + { + "currentSource" : "`packages/d2b-state/src/audit.rs` (main, 6faa5256): `AuditAppender`, `AuditRecordInput`, `SegmentBuilder`, `checkpoint`, `decide_retention`, `detect_gap`, `read_audit_segment`; Zone audit stream interface from ADR046-bus contracts (see ADR-046-componentsession-and-bus); OTEL cardinality model from `packages/d2b-provider-observability-local/src/` (main, a1cc0b2d)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-pstate-001; Zone audit stream, OTEL provider owners", + "destination" : "`packages/d2b-provider-volume-local/src/audit.rs`; `packages/d2b-provider-volume-local/src/otel.rs`; `packages/d2b-provider-volume-local/tests/audit_unit.rs` (hermetic audit golden records, OTEL cardinality label tests)", + "detailedDesign" : "Volume-state audit event types and Zone audit emission; OTEL metric definitions with closed semantic label sets and no Zone or resource-name-derived label keys; Zone identity remains in the `d2b.zone` OTEL resource attribute", + "integration" : "Every state lifecycle transition calls `audit::emit_volume_event`; OTEL metrics exported via `observability-otel` Provider", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "Audit event golden records; no content/path/credential in audit payload; structural OTEL label-policy tests assert exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys and preserve `d2b.zone` as a resource attribute", + "workItemId" : "ADR046-pstate-008" + }, + { + "currentSource" : "`packages/d2b-state/tests/state.rs`, `async_state.rs` (main, 6faa5256): atomic, lock, quarantine, audit, lease tests; `packages/d2b-provider-toolkit/src/fixture.rs` (main, a1cc0b2d): `Fixture`, `FakeProvider`, `DeterministicClock`, `ProviderValues`, `Redacted`, `Secret`, `sample_lease_request`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-pstate-001 through ADR046-pstate-008; integration test owner", + "destination" : "`packages/d2b-provider-volume-local/tests/state.rs` (ported hermetic atomic/lock/quarantine/lease tests); `packages/d2b-provider-volume-local/tests/migration.rs` (ported migration fault-injection, cross-component N-Volume coordination); `packages/d2b-provider-volume-local/integration/provider_state.rs` (end-to-end: live daemon, real Host Volume mount, cross-process worker subview, full audit stream); `packages/d2b-provider-volume-local/integration/audit.rs` (live Zone audit stream emission and OTEL export)", + "detailedDesign" : "Port all d2b-state integration tests replacing ADR 0045 contract setup with v3 Volume/StateEnvelope; add provider-state-specific migration, marker, quota, sealing, relocation, snapshot, incident-hold, and unclaimed-GC tests; include cross-component N-Volume coordination test", + "integration" : "Tests run against the real volume-local Provider over a fake Zone runtime (no live daemon required) using the standard controller-toolkit fake clients", + "removalProof" : "`d2b-state` crate retired from workspace only after every caller migrates to v3 Volume state helpers and all ported tests pass", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "All ported tests pass under v3 contracts; test coverage includes every fault-injection scenario listed in d2b-state/tests/state.rs plus new provider-state cases; stateless-component-declares-no-Volume test passes; shared-Volume attempt rejected; `guest-local` Volume creation inside Guest domain (source.executionRef=Guest, no Export created, Host volume-local holds no dirfd/path); `host-backed-guest` Volume creation (source on Host, Export created, Export reaches Ready, Guest Process mounts source Volume view); `host-backed-guest` without `hostCustodyPermitted: true` → `placement-host-custody-violation`; credential/audit schema with `host-backed-guest` → `guest-local-required`; cross-domain isolation: Guest-local volume-local does not create or observe Host-domain Volumes", + "workItemId" : "ADR046-pstate-009" + }, + { + "currentSource" : "`nixos-modules/manifest.nix` (current `manifest.json` emitter, v3 baseline `fd5b0067`); `packages/d2b-core/src/storage.rs` (`StorageAuthority::NixModule`-owned rows); `packages/xtask/src/main.rs` (`gen-schemas` command, same baseline)", + "dataMigration" : "`nixos-modules/manifest.nix` provider-registration and storage-authority rows superseded by `zone-resources.nix`; prior `manifest.json` format retired after Zone daemon migration to bundle format", + "dependencyOwner" : "ADR046-pstate-001; Zone resource bundle owner, NixOS module owner, `d2b-core-controller` owner (ADR-046-core-controllers)", + "destination" : "`nixos-modules/zone-resources.nix` (per-Zone bundle emitter NixOS module); `packages/d2b-core/src/v3/zone_bundle.rs` (shared bundle DTOs: `ZoneResourceBundle`, `BundleResource`, `contentId` computation); `packages/d2b-core-controller/src/configuration.rs` (diff/apply loop, name-conflict detection, `pending-cleanup` Zone status, `maxFinalizerDurationSeconds` stall detection — NOT in `d2b-provider-volume-local`); `packages/d2b-core-controller/tests/configuration.rs` (hermetic bundle diff, absent-resource Delete dispatch, name-conflict `Degraded/name-conflict` item, integrity-failure abort); `packages/d2b-core-controller/integration/configuration.rs` (container-based generation activation with running Providers, absent-resource cleanup, rollback, finalizer-timeout stall detection)", + "detailedDesign" : "Generic `d2b.zones..resources. = { type = \"…\"; spec = { …exact ResourceTypeSpec fields… }; }` attrset; `metadata.name` derived from attr key, `metadata.zone` from Zone key, `apiVersion` defaulted; `status` omitted (read-only); Nix option types for `spec.*` generated from committed `ResourceTypeSchema` for each `type`; Nix option types for `spec.config.*` generated from the Provider artifact's config schema module (resolved at eval time via `spec.artifactId` from `d2b.artifacts`); credential-ref guard (`credentialRef: true` schema fields accept only `Credential/[a-z][a-z0-9-]*`); build-phase full JSON Schema validation of rendered `spec` against Provider manifest; canonical sorted bundle emission with `contentId` (SHA-256 of sorted `resources` array); configuration service sets `metadata.managedBy: configuration` and `configurationGeneration` in the resource store when persisting activated bundle resources (not in the bundle input; user authors only `type` + `spec`); diff compares new configured set against persisted resource store entries where `managedBy: configuration` (not against the prior bundle file); name-conflict detection: `(type, name)` collision with existing `managedBy: controller` or `managedBy: api` resource → `Degraded/name-conflict` activation item, existing resource untouched, `managedBy` never seized; unchanged-spec resources receive updated `configurationGeneration` with no controller reconcile triggered; absent-resource async Delete with owner-child/finalizer ordering; `Degraded/pending-cleanup` Zone status condition; per-Zone prior bundle count retention (range 1..16, default 3; no TTL); `maxFinalizerDurationSeconds` stall detection and `Degraded/finalizer-timeout` condition (no force-clear)", + "integration" : "NixOS build emits `/etc/d2b/zones//resources.json`; Zone daemon watches path via systemd path unit; reconcile loop runs generation diff on change; Provider controller receives `deletionRequestedAt` watch event when configuration-owned Provider is absent from new bundle", + "removalProof" : "`nixos-modules/manifest.nix` provider-registration rows retired only after all Provider registrations use the bundle format and all consumers (broker, Zone daemon) complete bundle-format migration", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "All eight removed-resource cleanup tests enumerated in [Required tests for removed-resource cleanup](#required-tests-for-removed-resource-cleanup); eval credential-ref guard (test: raw value in `credentialRef` field → NixOS eval error, no bundle emitted); Provider schema conformance golden vector (test: unknown `config` key → build fails); `contentId` determinism (test: two independent builds of identical Nix inputs produce byte-identical bundles)", + "workItemId" : "ADR046-pstate-010" + }, + { + "currentSource" : "`packages/xtask/src/main.rs` (`gen-schemas` and workspace-policy checks, v3 baseline `fd5b0067`); `tests/unit/gates/drift-check.sh` (schema drift gate, same baseline); `packages/d2b-contract-tests/` (contract-test policy, frozen per AGENTS.md)", + "dataMigration" : "Not applicable", + "dependencyOwner" : "ADR046-pstate-003; workspace policy and xtask owners", + "destination" : "`packages/xtask/src/provider_crate_policy.rs`; `tests/unit/gates/provider-crate-layout-check.sh`", + "detailedDesign" : "`cargo xtask check-provider-crate-layout` command: walks every workspace member matching `d2b-provider-*`; for each, asserts presence of `src/`, `tests/`, `integration/`, and `README.md`; asserts `integration/` contains at least one `.rs` file and a `README.md`; fails closed with a typed `missing-provider-crate-path` error listing every absent path; wired into `make test-policy` via `tests/unit/gates/provider-crate-layout-check.sh`; output is machine-readable JSON (`{ \"crate\": \"…\", \"missing\": [\"integration/\"] }` per violation) Primary reuse disposition: `adapt`. Preserved source-plan detail: extend.", + "integration" : "`make test-policy` runs `cargo xtask check-provider-crate-layout`; GitHub CI runs `make test-policy` on every PR; `make check` includes `test-policy` as a required Layer-1 shard; workspace policy tests in `packages/d2b-contract-tests/` are extended with a static manifest check that asserts `provider_crate_layout` policy version is current", + "removalProof" : "Not applicable (permanent gate)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "Policy gate detects missing `src/` → error; missing `tests/` → error; missing `integration/` → error; missing `README.md` → error; empty `integration/` (no `.rs` files) → error; all four paths present and non-empty → pass; existing non-provider `d2b-*` crates not flagged; gate is idempotent across re-runs", + "workItemId" : "ADR046-pstate-011" + }, + { + "currentSource" : "`packages/d2b-core/src/status.rs` (v3 common status/observedGeneration/conditions); Provider descriptor state-namespace declaration from ADR046-pstate-002", + "dataMigration" : "New; no prior bootstrap artifacts to migrate", + "dependencyOwner" : "ADR046-pstate-001, ADR046-pstate-002; Zone runtime owner (`d2b-core-controller`), volume-local Provider owner", + "destination" : "`packages/d2b-core-controller/src/optional_state_admission.rs` (storage-need admission: reject a declared namespace whose payload is derivable from spec/status/core ledger/external observation with `component-state-not-justified`; only declared namespaces produce a Volume; stateless components produce none); `packages/d2b-core-controller/tests/optional_state_admission.rs` (hermetic: stateless component → no Volume; declared `storageNeed` variants accepted; unjustified namespace rejected; status-first restart revalidation — controller re-derives observed state from status/core ledger/external observation after restart and never treats a status field as authority); `packages/d2b-provider-volume-local/tests/status_bounds.rs` (hermetic: total canonical serialized status cap and provider-specific detail cap enforced; oversize status write → typed rejection; status carries no secret/path/argv/PID/unit/stream/ring content)", + "detailedDesign" : "Optional state-Volume admission is a fixed step in Core ProviderDeployment: for each component, if it declares no `stateNamespaces` entry it gets no Volume; for each declared entry, verify the `storageNeed` justification (`secret` | `large-binary` | `private-unsafe-for-status` | `revision-unsuitable`) and reject a namespace whose payload is fully derivable from spec/status/core ledger/external observation with `component-state-not-justified`. Fixed bootstrap components (`system-core`, `system-minijail`, first `volume-local` instance) declare no state Volume and reach Ready using resource `status`, the core Operation ledger, and external observation only; there is no bootstrap-storage mechanism. Status-bound enforcement: reject a status write whose total canonical serialized size exceeds the single canonical status cap, or whose provider-specific detail exceeds the detail cap, or whose condition/count/list/map entries exceed the bounded limits, with the typed status-oversize rejection; status writes occur only on material change.", + "integration" : "`d2b-core-controller` runs optional state-Volume admission before creating any declared Volume and before launching a component Process; the status-bound check is applied on every status subresource write in the resource store.", + "removalProof" : "Not applicable (permanent admission + status-bound enforcement)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-state", + "specPath" : "docs/specs/ADR-046-provider-state.md", + "validation" : "Stateless component → no Volume created; each `storageNeed` variant accepted with a declared Volume; unjustified namespace → `component-state-not-justified`; status-first restart: controller re-derives observed state and reverifies against external reality, never treating status as authority; oversize/over-detail/over-cardinality status write → typed rejection; status contains no secret/path/argv/PID/unit/stream/ring content", + "workItemId" : "ADR046-pstate-012" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "P0; blocks all other runtime-qemu-media work items; owner: `runtime-qemu-media` Provider crate", + "destination" : "packages/d2b-provider-runtime-qemu-media/{src/lib.rs,tests/provider_layout.rs,integration/mod.rs,README.md}", + "detailedDesign" : "Crate scaffold and layout gate: create the crate with the four required paths, commit a README.md stub meeting §1 requirements, and wire the workspace policy gate so the crate cannot land without `src/`, `tests/`, `integration/`, and `README.md`.", + "integration" : "Workspace/Cargo policy consumes the new crate layout; later Guest schema, controller, QMP, Nix, and integration work items build inside this crate.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`make test-policy` (workspace policy gate)", + "workItemId" : "ADR046-qemu-media-001" + }, + { + "currentSource" : "`packages/d2b-core/src/host.rs` — `HostQemuMedia`, `QemuMediaSourceIntent` field names/types only; raw path/credential fields are discarded", + "dataMigration" : "Full d2b 3.0 reset; media guests are reauthored as `Guest`/`Volume`/`Device` resources rather than importing v2 host media config", + "dependencyOwner" : "P0; depends on ADR046-qemu-media-001; owner: runtime-qemu-media type/schema implementation", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/types/guest.rs", + "detailedDesign" : "Guest ResourceType schema and serde: define `GuestSpec`, `GuestStatus`, and `GuestProviderSpecSettings` with serde and `schemars` JSON Schema. Fields must match §4, §5, and §16 exactly. Enforce `bootMediaRef` as a `Volume/` ResourceRef, `removableVolumeRefs` max 4 entries, `providerPhase` max 64 chars with the closed value set, and no argv/path/credential bytes in any serialized type. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt selected baseline field concepts; discard raw paths, argv, and credential-carrying fields.", + "integration" : "Nix-rendered Guest resources and ResourceAPI admission use these types; the controller consumes the validated spec and writes matching status; conformance and schema tests consume the generated schema.", + "removalProof" : "`HostQemuMedia`/`QemuMediaSourceIntent` raw path surfaces are superseded once all media Guest specs use ResourceRefs and schema tests prove no path/argv/credential fields remain", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/guest_schema_roundtrip.rs`; `tests/guest_provider_settings_bounds.rs`", + "workItemId" : "ADR046-qemu-media-002" + }, + { + "currentSource" : "`packages/d2b-core/src/runtime.rs` — timeout/quota concepts only", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "P0; depends on ADR046-qemu-media-001; owner: runtime-qemu-media Provider config/schema implementation", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/config.rs", + "detailedDesign" : "Provider config schema and projection: define `ProviderConfig`, derive JSON Schema, require `controllerExecutionRef`, validate bounds, and project config only to the controller component. Worker processes receive no root config, no ResourceAPI authority, and no d2b-bus authority. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt bounded timeout/quota concepts into v3 Provider config; project only to the controller component.", + "integration" : "Provider ResourceSpec admission validates this schema; ProviderDeployment injects the projected config into the controller; controller uses the provider refs and quotas when reconciling Guest, Volume, Network, Device, Endpoint, and Process resources.", + "removalProof" : "None — config projection is a new Provider resource surface; no prior owner is removed by this item", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/config_schema_projection.rs`", + "workItemId" : "ADR046-qemu-media-003" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — status-first controller state only; no runtime state is migrated into a Provider state Volume", + "dependencyOwner" : "P0; depends on ADR046-qemu-media-001 and ADR046-qemu-media-003; owner: runtime-qemu-media controller descriptor/state implementation", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/{descriptor.rs,state.rs}; no Volume management code for Provider state", + "detailedDesign" : "Controller status-first operational state (no state Volume): controller component descriptor declares an empty `stateNamespaces` list; ProviderDeployment creates no controller state Volume; controller writes reconcile stage, per-Guest launch/adoption observations, bounded counters, and closed-enum error detail to `status` on material change without secrets, paths, argv, PIDs, or unit names; restart re-derives observed state from the Zone resource store, core Operation ledger, and independent external observation with fresh pidfds. Worker Processes and the controller receive no state-Volume mount.", + "integration" : "ProviderDeployment reads the descriptor; the controller projects bounded observations to Guest status and the Operation ledger; restart/adoption logic consumes resource-store, ledger, and external runner observations rather than private state storage.", + "removalProof" : "None — this item prevents creation of a new Provider state Volume and has no prior state owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/state_status_spec.rs`; `tests/state_status_restart.rs`; `tests/state_mount_exclusivity.rs`", + "workItemId" : "ADR046-qemu-media-004" + }, + { + "currentSource" : "`packages/d2b-host/src/qemu_media_argv.rs` — `run_dir` and socket naming pattern only; raw path construction is discarded", + "dataMigration" : "Full d2b 3.0 reset; runtime tmpfs state is ephemeral and not imported from v2 run directories", + "dependencyOwner" : "P1; depends on ADR046-qemu-media-001 and ADR046-qemu-media-003; owner: runtime-qemu-media controller Volume reconciliation", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/controller/volume.rs", + "detailedDesign" : "Runtime tmpfs Volume resource: controller creates the per-Guest runtime tmpfs Volume specified in §6.1. The emitted spec must exactly match the canonical YAML, including all layout entries, views, quota, and `cleanupPolicy: vm-stop-with-proof`. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt naming intent into controller-created Volume resources; replace raw runtime directory paths with `Volume` specs.", + "integration" : "Guest reconcile creates/updates this Volume through the ResourceAPI; `volume-local` materializes the tmpfs and returns attachments to the Process launch flow; finalize proves cleanup before Guest finalization.", + "removalProof" : "Legacy raw run-directory handling from `qemu_media_argv.rs` is superseded once runtime storage is represented only by controller-created Volume resources", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/runtime_volume_spec.rs`; `tests/volume_cleanup_policy.rs`", + "workItemId" : "ADR046-qemu-media-005" + }, + { + "currentSource" : "`packages/d2b-core/src/host.rs` `QemuMediaSourceKind` — media kind enumeration only", + "dataMigration" : "Full d2b 3.0 reset; operator-authored media is declared as Volume resources rather than imported from raw qemu-media source paths", + "dependencyOwner" : "P1; depends on ADR046-qemu-media-002 and ADR046-qemu-media-005; owner: runtime-qemu-media media dependency controller", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/controller/media_watch.rs", + "detailedDesign" : "Media Volume watch and virtio-blk attachment validation: controller watches `bootMediaRef` and `removableVolumeRefs` Volumes for `Ready` status and validates that each has a `virtio-blk` attachment for the owning Guest. It performs no path inspection. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt media kind concepts to Volume source-kind assertions and ResourceRef watches.", + "integration" : "Guest reconcile gates Process launch on watched Volume readiness; Volume attachment status feeds LaunchTicket media fd assembly and Guest conditions.", + "removalProof" : "Legacy media source path handling is superseded once media is delivered only through Volume ResourceRefs and virtio-blk attachments", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/media_volume_watch.rs`; `tests/media_attachment_validation.rs`", + "workItemId" : "ADR046-qemu-media-006" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "P1; depends on ADR046-qemu-media-002; owner: runtime-qemu-media Device dependency controller", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/controller/device_watch.rs", + "detailedDesign" : "KVM Device watch: controller watches `Device/host-kvm` from `spec.deviceAttachments` for `Ready` status and gates runner launch on it, propagating Pending/Ready/Failed transitions to Guest conditions.", + "integration" : "Device resource status drives Guest reconcile dependency gating; a Ready KVM Device contributes the sealed kvm fd slot to the LaunchTicket through the Process provider chain.", + "removalProof" : "None — Device-gated KVM readiness is a new v3 Resource dependency, not a removal item", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/kvm_device_watch.rs`", + "workItemId" : "ADR046-qemu-media-007" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "P1; depends on ADR046-qemu-media-002 and the `display-wayland` Provider dossier; owner: runtime-qemu-media display integration", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/controller/display.rs", + "detailedDesign" : "WaylandSession resource management: when `spec.provider.settings.displayWindow = true`, controller creates, updates, deletes, and watches a `display-wayland.d2bus.org.WaylandSession` resource using the exact ResourceSpec from the display-wayland dossier. It reads the EndpointRef attachment from status using only display-wayland-defined field names. Primary reuse disposition: `create`. Preserved source-plan detail: net-new against the display-wayland Resource contract.", + "integration" : "Guest reconcile produces WaylandSession resources; display-wayland publishes Endpoint attachments; LaunchTicket assembly consumes the display fd only when the session is Ready; finalize deletes the session.", + "removalProof" : "None — display proxy work is delegated to a new WaylandSession Resource dependency rather than removing a baseline owner in this item", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/wayland_session_create.rs`; `tests/wayland_session_attachment_read.rs`; `tests/wayland_session_missing_provider.rs`", + "workItemId" : "ADR046-qemu-media-008" + }, + { + "currentSource" : "`packages/d2b-host/src/qemu_media_argv.rs` fd-index arg shape; `packages/d2b-core/src/processes.rs` `ProcessRole::QemuMediaRunner` sandbox/budget baseline", + "dataMigration" : "Full d2b 3.0 reset; existing QEMU runner process state is not imported and launch state is rebuilt from resources", + "dependencyOwner" : "P1; depends on ADR046-qemu-media-002, ADR046-qemu-media-005, ADR046-qemu-media-006, ADR046-qemu-media-007, ADR046-qemu-media-008, and ADR046-qemu-media-012; owner: runtime-qemu-media Process launch builder", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/controller/process_builder.rs", + "detailedDesign" : "Process spec builder and LaunchTicket attachment resolution: build the canonical `qemu-media-runner` Process ResourceSpec from §10.1 and supply only opaque Network/Endpoint refs to Core's attachment resolver. Core, not the qemu controller, resolves authorized kvm, tap, media, and optional display attachments and seals their fd slots in the LaunchTicket. The qemu Provider/controller receives no broker operation or fd. No raw path, argv, executable path, or principal appears in any public field. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt sandbox/budget concepts to canonical Process resources and Core-sealed LaunchTickets; do not copy raw argv strings or path construction.", + "integration" : "Controller emits Process resources; Core resolves private attachments; system-minijail/Process Provider consumes the sealed LaunchTicket. The NetworkEffectPort adapter transfers its connected tap `OwnedFd` directly to ProviderSupervisor without ResourceAPI, ComponentSession, or d2b-bus serialization; QEMU receives only the declared child fd slot. Endpoint resources represent QMP/serial connections.", + "removalProof" : "`ProcessRole::QemuMediaRunner` and raw qemu-media argv launch surfaces are removable after canonical Process specs and LaunchTickets cover every runner launch", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/process_spec_golden.rs`; `tests/launch_ticket_fd_slots.rs`; `tests/launch_ticket_tap_fd_lifetime.rs`; `tests/no_controller_fd_or_broker_op.rs`; `tests/no_raw_argv_in_spec.rs`", + "workItemId" : "ADR046-qemu-media-009" + }, + { + "currentSource" : "`packages/d2b-host/src/media.rs` QMP command set; `packages/d2b-contracts/src/broker_wire.rs` `QemuMedia*` command payload shapes only", + "dataMigration" : "Full d2b 3.0 reset; no v2 QMP socket path/session state is imported", + "dependencyOwner" : "P1; depends on ADR046-qemu-media-009; owner: runtime-qemu-media QMP client implementation", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/qmp/", + "detailedDesign" : "QMP endpoint attachment handling: consume `qmp` and `serial` Endpoint connection attachments delivered by the ProviderSupervisor ComponentSession channel; implement QMP capability negotiation, command dispatch, and health check using only the delivered fd, never direct socket path access. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt QMP command payloads to internal DTOs; discard broker wire ops and all socket path/fd-open code.", + "integration" : "Process Provider publishes Endpoint attachments; the controller QMP client consumes those fds through ComponentSession; Guest status and health checks reflect QMP outcomes.", + "removalProof" : "`QemuMedia*` broker wire operations are superseded as public control surfaces once QMP is driven solely through Endpoint attachments and internal DTOs", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/qmp_capability_negotiation.rs`; `tests/qmp_command_dispatch.rs`; `tests/qmp_greeting_timeout.rs`; `tests/qmp_health_check.rs`", + "workItemId" : "ADR046-qemu-media-010" + }, + { + "currentSource" : "`packages/d2b-contracts/src/broker_wire.rs` `QemuMediaAttach` and `QemuMediaDetach` command bodies only", + "dataMigration" : "Full d2b 3.0 reset; removable media hotplug state is reconciled from Guest spec and Volume status, not imported from broker op history", + "dependencyOwner" : "P2; depends on ADR046-qemu-media-006 and ADR046-qemu-media-010; owner: runtime-qemu-media hotplug controller", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/controller/hotplug.rs", + "detailedDesign" : "Hotplug attach/detach protocol: on `removableVolumeRefs` update, request a Volume fd from the `volume-local` ComponentSession service and issue `blockdev-add`/`device_add` QMP commands; reverse the sequence for detach; QMP failures set Degraded with `hotplug-media-failed`. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt QMP hotplug command bodies; delete broker op wiring.", + "integration" : "Guest spec updates trigger controller reconcile; volume-local supplies media fds; QMP client executes attach/detach; Guest status records hotplug outcomes.", + "removalProof" : "`QemuMediaAttach`/`QemuMediaDetach` broker operations are removed after hotplug is implemented through Volume fd acquisition plus QMP Endpoint dispatch", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/hotplug_attach_sequence.rs`; `tests/hotplug_detach_sequence.rs`; `tests/hotplug_qmp_failure.rs`", + "workItemId" : "ADR046-qemu-media-011" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "P1; depends on ADR046-qemu-media-002, ADR046-network-005, and Provider config `networkProviderRef`; owner: runtime-qemu-media network dependency integration", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/controller/network.rs", + "detailedDesign" : "Network attachment routing: project each Guest network attachment as an opaque `Network/` ref and condition only. The network-local controller declares the opaque semantic effect; its Core-owned NetworkEffectPort adapter maps it to `CreatePersistentTap`, then `SetBridgePortFlags`, and transfers the already-authorized connected `OwnedFd` directly to ProviderSupervisor for the QEMU Process LaunchTicket. The adapter and supervisor keep `FD_CLOEXEC` set on parent copies; only the declared child slot is made inheritable immediately before exec. On cancellation, ticket rejection, or spawn failure, all fd copies close before generation-fenced `DeletePersistentTap`, and the opaque realization is retained until deletion confirmation. The qemu Provider/controller receives no broker operation, fd, bridge name, or interface name, and the fd is never serialized through ResourceAPI, ComponentSession, or d2b-bus. Primary reuse disposition: `create`.", + "integration" : "Guest `networkAttachments` drive opaque dependency watches; ADR046-network-005 owns the NetworkEffectPort effect chain and ProviderSupervisor owns fd handoff. Process LaunchTicket carries the fd directly to QEMU; Guest conditions report authorization/resolution failures without exposing the fd or broker operation.", + "removalProof" : "None — Core-routed Network attachment resolution is a new v3 dependency path", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/tap_launch_routing.rs` proves `CreatePersistentTap → SetBridgePortFlags → ProviderSupervisor LaunchTicket` ordering; `tests/tap_fd_lifetime.rs` proves CLOEXEC, single child ownership, and close-before-`DeletePersistentTap`; `tests/tap_fd_no_bus_serialization.rs` rejects fd/broker DTOs at the qemu controller boundary; `tests/tap_fd_unavailable.rs` covers authorization and resolution failure", + "workItemId" : "ADR046-qemu-media-012" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; lifecycle state is re-derived from Resource specs/status and Operation ledger rather than imported from v2 daemon state", + "dependencyOwner" : "P1; depends on ADR046-qemu-media-005 through ADR046-qemu-media-012; owner: runtime-qemu-media controller", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/controller/reconcile.rs", + "detailedDesign" : "Reconcile loop and finalize: implement the full async reconcile loop from §11.3 and finalize sequence from §11.4, including dependency gating, providerPhase transitions, condition management, runner exit handling, and WaylandSession cleanup. Primary reuse disposition: `create`. Preserved source-plan detail: net-new reconcile/finalize implementation using the v3 Resource API.", + "integration" : "Resource watches feed the controller; the controller creates/updates/deletes Volume, WaylandSession, Endpoint, and Process resources; Guest status and finalizers expose lifecycle outcomes to core and CLI.", + "removalProof" : "Legacy daemon-owned qemu-media lifecycle paths can be removed once reconcile/finalize owns all Guest lifecycle transitions", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/reconcile_dependency_gating.rs`; `tests/reconcile_runner_exit_handling.rs`; `tests/finalize_sequence.rs`; `tests/finalize_wayland_session_cleanup.rs`", + "workItemId" : "ADR046-qemu-media-013" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — status schema is new v3 observation state; no v2 status import", + "dependencyOwner" : "P1; depends on ADR046-qemu-media-013; owner: runtime-qemu-media status/error implementation", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/controller/status.rs", + "detailedDesign" : "Status, conditions, and error reporting: implement all phase transitions from §16.1, providerPhase values from §16.2, condition types from §16.3, error codes from §16.4, and bounds enforcement on `providerPhase`. Primary reuse disposition: `create`. Preserved source-plan detail: net-new status/error projection for the v3 Guest ResourceType.", + "integration" : "Controller reconcile writes Guest status; ResourceAPI stores bounded status; CLI/support tooling reads status without paths, argv, fds, socket paths, VM names as labels, or secret material.", + "removalProof" : "None — this item adds v3 status projection and does not by itself remove a prior owner", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/status_phase_transitions.rs`; `tests/condition_reason_codes.rs`", + "workItemId" : "ADR046-qemu-media-014" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — audit-only work; no runtime state import", + "dependencyOwner" : "P2; depends on ADR046-qemu-media-013 and ADR046-qemu-media-014; owner: runtime-qemu-media audit integration", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/audit.rs", + "detailedDesign" : "Audit event emission: emit all audit events in §17 and verify that no sensitive fields such as paths, argv, fds, or socket paths appear in any payload. Primary reuse disposition: `create`. Preserved source-plan detail: net-new audit emission for the Provider events in §17.", + "integration" : "Controller lifecycle and QMP/hotplug operations call audit helpers; the audit subsystem records bounded event kinds and outcomes; support tooling consumes redacted payloads.", + "removalProof" : "None — audit helpers are new for this Provider; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/audit_event_shapes.rs`; `tests/audit_no_sensitive_fields.rs`", + "workItemId" : "ADR046-qemu-media-015" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — telemetry-only work; no runtime state import", + "dependencyOwner" : "P2; depends on ADR046-qemu-media-013 and ADR046-qemu-media-014; owner: runtime-qemu-media telemetry integration", + "destination" : "packages/d2b-provider-runtime-qemu-media/src/telemetry.rs", + "detailedDesign" : "Metrics and OTEL spans: implement all metrics from §18 and OTEL trace spans with structural closed-label enforcement and no Zone/VM/resource name, user identity, path, or other sensitive value in any metric label. Span attributes use only the exact fixed semantic fields and `outcome` listed in §18; no resource name, UID, shortened UID, digest, ref, or derived identity is admitted. Retain identity only in allow-listed OTEL Resource attributes and permitted bounded audit fields. Primary reuse disposition: `create`. Preserved source-plan detail: net-new telemetry emission for the Provider metrics and spans in §18.", + "integration" : "Controller, QMP, hotplug, and dependency-watch paths call telemetry helpers; OTEL/metrics exporters consume only closed, bounded labels for support dashboards.", + "removalProof" : "None — telemetry helpers are new for this Provider; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/metrics_label_cardinality.rs` asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus Guest/Zone-name canary absence; `tests/otel_span_attributes.rs` asserts the exact per-span semantic allowlist, preserves allowed OTEL Resource identity attributes, and rejects Zone/Guest/Process/Provider-resource names, refs, UIDs, shortened UIDs, digests, and identity canary values in span attributes", + "workItemId" : "ADR046-qemu-media-016" + }, + { + "currentSource" : "`nixos-modules/components/qemu-media.nix` option names only; `nixos-modules/assertions.nix` assertion framework", + "dataMigration" : "Full d2b 3.0 reset; users reauthor qemu-media configuration as v3 resources and raw path options are not imported", + "dependencyOwner" : "P1; depends on ADR046-qemu-media-002 and ADR046-qemu-media-003; owner: Nix resource compiler and runtime-qemu-media options", + "destination" : "nixos-modules/options-guest-qemu-media.nix; nixos-modules/assertions.nix", + "detailedDesign" : "Nix module and assertions: implement the Guest resource declaration from §19 and eval-time assertions from §19.8. Rewrite qemu-media options as v3 spec fields and reject raw path options. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt option names into v3 Guest/Provider spec fields; remove raw path options; extend existing assertion predicates.", + "integration" : "Nix authoring emits Provider, Guest, Volume, and Device resource JSON; assertions fail invalid configs before build; emitted resources feed ResourceAPI admission and controller reconcile.", + "removalProof" : "`nixos-modules/components/qemu-media.nix` raw path option surface is superseded once v3 Guest resource emission and assertions cover the configuration", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`tests/unit/nix/cases/guest-qemu-media-spec.nix`; `tests/assertions-eval.sh` new assertion cases", + "workItemId" : "ADR046-qemu-media-017" + }, + { + "currentSource" : "d2b-provider-toolkit conformance kit", + "dataMigration" : "None — test-only work; no runtime state import", + "dependencyOwner" : "P2; depends on ADR046-qemu-media-013 through ADR046-qemu-media-016; owner: runtime-qemu-media conformance tests", + "destination" : "packages/d2b-provider-runtime-qemu-media/tests/conformance_guest.rs", + "detailedDesign" : "d2b-provider-toolkit conformance: pass the Provider conformance kit for the Guest ResourceType axis, including reconcile/finalize contract, phase machine, condition typing, audit shape, and telemetry cardinality. Primary reuse disposition: `adapt`. Preserved source-plan detail: reuse conformance harness; add runtime-qemu-media Guest ResourceType coverage.", + "integration" : "Conformance tests instantiate the Provider against fake ResourceAPI/ComponentSession dependencies and verify the public Provider contract consumed by core CI.", + "removalProof" : "None — conformance coverage is additive test proof", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`make test-rust` (runs conformance suite)", + "workItemId" : "ADR046-qemu-media-018" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — test-only work; no runtime state import", + "dependencyOwner" : "P2; depends on ADR046-qemu-media-005 through ADR046-qemu-media-018; owner: runtime-qemu-media integration fixtures", + "destination" : "packages/d2b-provider-runtime-qemu-media/integration/", + "detailedDesign" : "Integration tests: implement container/fake-Host scenarios for full reconcile from Created to Ready with fake dependencies, finalize sequence, hotplug attach/detach, and restart recovery. Primary reuse disposition: `create`. Preserved source-plan detail: net-new integration fixtures.", + "integration" : "Integration fixtures launch the Provider with fake or containerized Host/Guest/Volume/Network/Device dependencies; CI `make test-integration` consumes the fixtures as the cross-process proof lane.", + "removalProof" : "None — integration coverage is additive test proof", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-runtime-qemu-media", + "specPath" : "docs/specs/providers/ADR-046-provider-runtime-qemu-media.md", + "validation" : "`make test-integration`", + "workItemId" : "ADR046-qemu-media-019" + }, + { + "currentSource" : "`packages/d2b-realm-router/src/lib.rs`, `mux_session.rs`, `session_lifecycle.rs`; `packages/d2bd/src/supervisor/dag.rs`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "W0/W1a; controller toolkit owner", + "destination" : "`packages/d2b-controller-toolkit/src/lib.rs`, `runner.rs`, `queue.rs`, `context.rs`, `result.rs`", + "detailedDesign" : "Async ResourceReconciler, watch receiver, coalescing, per-resource serialization, parallel tasks, retry/checkpoint/finalize; expedited priority lane and `CommittedRevisionProof`-gated effects (D090); `assess_update`/`plan_upgrade`/`execute_upgrade` methods serialized in the same single-flight (D091) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Provider controller binaries wrap handlers with toolkit", + "removalProof" : "Current per-role orchestration removed only after ResourceType successors", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resource-reconciliation", + "specPath" : "docs/specs/ADR-046-resource-reconciliation.md", + "validation" : "Golden state-machine vectors, deterministic clocks, conflict/restart/queue tests; D090: commit-fails/Abort → no effect, controller finishes-before-commit gated on proof, effects-gate, status-write-delayed (`statusPersistence: pending`), normal-queued no-op/rejoin, concurrent mutation, delete event-only projection, expedited timeout committed-but-pending, restart re-entry no duplicate; D091: current/non-disruptive/each-trigger assess, UpgradeRequired-not-in-place, dependency propagation/topological drain-recycle-restart, GPU blocking, state/TPM preservation, crash/re-entry resume, single-flight reconcile-vs-upgrade serialization", + "workItemId" : "ADR046-reconcile-001" + }, + { + "currentSource" : "`d2b-realm-core/src/route_engine.rs`, `allocator_engine.rs`; `d2b-realm-router/tests/transport_topology_harness.rs`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Store/API + ADR046-reconcile-001; core controller", + "destination" : "`packages/d2b-core-controller/src/hints.rs`, `dependencies.rs`, `owner_reconcile.rs`", + "detailedDesign" : "Watch-plan validation, indexes, suppression, owner/dependency hints, leases, startup relist, fair admission", + "integration" : "Store post-commit dispatcher → d2b-bus controller streams", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resource-reconciliation", + "specPath" : "docs/specs/ADR-046-resource-reconciliation.md", + "validation" : "Owner/dependency chains, suppression/no-loss, restart/relist, lease withdrawal", + "workItemId" : "ADR046-reconcile-002" + }, + { + "currentSource" : "`d2bd/src/supervisor/dag.rs`, `pidfd.rs`, unsafe-local blocked supervisor, guest exec runner", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Process Providers + benchmark owner", + "destination" : "`packages/d2b-controller-toolkit/benches/reaction.rs`, Process Provider integration tests", + "detailedDesign" : "Commit-to-handler/launch fast path, nonblocking watch, parallel ready resources", + "integration" : "Resource store → bus/session → controller → Process effect/status", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resource-reconciliation", + "specPath" : "docs/specs/ADR-046-resource-reconciliation.md", + "validation" : "Hard <=5 ms/<=20 ms p95 gates and 1/10/100 Process concurrency", + "workItemId" : "ADR046-reconcile-003" + }, + { + "currentSource" : "`packages/d2bd/src/storage_lifecycle.rs` (`run_startup_contract_check`, bundle-versioned contract validation pattern); `packages/d2bd/src/ownership_preflight.rs` (`EntrySpec`, legacy-recovery-artifact optionality); `packages/d2b/src/lib.rs` `build_storage_migration_plan`/`storage_migration_checkpoint_id`", + "dataMigration" : "New; no prior inventory/snapshot format exists", + "dependencyOwner" : "W0 shared contract root; storage/broker integrator", + "destination" : "`packages/d2b-cutover/src/{inventory,snapshot,checkpoint}.rs`", + "detailedDesign" : "Implement the seven closed inventories of [Authoritative inventories](#authoritative-inventories); the `checkpoint_id` digest algorithm of [Preflight and immutable snapshot](#preflight-and-immutable-snapshot); the atomic snapshot-write sequence (temp file, fsync, rename, parent fsync, post-rename immutability) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "`d2b host cutover preflight`/`plan` CLI commands consume this crate exclusively; no other crate re-implements inventory walking", + "removalProof" : "Not applicable (net-new capability)", + "reuseAction" : "adapt", + "reuseSource" : "None from main; this is a v3-only cross-cutting concern with no main-branch equivalent", + "specId" : "ADR-046-reset-and-cutover", + "specPath" : "docs/specs/ADR-046-reset-and-cutover.md", + "validation" : "`checkpoint_id` determinism property test; snapshot atomic-write crash-injection test; `cutover_preflight_refuses_dirty_flake_check`", + "workItemId" : "ADR046-reset-001" + }, + { + "currentSource" : "`nixos-modules/bundle-artifacts.nix`, `nixos-modules/assertions.nix` (existing eval-time validation precedent); `ADR-046-nix-configuration` \"Bundle and generation emission\"", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-reset-001; `d2b-resource-store-redb` owner; `d2b-provider` catalog owner", + "destination" : "`packages/d2b-cutover/src/{bundle_validate,trust_preflight}.rs`", + "detailedDesign" : "Independent legacy-flake-check gate; candidate v3 bundle schema/cross-ref/determinism validation per [Config/artifact/schema validation](#configartifactschema-validation); Provider trust preflight per `ADR-046-provider-model-and-packaging` \"Trust\"", + "integration" : "Invoked by `preflight` before the snapshot is written; failures block `plan` from being offered", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "None from main", + "specId" : "ADR-046-reset-and-cutover", + "specPath" : "docs/specs/ADR-046-reset-and-cutover.md", + "validation" : "`cutover-candidate-bundle-validation.nix`; trust-preflight rejection tests for each of digest/publisher/signature/deny/provenance/conformance failure modes", + "workItemId" : "ADR046-reset-002" + }, + { + "currentSource" : "`packages/d2b/src/lib.rs` `require_explicit_mutation_flag`, `cmd_host_destroy` (dry-run/apply precondition pattern); [ADR 0040](../adr/0040-graceful-vm-shutdown.md) graceful shutdown path", + "dataMigration" : "Destructive; this is where Phase 3/4 boundary-of-no-return-approach begins (rollback still open through end of Phase 4)", + "dependencyOwner" : "ADR046-reset-001, ADR046-reset-002; Process/Guest lifecycle owner", + "destination" : "`packages/d2b-cutover/src/{consent,drain,disposition}.rs`", + "detailedDesign" : "Exact-consent-phrase gate bound to `checkpoint_id`; Phase 3 drain algorithm (§ [Old daemon/unit/process drain](#old-daemonunitprocess-drain)); the [Disposition framework](#disposition-framework)'s Adopt/Preserve/Destroy executor, delegating every Adopt to ADR046-reset-004 Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "`d2b host cutover apply` orchestrates drain then disposition execution then hands off to Phase 5 (ADR046-reset-005)", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "None from main", + "specId" : "ADR-046-reset-and-cutover", + "specPath" : "docs/specs/ADR-046-reset-and-cutover.md", + "validation" : "`cutover_apply_requires_exact_consent_phrase`; `cutover_drain_refuses_on_live_process`", + "workItemId" : "ADR046-reset-003" + }, + { + "currentSource" : "`packages/d2b-priv-broker/src/ops/swtpm_dir.rs` (marker fail-closed pattern); `packages/d2b-host/src/hardlink_farm.rs` (same-filesystem rename pattern)", + "dataMigration" : "This work item *is* the data migration mechanism for TPM/store-view/disk-image/unsafe-local-scope bytes", + "dependencyOwner" : "ADR046-reset-003; `ADR-046-provider-state` owner; `device-tpm`/`volume-local` Provider owners", + "destination" : "`packages/d2b-cutover/src/adopt.rs`, thin wrapper invoking `ADR-046-provider-state`'s migration `EphemeralProcess` prepare/stage/commit/rollback machinery with cutover-specific source paths", + "detailedDesign" : "One Adopt invocation per matrix row tagged `Adopt` in the [migration/disposition matrix](#migrationdisposition-matrix); marker re-validation before every step; new-marker-before-old-removal ordering; idempotent re-run safety per [Crash/power-loss/retry/idempotency journals](#crashpower-lossretryidempotency-journals)", + "integration" : "Called by ADR046-reset-003's disposition executor for every Adopt row; writes to the state Volumes ADR046-device-tpm-004/ADR046-vl-004/ADR046-vl-006 define", + "removalProof" : "Not applicable (the mechanism is retained permanently for later Full/Provider/Guest reset relocation use, not retired after first use)", + "reuseAction" : "adapt", + "reuseSource" : "None from main", + "specId" : "ADR-046-reset-and-cutover", + "specPath" : "docs/specs/ADR-046-reset-and-cutover.md", + "validation" : "Crash-injection at every step boundary (Type 10 `cutover-crash-resume.nix`); TPM/durable-Volume Destroy-exclusion property test", + "workItemId" : "ADR046-reset-004" + }, + { + "currentSource" : "None (bootstrap sequencing over Zone runtime startup, which is itself ADR-only)", + "dataMigration" : "Destructive v3 bootstrap; no v2 resource import (per `ADR046-store-003`, `ADR046-object-001`)", + "dependencyOwner" : "ADR046-reset-004; `d2b-resource-store-redb` owner (`ADR046-store-003`); core-controller owner (`ADR046-core-001`)", + "destination" : "`packages/d2b-cutover/src/{store_bootstrap,provider_sequence}.rs`", + "detailedDesign" : "Phase 5 store creation per [Resource-store initialization](#resource-store-initialization); Phase 6 topological Provider install per [Provider install/topological start](#provider-installtopological-start), including the fixed staged default order and cycle-rejection check", + "integration" : "Invoked immediately after ADR046-reset-003/004 complete; hands off to Phase 7 (ADR046-reset-006)", + "removalProof" : "Not applicable", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-reset-and-cutover", + "specPath" : "docs/specs/ADR-046-reset-and-cutover.md", + "validation" : "Provider install topological-order determinism test; cycle-rejection test; store-identity mismatch fail-closed test", + "workItemId" : "ADR046-reset-005" + }, + { + "currentSource" : "ADR 0032 gateway guest custody evidence (`d2b-realm-router/src/service_v2.rs` `CredentialCustody`); frozen target model in `ADR-046-resources-zone-control` §3.1/§10.3 and `ADR-046-nix-configuration` \"Zone declaration\"/\"ZoneLink\"", + "dataMigration" : "None (the child-local ZoneLink is ordinary Nix-authored configuration, `parentZone` is recompiled into sealed allocator state, and neither is migrated credential material)", + "dependencyOwner" : "ADR046-reset-005; `ADR-046-zone-routing` owner; `ADR-046-resources-zone-control` owner", + "destination" : "`packages/d2b-cutover/src/{zonelink_cutover,guest_activation}.rs`", + "detailedDesign" : "Phase 7 translation from `EntrypointMode::GatewayBacked`/`HostResident` per [Zone/ZoneLink cutover](#zonezonelink-cutover): the child authors/stores one local uplink with self-matching `childZoneName`; compiler-only `parentZone` selects the allocator; the parent retains only sealed allocator/route state and owns no reciprocal row or ZoneLink handler. Phase 8 follows Network→Volume→Device→Guest ordering per [Guest/runtime/network/store view activation](#guestruntimenetworkstore-view-activation). The parent inventory never enumerates gateway-guest-internal credential/audit state", + "integration" : "Consumes Providers installed by ADR046-reset-005; hands off to ADR046-reset-007 (verification)", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "None from main", + "specId" : "ADR-046-reset-and-cutover", + "specPath" : "docs/specs/ADR-046-reset-and-cutover.md", + "validation" : "Gateway-custody-boundary test asserting the parent inventory never contains a gateway-guest-internal path; child-local ZoneLink test asserting one uplink, self-matching `childZoneName`, and child-store ownership; compiler test asserting `parentZone` selects the allocator but appears only in sealed bootstrap state; no-reciprocal-parent-row/no-parent-handler test; child-local ZoneLink `Degraded/waiting-on-remote` non-blocking test", + "workItemId" : "ADR046-reset-006" + }, + { + "currentSource" : "ADR 0034 degraded-state ledger taxonomy and repair-never-trusts-ledger-paths invariant", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-reset-006; telemetry-audit-and-support owner (`d2b-audit`)", + "destination" : "`packages/d2b-cutover/src/{verify,doctor,degraded}.rs`", + "detailedDesign" : "The ten `verify` checks in [Post-cutover verification](#post-cutover-verification); the `cutover-quarantined` degraded class and `doctor` reporting in [Failure/quarantine/manual recovery](#failurequarantinemanual-recovery); audit chain closure/genesis-record cross-check (check 9) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "`d2b host cutover verify`/`doctor` CLI commands; consumed by the Phase 10 finalize gate table", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "None from main", + "specId" : "ADR-046-reset-and-cutover", + "specPath" : "docs/specs/ADR-046-reset-and-cutover.md", + "validation" : "Injected-digest-mismatch test for TPM/durable-Volume verify checks; audit-genesis-cross-check test; `cutover-full-rehearsal.nix`", + "workItemId" : "ADR046-reset-007" + }, + { + "currentSource" : "`ADR-046-cli-and-operations` \"Removal notes\" (live-successor-before-deletion criterion)", + "dataMigration" : "This work item is where every previously-Preserved legacy artifact is finally Destroyed, one gate at a time", + "dependencyOwner" : "ADR046-reset-007; owner of each retiring artifact's ADR 0046 successor work item", + "destination" : "`packages/d2b-cutover/src/finalize.rs`", + "detailedDesign" : "Per-candidate independent gate evaluation exactly as tabled in [Old artifact/unit/schema removal gates](#old-artifactunitschema-removal-gates); separate consent phrase from `apply`; never partial-destroys a candidate", + "integration" : "`d2b host cutover finalize` CLI command; reads gate status from ADR046-reset-007's verify results plus each named policy-lint/integration test's pass/fail recorded in CI", + "removalProof" : "Each candidate's own row in [Old artifact/unit/schema removal gates](#old-artifactunitschema-removal-gates) states its exact removal proof", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-reset-and-cutover", + "specPath" : "docs/specs/ADR-046-reset-and-cutover.md", + "validation" : "`policy_no_destroy_without_gate`; `policy_legacy_cli_verbs_absent_after_gate`; `tpm-adopt-retirement.nix`", + "workItemId" : "ADR046-reset-008" + }, + { + "currentSource" : "ADR 0034 \"dry-run and preflight output print the checkpoint id and exact rollback command before any apply step begins\"", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-reset-003 through ADR046-reset-006; `ADR-046-provider-state` incident-hold owner", + "destination" : "`packages/d2b-cutover/src/{journal,rollback,hold}.rs`", + "detailedDesign" : "Append-only journal per [Crash/power-loss/retry/idempotency journals](#crashpower-lossretryidempotency-journals); [Rollback boundary](#rollback-boundary) enforcement (`cutover-rollback-window-closed` past phase 4); cutover-wide incident hold per [Incident hold (cutover-wide)](#incident-hold-cutover-wide)", + "integration" : "`d2b host cutover rollback`/`hold` CLI commands; consulted by ADR046-reset-003's disposition executor and ADR046-reset-008's finalize gate before every mutating step", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "None from main", + "specId" : "ADR-046-reset-and-cutover", + "specPath" : "docs/specs/ADR-046-reset-and-cutover.md", + "validation" : "`cutover_rollback_window_closes_after_phase_5`; incident-hold-blocks-destructive-step test", + "workItemId" : "ADR046-reset-009" + }, + { + "currentSource" : "`ADR-046-resources-zone-control` §2.6 (`core.zone-drain` finalizer algorithm), §9.4 (out-of-band destructive reset, uid=0 authentication)", + "dataMigration" : "None (this is a post-cutover recovery operation, not part of the cutover data migration itself)", + "dependencyOwner" : "ADR046-reset-005 through ADR046-reset-007; `ADR-046-resources-zone-control` owner (§2.6/§9.4)", + "destination" : "`packages/d2b-cutover/src/reset_scope.rs`; `d2b host reset` CLI command", + "detailedDesign" : "The three reset scopes and their comparison table in [Full Zone reset vs Provider reset vs Guest reset](#full-zone-reset-vs-provider-reset-vs-guest-reset); durable-Volume preserve-by-default with explicit `--destroy-durable-volumes`/`--destroy-volumes` opt-in; OS-level authentication for the zone scope only", + "integration" : "Standalone from the cutover Phases 0-10 above; usable at any later time as a recovery/maintenance lever once a Zone exists", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "None from main", + "specId" : "ADR-046-reset-and-cutover", + "specPath" : "docs/specs/ADR-046-reset-and-cutover.md", + "validation" : "`host_reset_scope_isolation`; `zone-provider-guest-reset-isolation.nix`; durable-Volume-preserved-by-default property test for both Provider and Guest scopes", + "workItemId" : "ADR046-reset-010" + }, + { + "currentSource" : "`tests/integration/live/` conventions; `tests/host-integration/hardware/` conventions", + "dataMigration" : "None (validation only)", + "dependencyOwner" : "ADR046-reset-001 through ADR046-reset-010, fully landed", + "destination" : "`tests/integration/live/cutover-real-host.sh`, `tests/integration/live/cutover-real-host-cloud-guest.sh`, `tests/host-integration/hardware/cutover-real-tpm.sh`, `tests/host-integration/hardware/cutover-real-usbip-security-key.sh`", + "detailedDesign" : "Manual, `D2B_LIVE=1`/hardware-gated validation scripts described in [Tests](#tests) Type 11/12 rows; never run in CI; require operator sign-off and an independent out-of-band backup before execution", + "integration" : "Run manually by an operator against a real host/device before the reset-and-cutover implementation is declared production-ready", + "removalProof" : "Not applicable", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-reset-and-cutover", + "specPath" : "docs/specs/ADR-046-reset-and-cutover.md", + "validation" : "Manual pass/fail sign-off recorded per the project's existing live-host/hardware validation conventions", + "workItemId" : "ADR046-reset-011" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-session/` and `packages/d2b-contracts/src/v2_component_session.rs` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 session runtime state/config import", + "dependencyOwner" : "ADR046-telem-001 (d2b-telemetry must exist first for MetricsSink injection); session owner", + "destination" : "`packages/d2b-session/` copied verbatim; v3 contract extension renames endpoint purpose values while preserving backward wire compatibility during transition.", + "detailedDesign" : "Noise handshake, record protection, scheduler, named streams, cancellation, attachments, deadlines, metrics injection, ttrpc serving, and generation-discovery behavior from the selected source. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy/adopt the core ComponentSession runtime; adapt EndpointPurpose/ServicePackage/RealmSessionAuthority names listed in Excluded ADR45 assumptions and inject a `d2b-telemetry` MetricsSink..", + "integration" : "`d2b-bus` route handler calls `serve_ttrpc_services`; `d2b-session-unix` provides `OwnedTransport` impl; `d2b-telemetry` `MetricsSink` impl feeds `d2b_session_*` metrics inventory from this spec.", + "removalProof" : "ADR45 topology enum names listed in Excluded ADR45 assumptions are adapted; no prior v3 session runtime owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "Adopt `tests/component_session.rs` and `tests/noise_vectors.rs` unchanged; extend with v3 `EndpointPurpose` enum gate test; add `d2b-contract-tests/tests/component_session_v2_vectors.rs` (existing at `a1cc0b2d`) as-is.", + "workItemId" : "ADR046-reuse-001" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-session-unix/` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 transport runtime state/config import", + "dependencyOwner" : "ADR046-reuse-001; Unix transport owner", + "destination" : "`packages/d2b-session-unix/` copied verbatim.", + "detailedDesign" : "SO_PASSCRED verification, pidfd identity verification, multi-scope credit reservation, emergency headroom, seqpacket atomic CLOEXEC transfer, stream reassembly, vsock transport, and descriptor policy enforcement. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy verbatim; retain `host-socket`/`native-vsock` feature flags; no ADR45 topology adaptation required..", + "integration" : "`d2b-bus` Zone-local listeners use `UnixSeqpacketTransport`; Provider agent connections use vsock transport from this crate; `CreditPool`/`CreditScopeSet` enforce per-Zone attachment FD budgets.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "Adopt all `unix_session.rs` tests unchanged.", + "workItemId" : "ADR046-reuse-002" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-client/` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 client state/config import", + "dependencyOwner" : "ADR046-reuse-001; client owner", + "destination" : "`packages/d2b-client/` copied; DaemonClient/GuestClient adapted to v3 service packages; `MetadataInput::with_trace` drives `TraceContext` propagation.", + "detailedDesign" : "MetadataInput trace/correlation/idempotency, retry policy, cancellation token, generic client over resolver/connector/clock, typed service proxies, stream terminal, and host-socket verifier. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy client layer; adapt service package/method sets, local-root Zone identity, and TraceContext propagation for v3..", + "integration" : "Every controller/service that makes outbound calls uses `Client`; `MetadataInput::with_trace` feeds `d2b_api_request_duration_seconds` trace-id into `d2b.bus.route` span.", + "removalProof" : "ADR45 Daemon/Guest wrappers and RealmPath identity assumptions are adapted to v3 service packages/Zone name; no prior v3 client owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "Adopt typed-route, proxy-reuse, and cancel tests unchanged. Add v3 service-package name gate test.", + "workItemId" : "ADR046-reuse-003" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-provider/` and `packages/d2b-provider-toolkit/` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 ProviderRegistry runtime state import", + "dependencyOwner" : "ADR046-reuse-001 + ADR046-reuse-003; Provider owner", + "destination" : "`packages/d2b-provider/` and `packages/d2b-provider-toolkit/` copied with v3 session admission and bus routing adaptations.", + "detailedDesign" : "ProviderRegistry factory registration, bounded admission, RPC proxy fail-closed behavior, generated provider service admission/dispatch, redaction wrappers, and conformance gates. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy provider runtime/toolkit; adapt SessionIdentity Zone name and route through `d2b-bus` instead of daemon embedding..", + "integration" : "Each v3 Provider process embeds `ProviderRegistry` + `GeneratedProviderServiceServer`; `check_provider_conformance` runs in Provider install-time conformance check (feeds `d2b_provider_reconcile_total{outcome=\"error\"}` on failure).", + "removalProof" : "Daemon-embedded ADR45 ProviderRegistry wiring is excluded; v3 provider-process registry replaces it without importing state.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "Adopt all `conformance.rs` and `runtime.rs` tests unchanged. Add v3 `SessionIdentity` zone-name gate. Add conformance-failure → `d2b_provider_reconcile_total` metric integration test.", + "workItemId" : "ADR046-reuse-004" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-gateway-runtime/src/provider_agent.rs` at `a1cc0b2d` plus `tests/provider_agent_v2.rs`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 provider-agent runtime state import", + "dependencyOwner" : "ADR046-reuse-004 + ADR046-audit-003; Provider agent / observability-otel owner", + "destination" : "`packages/d2b-provider-observability-otel/src/agent.rs` adapted; ComponentSessionDriver mock becomes shared Provider session fixture.", + "detailedDesign" : "Session-bound ProviderAgentProcess construction, bounded audit snapshot, closed-set ProviderAgentError paths, and full ComponentSessionDriver mock for hermetic Provider tests. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt provider-agent session wrapper and audit ring; replace gateway-runtime audit envelope with v3 `d2b-audit` records..", + "integration" : "`observability-otel` Provider embeds a `ProviderAgentProcess`; session connect/disconnect emits `SessionConnect` audit records via `d2b-audit`; `ProviderAgentAuditEvent` ring feeds `d2b_provider_reconcile_total` metric on session error.", + "removalProof" : "ADR45 gateway-runtime AuditEnvelope/realm custody and launch path are excluded; v3 provider agent is supervised by Provider/system-core or the owning Provider supervisor.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "Adopt `provider_agent_v2.rs` mock harness unchanged as shared v3 Provider session fixture. Add v3 audit-bridge test: provider-agent session → `SessionConnect{transport_class=\"zone_link\"}` record emitted.", + "workItemId" : "ADR046-reuse-005" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-realm-router/` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 routing state import", + "dependencyOwner" : "ADR046-reuse-001 + ADR046-bus-001; bus routing owner", + "destination" : "`packages/d2b-bus/src/routing.rs` adapted from `service_v2.rs`; RemoteNodeErrorKind stable code pattern becomes v3 BusErrorKind.", + "detailedDesign" : "Realm/Zone service ttrpc handler table, session authority constructors, host/gateway credential custody split, stable low-cardinality error codes, and binding/shortcut bounds. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt realm service routing to Zone service routing; preserve credential-custody, bounded routing, and stable error-code behavior..", + "integration" : "`d2b-bus` route handler adapts `RealmServiceServer` dispatch table; `RemoteNodeErrorKind::code()` values feed `d2b_bus_route_total{outcome}` metric labels; `CredentialCustody::Host` maps to `purpose_class=local` in `d2b_session_connect_total`.", + "removalProof" : "ADR45 RealmId and RealmService names are adapted to Zone/resource UID routing; no prior v3 bus routing owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "Adopt `authority_keeps_remote_credentials_in_gateway_guests` test renamed to `authority_keeps_remote_credentials_in_zone_link_sessions`; adapt `RealmId` → Zone name; adopt `authenticated_bootstrap_enrollment_route_and_shortcut_lifecycle` renamed with zone terminology.", + "workItemId" : "ADR046-reuse-006" + }, + { + "currentSource" : "Main commit source row below: `packages/d2bd/` routing/provider effects files and tests at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 daemon routing/provider-effects state import", + "dependencyOwner" : "ADR046-reuse-004 + ADR046-reuse-006; core-controller routing owner", + "destination" : "`packages/d2b-bus/src/service_router.rs` and `packages/d2b-core-controller/src/provider_effects.rs`.", + "detailedDesign" : "Closed-set service.package route gates, provider lifecycle effect-adapter composition, closed-set error mapping, fixed local policy invariants, and bootstrap/enrolled credential binding shape. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt route-gate and ProviderLifecycleDispatch patterns; exclude daemon-embedded effect composition and route through v3 bus/provider processes..", + "integration" : "Bus service router uses `service.package` closed-set matching from route-gate pattern; `ProviderLifecycleDispatch` feeds `d2b_provider_component_phase` metric.", + "removalProof" : "ADR45 daemon-embedded effect adapters and realm child supervisor assumptions are excluded; v3 bus/provider effects own the adapted behavior.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "Port `local_daemon_policy_is_fixed_and_has_no_negotiation_or_fd_surface` invariant to v3 bus local policy test; port `every_generated_daemon_method_has_one_typed_adapter` to v3 bus method adapter completeness test.", + "workItemId" : "ADR046-reuse-007" + }, + { + "currentSource" : "Main commit source row below: ComponentSession vector/conformance tests at `a1cc0b2d`.", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-reuse-001; contract-tests owner", + "destination" : "`packages/d2b-contract-tests/tests/component_session_v2_vectors.rs` and `tests/noise_vectors.rs` copied verbatim.", + "detailedDesign" : "Pinned Noise KAT vectors, transcript/PSK mutation rejection, typed bootstrap admission fixture, and public-key corruption rejection remain the session wire security ground truth. Primary reuse disposition: `adapt`. Preserved source-plan detail: Copy the vector and conformance tests verbatim; add v3 constant gate for ComponentSession major/minor version..", + "integration" : "These tests run in `make test-rust` / `cargo test -p d2b-contract-tests` and `cargo test -p d2b-session`. They are gating for any Noise library update.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "These tests are self-validating. Add one gate: assert `COMPONENT_SESSION_MAJOR = 2` and `COMPONENT_SESSION_MINOR = 0` constants are unchanged in v3 contract.", + "workItemId" : "ADR046-reuse-008" + }, + { + "currentSource" : "Main commit source row below: `packages/d2b-session/src/metrics.rs` and `MetricLabels` in `packages/d2b-contracts/src/v2_component_session.rs` at `a1cc0b2d`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 telemetry metric state import", + "dependencyOwner" : "ADR046-reuse-001 + ADR046-telem-001 + ADR046-telem-003; telemetry/session owner", + "destination" : "`packages/d2b-telemetry/src/session_metrics_sink.rs`.", + "detailedDesign" : "Map every MetricEvent variant to the `d2b_session_*` inventory; convert MetricLabels closed sets to allowed v3 label strings; keep NoopMetrics for hermetic tests. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt MetricsSink bridge to v3 OTEL instruments and rename ADR45 endpoint-purpose label values to v3 closed-set labels..", + "integration" : "`serve_ttrpc_services` receives a `Box` from `d2b-telemetry`; all session endpoints call through this bridge.", + "removalProof" : "ADR45 EndpointPurpose metric label strings are replaced by v3 purpose names; no prior v3 MetricsSink bridge owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "New test `packages/d2b-telemetry/tests/session_sink_bridge.rs`: drive `MetricEvent` variants through the sink; assert OTEL counter/gauge values; assert `MetricLabels` closed-set values map only to allowed label strings (no `DaemonLocal` string in v3 metric output).", + "workItemId" : "ADR046-reuse-009" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/routing.rs`: `RouteAdvertisement`, `RouteNamespaceAllocation`, `RealmTreeEdge`, `DescendantRoute`, `TreeRoutePath`, `TreeRouteHop`, `RouteFailClosedReason`, `DirectShortcutAuthorizationRequest`, `DirectShortcutAuthorizationDecision`, `DirectShortcutTeardown`, `DirectShortcutTeardownReason`, `RouteAuditEventKind`, `RouteRealmClass`, `RoutePlacementClass`, `RouteAuditEventMetadata`, all route newtypes; `packages/d2b-realm-core/src/realm.rs`: `RealmPath`, `MAX_REALM_LABELS`, `MAX_REALM_PATH_BYTES`, `RealmControllerPlacement`, `EntrypointMode`; `packages/d2b-realm-core/src/ids.rs`: `RealmId`, `RouteId`, `ControllerGenerationId`, `WorkloadId`, `NodeId`, `ProviderId` (evidence: **A** for ids.rs — used in production; **B** for routing.rs — types exist with tests but no production daemon routing callers)", + "dataMigration" : "Full reset; no v2 Realm route compatibility", + "dependencyOwner" : "W0/W1a; zone routing contract owner", + "destination" : "`packages/d2b-contracts/src/v3/zone_routing.rs`", + "detailedDesign" : "Rename RealmPath → ZonePath, RealmId → ZoneLabelId, RouteId → ZoneRouteId, ControllerGenerationId → ZoneLinkControllerGeneration; preserve all bounds/validation/serde; add ZoneLink-specific advertisement envelope fields (v3 schema version and private allocated-capability field); preserve `RouteFailClosedReason` + add `zone-link-disconnected`, `hop-limit-exceeded`, `relay-denied`, `attachment-not-permitted-over-zone-link`; freeze v3 protobuf numbers separately from v2 Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "d2b-bus zone route engine and ZoneLink controller consume these types", + "removalProof" : "v3 old `RealmPath` route types retired after zone-routing engine is live and all callers switched", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 baseline commit `b5ddbed6`", + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "Golden advertisement/path/failure vectors shared by Rust/Nix; property tests for NCA/loop/allocated-capability narrowing; replay-window tests; hop-count tests", + "workItemId" : "ADR046-routing-001" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/route_engine.rs`: full `RouteTreeEngine` struct and impl; `RouteInventoryEntry`, `RoutePruneReport`, `DirectShortcutAuthorizationRequest/Decision/Teardown`; all helper functions", + "dataMigration" : "None (pure in-memory engine)", + "dependencyOwner" : "ADR046-routing-001; zone route engine owner", + "destination" : "`packages/d2b-zone-routing/src/engine.rs`", + "detailedDesign" : "Adapt RouteTreeEngine to ZoneRouteEngine using ZonePath/ZoneRouteId/ZoneLinkControllerGeneration from ADR046-routing-001; preserve all NCA/loop/multi-parent/capability/replay/capacity logic; add `decide_route` enforcement of the fixed protocol hop budget; add hop-counter decrement in relay path; expose `ZoneRoutePath` in v3 types Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "d2b-bus calls `ZoneRouteEngine::decide_route` for every cross-Zone ResourceClient call; ZoneLink controller calls `admit_advertisement`/`admit_withdrawal`", + "removalProof" : "`RouteTreeEngine` on v3 RealmPath types retired after ZoneRouteEngine is exercised in all bus routing paths", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 baseline commit `b5ddbed6`", + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "Copy exact `route_engine.rs` test suite adapted to ZonePath; add relay/hop-count/RBAC-narrowing/shortcut integration tests", + "workItemId" : "ADR046-routing-002" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/routing.rs`: `RouteNamespaceAllocation`; `packages/d2b-realm-core/src/access.rs`: `RealmAccessResolverRequest/Response/Error`, `RealmAccessBinding`, `RealmTransportBinding`, `RealmAccessClientContract`, `UnixSocketPath`, `AccessBindingRef`, all access types (evidence: **B** — complete implementation, no production callers); `packages/d2bd/src/realm_access_resolver.rs`: `resolve_local_root_realm_access()`, `local_root_realm_access_client_contract()` (evidence: **B** — `pub mod` at `d2bd/src/lib.rs:117`, no callers from running daemon); `packages/d2b-realm-router/src/target_resolver.rs`: `RealmEntrypointTable`, `DispatchTarget`, `RealmEntrypoint`, `ResolveError` (evidence: **A**); `packages/d2b/src/lib.rs:5240`: `load_realm_entrypoint_table()` (evidence: **A**); `packages/d2b/src/target_routing.rs`: `Route::Local`/`Route::GatewayBacked` dispatch (evidence: **A**); `packages/d2b-realm-core/src/realm.rs`: `EntrypointMode`, `RealmControllerPlacement` (evidence: **A** as types; routing use **B**); `packages/d2b-core/src/realm_controller_config.rs`: `RealmControllersJson` (evidence: **C**); `nixos-modules/realm-controller-config-json.nix` (evidence: **C**)", + "dataMigration" : "None; ZoneLink resources created from Nix configuration at v3 reset", + "dependencyOwner" : "ADR046-routing-001, ADR046-routing-002; Zone route resolver owner", + "destination" : "`packages/d2b-zone-routing/src/resolver.rs` (ZoneEntrypointResolver)", + "detailedDesign" : "Consume the canonical ZoneLink spec/status/intent types owned by ADR046-zone-control-002; ZoneLinkNamespaceAllocation is issued by the exact parent allocator selected in sealed Nix-compiled `parentZone` topology; ZoneEntrypointResolver uses longest-suffix match over ZonePath (adapted from `RealmEntrypointTable::resolve`) and is driven only by sealed `{ childZone, parentZone }` rows plus authenticated admitted route projections; no reciprocal parent-store resource or parent ZoneLink handler; fail closed on unknown topology, absent/stale projection, or unauthenticated route Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Child core-controller ZoneLink handler manages child-store ZoneLink resources; parent d2b-bus feeds sealed topology and authenticated `ZoneRouteEngine` projection state to ZoneEntrypointResolver for per-call dispatch", + "removalProof" : "`RealmEntrypointTable` retired after all host-daemon routing paths use ZoneEntrypointResolver", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 baseline commit `b5ddbed6`", + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "Longest-suffix match vectors over sealed topology; child-local ZoneLink spec validation; resolver rejects unknown/stale/withdrawn/unauthenticated route projections; parent-store fixture contains no ZoneLink row or handler", + "workItemId" : "ADR046-routing-003" + }, + { + "currentSource" : "`packages/d2b-realm-router/src/remote_node.rs`: `RemoteNodeRegistry`, `RemoteNodeEntry`, `RemoteNodeAvailability`, `RemoteNodeErrorKind`, `RemoteRetryAction`, `ensure_remote_execution_generation` (evidence: **B** — only in `d2bd/src/realm_stubs.rs` dead_code seam); `packages/d2b-realm-router/src/session_lifecycle.rs`: `SessionLifecycle`, `SessionPhase` (evidence: **B** — same seam); `packages/d2bd/src/realm_stubs.rs`: compile-only seam (`#[allow(dead_code)]`, declared at `d2bd/src/lib.rs:249`); `packages/d2b-realm-core/src/enrollment.rs`: `EnrollmentRecord`, `EnrollmentStatus`, `KeyRotationPlan`, `RevocationRecord`, `SessionTeardownDirective`, `RecoveryProcedure`, `IdentityAuditEventKind` (evidence: **B** — consumed by `RealmIdentityStore` which itself has no production callers); `packages/d2b-realm-core/src/identity_store.rs`: `RealmIdentityStore` (evidence: **B** — no production callers); `packages/d2b-realm-core/src/identity_config.rs`: `RealmIdentityConfigJson` (evidence: **A** — loaded at d2bd/priv-broker startup, routing inert); `nixos-modules/realm-identity-config-json.nix` (evidence: **C**); `packages/d2bd/src/workload_target_index.rs`: `WorkloadTargetIndex` (evidence: **A** — called at `d2bd/src/lib.rs:16745`; this is the live bridge from realm metadata to VM-name dispatch; retires with Guest/Host resource lookups)", + "dataMigration" : "New ZoneLink resources from Nix configuration; no prior enrollment compatibility", + "dependencyOwner" : "ADR046-routing-003; core-controller ZoneLink handler owner", + "destination" : "`packages/d2b-core-controller/src/zone_links.rs`", + "detailedDesign" : "Child-local ZoneLink handler in core-controller: consumes the exact six-field ZoneLink schema from ADR046-zone-control-002 and manages local ResourceSpec→allocator-bound session→advertisement lifecycle; session state machine (Pending/Established/Disconnected/Reconnecting/Revoked); Provider-internal reconnect backoff bounded by `spec.limits`; advertisement issuance/renewal/withdrawal using enrolled KK ComponentSession; child-store route cursor and bounded outbound intent queue; private allocator capability-scope changes; D088 `status.resource` writer; aggregate metrics use only closed semantic phase/reason/outcome labels and never `link_name_hash` or another ZoneLink/Zone/resource identity label; Nix-compiled `parentZone` selects the parent allocator, which alone owns privileged listeners, placement, and route namespace and creates no reciprocal resource. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Child core-controller process → local transport Provider → sealed binding for the allocator selected by `parentZone` → d2b-bus ComponentSession; child ZoneLink handler exchanges advertisements while that parent ZoneRouteEngine admits/withdraws them", + "removalProof" : "`RemoteNodeRegistry` retired after all enrolled peer routing moves to ZoneLink handler", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 baseline commit `b5ddbed6`", + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "Session lifecycle tests; reconnect/disabled/revocation/allocator-policy-change; intent queue drain; cursor resync; advertisement renewal timing; fake-child tests; structural metric descriptor test asserts `vm`, `zone`, `zone_id`, `zone_uid`, and `link_name_hash` are absent and a ZoneLink-name canary never enters label values", + "workItemId" : "ADR046-routing-004" + }, + { + "currentSource" : "`packages/d2b-realm-router/src/lib.rs`: `OperationRouter`, `RouteDecision`, `DEFAULT_RETENTION`, `DEFAULT_NO_REUSE_HORIZON`, `DEFAULT_MAX_DEDUP_RECORDS` (evidence: **B** — only in dead_code seam); `packages/d2b-realm-router/src/mux_session.rs`: `MuxSession` stream/operation forwarding (evidence: **B** for zone relay — stream-forwarding path for Zone relay does not exist yet; **D** within realm-router display-session tests); `packages/d2b-realm-core/src/frame.rs`: `ConstellationFrame`, `Handshake*`, `OperationRequest/Response`, `StreamOpen/Data/Flow/Close/Resume`, `OperationKind` (evidence: **B** from d2bd routing perspective; **A** within realm-router + `d2b-gateway-runtime` for individual types); `packages/d2b-realm-router/src/session.rs`: `PeerSession` (evidence: **B** from d2bd; **A** within realm-router tests); `packages/d2b-realm-router/src/secure_session.rs`: `SecurePeerSession`, `SecureSessionKey`, `NonceReplayGuard` (evidence: **B** from d2bd; reachable within realm-router)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-routing-002, ADR046-routing-007 (from ComponentSession spec); d2b-bus owner", + "destination" : "`packages/d2b-bus/src/zone_route.rs` (cross-Zone bus routing), `packages/d2b-bus/src/relay.rs` (per-hop relay handler)", + "detailedDesign" : "Cross-Zone routing path in d2b-bus: ZoneEntrypointResolver consumes sealed topology plus authenticated route projections → ZoneRouteEngine::decide_route → admitted ComponentSession established by each next-hop child's local ZoneLink; hop-counter decrement and enforcement; independent target-verb plus canonical ZoneLink-scoped `relay` checks at each intermediate hop; idempotency key namespace (full 6-tuple) in ZoneLinkIdempotencyKey; pinned reverse path tracking; cancellation forwarding; watch cursor forwarding and revision-expired handling; no-FD/credential structural rejection at serialization boundary. No parent route step performs Resource API Get/List/Watch on ZoneLink Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "ResourceClient → d2b-bus → ZoneLink CS → intermediate zone → target zone; cancel/watch/stream all use the same routing path", + "removalProof" : "Old direct-dispatch and gateway-backed paths retired per bus routing parity", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 baseline commit `b5ddbed6`", + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "End-to-end K0→K1→K2 resource call; relay-missing, target-verb-missing, wildcard/self-asserted relay, hop-limit, and FD-rejection tests; prove relay alone grants no CRUD/local lifecycle; idempotency namespace collision tests; cancellation delivery tests; watch resync tests", + "workItemId" : "ADR046-routing-005" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/route_engine.rs` inline `#[cfg(test)] mod tests` block at line 1202 (45 test functions covering NCA, advertisement admission/withdrawal, loop/multi-parent detection, capability ceiling, replay window, DirectShortcut authorization/teardown; evidence: **implemented-but-unwired** — tests are in-file, not in a separate `tests/` directory; no external test crate at `packages/d2b-realm-core/tests/*.rs`)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-routing-002; benchmark owner", + "destination" : "`packages/d2b-zone-routing/tests/route_engine_vectors.rs`, `packages/d2b-zone-routing/benches/route_decision.rs`", + "detailedDesign" : "Copy exact advertisement/NCA/loop/capability/replay test vectors adapted to ZonePath; add K0/K1/K2 topology scenarios; add hop-count boundary tests; benchmark: p95 route decision for 1/10/100 active Zone tree entries <= 1 ms", + "integration" : "Zone route engine correctness gate; bus relay integration tests", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 baseline commit `b5ddbed6`", + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "All v3 baseline route_engine test cases must pass; p95 benchmark gate", + "workItemId" : "ADR046-routing-006" + }, + { + "currentSource" : "None in v3 pre-ADR45 baseline.", + "dataMigration" : "None (new infrastructure)", + "dependencyOwner" : "W0; d2b-bus owner", + "destination" : "`packages/d2b-bus/src/session/`", + "detailedDesign" : "Copy `d2b-session` crate wholesale into `d2b-bus/src/session/`; adapt `EndpointPurpose`/`EndpointRole`/`ServicePackage` closed-enum tags for v3 purposes; strip `GUEST_SESSION_CREDENTIAL_*` types; strip `serve_ttrpc_services` fixed-endpoint binding (replaced by allocator-issued FD bootstrap); adapt `SessionEngine` as ZoneLink session drive loop; keep all Noise profiles (Nn/Kk/IKpsk2), generation discovery, record/fragment/keepalive/credit/cancellation/attachment logic verbatim Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "ZoneLink controller instantiates one `ComponentSessionDriver` per ZoneLink, typed as Kk for enrolled peers and Nn for initial bootstrap; d2b-bus routes ResourceClient calls through these drivers", + "removalProof" : "Not applicable (new crate)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "Port all `component_session.rs` tests; port `noise_vectors.rs`; add ZoneLink-specific KK enrollment test; add ZoneLink reconnect/revocation integration test", + "workItemId" : "ADR046-routing-007" + }, + { + "currentSource" : "None in v3 pre-ADR45 baseline (vsock and seqpacket paths are provider-specific in v3).", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-routing-007; transport-provider owner", + "destination" : "`packages/d2b-bus/src/transport/unix.rs`, `packages/d2b-bus/src/transport/credit.rs`", + "detailedDesign" : "Copy `UnixSeqpacketTransport`/`UnixStreamTransport`/credit modules verbatim; adapt `PeerIdentityPolicy` for v3 Zone principal model; adapt `InheritedSocketTransport` to receive allocator-issued FD directly (not SD_LISTEN_FDS); vsock paths adapted as transport-Provider-specific implementations (not hardcoded); `PidfdIdentityPolicy` adapted for v3 Process resource pidfd model Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Unix transport Provider instantiates `UnixSeqpacketTransport`; vsock transport Provider instantiates vsock transports; both implement `OwnedTransport` consumed by `SessionEngine` in ADR046-routing-007", + "removalProof" : "Not applicable (new infrastructure)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "Port all `unix_session.rs` tests; add allocator-issued FD handoff test; add inherited-socket no-SD-listen test", + "workItemId" : "ADR046-routing-008" + }, + { + "currentSource" : "None in v3 pre-ADR45 baseline.", + "dataMigration" : "None (new contract module)", + "dependencyOwner" : "ADR046-routing-007; contracts owner", + "destination" : "`packages/d2b-contracts/src/v3/zone_session.rs`", + "detailedDesign" : "Copy all protocol constants verbatim; copy `BoundedVec`, `BinaryError`, `ContractError`; copy `ComponentSessionPreface`, `LimitProfile`, `AttachmentDescriptor`/`AttachmentKind`/`AttachmentCreditClass`/`ChannelId`/`RecordHeader`/`FragmentHeader`/`SessionErrorCode`/`CloseReason`/`Remediation` verbatim; extend `ServicePackage` closed-enum with `ZoneV3`/`ResourceV3`/`ZoneLinkV3` variants at new tag values; extend `EndpointRole` with `ZoneController`/`ZoneRelay`/`ZoneBootstrap` variants; extend `EndpointPurpose`/`PurposeClass` with v3 Zone purposes; strip `GUEST_SESSION_CREDENTIAL_*` constants and types; re-freeze protobuf field numbers for v3 services independently from v2 assignments Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "All d2b-bus session/transport code imports from `d2b-contracts::v3::zone_session`", + "removalProof" : "v2 contracts remain; v3 module is additive", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "Updated `negotiate_offer`/`validate_exact` round-trip tests for v3 purposes; canonical encoding stability test; closed-enum exhaustiveness tests", + "workItemId" : "ADR046-routing-009" + }, + { + "currentSource" : "None in v3 pre-ADR45 baseline.", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-routing-007, ADR046-routing-009; resource-client owner", + "destination" : "`packages/d2b-resource-client/`", + "detailedDesign" : "Copy `Client` generic structure; rename `ServiceOwner::Realm(RealmId)` → `ServiceOwner::Zone(ZonePath)`, `ServiceOwner::Workload{realm,workload}` → `ServiceOwner::Guest{zone,guest}`, `ServiceOwner::LocalRoot` → `ServiceOwner::ZoneLocal`; rename `TargetInput` variants to match; add `TargetInput::ZoneService(ZonePath, ZoneServiceKind)` for cross-Zone service targeting; replace `ServiceKind` (25 ADR45 variants) with v3 service inventory (`Resource`, `Zone`, `ZoneLink`, `Provider`, plus retained guest/daemon variants); adapt `RouteTable` to route by `ZonePath`; replace `HostSocketConnector` uid-based trust with allocator-issued FD + KK static key pinning; keep `SessionFailure`/retry/cancellation/`MetadataInput`/`RetryPolicy` logic verbatim Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Zone runtime uses `ResourceClient` for all cross-Zone ResourceType calls; d2b-bus wraps `ComponentSessionDriver` (ADR046-routing-007) as the underlying session", + "removalProof" : "v2 `d2b-client` package remains for ADR45 callers; v3 `d2b-resource-client` is additive", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "Port `client.rs` tests; add ZonePath routing test; add cross-Zone K0→K1 end-to-end test; add retry/cancellation forwarding test", + "workItemId" : "ADR046-routing-010" + }, + { + "currentSource" : "`nixos-modules/options-realms-workloads.nix` (v3 baseline `b5ddbed6`): `d2b.realms..*` option declarations (evidence: **C** — eval contract; bespoke field names that do NOT mirror canonical ResourceSpec); `nixos-modules/assertions.nix`: realm-name regex, platform-gate, CIDR assertions (evidence: **A**); `nixos-modules/realm-controller-config-json.nix`: `realmControllersJson` emitter, bundle.nix:59 wiring (evidence: **C**)", + "dataMigration" : "None; Zone options are new; Realm options retained until migration PR", + "dependencyOwner" : "ADR046-routing-001; Nix module owner", + "destination" : "`nixos-modules/options-zones.nix` (new structural base), `nixos-modules/generated/resource-types.nix` (generated registry), `nixos-modules/generated/options-zones-.nix` (generated per ResourceType by `xtask gen-zone-nix-options`), `nixos-modules/assertions.nix` (new Zone assertions)", + "detailedDesign" : "Declare compiler-only scalar `d2b.zones..parentZone` plus structural `d2b.zones..resources. = { type = ...; spec = {}; }` as specified in the \"Option schema\" section above. `parentZone` has no default, is required for every non-root Zone, forbidden on `local-root`, resolves to one declared Zone, and never enters a ResourceSpec. Wire `options-zones.nix` and all `generated/options-zones-*.nix` files into `nixos-modules/default.nix`. Add a new `xtask gen-zone-nix-options` command that reads `docs/reference/schemas/v3/.schema.json` for each ResourceType, derives `generated/resource-types.nix`, and emits a generated submodule overlaying typed spec options (types, bounds, enum constraints, defaults, docs) onto `d2b.zones..resources..spec`. The generated registry's standard subset must equal the canonical 19-type registry from `ADR-046-resource-object-model` exactly; installed signed Provider schemas may append qualified types. These generated modules are committed and kept in sync by `xtask gen-zone-nix-options && git diff --exit-code` wired into `make test-drift`. The ZoneLink module is generated from the exact six-field schema owned by ADR046-zone-control-002 and rejects any seventh field. Because the generated options carry field-level type constraints, field-level eval errors (wrong enum, out-of-range int, malformed ref) are caught without explicit assertions. Explicit assertions in `nixos-modules/assertions.nix` cover cross-resource invariants only: zone/resource key name regex, reserved names, `parentZone` required/forbidden/existence/self/cycle/16-name-depth constraints, child-local `childZoneName == zone`, at most one uplink resource per non-root Zone and none in local root, ref resolution, count limits, and transportSettings secret-key exclusion (listed in the eval-time assertions table). Conflicting parent scalar definitions fail through standard Nix module merging. Each new assertion must have a matching case in `tests/unit/nix/cases/zone-assertions.nix` (nix-unit auto-discovered). `d2b.realms` option namespace is NOT removed in this work item. Primary reuse disposition: `adapt`. Preserved source-plan detail: new module following same pattern.", + "integration" : "ADR046-routing-012 consumes the validated `parentZone` map for private allocator bootstrap sealing and iterates `d2b.zones..resources.*` for resource-bundle emission", + "removalProof" : "`nixos-modules/options-realms-workloads.nix` `d2b.realms` namespace retires after all hosts migrate to `d2b.zones`", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 baseline `b5ddbed6`; `assertions.nix` pattern reused for Zone assertions; `realm-controller-config-json.nix` is the structural template", + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "`nix-unit: zone-name-regex`, all five `nix-unit: zone-parent-*` vectors, `nix-unit: zone-link-credential-ref`, `nix-unit: zone-link-child-name`, `nix-unit: zone-link-one-uplink`, `nix-unit: zone-link-closed-spec`, `nix-unit: zone-link-limits`, `nix-unit: transport-settings-secret-key`; add `drift: standard-resource-type-registry` asserting the generated standard subset is exactly all 19 canonical types with no omission/addition/duplicate/reordering, plus `drift: zone-nix-options` (`xtask gen-zone-nix-options && git diff --exit-code`); run `make nix-unit-pin` after adding eval cases", + "workItemId" : "ADR046-routing-011" + }, + { + "currentSource" : "`nixos-modules/realm-controller-config-json.nix` (v3 baseline `b5ddbed6`): `builtins.toJSON` emitter for `realm-controllers.json` (bundle.nix:59); `nixos-modules/bundle-artifacts.nix`: install table (root:d2bd 0640); `nixos-modules/bundle.nix`: artifact wiring (evidence: **C**); `packages/xtask/src/main.rs` `gen-schemas` subcommand (evidence: **A** — wired into `make test-drift`)", + "dataMigration" : "None; new artifact file", + "dependencyOwner" : "ADR046-routing-011, ADR046-routing-001; bundle emitter owner", + "destination" : "`nixos-modules/zone-resources-json.nix` (new), private local-root allocator bootstrap compiler/sealer input (not a ResourceSpec or public bundle), `nixos-modules/bundle-artifacts.nix` (new row for per-Zone `resource-bundle.json`), `packages/xtask/src/main.rs` (`gen-zone-schemas` subcommand emitting `docs/reference/schemas/v3/.schema.json` for Zone and ZoneLink; `gen-zone-nix-options` subcommand emitting `nixos-modules/generated/options-zones-.nix`)", + "detailedDesign" : "`zone-resources-json.nix` iterates `d2b.zones..resources.*` to produce the canonical sorted resource list: for each entry, render `{ apiVersion, type, metadata: { name, zone, ownerRef: , labels: , annotations: }, spec: }`. Separately canonicalize sorted `{ childZone, parentZone }` rows from the compiler-only topology and seal them into the private allocator bootstrap input; `parentZone` never enters a resource bundle or `Zone.spec`, and a topology digest change releases/reallocates affected edges independently of resource `generationId`. Per-Zone generation is strict: local root's generated bundle contains no ZoneLink; a non-root Zone's enabled uplink and referenced transport Provider appear together only in that child's bundle; no emitter copies either resource into the selected parent's bundle. The bundle JSON omits `managedBy` and `configurationGeneration`; the configuration service/core sets those fields when activating the validated bundle. Sort all resources by `(type, zone, name)`. Compute `generationId` as SHA-256 (lower hex) of the UTF-8 bytes of the sorted `resources` array JSON. Compute `integrity` as SHA-256 (base64url, no padding) of the full bundle JSON with integrity field zeroed. Install at `/etc/d2b/zones//resource-bundle.json` root:d2bd 0640. Canonical form: all object keys sorted lexicographically; order-significant arrays preserved; schema-declared set-like arrays sorted lexicographically; all optional fields emitted with defaults; no field renaming or restructuring. Build-time validation runs in a Nix derivation: (1) validate the complete parent map (non-root required, local-root forbidden, declared target, one scalar parent, not self, acyclic, max 16 names); (2) validate each resource against the committed JSON Schema, including the exact six-field ZoneLink schema from ADR046-zone-control-002; (3) validate `transportSettings` for each child-local ZoneLink against its same-Zone Provider's `transportSettingsSchema` — `transportProviderRef` is always explicit, never inferred or defaulted; (4) resolve every same-Zone `transportCredentials` ref; (5) verify `childZoneName == metadata.zone`, at most one uplink resource per non-root Zone, and no local-root uplink; (6) check for duplicate `(type, zone, name)` tuples. Private route capability policy is sealed in allocator bootstrap state and is not a ZoneLink ResourceSpec field. Providers MUST commit their `transportSettingsSchema` before any ZoneLink can reference them. Drift gates: `xtask gen-zone-schemas && git diff --exit-code` and `xtask gen-zone-nix-options && git diff --exit-code` both wired into `make test-drift`. Add `checks.${system}.zone-schema-drift` to `flake.nix`. Primary reuse disposition: `adapt`. Preserved source-plan detail: extend and adapt.", + "integration" : "The local-root allocator consumes sealed parent topology independently of resource bundles; `nixos-modules/bundle-artifacts.nix` installs each per-Zone `resource-bundle.json`; ADR046-routing-013 Zone runtime reads it on startup", + "removalProof" : "`realm-controllers.json` artifact retires after Zone runtime is live and all hosts migrated", + "reuseAction" : "adapt", + "reuseSource" : "`realm-controller-config-json.nix` structural template; `xtask gen-schemas` extension point (main `a1cc0b2d` unchanged in this area)", + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "`drift: zone-resource-schema`, `drift: zone-nix-options`, `build: zone-bundle-deterministic`, `build: parent-topology-sealed`, `build: child-local-zonelink-bundle` (K0 has no ZoneLink; K1 contains its self-matching ZoneLink and same-Zone transport Provider; neither is copied to K0), `build: zone-link-exact-six-fields`, `build: transport-settings-unknown-field`, `build: transport-credential-ref`, `build: missing-transport-provider`; run `make flake-matrix-pin` after adding flake checks", + "workItemId" : "ADR046-routing-012" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/realm_controller_config.rs`: `RealmControllersJson`, `RealmControllerRow`, `RealmControllerConfigError` (evidence: **C** — loaded but routing inert); `packages/d2bd/src/realm_access_resolver.rs`: `resolve_local_root_realm_access()`, `RealmAccessResolverRequest`, `RealmAccessBinding` (evidence: **B** — pub mod at lib.rs:117, no callers); `packages/d2b-state/src/` (both baselines): atomic state, OFD locks, lease primitives (evidence: **A** for locks/leases, **B** for realm-specific storage); `nixos-modules/host-daemon.nix:220–221`: bundle artifact install paths, daemon SIGHUP wiring (evidence: **A**)", + "dataMigration" : "None; new runtime component", + "dependencyOwner" : "ADR046-routing-012, ADR046-routing-003; `d2b-core-controller` owner (ADR-046-core-controllers)", + "destination" : "`packages/d2b-core-controller/src/configuration.rs` (defined by ADR-046-core-controllers); shared bundle DTOs may live in `packages/d2b-core/`", + "detailedDesign" : "Implement the configuration ownership and cleanup contract from the \"Configuration ownership and cleanup contract\" section. `configuration.rs` owns: (1) reading and integrity-verifying `/etc/d2b/zones//resource-bundle.json` on startup and SIGHUP; (2) diffing against active generation by `generationId` (no-op if unchanged); (3) queuing Create/UpdateSpec/Delete intents — core sets `configurationGeneration` and `managedBy` when applying Create/UpdateSpec; Delete targets only resources where BOTH `managedBy` equals the configuration service's value AND `configurationGeneration` matches the prior bundle — resources with `managedBy=controller` or `managedBy=api` are never seized; (4) setting `deletionRequestedAt` on pending-delete resources immediately and adding a Pending condition; (5) writing the prior bundle into the capped ring at `/var/lib/d2b/zones//configuration/prior/.json` (default retentionCount=3, range 1..16, no TTL; prune oldest when count would exceed limit); (6) enforcing boundary invariants (no diff-delete for absent `configurationGeneration`, `managedBy` collision guard, live controller-child teardown guard); (7) driving finalizer drain + controller-child cascade before completing a Delete; (8) on successful deletion: one store transaction writes the `Deleted` revision/change event and removes the resource row and all index entries; the authoritative audit record (`zone-resource-cleanup`) is appended from the committed revision with dedup/exactly-once recovery and is NOT part of the store transaction; (9) tracking `deletionRequestedAt`/`cleanupConfigGeneration`/`cleanupError`/`cleanupAttempt` per resource; (10) on rollback: clearing `deletionRequestedAt` and Pending condition for revived resources; (11) never pruning a prior bundle while a Delete intent from its `configurationGeneration` is in flight. OFD lock on the bundle file prevents concurrent activation races. Generation state persisted atomically at `/var/lib/d2b/zones//configuration/generation.json` (root:d2bd 0640). The `spec` object comparison for UpdateSpec detection uses the canonical JSON form so two identical specs always compare equal regardless of Nix rendering order. Resource phase transitions: Pending while Create/UpdateSpec in-flight; Degraded while cleanup pending; Ready when clean; Failed on permanent error. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "`d2b-core-controller` configuration service activates on bundle install and SIGHUP; zone-controller reconcile loops in `d2b-core-controller` consume the queued intents; d2b-bus resource API exposes `status.phase` and `pendingCleanup` via Get/Watch on the active generation resource", + "removalProof" : "`realm_access_resolver.rs` (B) retires after `d2b-core-controller` configuration tracking is live; `RealmControllersJson` (C) retires after all hosts migrated to Zone bundles", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "`host-integration: cleanup-removed-zonelink`, `host-integration: rollback-restores-zonelink`, `host-integration: dynamic-child-not-deleted`, `host-integration: zonelink-no-reciprocal-row`; unit tests: deterministic generationId, no-op on same generationId, cross-ownership invariant enforcement, prior-bundle write/prune cycle, UpdateSpec canonical comparison, store-transaction-then-audit-append ordering, exactly-once audit dedup", + "workItemId" : "ADR046-routing-013" + }, + { + "currentSource" : "None in v3 pre-ADR45 baseline (provider traits exist in `d2b-realm-provider` but are unregistered).", + "dataMigration" : "None (pure runtime)", + "dependencyOwner" : "ADR046-routing-007; Provider resource owner", + "destination" : "`packages/d2b-provider/src/` (adapted in place)", + "detailedDesign" : "Retain `ProviderRegistry`/`ProviderRegistryBuilder`/`ProviderRegistryManager` lifecycle verbatim; adapt `SessionIdentity` to carry `ZonePath` instead of `RealmId`; adapt `AdmissionOptions::peer_role` to v3 Zone principal + RBAC binding; adapt `ProviderDescriptor` schema version to v3; `RegistryLimits` unchanged; `RpcProviderProxy` field adaptations to match v3 session identity; `ProviderInstance` variants retain all 11 types Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Zone runtime `ProviderComposition` builds a `ProviderRegistry` per Zone; Provider resource controller admits calls through `ProviderRegistry::admit()`", + "removalProof" : "Provider registry is v3 core infrastructure; no retirement", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "Port inline registry lifecycle/drain/shutdown tests; add v3 ZonePath routing test; prove provider admission cannot self-assert relay and each forward requires relay plus the target verb", + "workItemId" : "ADR046-routing-014" + }, + { + "currentSource" : "None in v3 pre-ADR45 baseline.", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-routing-014; Provider agent process owner", + "destination" : "`packages/d2b-provider-toolkit/src/` (adapted in place)", + "detailedDesign" : "Retain `GeneratedProviderServiceServer` ttrpc dispatch verbatim; adapt `ProviderAgentProcess::from_registry` to receive ComponentSession FD from Zone allocator bootstrap instead of SD_LISTEN_FDS; adapt audit event types for v3 Zone principal; `ProviderAgentAdapter` (client-side proxy) adapted for v3 ZoneLink session; conformance kit extended for v3 Provider resource conformance checks; redaction helpers unchanged Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Provider Processes (EphemeralProcess or Process resources with `executionRef`) spawn the provider agent entrypoint; Zone bus instantiates `ProviderAgentAdapter` as the proxy inside the Zone runtime", + "removalProof" : "Provider toolkit is v3 core infrastructure; no retirement", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "Port `audit_capacity_is_closed_and_bounded`; add v3 bootstrap-via-allocator test; add conformance test for new Provider ResourceType schema", + "workItemId" : "ADR046-routing-015" + }, + { + "currentSource" : "`packages/d2b-realm-router/src/service_v2.rs` (v3 baseline `b5ddbed6`): `RealmServiceServer` (bootstrap/enroll/resolve_route/authorize_shortcut/revoke_shortcut/report_shortcut_close/inspect/cancel), `RealmServiceProcess`, `RealmSessionAuthority`, `CredentialCustody`, `RealmServiceLimits`, `RealmAuditEvent`/`RealmMethod`/`RealmAuditOutcome`, `BootstrapBinding`/`EnrollmentBinding`/`ShortcutBinding`/`MutationRecord`; constants `DEFAULT_MAX_REALM_BINDINGS=256`, `DEFAULT_MAX_SHORTCUTS=256`, `DEFAULT_MAX_MUTATION_RECORDS=1024`, `DEFAULT_AUDIT_CAPACITY=1024`, `MAX_CONFIGURED_BOUND=4096`, `MAX_DISPATCH_IN_FLIGHT=64`, `SHUTDOWN_TIMEOUT=5s` (evidence: v3 baseline, not main; see Baseline section — **B** from d2bd/CLI perspective, **A** within realm-router display-session use)", + "dataMigration" : "None; v3 Zone service is new; no v2 realm-service compatibility", + "dependencyOwner" : "ADR046-routing-007, ADR046-routing-002, ADR046-routing-004, ADR046-routing-012; Zone service owner", + "destination" : "`packages/d2b-zone-routing/src/service.rs`", + "detailedDesign" : "Rename `RealmServiceServer` → `ZoneServiceServer`; service wire name `d2b.realm.v2.RealmService` → `d2b.zone.v3.ZoneService`; rename methods (bootstrap→zone-bootstrap, enroll→zone-enroll, resolve_route→resolve-zone-route, authorize_shortcut→authorize-zone-shortcut, revoke_shortcut→revoke-zone-shortcut, report_shortcut_close→report-zone-shortcut-close, inspect→zone-inspect) and add list/watch topology-projection methods. The read-only projection starts from the sealed sorted `{ childZone, parentZone }` compiler input and joins only authenticated, admitted `ZoneRouteEngine` route/projection status. It exposes no ZoneLink resource name, UID, spec, status, Provider ref, fingerprint, transport setting, or handle. Replace `RealmSessionAuthority` with Zone principal + RBAC binding; replace `BootstrapBinding` with allocator-issued PSK binding associated with the child's local ZoneLink; replace `EnrollmentBinding` with the corresponding KK enrollment record; add independent target-verb plus canonical `relay` RBAC checks per forwarding hop; adapt shortcut model to ZonePath addressing; `RealmServiceLimits` defaults preserved; `MAX_DISPATCH_IN_FLIGHT=64`, `SHUTDOWN_TIMEOUT=5s` preserved; `CredentialCustody::GatewayGuest` excluded (all ZoneLink sessions are direct KK) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Zone runtime instantiates one `ZoneServiceServer` per Zone; d2b-bus routes `d2b.zone.v3.ZoneService` calls to this server; CLI uses `ZoneServiceClient` (from ADR046-routing-010) for topology list/inspect/watch, enrollment, and route resolution", + "removalProof" : "`RealmServiceServer` on `d2b.realm.v2` retires after `ZoneServiceServer` handles all routing; display-session path migrates separately as part of Provider resource work", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-zone-routing", + "specPath" : "docs/specs/ADR-046-zone-routing.md", + "validation" : "Bootstrap/enroll/resolve-route/shortcut integration tests against a child-local fake ZoneLink; topology list/inspect/watch golden vectors contain exact `{ childZone, parentZone }` rows plus authenticated status and no ZoneLink fields; stale/withdrawn/unauthenticated projection tests; parent-store no-row/no-handler test; relay-plus-target-verb RBAC tests; KK enrollment test; shortcut ZonePath addressing test; concurrent dispatch bound test (64 in-flight)", + "workItemId" : "ADR046-routing-016" + }, + { + "currentSource" : "`packages/d2b-contract-tests/tests/policy_observability.rs` (existing v3 cardinality/label policy gate)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "All ResourceType/Provider specs; owned by the security/telemetry integrator alongside `ADR046-telem-008`", + "destination" : "`packages/d2b-contract-tests/tests/policy_telemetry_redaction.rs`", + "detailedDesign" : "One policy test enumerating every forbidden metric-label/audit-field value from §21 and the content-secrecy table in §20 (store paths, `no_isolation`, credential bytes, raw paths/argv/PID/cgroup, CTAP/clipboard/terminal/notification content) and asserting, by static scan of instrumentation call sites plus a redaction-guard runtime test, that no `ADR046-*` Provider crate emits any of them Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Runs as part of `make test-lint`/`make test-rust`; every Provider crate's own redaction test (e.g. `tests/stream_redaction.rs`) is a per-Provider instance of the same closed list", + "removalProof" : "Not applicable — this is a permanent gate, not a migration", + "reuseAction" : "adapt", + "reuseSource" : "None (new cross-cutting gate; no equivalent exists in main)", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic (`cargo test -p d2b-contract-tests policy_telemetry_redaction`); fails the build if a new Provider crate is added without a corresponding redaction test file under its `tests/`", + "workItemId" : "ADR046-security-001" + }, + { + "currentSource" : "main `a1cc0b2d`: `d2b-session/tests/noise_vectors.rs`, `d2b-session/tests/component_session.rs`, `d2b-session-unix/tests/unix_session.rs`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-session-001`/`ADR046-bus-001` (ComponentSession/d2b-bus implementation)", + "destination" : "`packages/d2b-session/tests/noise_conformance.rs`, `packages/d2b-session/fuzz/fuzz_targets/{handshake_offer,record_frame}.rs`", + "detailedDesign" : "Property/fuzz test suite over the three Noise profiles (§7): exact NN/KK/IKpsk2 vectors and rejection mutations (copied), plus new `cargo-fuzz` targets mutating the canonical handshake offer, preface, and encrypted record frame to assert no panic/UB and that every malformed input is a typed rejection (never a partial accept) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Wired into `make test-rust` (vectors) and a separate `make test-fuzz` target (new; time-boxed nightly run, not part of the PR-blocking gate)", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "Same main commit/paths", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic vector tests plus fuzz corpus with a minimum 4-hour nightly run and zero crashes/hangs as acceptance", + "workItemId" : "ADR046-security-002" + }, + { + "currentSource" : "`packages/d2bd/src/admission.rs` (`verb_requires_admin()` baseline verb table)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-zone-control-004`/`ADR046-zone-control-005` (Role/RoleBinding implementation)", + "destination" : "`packages/d2b-resource-store/tests/rbac_property.rs`", + "detailedDesign" : "Property test asserting, for a randomly generated Role/RoleBinding/request corpus: (1) no request whose payload sets a subject/role field ever changes the resolved `AuthenticatedSubjectContext.subjectRef`; (2) no non-core Role with a wildcard grant is ever admitted; (3) `scopeNarrowing` never widens beyond the referenced Role; (4) RoleBinding deletion never leaves an observable intermediate state under concurrent readers; (5) `relay` is accepted only as the canonical ZoneLink-scoped session verb with core-generated or explicit admin-policy provenance and exact target bounds; and (6) every forwarding hop independently requires relay plus the target verb Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Runs against the real redb-backed resource store test harness, not a mock", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "None beyond the verb-table adaptation already tracked by `ADR046-zone-control-004`", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic property test (`proptest`/`quickcheck`-style, minimum 10,000 cases per property)", + "workItemId" : "ADR046-security-003" + }, + { + "currentSource" : "None (v3 ZoneLink relay is `ADR-only`)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-routing-005`/`ADR046-bus-001` (ZoneLink/d2b-bus relay implementation)", + "destination" : "`packages/d2b-bus/fuzz/fuzz_targets/zonelink_frame.rs`, `packages/d2b-bus/tests/zonelink_structural_rejection.rs`", + "detailedDesign" : "Fuzz + property suite asserting that no mutation of a ZoneLink-bound frame (attachment count, credential-shaped byte runs, path-shaped strings, PID-shaped integers) is ever forwarded — every such mutation is rejected at serialization with `attachment-not-permitted-over-zone-link` or the transport-specific equivalent, never silently dropped or partially forwarded Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (design copied from `ADR-046-zone-routing.md` structural-rejection sections).", + "integration" : "`make test-fuzz`; a companion container test (`tests/integration/containers/zonelink-cross-zone.rs`) runs two real Zone runtime containers connected by a real ZoneLink and asserts the same property end to end over the wire, not just in the frame-serialization unit", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Fuzz corpus (`cargo fuzz run zonelink_frame -- -runs=1000000`, zero crashes); container test passes in `make test-integration`", + "workItemId" : "ADR046-security-004" + }, + { + "currentSource" : "None (compile-time dependency audit does not exist yet; per-Provider \"imports no broker DTO\" claims are currently prose-only in each dossier)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Every Provider dossier's own crate; owned by the Provider packaging integrator (`ADR-046-provider-model-and-packaging`)", + "destination" : "`packages/xtask/src/effectport_boundary_check.rs`, wired into `make test-policy`", + "detailedDesign" : "For every crate under `packages/d2b-provider-*`, walk its `Cargo.toml` dependency graph and fail the build if it transitively depends on `d2b-priv-broker` or any crate exposing a raw broker client/DTO type; separately, grep-scan for direct syscalls forbidden per dossier (e.g. `socket(AF_VSOCK` in `transport-vsock`, `Command::new(\"systemctl\"` in `system-systemd`)", + "integration" : "`make test-policy`; blocks any PR adding a forbidden dependency edge or forbidden syscall string to a Provider crate", + "removalProof" : "Not applicable — permanent gate", + "reuseAction" : "adapt", + "reuseSource" : "`cargo-deny`/`cargo tree`-style dependency graph tooling already used by `nix flake check`'s `rust-deny`/`rust-audit` derivations (`AGENTS.md` \"Disk hygiene contract\")", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic (`cargo xtask effectport-boundary-check`); a negative test intentionally adds a forbidden dependency to a scratch crate and asserts the check fails", + "workItemId" : "ADR046-security-005" + }, + { + "currentSource" : "`packages/d2b-priv-broker/src/sys.rs` (`clone3_spawn_runner` user-namespace path); `packages/d2b-host/src/virtiofsd_argv.rs`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-minijail-002`/`ADR046-minijail-003` (LaunchTicket/EffectPort implementation)", + "destination" : "`packages/d2b-provider-system-minijail/tests/launchticket_toctou.rs`", + "detailedDesign" : "Fault-injection test that issues a `LaunchTicket`, then mutates the referenced `CompiledSandboxPlan` digest (simulating a race between issue and exec) before the broker execs, and asserts the spawn fails closed rather than launching with the old plan; a companion test kills the broker mid-`clone3` and asserts no half-initialized process (missing cgroup placement, non-zero host capabilities) is ever observable by a concurrent reader", + "integration" : "`make test-rust` (unit-level fault injection via a fake clock/fault-injecting `EffectPort` test double); a host/KVM integration test (`tests/host-integration/launchticket-toctou.nix`) repeats the same scenario against the real broker and real `clone3(2)`", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 paths (already `implemented-and-reachable` per the migration map)", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic fault-injection test plus `make test-host-integration` NixOS/KVM test; acceptance is zero observable non-zero-capability or missing-cgroup-placement windows across 10,000 injected-fault iterations", + "workItemId" : "ADR046-security-006" + }, + { + "currentSource" : "`packages/d2b-priv-broker/src/ops/swtpm_dir.rs` (existing fail-closed marker/quarantine pattern, `implemented-and-reachable`)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-minijail-005`, `ADR046-systemd-002`, `ADR046-aca-001`, `ADR046-volume-001` (every quarantine-on-ambiguity implementation)", + "destination" : "`packages/d2b-contract-tests/tests/quarantine_not_kill_matrix.rs`", + "detailedDesign" : "One parameterized fault-injection matrix test, run once per adoption-capable Provider (`system-minijail`, `system-systemd`, `runtime-cloud-hypervisor`, `runtime-azure-container-apps`, `volume-local`), that restarts the controller with a deliberately ambiguous adoption candidate (duplicate InvocationID, mismatched marker inode, stale ACA operation handle) and asserts: (a) the resource transitions to `Degraded`/`Quarantined`, never `Deleted` or silently re-adopted; (b) no signal is sent to the ambiguous candidate process; (c) a `runtime-security-violation`-class audit record is emitted Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "`make test-rust` for the in-process cases; `make test-host-integration` for the real-pidfd/real-cgroup cases (`tests/host-integration/quarantine-not-kill.nix`)", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 path, generalized", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Matrix covers all five Providers listed; acceptance is 100% pass across all five with no signal sent to the ambiguous candidate in any case", + "workItemId" : "ADR046-security-007" + }, + { + "currentSource" : "`packages/d2b-contracts/src/public_wire.rs:267` (`WorkloadPublicSummary.execution_posture`, `implemented-and-reachable`); `packages/d2bd/src/unsafe_local_helper.rs` (`HelperRegistry::dispatch_launch`, current gap: does not emit a `ProcessEffect`-class event)", + "dataMigration" : "None (closes the current `HelperRegistry::dispatch_launch` audit gap noted in `ADR-046-telemetry-audit-and-support.md`)", + "dependencyOwner" : "`ADR046-exec-002` (Host `isolationPosture` propagation)", + "destination" : "`packages/d2b-provider-system-core/tests/no_isolation_propagation.rs`", + "detailedDesign" : "Integration test that creates a user-only `Host`, launches a Process on it, and asserts all three non-suppressible surfaces simultaneously: `status.isolationPosture == \"none\"` in JSON output; the unconditional stderr warning string is present with `--json` and non-JSON CLI invocation and cannot be suppressed by any combination of flags; the corresponding `ProcessEffect` audit record carries `no_isolation: true`; and a companion negative assertion that `no_isolation` never appears in any OTEL metric/span emitted during the same test run", + "integration" : "`make test-rust` (CLI/status/audit assertions) plus a Nix eval test (`tests/unit/nix/cases/no-isolation-null-posture-rejected.nix`) asserting the D042/D067 bidirectional-rejection eval assertions fire", + "removalProof" : "Legacy `d2b-unsafe-local-helper` v2-protocol warning path removed only after this test passes against the v3 replacement and the legacy crate has no remaining callers", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 paths", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic CLI/audit integration test; Nix eval test; acceptance is zero code paths reaching a live user-only-Host Process without all three surfaces firing", + "workItemId" : "ADR046-security-008" + }, + { + "currentSource" : "`tests/unit/nix/cases/per-vm-state-ownership.nix` (existing v3 ownership test, adapted target)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-pstate-003` (Volume identity marker)", + "destination" : "`packages/d2b-provider-volume-local/tests/marker_tamper_fault_injection.rs`", + "detailedDesign" : "Fault-injection test that provisions a Volume, then out-of-band (as a simulated attacker with filesystem access) replaces the marker file, swaps the backing directory for a different inode on the same `st_dev`, and deletes the marker entirely — three separate scenarios — and asserts each transitions the Volume to `Failed` with `markerStatus: missing`/`replaced` respectively, never a silent re-provision, and that operator-only remediation is the only recovery path exercised", + "integration" : "`make test-rust`; a host-integration variant (`tests/host-integration/volume-marker-tamper.nix`) repeats the inode-swap scenario against the real broker-maintained marker root on a real filesystem", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "None new", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic + host/KVM fault-injection test; acceptance is 100% fail-closed across all three tamper scenarios", + "workItemId" : "ADR046-security-009" + }, + { + "currentSource" : "None (zero-secret invariant has no existing automated gate)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-credential-001` through `ADR046-credential-007`", + "destination" : "`packages/d2b-contract-tests/tests/zero_secret_invariant.rs`", + "detailedDesign" : "Static + dynamic gate: (1) static — every DTO type reachable from a `Credential`-adjacent module must implement a hand-written redacted `Debug` and must not derive `Debug`, enforced by a `#[forbid(clippy::derive_debug_ambient)]`-style custom lint or an `xtask` AST scan; (2) dynamic — a property test that generates random `Credential` delivery sessions and asserts the delivered token/`SignChallenge` byte sequence never appears, byte-for-byte, in any captured audit record, OTEL span, log line, or resource-store row taken during the same test run Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (design from `ADR-046-resources-credential.md` §1.1).", + "integration" : "`make test-lint` (static scan) and `make test-rust` (dynamic property test)", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic; the dynamic test additionally runs as a canary-byte test (a unique random marker is embedded in the token and searched for across every observability surface)", + "workItemId" : "ADR046-security-010" + }, + { + "currentSource" : "`packages/d2b-unsafe-local-helper/src/{shell_runtime,shell_supervisor}.rs` (existing terminal-byte redaction discipline, `implemented-and-reachable`, adapted target)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-notify-004`, clipboard/terminal/security-key Provider work items", + "destination" : "`packages/d2b-provider-{clipboard-wayland,shell-terminal,device-security-key,notification-desktop}/tests/stream_redaction.rs` (one per Provider, same shared test helper crate)", + "detailedDesign" : "Shared canary-byte test helper: each Provider's test injects a unique random marker into its sensitive content path (clipboard bytes, terminal output, CTAP payload, notification body) and asserts the marker never appears in audit, OTEL, Debug output, or CLI error text captured during the test", + "integration" : "`make test-rust`; a container integration test (`tests/integration/containers/content-secrecy.rs`) runs a real Wayland/D-Bus mock session end to end for the clipboard/notification cases", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 paths", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic canary-byte test per Provider (4 Providers, shared helper crate); container test for the two D-Bus/Wayland-mediated cases", + "workItemId" : "ADR046-security-011" + }, + { + "currentSource" : "`packages/d2bd/src/daemon_audit.rs` (existing audit-write path, adapted target)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-audit-002` (privileged audit durability)", + "destination" : "`packages/d2b-audit/tests/privileged_fail_closed.rs`", + "detailedDesign" : "Fault-injection test that makes the audit sink's fsync fail (simulated ENOSPC/EIO) during a privileged `ResourceMutation`/`RBACChange`/`StateReset` write, and asserts the originating operation itself fails with `audit-unavailable` rather than completing with a lost audit record; a companion test floods `Standard`/`Best-effort` records past `DEFAULT_AUDIT_WRITES_PER_SECOND` and asserts privileged records are never dropped or delayed by the resulting backpressure", + "integration" : "`make test-rust`", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 path", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic fault-injection test; acceptance is zero privileged-class operations that complete despite a failed durable audit write", + "workItemId" : "ADR046-security-012" + }, + { + "currentSource" : "None new; ceiling values are already enumerated in `ADR-046-zone-routing.md`/`ADR-046-cli-and-operations.md`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-zone-control-009`/`ADR046-zone-control-010` (Quota/EmergencyPolicy), `ADR046-session-001` (session limits)", + "destination" : "`packages/d2b-bus/tests/dos_ceiling_fault_injection.rs`", + "detailedDesign" : "Fault-injection/load test suite: (1) attachment-credit exhaustion at each of the six scopes (Packet/Request/Operation/Session/Process/Host), asserting typed rejection never a panic; (2) reconnect-storm exceeding `MAX_RECONNECT_ATTEMPTS`/`MAX_RECONNECT_WINDOW_MS`, asserting the session fails closed rather than looping; (3) ZoneLink hop-count/route-advertisement replay flood, asserting `hop-limit-exceeded`/`zone-advertisement-replay` rather than unbounded forwarding; (4) a stalled data stream under load, asserting control/cancellation traffic is never starved (priority-scheduling property) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "`make test-rust`; item (4) additionally runs as a container load test (`tests/integration/containers/backpressure-priority.rs`) with a real slow consumer", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `d2b-session` credit-accounting/priority-scheduling tests, copied for the checked-arithmetic/priority-ordering assertions", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic fault-injection suite; container load test; acceptance is zero panics/unbounded-growth across all four scenarios", + "workItemId" : "ADR046-security-013" + }, + { + "currentSource" : "None (both commands are `ADR-only`)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-doctor-001`/`ADR046-doctor-002` (doctor/support-bundle commands)", + "destination" : "`packages/d2b/src/commands/{doctor,support_bundle}.rs`", + "detailedDesign" : "`d2b zone doctor` performs read-only status/audit-hash-chain checks with the redaction rules from §21 enforced on every field it prints; `d2b zone support-bundle` assembles a bounded archive of metadata+status (never spec bytes or `metadata.name`) and sets `bundle_completeness: \"partial\"` when any Provider in scope is quarantined, rather than omitting the gap silently Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (design from `ADR-046-telemetry-audit-and-support.md`).", + "integration" : "`make test-rust` (CLI integration tests); a container test (`tests/integration/containers/support-bundle-quarantined.rs`) runs a real Zone with one quarantined Provider and asserts the bundle correctly reports `partial`", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic CLI test asserting no spec byte or `metadata.name` appears in a generated bundle; container test for the quarantined-Provider case", + "workItemId" : "ADR046-security-014" + }, + { + "currentSource" : "Historical main ADR 0045 factory-reset design (`a1cc0b2d^:docs/adr/0045-provider-and-transport-framework.md`, reset process overview and apply-command verification steps) — reused only as a design precedent for atomicity/fail-closed sequencing, not as v3 architecture (see [Reset boundary](#reset-boundary) for the explicitly excluded assumptions)", + "dataMigration" : "None (v3-native; no v1/v2 reset-generation state to migrate)", + "dependencyOwner" : "`ADR046-audit-001` (`StateReset` record), Volume/Credential lifecycle work items", + "destination" : "`packages/d2b-core-controller/src/reset.rs`, `packages/d2b-core-controller/tests/reset_atomicity.rs`", + "detailedDesign" : "Implements the `scope` (`zone`, `provider`, `host`, or `guest`) `StateReset` flow from §25: quiesce via `EmergencyPolicy`, revoke open Credential leases in scope, destroy Volumes in scope (key-shred first), commit the `StateReset` audit record durably, and only then report the reset complete. A crash-recovery path re-derives \"was this reset already committed?\" solely from the durable `StateReset` record, never from partial filesystem state Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (pattern only).", + "integration" : "`make test-rust` (unit-level state machine); a host/KVM integration test (`tests/host-integration/reset-atomicity.nix`) kills the process mid-reset at each of the four phases (quiesce, credential revoke, Volume destroy, audit commit) and asserts recovery never double-destroys, never silently completes without the audit record, and never leaves an orphaned sealed-Volume-without-key state", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : "Same historical commit, sequencing pattern only (no code reuse; historical implementation was bash/systemd-generation-based and does not exist in any Rust crate)", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic state-machine test; host/KVM crash-injection test at all four phases; acceptance is zero non-atomic outcomes across all injected crash points", + "workItemId" : "ADR046-security-015" + }, + { + "currentSource" : "None (cross-reference check does not exist)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Documentation/CI integrator; depends on every Provider dossier's own `Security` section existing", + "destination" : "`tests/unit/gates/security-matrix-coverage.sh`", + "detailedDesign" : "A drift-style gate that parses [Per-ResourceType threat matrix](#per-resourcetype-threat-matrix) and [Per-Provider-family threat matrix](#per-provider-family-threat-matrix), confirms every one of the 19 standard ResourceTypes and all 27 Provider dossiers under `docs/specs/providers/` has a row, and confirms every referenced dossier file actually contains a `## Security`-class section (by heading grep) — failing the gate if a new ResourceType/Provider is added without a corresponding row and dossier section Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (pattern from `tests/unit/gates/drift-check.sh`).", + "integration" : "`make test-drift`", + "removalProof" : "Not applicable — permanent gate", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Hermetic shell-script gate; a negative test adds a scratch Provider dossier missing a Security section and asserts the gate fails", + "workItemId" : "ADR046-security-016" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-routing-004`, gateway-custody Provider work items (`ADR046-aca-*`, `ADR046-azure-vm-*`, `ADR046-transport-relay-*`)", + "destination" : "`tests/integration/containers/malicious-child-zone.rs`", + "detailedDesign" : "Container-based penetration test running a real parent Zone and a deliberately malicious child Zone container. Before attack injection, assert the one ZoneLink row/handler exists only in the child store/runtime and the parent has only sealed `{ childZone, parentZone }` topology plus authenticated allocator/route projection state. The child then attempts, over that link: FD smuggling, credential-shaped byte injection, cross-Zone `ownerRef` forgery, capability-ceiling widening claims, and route-advertisement replay. Every attempt must be rejected by the parent with the specific typed error named in §10, and none may create a parent ZoneLink row or reach the parent's resource store, Credential state, or Host substrate Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "`make test-integration` (requires podman, per `AGENTS.md` \"Local Layer 1 + container integration\")", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Container integration test; acceptance is zero successful attacks across all five attempted vectors", + "workItemId" : "ADR046-security-017" + }, + { + "currentSource" : "None (manual cloud/hardware validation has no automated equivalent by design)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Manual validation owner; depends on `ADR046-azure-vm-*`, `ADR046-transport-relay-*`, `ADR046-aca-*`, device Provider work items reaching a testable state", + "destination" : "`docs/reference/security-manual-validation-checklist.md` (new reference doc, out of scope for this spec's own file but named here as the required destination for the future implementation PR)", + "detailedDesign" : "A checklist covering the scenarios that cannot be hermetically or even container-tested: (1) real Azure Container Apps/Azure VM credential rotation and revocation under `AzureEffectPort`, confirming zeroization on a real managed-identity/Entra token; (2) real TPM 2.0 hardware NVRAM persistence/tamper-marker behavior across a real host reboot; (3) real USBIP/security-key hardware mutual-exclusion enforcement with a physical FIDO2 device; (4) real Azure Relay listener/sender credential acquisition and relay-identity-not-local-auth verification against a live relay namespace Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (checklist pattern from `SECURITY.md`'s existing portability-roadmap manual milestones and `tests/README.md`'s manual hardware tier).", + "integration" : "Run manually before each tagged release touching a cloud/hardware Provider, per the existing `tests/README.md` manual-tier convention", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Checklist sign-off recorded in the release's validation evidence, not a CI gate (matches `D2b_LIVE=1` manual-tier precedent in `AGENTS.md`)", + "workItemId" : "ADR046-security-018" + }, + { + "currentSource" : "`packages/d2bd/src/supervisor/pidfd_table.rs` (`BrokerReapLog`) and `packages/d2b-priv-broker/src/sys.rs` (`clone3_spawn_runner`)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-exec-003`/`ADR046-exec-007` and `ADR046-minijail-003` through `ADR046-minijail-005`; system-minijail/broker integration owner", + "destination" : "`packages/d2b-contract-tests/tests/minijail_process_ownership.rs`; `tests/host-integration/minijail-cgroup-kill.nix`", + "detailedDesign" : "Hermetic contract test proves only the broker that called `clone3` can produce the identity-bound `BrokerTerminalResult`; a non-parent poll-readable pidfd cannot be converted to status, while a verified duplicate holder can still request exact-main `pidfd_send_signal`. Host integration launches an owned descendant that calls `setsid(2)` plus an unrelated recycled-PGID decoy, performs graceful exact-main stop followed by anchored leaf `cgroup.kill`, and proves the owned leaf reaches `populated 0`, the broker reaps exactly once, the decoy survives, and rmdir/finalizer clearing wait for both proofs. Negative cases prove ambiguous adoption emits no signal/`cgroup.kill`, and Linux <5.14 or missing/unwritable `cgroup.kill` fails before spawn. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Hermetic contract test in `make test-rust`; real pidfd/cgroup scenario in `make test-host-integration`", + "removalProof" : "Old ProviderSupervisor/controller wait/reap and PGID-kill paths are removed only after this gate passes against the replacement", + "reuseAction" : "adapt", + "reuseSource" : "Same v3 paths, adapted to the corrected ownership contract", + "specId" : "ADR-046-security-and-threat-model", + "specPath" : "docs/specs/ADR-046-security-and-threat-model.md", + "validation" : "Parent-only wait/reap, poll-readability-not-status, duplicate-holder signaling, setsid/PGID-reuse resistance, quarantine no-kill, exact-once reap, and Linux ≥5.14 platform-gate assertions all pass", + "workItemId" : "ADR046-security-019" + }, + { + "currentSource" : "`packages/d2bd/src/security_key.rs` — baseline internal `SecurityKeyState` (renamed `RelaySessionTable` in v3 so state terminology remains reserved for Resource status), `LeaseState`, `LeaseId`, `CidTranslator`, `try_acquire_lease`, `release_lease`, `CEREMONY_TIMEOUT`, `QUEUE_WAIT_TIMEOUT` (implemented-and-reachable)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR-046 provider-device-security-key session/relay owner; depends on ADR046-security-key-008 and the ComponentSession/Process contracts.", + "destination" : "Move to `packages/d2b-provider-device-security-key/src/session.rs` and `cid.rs`; adapt to Provider Process model (remove daemon Mutex wrapping, add async relay protocol)", + "detailedDesign" : "Extract the baseline lease/session constants and CID mapping into provider-local modules. Preserve `LeaseId` stale-release protection, cancel-all-active-CIDs, `CEREMONY_TIMEOUT`, `QUEUE_WAIT_TIMEOUT`, and bounded fair queue semantics; remove daemon-global `Mutex` ownership; keep the authority relay's DeviceGrant/OFD lease for its lifetime. Ceremony rows remain high-churn session records, never Resources. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "ADR046-security-key-010 owns the relay loop; ADR046-security-key-011/ADR046-security-key-012 consume this extracted foundation; ADR046-security-key-009 consumes lifecycle events and writes bounded Service/Binding observations; ComponentSession/encrypted named streams carry CTAPHID bytes.", + "removalProof" : "ADR046-security-key-030 deletes the superseded daemon-internal `packages/d2bd/src/security_key.rs` `SecurityKeyState`, `LeaseState`, `SkRegistry`, and accept-loop ownership after the provider relay/session tests pass.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`session_state_machine.rs`, `session_ring.rs`, `cancel_propagation.rs`, `session_timeout.rs`, `fair_queue.rs`, and `cid_isolation.rs` verify queue/active/completed/timeout transitions, ring eviction, LeaseId stale-release denial, cancel-all-CIDs, fair timeout, and per-session CID isolation with no daemon-global lease state or ceremony Resource.", + "workItemId" : "ADR046-security-key-001" + }, + { + "currentSource" : "`packages/d2bd/src/security_key.rs` — CTAPHID relay loop, `SkAcceptHandle`, `relay_one_ceremony` (implemented-and-reachable)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR-046 provider-device-security-key relay extraction owner; depends on ADR046-security-key-008, ADR046-security-key-001, and the frozen ComponentSession/Endpoint contracts.", + "destination" : "Move to `packages/d2b-provider-device-security-key/src/relay.rs`; replace daemon-internal Unix socket proxy with ComponentSession over the owned Service Endpoint", + "detailedDesign" : "Extract the CTAPHID ceremony relay behavior into the provider relay binary. Preserve one-ceremony-at-a-time proxy semantics and CTAPHID cancel handling, but replace daemon-internal Unix socket proxying with the `d2b.security-key.v3` ComponentSession over the owned CTAPHID Endpoint and named `ctaphid` stream. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Core launches the relay Process with a LaunchTicket DeviceGrant and Endpoint attachment; transport-vsock resolves `Endpoint/-ctaphid-relay`; frontend Process connects as ComponentSession initiator; controller receives session events over the manifest-declared internal channel.", + "removalProof" : "ADR046-security-key-030 and ADR046-security-key-031 remove `start_sk_accept_loop`, `SkAcceptHandle`, `relay_one_ceremony`, and the daemon-internal Unix socket proxy bind from `packages/d2bd/src/security_key.rs` and `packages/d2bd/src/lib.rs`.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`host_relay_guest_frontend/` integration fixture, `device_grant_no_path.rs`, `descriptor_validation.rs`, and `cancel_propagation.rs` prove relay fd injection, ComponentSession transport, cancel propagation, and absence of daemon-internal socket proxying.", + "workItemId" : "ADR046-security-key-002" + }, + { + "currentSource" : "`packages/d2b-sk-frontend/src/` — `main.rs`, `uhid.rs` (implemented-and-reachable); `framing.rs` and `vsock.rs` are obsolete under v3 (replaced by ComponentSession transport)", + "dataMigration" : "Full d2b 3.0 reset; no frontend session state import", + "dependencyOwner" : "ADR-046 provider-device-security-key frontend extraction owner; depends on ADR046-security-key-008 and frozen Process/ComponentSession contracts.", + "destination" : "Adopt `main.rs` and `uhid.rs` as the v3 Process binary entry point; replace `framing.rs`/`vsock.rs` with ComponentSession client from `d2b-session-unix/src/vsock.rs`; wire as Process service in Provider crate", + "detailedDesign" : "Retain UHID creation and frontend entry behavior, but run it as a Binding-owned v3 user-domain Process receiving a pre-opened `/dev/uhid` fd from the `Provider/system-core` Guest-substrate DeviceGrant. Delete raw frame/vsock protocol and use the ComponentSession client/named `ctaphid` stream. No virtual/projected Device exists. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "ADR046-security-key-026 defines Service/Binding ownership; ADR046-security-key-020/N17 wire the Binding-owned frontend Process/private Endpoint and same-Zone Service resolution; Core injects UHID from the Guest substrate.", + "removalProof" : "ADR046-security-key-032 removes the legacy `d2b-sk-frontend.service` unit declaration, and the v3 frontend excludes the obsolete `packages/d2b-sk-frontend/src/framing.rs` and `vsock.rs` raw transport behavior.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`host_relay_guest_frontend/`, `device_grant_no_path.rs`, `descriptor_validation.rs`, and guest Nix migration tests prove UHID fd injection, no `/dev/uhid` path, ComponentSession client use, and no raw `framing.rs`/`vsock.rs` protocol.", + "workItemId" : "ADR046-security-key-003" + }, + { + "currentSource" : "`packages/d2b-priv-broker/src/ops/security_key.rs` — `live_open_hidraw_security_key`, FIDO usage page revalidation, group validation, `ALLOWED_GROUPS` (implemented-and-reachable)", + "dataMigration" : "Full d2b 3.0 reset; no v2 device state import", + "dependencyOwner" : "Core LaunchTicket/privileged broker reuse owner; depends on ADR-046-resources-device and ADR046-security-key-013 probe/device-token population.", + "destination" : "Preserve revalidation logic; update `SecurityKeyOpenDevice` to use bundle device table `device_token` as sole open target (no iterative sysfs scan); add zone-field handling; remove sysfs fallback. **Core's LaunchTicket calls this internally; the Provider does not call it.**", + "detailedDesign" : "Keep the FIDO usage-page and post-open revalidation logic, but make the trusted private bundle `device_token` the only open target. Add Zone-aware request handling, reject path/sysfs fallback inputs, and keep the operation internal to Core LaunchTicket DeviceGrant resolution rather than callable by the Provider controller.", + "integration" : "Provider activation records label-to-`device_token` mappings; Core LaunchTicket resolves `deviceUsage` for the relay Process; broker opens and revalidates the hidraw fd; relay receives only an inherited fd; Core emits path-free `device-grant` audit.", + "removalProof" : "The superseded iterative sysfs scan/fallback behavior in `packages/d2b-priv-broker/src/ops/security_key.rs` is removed once bundle-token lookup and revalidation tests pass.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`packages/d2b-priv-broker/tests/security_key_broker.rs` updates for bundle table lookup and zone-field round trip; `device_grant_no_path.rs` proves Provider code does not call the broker and sees no device path; audit tests prove path-free grant records.", + "workItemId" : "ADR046-security-key-004" + }, + { + "currentSource" : "`packages/d2b-contracts/src/security_key.rs` — `SecurityKeySessionId`, `SecurityKeyDeviceLabel`, `SecurityKeySession`, `SecurityKeySessionResult`, `SecurityKeyStatusResponse`, `SecurityKeySessionsResponse`, `SecurityKeyOpenDeviceRequest`, `SecurityKeyEvent` (implemented-and-reachable)", + "dataMigration" : "Full d2b 3.0 reset; no v2 DTO compatibility migration beyond serde-shape preservation where possible", + "dependencyOwner" : "`d2b-contracts` security-key ceremony/effect DTO owner; depends on ADR046-provider-004, ADR-046-resource-object-model, ADR-046-resources-device, and ADR046-security-key-008.", + "destination" : "Adapt to v3 Zone/ResourceRef identifiers; preserve serde shapes for zero downstream breakage where possible; remove `SecurityKeyApplyUdevRulesRequest` (ADR046-security-key-035)", + "detailedDesign" : "Rebase wire DTOs onto v3 Zone/ResourceRef identifiers; consume the shared ADR046-provider-004 `security-key.d2bus.org` Service/Binding bases and define only strict `device-security-key.d2bus.org` Provider-extension DTOs; reject `spec.provider` on Core projections; place authority/import/attachment semantic observations only under `status.resource` and implementation observations only under `status.provider`; preserve opaque bounded ceremony records as non-Resource DTOs; add `zone` to `SecurityKeyOpenDeviceRequest`; drop the udev-rules request because UHID comes from the Guest-substrate DeviceGrant. No provider-named ResourceType alias is admitted.", + "integration" : "Core LaunchTicket, broker open op, Provider controller Service/Binding status/audit, CLI session readers, and provider tests consume the v3 DTOs.", + "removalProof" : "ADR046-security-key-035 removes `SecurityKeyApplyUdevRulesRequest`, the `SecurityKeyApplyUdevRules` broker op, and related broker code after UHID DeviceGrant coverage is live.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "DTO serde round trips, exact provider-neutral ResourceType identity, provider-named alias rejection, canonical minimal base acceptance, Core projection `spec.provider` rejection, D088 `status.resource`/`status.provider` layering, base/Provider-extension field separation, unknown-field denial, zone-field round trip, path-redaction tests, and updated `usb_sk_contract.rs` assertions in the provider crate.", + "workItemId" : "ADR046-security-key-005" + }, + { + "currentSource" : "`packages/d2b-contract-tests/tests/usb_sk_contract.rs` — DTO serde round-trips, unknown-field denial, broker capability set (implemented-and-reachable)", + "dataMigration" : "None — test-only move; no runtime state", + "dependencyOwner" : "Provider crate test owner; depends on ADR046-security-key-005 v3 DTOs and ADR046-security-key-008 provider crate layout.", + "destination" : "Move to `packages/d2b-provider-device-security-key/tests/`; update imports and v3 type names", + "detailedDesign" : "Move the reusable semantic assertions for security-key DTO serde, unknown-field denial, and broker capability shape into the provider crate's hermetic `tests/` suite, updating imports and names to the v3 contract modules without weakening assertions. Primary reuse disposition: `adapt`. Preserved source-plan detail: move and adapt.", + "integration" : "`cargo test -p d2b-provider-device-security-key --lib --tests` runs the moved contract tests with the provider's DTO/controller test matrix; old contract-test manifests point to the successor coverage before deletion.", + "removalProof" : "ADR046-security-key-033 deletes `packages/d2b-contract-tests/tests/usb_sk_contract.rs` only after the provider-crate successor test covers all prior assertions.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Moved tests pass under the provider crate; contract assertions are retained; D094 disposition records moved/adapted coverage before old duplicate tests are deleted.", + "workItemId" : "ADR046-security-key-006" + }, + { + "currentSource" : "`packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` — minijail profile shape, `ProcessRole::SecurityKeyFrontend` (implemented-and-reachable)", + "dataMigration" : "None — test-only move; no runtime state", + "dependencyOwner" : "Provider crate test/minijail adaptation owner; depends on ADR046-security-key-008 and the frozen Process sandbox contract.", + "destination" : "Move to `packages/d2b-provider-device-security-key/tests/`; update for v3 Process resource minijail profile; retain zero-capabilities assertion", + "detailedDesign" : "Move the reusable minijail/sandbox assertions into the provider crate and retarget them from `ProcessRole::SecurityKeyFrontend` to the v3 Process resource templates and relay/controller minijail profiles. Preserve zero-capabilities and seccomp-class assertions while recognizing the frontend uses `Provider/system-systemd` hardening rather than a minijail profile. Primary reuse disposition: `adapt`. Preserved source-plan detail: move and adapt.", + "integration" : "Provider tests validate Nix minijail profile entries, Process resource sandbox templates, and system-minijail/system-systemd conformance expectations before old contract tests are retired.", + "removalProof" : "ADR046-security-key-033 deletes `packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` only after the provider-crate successor test covers all prior assertions.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Provider-crate tests retain zero-capability and seccomp assertions for relay/controller and assert no minijail profile is used for the frontend Process.", + "workItemId" : "ADR046-security-key-007" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR-046 provider-device-security-key crate owner; depends on provider-model/package workspace policy.", + "destination" : "New crate `packages/d2b-provider-device-security-key/` with `src/`, `tests/`, `integration/`, `README.md` (workspace policy requires all four)", + "detailedDesign" : "New crate `packages/d2b-provider-device-security-key/` with `src/`, `tests/`, `integration/`, `README.md` (workspace policy requires all four)", + "integration" : "Workspace/package descriptor expose the crate to Core; ADR046-security-key-009 through ADR046-security-key-029 add controllers, resource contracts, relay/frontend, adapters, tests, and docs.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Workspace package-policy check rejects missing `src/`, `tests/`, `integration/`, or `README.md`; `cargo test -p d2b-provider-device-security-key --lib --tests` discovers the hermetic suite; README acceptance criteria from the provider crate standard layout are satisfied.", + "workItemId" : "ADR046-security-key-008" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "Provider controller owner; depends on ADR046-security-key-013 probe, ADR046-security-key-016 templates, ADR046-security-key-025 effect port, ADR046-security-key-026 Service/Binding contracts, ADR046-security-key-027 status contract, and ADR-046-resource-reconciliation.", + "destination" : "`packages/d2b-provider-device-security-key/src/controller.rs`", + "detailedDesign" : "One controller implements standard reconcile for local physical Devices, authority/projection SecurityKeyServices, and SecurityKeyBindings. It observes Devices; realizes an authority Service as relay Process/Service-owned Endpoint; accepts projection Services only from Core/import after signed-factory admission; realizes each Binding as frontend Process/private Endpoint; enforces child-first finalizers and never creates an import or Device projection. Export/Import routing never treats an Endpoint as exported identity.", + "integration" : "Watches Device and both provider-neutral semantic types filtered by `providerRef=Provider/device-security-key`, plus Process, Endpoint, Guest/User, ResourceExport/Import, and Service/export/import-owner dependency indexes; writes semantic base plus signed Provider-extension status/finalizers; drives relay-control messages.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`controller_reconcile.rs`, `service_binding_projection.rs`, `mutual_exclusion.rs`, `status_binding.rs`, and deletion/finalizer tests cover authority/projection/Binding branches, signed-factory admission before projection reconcile, Service-owned Endpoint isolation from Export/Import identity, no Device projection, and no Volume API calls.", + "workItemId" : "ADR046-security-key-009" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no relay session state import", + "dependencyOwner" : "Authority Service relay owner; depends on ADR046-security-key-001, ADR046-security-key-002, ADR046-security-key-011, ADR046-security-key-012, ADR046-security-key-014, ADR046-security-key-016, and ADR046-security-key-018.", + "destination" : "`packages/d2b-provider-device-security-key/src/relay.rs`", + "detailedDesign" : "Authority Service-owned relay entry point: bounded authenticated Binding connections, one LeaseId-guarded fair ceremony queue, CID translation/cancel-all-CIDs, hidraw fd from Core DeviceGrant, CTAPHID named stream, and internal relay-control channel.", + "integration" : "Core injects the Service's physical DeviceGrant, relay Endpoint, and controller channel; Bindings that reference authority or projection Services connect through same-Zone Service Endpoints; Core releases grant on relay exit.", + "removalProof" : "Supersedes daemon-internal relay behavior removed by ADR046-security-key-030/ADR046-security-key-031 after relay Process tests pass.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`host_relay_guest_frontend/`, `fair_queue.rs`, `device_grant_no_path.rs`, `descriptor_validation.rs`, `cancel_propagation.rs`, and `cid_isolation.rs` prove one authority open, multi-Binding fair serialization, fd-only access, LeaseId cancel, and CID isolation.", + "workItemId" : "ADR046-security-key-010" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no session ring import", + "dependencyOwner" : "Relay ceremony-session foundation owner; depends on ADR046-security-key-001 and ADR046-security-key-008.", + "destination" : "`packages/d2b-provider-device-security-key/src/session.rs`", + "detailedDesign" : "`SessionStateMachine` with Idle/Queued/Active/Completed/TimedOut, bounded FIFO queue, monotonic LeaseId stale-release guard, per-Binding session ring, timeout/cancel, and ring eviction. Ceremony rows are non-Resource records; DeviceGrant remains held for relay lifetime.", + "integration" : "ADR046-security-key-010 consumes it; controller receives lifecycle messages; Service/Binding status receives aggregates only; session query/audit consumes bounded non-secret rows.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`session_state_machine.rs`, `session_ring.rs`, `fair_queue.rs`, `session_timeout.rs`, and `cancel_propagation.rs` cover queue fairness, eviction, stale LeaseId rejection, timeout, and cancel.", + "workItemId" : "ADR046-security-key-011" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; CID maps are transient and not imported", + "dependencyOwner" : "Relay CID-translation foundation owner; depends on ADR046-security-key-001 and ADR046-security-key-008.", + "destination" : "`packages/d2b-provider-device-security-key/src/cid.rs`", + "detailedDesign" : "CID translator: per-active-ceremony u32→u64 host-CID allocation, bimap, cancel-all-active-CIDs, and eviction on ceremony end", + "integration" : "Relay rewrites frontend CTAPHID CIDs before sending to hidraw fd and reverses responses before writing the ComponentSession named stream; session teardown drops the map.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`cid_isolation.rs` verifies per-session allocation, round trip, no sharing across relays, and eviction on session end.", + "workItemId" : "ADR046-security-key-012" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 probe state import", + "dependencyOwner" : "Probe/effect-port and activation owner; depends on ADR046-security-key-025 effect port and Core private bundle device table support.", + "destination" : "`packages/d2b-provider-device-security-key/src/probe.rs`; Provider activation/Core private bundle device table population for label → `device_token`", + "detailedDesign" : "hidraw probe: `probe.rs` — calls `SecurityKeyEffectPort::observe_inventory(&device_id, &policy_id)` with opaque types injected by Core; interprets `InventoryObservation`; never reads `/sys/class/hidraw/` directly; bundle device table population at activation time (Provider activation resolves label → `device_token` via Core; stored in private bundle)", + "integration" : "Controller scheduled-observe invokes `probe.rs`; Core adapter implements `SecurityKeyEffectPort`; Nix activation emits private label-to-token bundle entries; Device status receives `DevicePresent` and phase updates.", + "removalProof" : "Supersedes provider-side or broker fallback sysfs scanning; ADR046-security-key-004/ADR046-security-key-018 removal proof verifies only bundle `device_token` lookup remains.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`controller_reconcile.rs` scheduled-observe tests, `descriptor_validation.rs` Debug-redaction capture, and path-safety tests prove Provider never reads sysfs and receives only opaque observations.", + "workItemId" : "ADR046-security-key-013" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 transport/session state import", + "dependencyOwner" : "ComponentSession/security descriptor contract owner; depends on ADR046-security-key-008, ADR046-security-key-005, and ADR-046-componentsession-and-bus.", + "destination" : "`packages/d2b-provider-device-security-key/src/descriptor.rs`", + "detailedDesign" : "Declare relay↔controller service and relay↔Binding-frontend `d2b.security-key.v3` fingerprints, Noise profiles, canonical Service/Binding subject pairing, bounded encrypted-stream records, and descriptor validation; no ambient path or raw vsock CID.", + "integration" : "Provider descriptor declares services and fingerprints; LaunchTicket injects internal channel and Endpoint transport; relay/controller/frontend validate descriptors and peer authority before exchanging messages.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`descriptor_validation.rs` covers wrong service, wrong descriptor digest, wrong SO_PEERCRED uid, unenrolled key, oversized records, no ambient path, and redacted opaque IDs.", + "workItemId" : "ADR046-security-key-014" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no sandbox state import", + "dependencyOwner" : "Sandbox/minijail foundation owner; depends on ADR046-security-key-008, ADR046-security-key-007, and ADR-046-components-processes-and-sandbox.", + "destination" : "`nixos-modules/minijail-profiles.nix` entries for relay and controller; provider descriptor sandbox templates for relay/controller/frontend", + "detailedDesign" : "Minijail profiles for relay and controller only; frontend uses `Provider/system-systemd` hardening directives compiled from `SandboxSpec` (no minijail profile for frontend). Add relay and controller entries to `nixos-modules/minijail-profiles.nix`; `capabilityClasses: []`; `seccompClass: sk-relay` and `seccompClass: sk-controller`", + "integration" : "Nix minijail profiles feed system-minijail Process launches for controller/relay; frontend Process template feeds system-systemd hardening; provider tests assert the split.", + "removalProof" : "Supersedes `ProcessRole::SecurityKeyFrontend`-centric minijail test ownership removed by ADR046-security-key-033/ADR046-security-key-034 after Process-resource coverage passes.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`minijail_sk_frontend` successor tests, sandbox template tests, and zero-capability/seccomp assertions cover relay/controller minijail profiles and no frontend minijail profile.", + "workItemId" : "ADR046-security-key-015" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 processes.json import", + "dependencyOwner" : "Provider process/Endpoint-template owner; depends on ADR046-security-key-008, ADR046-security-key-015, and ADR046-security-key-026 Service/Binding ownership contracts.", + "destination" : "Provider descriptor Process templates and owned CTAPHID `Endpoint` template for `Provider/device-security-key`", + "detailedDesign" : "Templates: Provider controller; authority Service-owned relay Process/relay Endpoint; Binding-owned frontend Process/private Endpoint; projection Service local Endpoint. Frontend requires Guest/User and the system-core UHID DeviceGrant; no virtual Device template exists.", + "integration" : "Core creates controller; Provider controller realizes authority Services and Bindings plus each projection Service's ordinary local import-route Endpoint; Process Providers launch children and preserve ownerRef boundaries.", + "removalProof" : "Supersedes the legacy readiness-only `ProcessRole::SecurityKeyFrontend` tracking node removed by ADR046-security-key-034 after v3 Process resources are live.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`controller_reconcile.rs`, Process template golden tests, Endpoint resource tests, and frontend `userRef` admission tests prove templates and Endpoint shape.", + "workItemId" : "ADR046-security-key-016" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no provider descriptor import", + "dependencyOwner" : "Provider package descriptor owner; depends on ADR046-security-key-008, ADR046-security-key-005, ADR046-security-key-014, ADR046-security-key-016, ADR046-security-key-026, ADR046-security-key-027, and ADR-046-provider-model-and-packaging.", + "destination" : "Signed Provider descriptor JSON for `Provider/device-security-key` in the provider package", + "detailedDesign" : "Signed descriptor: config; physical Device integration; implementation claim for the provider-neutral `security-key.d2bus.org` Service/Binding base schemas/fingerprints; strict `device-security-key.d2bus.org` spec/status extensions; authority/projection union and D097 descriptor; a D096 projection factory with exact `serviceType`, `projectionSchemaFingerprint`, and semantic `factoryFingerprint`; controller/relay/frontend/Endpoint templates; export/import adapter capability; ComponentSession services; empty ProviderStateSet; permission claims. Provider/adapter identity is signed separately and Service-owned Endpoints are not factory or Export fields.", + "integration" : "Core ProviderDeployment verifies the signed descriptor, installs ResourceApiBinding and component descriptors, exposes service fingerprints to ComponentSession validation, and supplies permission claims/RBAC bindings.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Descriptor schema validation, semantic-base versus Provider-extension fingerprints, exact projection-schema/factory fingerprint derivation and stability under Provider/adapter identity changes, exact type/no-alias tests, service inventory tests, permission claim tests, empty ProviderStateSet tests, and README/provider package conformance checks.", + "workItemId" : "ADR046-security-key-017" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 broker state import", + "dependencyOwner" : "Core LaunchTicket/broker owner; depends on ADR046-security-key-004, ADR046-security-key-005, ADR046-security-key-013, and ADR-046-resources-device.", + "destination" : "v3 `SecurityKeyOpenDevice` broker op and Core LaunchTicket DeviceGrant resolution path", + "detailedDesign" : "v3 `SecurityKeyOpenDevice` broker op update: add `zone` field; implement bundle device table `device_token` lookup as sole open path; remove iterative sysfs scan from broker; add post-open revalidation steps (fstat, HIDIOCGRAWINFO, HIDIOCGRDESC). This is an internal Core operation called by LaunchTicket; the Provider controller does not call it.", + "integration" : "Authority Service controller derives relay `deviceUsage` from `spec.provider.settings.deviceRef`; Core admits authority then resolves DeviceGrant through the private bundle table; broker returns an fd to Core; projection Services never enter this path.", + "removalProof" : "Superseded broker iterative sysfs scan behavior is removed; tests prove only bundle `device_token` lookup is accepted for `SecurityKeyOpenDevice`.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Broker unit tests for zone field and token lookup, path-rejection tests, post-open revalidation tests, and provider tests proving no Provider broker call or sysfs path.", + "workItemId" : "ADR046-security-key-018" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; current Nix options migrate to v3 Zone resources without state import", + "dependencyOwner" : "Nix resource compiler owner; depends on ADR046-security-key-017, ADR046-security-key-026, ADR046-zone-control-024, and ADR-046-nix-configuration.", + "destination" : "`nixos-modules/` resource compiler/eval assertions for physical Device, authority Service, ResourceExport/Import, and consumer Binding", + "detailedDesign" : "Compile the owner Device→Service→export and consumer import→projection-Service→Binding shape. Emit Export `resourceRef`, `serviceType`, `projectionSchemaFingerprint`, and `factoryFingerprint`, and matching Import `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`; the Import `exportKey` identifies the ResourceExport. Reject Export Endpoint/custom-key fields, authored projections, projection `spec.provider`, Device export/projection, cross-Zone refs, duplicate authorities/Bindings, paths, and any security-key/USB configuration that does not collide through the exact Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` tuple after trusted identity resolution.", + "integration" : "Nix emits Device/authority Service/export/import/Binding with canonical D096 fields; Core alone creates projection Service; the Service controller alone owns relay/import-route Endpoints; bundle feeds Provider and authority-index admission.", + "removalProof" : "Supersedes current option shape only after v3 Zone resource option parity; legacy security-key/USBIP mutual-exclusion assertion is replaced by v3 resource assertion coverage.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Nix eval tests for label resolution, `busClass=hidraw`, exclusive arbitration, exact canonical Export/Import field emission and fingerprint matching, rejection of obsolete Export `endpointRef`/`exportedType`/`baseSchemaFingerprint`/`exportKey` and Import `expectedType`/`expectedBaseSchemaFingerprint`/`projectionType`, Core-only projection without `spec.provider`, byte-identical USB/security-key physical backing tuple collision, Provider-private-class bypass rejection, prohibited fields, and providerRef resolution.", + "workItemId" : "ADR046-security-key-019" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no legacy frontend unit state import", + "dependencyOwner" : "Guest Nix migration owner; depends on ADR046-security-key-003, ADR046-security-key-016, and ADR046-security-key-026 Binding-owned frontend contract.", + "destination" : "`nixos-modules/components/security-key-guest.nix` migration gate `d2b.securityKey._legacySystemdUnit`", + "detailedDesign" : "Guest Nix module migration gate: `d2b.securityKey._legacySystemdUnit` option, defaulting to false when Provider is installed; remove `d2b-sk-frontend.service` unit", + "integration" : "Guest Nix keeps `uhid` and the static frontend binary; Binding controller owns frontend lifecycle and system-core supplies the UHID DeviceGrant; no Device row or udev rule is emitted.", + "removalProof" : "ADR046-security-key-032 deletes the superseded `nixos-modules/components/security-key-guest.nix` `d2b-sk-frontend.service` declaration after the gate defaults to false.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Nix eval tests show the legacy unit is absent by default with Provider installed, can be gated only during transition if required, and `uhid` module/binary wiring remains present.", + "workItemId" : "ADR046-security-key-020" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 audit import", + "dependencyOwner" : "Audit owner for Core device-grant and Service/Binding lifecycle; depends on ADR046-security-key-009, ADR046-security-key-018, and ADR-046-telemetry-audit-and-support.", + "destination" : "Core `device-grant` audit and Provider controller Service/Binding ceremony lifecycle audit", + "detailedDesign" : "Path-free authority-grant records from Core and bounded Service/Binding/session digests/outcomes from controller; no path, raw target identity, LeaseId, session content, or CTAP bytes. `resource_name_digest` is admitted only in the Core authority-grant audit after DeviceGrant authorization and is never copied to OTEL.", + "integration" : "Core emits grant audit; controller emits Service/Binding lifecycle audit; Zone stream stores bounded records; CLI/support consumes digests/outcomes.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Audit tests assert path-free fields, bounded digests, no guest name/session content/CTAP bytes, grant emitted by Core not Provider controller, and lifecycle emitted by controller.", + "workItemId" : "ADR046-security-key-021" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 telemetry import", + "dependencyOwner" : "Observability owner; depends on ADR046-security-key-010 relay, ADR046-security-key-009 controller, and ADR-046-telemetry-audit-and-support.", + "destination" : "Provider/controller bounded telemetry emitter and observability-otel handoff for security-key metrics", + "detailedDesign" : "OTEL metrics: `d2b_device_sk_session_total`, `d2b_device_sk_ceremony_duration_seconds`, `d2b_device_sk_relay_restarts_total` via bounded emitter ring; descriptors use only closed semantic labels and never Zone/resource-name-derived identity. Provider spans use only fixed operation/phase/outcome/error-class attributes. Neither metrics nor spans admit a resource name, UID, ref, digest (including `resource_name_digest`), session ID, or derived identity token, while `d2b.zone` and `d2b.provider` remain OTEL Resource attributes.", + "integration" : "Relay/controller write metric events to the bounded ring; observability-otel Provider drains and exports; dashboards/CLI consume closed labels and bounded histograms.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`telemetry_identity_canaries.rs` and metric inventory tests structurally assert closed label/span-attribute sets; exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, resource-name-derived keys, and `resource_name_digest`; Device/Service/Binding/Guest/Zone name, UID, ref, and digest canary absence from metrics and spans; retained `d2b.zone` Resource attributes; bounded ring behavior; and correct session/ceremony/restart counters.", + "workItemId" : "ADR046-security-key-022" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "Provider documentation owner; depends on ADR046-security-key-008 through ADR046-security-key-022 and ADR046-security-key-024 through ADR046-security-key-029 for complete behavior.", + "destination" : "`packages/d2b-provider-device-security-key/README.md`", + "detailedDesign" : "README: initial Provider identity, provider-neutral Service/Binding catalog, strict Provider-extension fields, physical Device, owner/export/import/projection/Binding chain, process ownership, RBAC, invariants, status/telemetry, no-alias rule, and commands", + "integration" : "Workspace/package policy and provider crate acceptance use the README as the human entry point; docs link to it for provider-local build/test/integration commands.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "README presence check from provider crate standard layout; documentation review verifies every listed section and command is present and matches the crate/package behavior.", + "workItemId" : "ADR046-security-key-023" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 transport state import", + "dependencyOwner" : "Endpoint/ComponentSession integration owner; depends on ADR046-security-key-003, ADR046-security-key-010, ADR046-security-key-014, ADR046-security-key-016, ADR046-security-key-026, and ADR-046-componentsession-and-bus.", + "destination" : "Authority/projection Service Endpoint and Binding private Endpoint resolution, including transport-vsock and ZoneLink encrypted streams", + "detailedDesign" : "Resolve each Binding only through its same-Zone Service Endpoint; enroll Noise KK for Service/Binding frontend; authority uses transport-vsock locally, projection uses per-import bounded encrypted stream with credits/backpressure/generation/deadline/cancel.", + "integration" : "Service/Binding-owned Endpoints produce opaque LaunchTicket attachments; the import adapter binds the projection Service's ordinary local import-route Endpoint; no remote Ref, FD, or raw locator is exposed.", + "removalProof" : "Supersedes baseline `vsock.sock_14320` raw port usage; tests prove no `vsockPort` or raw AF_VSOCK framing remains for security-key transport.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`host_relay_guest_frontend/` and `descriptor_validation.rs` verify Endpoint resolution, Noise KK enrollment, attachment opacity, and no raw vsock CID/port in status/spec.", + "workItemId" : "ADR046-security-key-024" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 effect-port state import", + "dependencyOwner" : "`d2b-contracts` neutral effect-port foundation owner; depends on ADR046-security-key-008 and ADR-046-resources-device.", + "destination" : "`d2b-contracts` neutral `SecurityKeyEffectPort` trait/types; `packages/d2b-provider-device-security-key/src/effect_port.rs` re-export; Core adapter implementation in `d2b-provider` or `d2b-provider-toolkit`", + "detailedDesign" : "Define/re-export the opaque redacting `SecurityKeyEffectPort` types in the neutral contract crate and implement the Core adapter; inject per physical Device into the Provider controller; relay and projection Service do not receive the port.", + "integration" : "Core resolves Zone/label to opaque IDs and injects the port into the controller; controller scheduled-observe calls the trait; Provider crate depends only on the neutral contract/re-export; relay path is unaffected.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Unit tests assert Debug redaction, controller calls `observe_inventory` with injected IDs, relay has no port dependency, and fake Core adapter returns bounded `InventoryObservation`.", + "workItemId" : "ADR046-security-key-025" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no legacy Device/claim projection import", + "dependencyOwner" : "Device-security-key Service/Binding implementation owner; depends on ADR046-provider-004, ADR046-security-key-008, ADR046-security-key-005, resource object/Device/D096/D097 contracts.", + "destination" : "`packages/d2b-provider-device-security-key/src/{resource_type,provider_extension,admission}.rs`; controller contracts; system-core Guest UHID authority-subresource DeviceGrant (common base lives under ADR046-provider-004)", + "detailedDesign" : "Bind the shared semantic authority/projection Service and Binding base versions/fingerprints from ADR046-provider-004, then define only the initial strict Provider extension and admission. The owner/Binding extension references the local physical Device/relay Endpoint and owns CTAPHID/fairness/frontend settings and observations. Projection is Core-owned by ResourceImport with `providerRef` plus semantic base/import fields, no `spec.provider`, and no Device/open; routing derives from the signed local descriptor, `providerRef`, and import record. Export admission binds the authority Service's `resourceRef` and `serviceType` to the signed projection-schema and factory fingerprints, never to its Endpoint. Binding is operator intent and the initial extension realizes its frontend Process/private Endpoint. Standard Device remains physical only; provider-named ResourceType aliases are rejected.", + "integration" : "ResourceExport targets Service with canonical type/fingerprint fields; ResourceImport supplies matching expected fields and creates projection Service; Binding references same-Zone Service; Service controllers retain Endpoint ownership; Core injects Guest UHID without a Device row.", + "removalProof" : "Supersedes legacy frontend/import Device modeling; ADR046-security-key-035 removes udev mutation and ADR046-security-key-032 removes the legacy unit once Binding-owned realization is live.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Fast schema/lifecycle conformance consumes the ADR046-provider-004 fixtures, accepts canonical minimal base without `spec.provider`, includes a fake alternate security-key Provider, and proves Device→provider-neutral Service→export→import→projection Service→provider-neutral Binding→frontend, exact canonical Export/Import fields, no Endpoint export, projection `spec.provider` rejection, D088 status layering, strict base/Provider-extension separation, exact types with no aliases, strict ownership/finalizers, no Device projection, and no local hidraw open in consumer Zone.", + "workItemId" : "ADR046-security-key-026" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "Provider state/status contract owner; depends on ADR046-security-key-008, ADR046-security-key-026, and ADR-046-provider-state.", + "destination" : "Provider descriptor state declaration, controller/status logic, Process templates, and Nix principal provisioning for `Provider/device-security-key`", + "detailedDesign" : "Empty ProviderStateSet and strict bounded status schemas: physical presence in Device `status.resource`; semantic authority/import aggregates in Service `status.resource`; attachment aggregates in Binding `status.resource`; initial physical-backing claim, relay, Endpoint, queue, and ceremony observations only in `status.provider`. No semantic field appears directly under `status`, and Core projections contain no `spec.provider`. Ceremony rows remain high-churn non-Resource session records; CTAP/fd/LeaseId/CID data stays transient. No Process has `/state`.", + "integration" : "ADR046-security-key-017 signs schemas; ADR046-security-key-009 writes resource-local status; Core Operation/session/audit surfaces own bounded records; Volume controllers see no request.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "`status_binding.rs` proves empty ProviderStateSet, no `/state` mounts, no Volume API calls, authority/import/attachment fields only under `status.resource`, implementation fields only under `status.provider`, no projection `spec.provider`, and no CTAP/fd/session secrets in status/log/audit/metrics.", + "workItemId" : "ADR046-security-key-027" + }, + { + "currentSource" : "None — net-new ADR 0046 cross-Zone sharing (D096)", + "dataMigration" : "Full d2b 3.0 reset; no cross-Zone sharing state", + "dependencyOwner" : "Cross-Zone adapter owner; depends on ADR046-security-key-024, ADR046-security-key-026, ADR046-security-key-029, ADR046-zone-control-019, and ADR046-zone-control-020.", + "destination" : "`packages/d2b-provider-device-security-key/src/share_adapter.rs`", + "detailedDesign" : "Signed adapters admit ResourceExport only when `resourceRef` names an authority SecurityKeyService, `serviceType` is `security-key.d2bus.org.SecurityKeyService`, and `projectionSchemaFingerprint` plus `factoryFingerprint` match the signed semantic factory. ResourceImport must supply the corresponding `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`; its `exportKey` identifies the ResourceExport. The Service's relay Endpoint stays a Service-owned implementation child and is never an Export field. Core invokes the factory to create one projection SecurityKeyService with `ownerRef: ResourceImport/`, `providerRef`, semantic base/import fields, and no `spec.provider`; route selection comes from the signed local descriptor and ResourceImport record. The semantic factory fingerprint binds factory metadata plus projection-protocol version only, while adapter identity is authenticated separately by the signed Provider descriptor. They never project Device or auto-create Binding. Route Binding ceremonies over bounded encrypted named streams to the single authority fair queue; no FD/USBIP/hidraw/ref crosses Zones. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (implement the signed security-key export/import adapter).", + "integration" : "Core export/import routing/projection lifecycle; ADR046-security-key-029 authority; ADR046-security-key-024 Endpoint streams; Nix/operator-authored Binding consumes the same-Zone projection.", + "removalProof" : "Not applicable (new surface)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Fast fake-stream conformance proves owner Service→export→import→projection Service→Binding→frontend; exact canonical Export/Import type and fingerprint fields; rejection of Export `endpointRef`, `exportedType`, `baseSchemaFingerprint`, and `exportKey` plus Import `expectedType`, `expectedBaseSchemaFingerprint`, and `projectionType`; rejection of projection `spec.provider`; semantic factory-fingerprint stability when signed adapter identity changes; separate signed-descriptor identity authentication; one fair LeaseId-guarded ceremony; ciphertext to intermediaries; no Device projection/local hidraw/FD/USBIP; revocation degradation; and audit metadata only.", + "workItemId" : "ADR046-security-key-028" + }, + { + "currentSource" : "`packages/d2bd/src/security_key.rs` (`CidTranslator`, `SecurityKeyState`, `LeaseId`/`LeaseState`, `CEREMONY_TIMEOUT` 120 s, `QUEUE_WAIT_TIMEOUT` 15 s, `parse_ctaphid_report`/`build_cancel_packet`); `packages/d2b-priv-broker/src/ops/security_key.rs` (`live_open_hidraw_security_key`, double `fstat` + FIDO usage-page 0xF1D0 + HID raw-info revalidation, `O_RDWR|O_NONBLOCK|O_NOFOLLOW`); `packages/d2b-sk-frontend/src/{main,uhid,vsock,framing}.rs` (UHID FIDO2 CTAPHID frontend, 64-byte report relay)", + "dataMigration" : "Full d2b 3.0 reset; no per-session/lease state persisted", + "dependencyOwner" : "D097 authority foundation owner; depends on ADR046-security-key-001, ADR046-security-key-002, ADR046-security-key-003, ADR046-security-key-004, ADR046-security-key-018, ADR046-security-key-026, ADR046-zone-control-024, and the D097 authority contract.", + "destination" : "`packages/d2b-provider-device-security-key/src/{authority,relay,streams}.rs`; D097 `AuthorityDescriptor` on authority SecurityKeyService", + "detailedDesign" : "The provider-neutral authority Service, not Device/Endpoint/Process, is the stable D097 owner and carries the semantic opaque Host-scoped zero-or-one descriptor. The initial Provider extension references the local physical Device and relay Endpoint and supplies service-specific physical-key derivation, Service+relay ownerProof, and bounded-fairness details. After trusted USB identity resolution, Core additionally derives `physical-usb-backing/v1` and atomically claims the exact `(Host, physical-usb-backing, opaqueKeyDigest)` tuple used by every USB Provider before any open, withhold, bind, module, relay, or attachment effect; Provider-private claims cannot replace it. Preserve sole Core open with double-fstat/FIDO/HID validation, async fd I/O, per-session CidTranslator, LeaseId stale-release guard, cancel-all-CIDs, one ceremony, bounded FIFO wait, and Binding-owned UHID frontend. Ceremony rows are not Resources. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — relay becomes the D097 hidraw authority; transport moves to Endpoint/named-stream.", + "integration" : "Authority Service owns relay/Endpoint; Core index admits it and LaunchTicket supplies physical DeviceGrant; ADR046-security-key-028 exports/imports Service; Binding owns frontend/private Endpoint; USBIP conflict remains Host-wide.", + "removalProof" : "The legacy daemon accept loop, raw CTAPHID framing, fixed `SK_VSOCK_PORT`, and broker sysfs `/sys/class/hidraw/` scan fallback are deleted only after the relay `Endpoint`/named-stream successor and the `device_token`-only broker open are green (coordinated with ADR046-security-key-034 `ProcessRole` removal and ADR046-security-key-004 broker-op revalidation).", + "reuseAction" : "adapt", + "reuseSource" : "Same baseline daemon/broker/frontend symbols", + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Fast hermetic tests adapt the existing `CidTranslator`/lease/cancel/UHID/broker-revalidation suites: CID alloc/translate/release, `LeaseId` stale-release, cancel-all-CIDs on disconnect, one-ceremony + 120 s timeout, 15 s fair-wait `ERR_CHANNEL_BUSY`, UHID frame round-trip, broker double-`fstat`+FIDO+HID revalidation, byte-identical USB/security-key backing tuple derivation for one fake token, and `physical-usb-backing-conflict` before effects under alternate labels/private authority classes — all with fakes/`FakeEffectPort`, no real hidraw. Integration proves cross-Zone CTAP ceremony **serialization** over the encrypted named stream and the shared physical USB collision.", + "workItemId" : "ADR046-security-key-029" + }, + { + "currentSource" : "`packages/d2bd/src/security_key.rs` — `start_sk_accept_loop`, `SecurityKeyState`, `LeaseState`, `SkRegistry`", + "dataMigration" : "Full d2b 3.0 reset; no daemon session state migration", + "dependencyOwner" : "Provider-device-security-key removal owner; depends on ADR046-security-key-001, ADR046-security-key-002, ADR046-security-key-010, ADR046-security-key-011, and ADR046-security-key-012 successor relay/session coverage.", + "destination" : "Removed from daemon; successor behavior lives in `packages/d2b-provider-device-security-key/src/relay.rs`, `session.rs`, and `cid.rs`", + "detailedDesign" : "Remove target `packages/d2bd/src/security_key.rs` — `start_sk_accept_loop`, `SecurityKeyState`, `LeaseState`, `SkRegistry` after v3 relay Process is live and stable; keep behind feature gate only if needed during transition. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete.", + "integration" : "d2bd no longer owns security-key accept/session state; Provider controller, authority Service relay, and Binding frontends own lifecycle; Core LaunchTicket owns hidraw/UHID grants.", + "removalProof" : "Concrete removed path/behavior: `packages/d2bd/src/security_key.rs` `start_sk_accept_loop`, `SecurityKeyState`, `LeaseState`, and `SkRegistry` daemon-internal accept/session ownership are absent.", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Provider relay/session tests pass; daemon build has no references to removed symbols; no legacy security-key accept loop starts under d2bd.", + "workItemId" : "ADR046-security-key-030" + }, + { + "currentSource" : "`packages/d2bd/src/lib.rs` — `start_sk_accept_loop` call site and daemon-internal Unix socket proxy bind", + "dataMigration" : "Full d2b 3.0 reset; no daemon socket state migration", + "dependencyOwner" : "d2bd integration removal owner; depends on ADR046-security-key-030.", + "destination" : "Removed from daemon startup; successor launch path is ProviderDeployment/controller-created relay Process plus Endpoint/ComponentSession transport", + "detailedDesign" : "Remove target `packages/d2bd/src/lib.rs` — `start_sk_accept_loop` call site and daemon-internal Unix socket proxy bind after ADR046-security-key-030. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete.", + "integration" : "d2bd startup no longer binds a security-key Unix socket proxy; Core/ProviderDeployment starts provider controller and relay Process resources; transport-vsock Endpoint supplies frontend connectivity.", + "removalProof" : "Concrete removed path/behavior: `packages/d2bd/src/lib.rs` no longer calls `start_sk_accept_loop` and no longer binds the daemon-internal security-key Unix socket proxy.", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "d2bd startup tests/build prove no `start_sk_accept_loop` call or security-key proxy bind remains; provider integration test proves CTAPHID flow through Endpoint/ComponentSession.", + "workItemId" : "ADR046-security-key-031" + }, + { + "currentSource" : "`nixos-modules/components/security-key-guest.nix` — `d2b-sk-frontend.service` systemd unit declaration", + "dataMigration" : "Full d2b 3.0 reset; no legacy unit state migration", + "dependencyOwner" : "Guest Nix module removal owner; depends on ADR046-security-key-020, ADR046-security-key-003, and ADR046-security-key-026 Binding frontend/UHID contract.", + "destination" : "Removed from guest Nix module; successor is Binding-owned `Process/binding--sk-frontend`", + "detailedDesign" : "Remove target `nixos-modules/components/security-key-guest.nix` — `d2b-sk-frontend.service` systemd unit declaration after ADR046-security-key-020 migration gate defaults to false. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete.", + "integration" : "Guest Nix keeps `uhid` and frontend binary only; Provider controller creates the Binding-owned frontend; system-systemd manages it.", + "removalProof" : "Concrete removed path/behavior: `nixos-modules/components/security-key-guest.nix` no longer declares the static `d2b-sk-frontend.service` unit.", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Nix eval tests prove no static `d2b-sk-frontend.service` is emitted with Provider installed; frontend Process integration proves replacement lifecycle.", + "workItemId" : "ADR046-security-key-032" + }, + { + "currentSource" : "`packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` and `packages/d2b-contract-tests/tests/usb_sk_contract.rs`", + "dataMigration" : "None — test-only move/delete; no runtime state", + "dependencyOwner" : "Test-suite migration/removal owner; depends on ADR046-security-key-006 and ADR046-security-key-007 provider-crate successor tests.", + "destination" : "Removed from `packages/d2b-contract-tests/tests/`; successor tests live in `packages/d2b-provider-device-security-key/tests/`", + "detailedDesign" : "Remove target `packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` and `packages/d2b-contract-tests/tests/usb_sk_contract.rs` after ADR046-security-key-006/ADR046-security-key-007 tests are in Provider crate and cover all prior assertions. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete after move/adapt.", + "integration" : "D094 disposition updates closed gate manifests, layer1 jobs, pins, ledgers, and CI shards so only the provider-crate successor suite remains.", + "removalProof" : "Concrete removed paths: `packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` and `packages/d2b-contract-tests/tests/usb_sk_contract.rs` are deleted after provider-crate successor coverage passes.", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Provider-crate tests pass with retained assertions; old contract-test paths are absent from manifests/CI; no duplicate old/new suite runs indefinitely.", + "workItemId" : "ADR046-security-key-033" + }, + { + "currentSource" : "`ProcessRole::SecurityKeyFrontend` in `d2b-core/src/processes.rs`", + "dataMigration" : "Full d2b 3.0 reset; no processes.json role migration", + "dependencyOwner" : "Core ProcessRole removal owner; depends on ADR046-security-key-016 Process resources, ADR046-security-key-015 sandbox templates, and system-minijail/system-systemd conformance.", + "destination" : "Removed from `d2b-core/src/processes.rs`; successor frontend is a v3 Process resource owned by `Provider/device-security-key`", + "detailedDesign" : "Remove target `ProcessRole::SecurityKeyFrontend` in `d2b-core/src/processes.rs` after relay and frontend are v3 Process resources; no other code reference expected. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete.", + "integration" : "ProcessRole disposition table confirms all security-key frontend lifecycle, sandbox, readiness, and DeviceGrant semantics are represented by Resource Process templates and Process Providers before enum removal.", + "removalProof" : "Concrete removed path/behavior: `d2b-core/src/processes.rs` no longer contains `ProcessRole::SecurityKeyFrontend` or a security-key frontend role in the legacy ProcessRole/VmProcessDag model.", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "Workspace build proves no `ProcessRole::SecurityKeyFrontend` references; provider Process template tests prove the v3 replacement; process conformance passes.", + "workItemId" : "ADR046-security-key-034" + }, + { + "currentSource" : "`SecurityKeyApplyUdevRules` broker op, `SecurityKeyApplyUdevRulesRequest` DTO in `packages/d2b-contracts/src/security_key.rs`, and all related broker code", + "dataMigration" : "Full d2b 3.0 reset; no udev rule state migration", + "dependencyOwner" : "Broker/contracts/Nix removal owner; depends on ADR046-security-key-005, ADR046-security-key-018, ADR046-security-key-020, and ADR046-security-key-026 Guest-substrate UHID replacement.", + "destination" : "Removed from contracts and broker; successor access is static guest Nix `uhid` module plus Core pre-opened `/dev/uhid` DeviceGrant for the frontend Process", + "detailedDesign" : "Remove `SecurityKeyApplyUdevRules`, its DTO, and related broker code after the Binding-owned frontend and system-core UHID DeviceGrant are live. Guest Nix loads `uhid` but emits no security-key udev rule. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete.", + "integration" : "Guest Nix/Process DeviceGrant path provides UHID access; contracts no longer expose the op/request; broker capability set drops the udev mutation; provider/contract tests assert absence.", + "removalProof" : "Concrete removed path/behavior: `SecurityKeyApplyUdevRules` broker operation, `SecurityKeyApplyUdevRulesRequest` in `packages/d2b-contracts/src/security_key.rs`, and related broker code are absent.", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-security-key", + "specPath" : "docs/specs/providers/ADR-046-provider-device-security-key.md", + "validation" : "DTO unknown-field/capability tests prove `SecurityKeyApplyUdevRulesRequest` and op are absent; `device_grant_no_path.rs` proves frontend has UHID fd without udev/plugdev; broker build has no related code.", + "workItemId" : "ADR046-security-key-035" + }, + { + "currentSource" : "v3 `d2b-realm-router/src/{session,secure_session,mux_session,lifecycle}.rs`, guest auth/transport", + "dataMigration" : "No v2 session compatibility; reconnect on v3", + "dependencyOwner" : "W0 shared contract root", + "destination" : "`packages/d2b-contracts/src/v3/component_session.rs`, `packages/d2b-session/`", + "detailedDesign" : "Reversion canonical offer/records; retain NN/KK/IKpsk2; add canonical subject/authorization context hooks Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "d2b-bus, resource/controller/Provider services", + "removalProof" : "v3 old Realm PeerSession removed only after all v3 peer routes move", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `d2b-contracts/src/v2_component_session.rs`, `d2b-session/src/{handshake,bootstrap,record,engine,scheduler,streams,lifecycle,transport}.rs`, Noise/component tests/vectors", + "specId" : "ADR-046-componentsession-and-bus", + "specPath" : "docs/specs/ADR-046-componentsession-and-bus.md", + "validation" : "Copied exact vectors/rejections plus subject/RBAC/revocation tests", + "workItemId" : "ADR046-session-001" + }, + { + "currentSource" : "v3 guest vsock/local seqpacket implementations and d2b-realm-transport traits", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-session-001; transport owner", + "destination" : "`packages/d2b-session-unix/`, future enrolled transport adapter crates", + "detailedDesign" : "Unix/socketpair/vsock owned transports, peer evidence, fd/pidfd/object validation, credits Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "ProviderSupervisor/Host/Guest/Zone listeners hand owned transports to session", + "removalProof" : "Ad hoc guest/public/helper transport removed only per service cutover", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `d2b-session-unix/src/{adapter,socket,descriptor,pidfd,vsock,systemd,credit}.rs`, `tests/unix_session.rs`", + "specId" : "ADR-046-componentsession-and-bus", + "specPath" : "docs/specs/ADR-046-componentsession-and-bus.md", + "validation" : "Copied fd/peer/credit tests plus Host/Guest subject mapping", + "workItemId" : "ADR046-session-002" + }, + { + "currentSource" : "None — net-new v3 qualified `ShellPool` and `ShellSession` resource schemas; superseded draft and legacy shell code do not define these canonical resources.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Resource schemas area; owned by `d2b-provider-shell-terminal` resource modules.", + "destination" : "`packages/d2b-provider-shell-terminal/src/resources/{pool,session}.rs`", + "detailedDesign" : "Implement `shell-terminal.d2bus.org.ShellPool` and `shell-terminal.d2bus.org.ShellSession` schemas with qualified names, common phases, and typed detail fields.", + "integration" : "Nix resource compiler, resource API admission, controller reconcile, status writers, and d2b-bus routing all consume the qualified pool/session schemas. Integration path: `packages/d2b-provider-shell-terminal/integration/resource-shape/`.", + "removalProof" : "None — net-new; no prior owner to remove.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/resource_schema.rs`", + "workItemId" : "ADR046-sterm-001" + }, + { + "currentSource" : "None — net-new v3 controller; legacy guestd and unsafe-local helper shell paths are not the controller/state authority.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Controller binary area; owned by `d2b-provider-shell-terminal` controller and core Operation ledger integration.", + "destination" : "`packages/d2b-provider-shell-terminal/src/bin/d2b-shell-terminal-controller.rs`", + "detailedDesign" : "Implement `d2b-shell-terminal-controller` with pool/session reconcile loops; assert ProviderStateSet is empty; publish bounded non-secret operational state to resource status and the core Operation ledger; no controller Provider state Volume or `/state` mount exists. Primary reuse disposition: `create`. Preserved source-plan detail: net-new controller; preserve status-first ProviderStateSet-empty rule.", + "integration" : "Core ProviderDeployment starts the controller Process; controller reconciles ShellPool/ShellSession resources, writes status, registers routes, and records operations without a Provider state Volume. Integration path: `packages/d2b-provider-shell-terminal/integration/controller-restart/`.", + "removalProof" : "None — net-new controller; legacy controller-equivalent state owner does not exist.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/controller_reconcile.rs`", + "workItemId" : "ADR046-sterm-002" + }, + { + "currentSource" : "Reuse narrow ring/runtime ideas from `packages/d2b-guestd/src/shell.rs` and adoption-shape ideas from `packages/d2b-unsafe-local-helper/src/runtime.rs`; both legacy authorities are superseded.", + "dataMigration" : "Full d2b 3.0 reset; no v2 shell state import; PTY/ring state is live process memory only.", + "dependencyOwner" : "Supervisor binary area; owned by `d2b-provider-shell-terminal` session supervisor runtime.", + "destination" : "`packages/d2b-provider-shell-terminal/src/bin/d2b-shell-session-supervisor.rs`", + "detailedDesign" : "Implement `d2b-shell-session-supervisor` as the sole PTY owner for Host and Guest pools. Primary reuse disposition: `adapt`. Preserved source-plan detail: reuse narrow mechanics only; move PTY authority into per-session supervisor and exclude legacy protocols/identities/state storage.", + "integration" : "Controller creates one user-domain supervisor Process per ShellSession; supervisor owns PTY, login shell, ring, attach bookkeeping, and private ComponentSession service. Integration path: `packages/d2b-provider-shell-terminal/integration/supervisor-host-guest/`.", + "removalProof" : "Supersedes `guestd/src/shell.rs` managed runtime and unsafe-local helper shell supervisor; removed once successor supervisor coverage passes.", + "reuseAction" : "adapt", + "reuseSource" : "`packages/d2b-guestd/src/shell.rs`; `packages/d2b-unsafe-local-helper/src/runtime.rs`.", + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/supervisor_runtime.rs`", + "workItemId" : "ADR046-sterm-003" + }, + { + "currentSource" : "Superseded draft templates included pool-wide/system-domain or management-worker concepts; canonical v3 templates are defined in this spec.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Process templates area; owned by Nix compiler plus shell-terminal controller.", + "destination" : "`packages/d2b-provider-shell-terminal/src/process_templates.rs`", + "detailedDesign" : "Teach the Nix compiler and controller to emit the canonical controller and user-domain supervisor `Process` templates. Primary reuse disposition: `replace`. Preserved source-plan detail: replace incorrect draft templates with canonical controller and user-domain supervisor Process templates.", + "integration" : "Nix compiler emits controller Process/Endpoint resources; controller emits per-session user-domain supervisor Processes; Provider/system-systemd realizes them. Integration path: `packages/d2b-provider-shell-terminal/integration/process-placement/`.", + "removalProof" : "Removes forbidden pool-wide supervisor, disguised system-domain supervisor, management EphemeralProcess, and sealed output Volume concepts from the template surface.", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/process_templates.rs`", + "workItemId" : "ADR046-sterm-004" + }, + { + "currentSource" : "None — net-new v3 `OpenSession` lifecycle; legacy shell protocols do not create ShellSession resources with inherited-field freeze.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "OpenSession lifecycle area; owned by controller service implementation.", + "destination" : "`packages/d2b-provider-shell-terminal/src/service/open_session.rs`", + "detailedDesign" : "Create sessions from pools, freeze inherited fields, and return `supervisorGeneration` to callers.", + "integration" : "`shell-terminal.v3.OpenSession` validates pool capacity and policy, creates ShellSession and supervisor Process, registers route data, and returns session/supervisor references to clients. Integration path: `packages/d2b-provider-shell-terminal/integration/open-session/`.", + "removalProof" : "None — net-new resource lifecycle; no prior owner to remove.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/open_session.rs`", + "workItemId" : "ADR046-sterm-005" + }, + { + "currentSource" : "Ring buffer mechanics may reuse ideas from `packages/d2b-guestd/src/shell.rs`; management workers and sealed output Volumes from prior draft are removed.", + "dataMigration" : "Full d2b 3.0 reset; no terminal byte or ring-state import.", + "dependencyOwner" : "PTY and ring area; owned by per-session supervisor runtime.", + "destination" : "`packages/d2b-provider-shell-terminal/src/session/{pty,ring}.rs`", + "detailedDesign" : "Implement the in-memory PTY owner model, bounded ring buffer, replay, and eviction counters; do not create any management worker or `EphemeralProcess`. Primary reuse disposition: `adapt`. Preserved source-plan detail: reuse ring semantics; keep bytes in supervisor memory and remove management worker/EphemeralProcess model.", + "integration" : "Supervisor named terminal stream replays bounded ring tail then streams live PTY I/O; status publishes only ring byte counters and attach count. Integration path: `packages/d2b-provider-shell-terminal/integration/ring-overflow/`.", + "removalProof" : "Proves no controller-owned PTY, management worker, EphemeralProcess, or sealed output Volume remains for shell output/management responses.", + "reuseAction" : "adapt", + "reuseSource" : "`packages/d2b-guestd/src/shell.rs` bounded ring ideas only.", + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/ring_buffer.rs`", + "workItemId" : "ADR046-sterm-006" + }, + { + "currentSource" : "Reuse verification/adoption shape only from `packages/d2b-unsafe-local-helper/src/runtime.rs` `ScopeRuntime` and `PersistedScope`; exclude helper protocol and state storage.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Adoption and routing area; owned by controller session adoption/routing module.", + "destination" : "`packages/d2b-provider-shell-terminal/src/session/adopt.rs`", + "detailedDesign" : "Implement restart adoption, InvocationID plus cgroup verification, route registration, and stale-generation invalidation. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt identity-verification shape; do not reuse helper protocol, identities, or state storage assumptions.", + "integration" : "Controller restart scans supervisor Processes, verifies owner/session/generation identity, re-registers exact d2b-bus routes, and rejects stale or ambiguous handles. Integration path: `packages/d2b-provider-shell-terminal/integration/adopt-after-restart/`.", + "removalProof" : "Supersedes unsafe-local helper adoption storage/protocol; proof is degraded-ambiguity and stale-generation tests with no helper state dependency.", + "reuseAction" : "adapt", + "reuseSource" : "`packages/d2b-unsafe-local-helper/src/runtime.rs` `ScopeRuntime` and `PersistedScope` adoption pattern.", + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/adoption.rs`", + "workItemId" : "ADR046-sterm-007" + }, + { + "currentSource" : "Supersedes unsafe-local Host shell path from `packages/d2b-unsafe-local-helper/src/services/shell/` while preserving explicit non-isolation warning semantics.", + "dataMigration" : "Full d2b 3.0 reset; no unsafe-local shell state/config import.", + "dependencyOwner" : "Host rules area; owned by shell-terminal Host policy module.", + "destination" : "`packages/d2b-provider-shell-terminal/src/host_rules.rs`", + "detailedDesign" : "Emit Host `isolationPosture=none` warnings, same-UID verification, and relay denial for Host user-domain pools. Primary reuse disposition: `replace`. Preserved source-plan detail: replace unsafe-local helper shell policy with resource-backed Host pool warnings and same-UID checks.", + "integration" : "Pool reconcile reads Host posture and User identity, writes warning status/audit, and admission rejects relay-authenticated Host user-domain access. Integration path: `packages/d2b-provider-shell-terminal/integration/host-warning/`.", + "removalProof" : "Supersedes unsafe-local Host shell supervisor path; proof requires no SSH/direct-host fallback and relay denial coverage.", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/host_rules.rs`", + "workItemId" : "ADR046-sterm-008" + }, + { + "currentSource" : "Supersedes guest persistent-shell runtime in `packages/d2b-guestd/src/shell.rs`; Guest user-domain placement moves to Provider/system-systemd.", + "dataMigration" : "Full d2b 3.0 reset; no guestd shell runtime state/config import.", + "dependencyOwner" : "Guest rules area; owned by shell-terminal Guest policy module.", + "destination" : "`packages/d2b-provider-shell-terminal/src/guest_rules.rs`", + "detailedDesign" : "Require Guest `allowedDomains` to include `user`, require `defaultUserRef`, and place supervisors through the Guest user manager. Primary reuse disposition: `replace`. Preserved source-plan detail: replace guestd shell authority with Guest resource user-domain admission and supervisor placement.", + "integration" : "Pool admission validates Guest capabilities and default user; controller creates user-domain supervisor Processes through the Guest user manager exposed by Provider/system-systemd. Integration path: `packages/d2b-provider-shell-terminal/integration/guest-user-domain/`.", + "removalProof" : "Supersedes independent guestd session-limit/runtime authority; proof is rejection of Guests without `user` domain and no guestd-managed shell path.", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/guest_rules.rs`", + "workItemId" : "ADR046-sterm-009" + }, + { + "currentSource" : "Existing public-wire `ShellOp`/unsafe-local surfaces are superseded; v3 requires ComponentSession service authorization.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "RBAC and relay denial area; owned by shell-terminal authorization module.", + "destination" : "`packages/d2b-provider-shell-terminal/src/authz.rs`", + "detailedDesign" : "Gate all verbs on `Role/shell-admin` or Zone-admin superset and fail closed for relay-authenticated Host user-domain callers. Primary reuse disposition: `replace`. Preserved source-plan detail: replace legacy shell operation authorization with Role/shell-admin or Zone-admin service gates.", + "integration" : "Controller and supervisor ComponentSession methods authorize before capacity or route lookup, preserving stale-handle non-disclosure and Host relay denial. Integration path: `packages/d2b-provider-shell-terminal/integration/authz/`.", + "removalProof" : "Supersedes public-wire shell protocol authorization; proof requires no `ShellOp` or `ShellOpResponse` path remains.", + "reuseAction" : "replace", + "reuseSource" : null, + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/authz.rs`", + "workItemId" : "ADR046-sterm-010" + }, + { + "currentSource" : "None — net-new v3 closed-label/redacted observability for shell-terminal; legacy shell paths must not leak names, paths, PIDs, or terminal bytes.", + "dataMigration" : "Full d2b 3.0 reset; no v2 audit/telemetry state import.", + "dependencyOwner" : "Audit and telemetry area; owned by shell-terminal audit/telemetry modules.", + "destination" : "`packages/d2b-provider-shell-terminal/src/{audit,telemetry}.rs`", + "detailedDesign" : "Implement closed-label metrics, redacted spans, and audit events with no usernames, session names, paths, or terminal bytes. Primary reuse disposition: `create`. Preserved source-plan detail: net-new redacted observability.", + "integration" : "Reconcile, OpenSession, Attach, Detach, Kill, terminal exit, degradation, and Host posture warnings emit only digest/enum surfaces consumed by audit and OTEL collectors. Integration path: `packages/d2b-provider-shell-terminal/integration/support-redaction/`.", + "removalProof" : "None — net-new observability surface; legacy paths must be removed or adapted to pass redaction tests.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/redaction.rs`", + "workItemId" : "ADR046-sterm-011" + }, + { + "currentSource" : "Superseded sources: `packages/d2b-guestd/src/shell.rs`, `packages/d2b-unsafe-local-helper/src/services/shell/`, and `packages/d2b-contracts/src/public_wire.rs` `ShellOp`/`ShellOpResponse`.", + "dataMigration" : "Full d2b 3.0 reset; no v2 shell state/config import.", + "dependencyOwner" : "Baseline removal area; owned by migration/removal implementation for shell-terminal.", + "destination" : "`packages/d2b-provider-shell-terminal/src/migration.rs`", + "detailedDesign" : "Delete superseded guestd shell runtime, unsafe-local helper shell supervisor, and public-wire `ShellOp` or `ShellOpResponse` shell protocol. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete superseded runtime/protocol surfaces after successor parity.", + "integration" : "Removal runs after shell-terminal resource, supervisor, service, RBAC, and integration coverage prove parity; workspace manifests, CI shards, and pins are updated so old and new suites do not run indefinitely. Integration path: `packages/d2b-provider-shell-terminal/integration/migration-baseline/`.", + "removalProof" : "Delete `packages/d2b-guestd/src/shell.rs` managed runtime flow, `packages/d2b-unsafe-local-helper/src/services/shell/`, and `ShellOp`/`ShellOpResponse`; update closed gate manifests, flake/matrix/Nix-unit pins, generated ledgers, and CI workflow shards.", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/migration.rs`", + "workItemId" : "ADR046-sterm-012" + }, + { + "currentSource" : "Reuse service-shape ideas only from main commit `a1cc0b2da4a08ca3240a770a972fe4da6f912bef` generated v2 shell services; exclude ADR 0045 session, realm, and constellation assumptions.", + "dataMigration" : "Full d2b 3.0 reset; no v2 service/session state import.", + "dependencyOwner" : "Supervisor service area; owned by shell-terminal controller/supervisor service modules and ComponentSession contracts.", + "destination" : "`packages/d2b-provider-shell-terminal/src/service/{controller,supervisor}.rs`", + "detailedDesign" : "Define and implement `shell-terminal.v3` and `shell-session-supervisor.v1` ComponentSession services and the named `terminal` stream contract. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt service-shape ideas into v3 ComponentSession services and named terminal stream.", + "integration" : "Controller service handles OpenSession/ListSessions/PoolStatus; supervisor service handles Attach/Detach/DetachAll/Kill/SupervisorStatus and terminal named stream routed by d2b-bus generation identity. Integration path: `packages/d2b-provider-shell-terminal/integration/service-contract/`.", + "removalProof" : "Supersedes public-wire shell protocol and helper supervisor protocol once ComponentSession service-contract tests pass.", + "reuseAction" : "adapt", + "reuseSource" : "`a1cc0b2da4a08ca3240a770a972fe4da6f912bef` `packages/d2b-contracts/src/generated_v2_services/shell.rs` and `shell_ttrpc.rs`.", + "specId" : "ADR-046-provider-shell-terminal", + "specPath" : "docs/specs/providers/ADR-046-provider-shell-terminal.md", + "validation" : "`packages/d2b-provider-shell-terminal/tests/service_contract.rs`", + "workItemId" : "ADR046-sterm-013" + }, + { + "currentSource" : "`packages/d2b-core/src/storage.rs`, `sync.rs`; `packages/d2bd/src/supervisor/state.rs`, `daemon_audit.rs`; `d2b-realm-router/src/lib.rs`", + "dataMigration" : "Full reset; logical backup only for v3 stores", + "dependencyOwner" : "W0; store integrator", + "destination" : "`packages/d2b-resource-store/src/lib.rs`, `packages/d2b-resource-store-redb/src/lib.rs`, `schema.rs`, `keys.rs`, `transaction.rs`", + "detailedDesign" : "redb tables/encodings, fd backend, store identity, fair actor, MVCC reads, atomic indexes/revisions/operations/conflicts Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Zone runtime owns store; resource API is sole caller", + "removalProof" : "Existing ledgers removed only by owning future work items", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resource-store-redb", + "specPath" : "docs/specs/ADR-046-resource-store-redb.md", + "validation" : "Unit/property/fault tests and hard benchmark", + "workItemId" : "ADR046-store-001" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/mux.rs`, `d2b-realm-router/src/mux_session.rs`, `route_engine.rs`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-store-001; watch/reconciliation integrator", + "destination" : "`packages/d2b-resource-store-redb/src/revision_log.rs`, `packages/d2b-resource-api/src/watch.rs`", + "detailedDesign" : "replay/live no-gap watch, cursors, owner hints, compaction floor, expired relist", + "integration" : "d2b-bus named streams; controller toolkit", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resource-store-redb", + "specPath" : "docs/specs/ADR-046-resource-store-redb.md", + "validation" : "deterministic watch/compaction/disconnect/fan-in tests", + "workItemId" : "ADR046-store-002" + }, + { + "currentSource" : "`nixos-modules/storage-json.nix`, `packages/d2b-priv-broker/src/ops/storage_contract.rs`, existing marker/ownership tests", + "dataMigration" : "Destructive v3 bootstrap; v3-to-v3 logical restore", + "dependencyOwner" : "ADR046-store-001; storage/broker integrator", + "destination" : "`packages/d2b-resource-store-redb/src/backup.rs`, `migration.rs`; generated v3 storage row", + "detailedDesign" : "fd-backed provision/open, marker identity, logical backup, staged restore/upgrade, corruption quarantine", + "integration" : "Broker/Host/Guest storage owner passes File to Zone runtime", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resource-store-redb", + "specPath" : "docs/specs/ADR-046-resource-store-redb.md", + "validation" : "marker replacement, crash publication, backup/restore/upgrade tests", + "workItemId" : "ADR046-store-003" + }, + { + "currentSource" : "None — net-new ADR 0046 spec tooling; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "`packages/xtask` owner; no dependency on any other streamline item", + "destination" : "`docs/specs/ADR-046-spec-set.json`, `docs/specs/ADR-046-work-items.json`, `docs/specs/ADR-046-implementation-graph.json`, `docs/specs/ADR-046-implementation-graph.md` (generated, committed non-member artifacts; named in `docs/specs/README.md`)", + "detailedDesign" : "New `cargo run -p xtask -- spec-registry` subcommand parsing every spec's metadata table + work-item tables with a Markdown-table parser, emitting the two manifest JSON files; the `implementation-graph` generator then maps every member spec and every work item exactly once to a wave and file-disjoint parallel group, emits typed edges (`spec-depends-on`, `work-item-depends-on`, `implements-spec`, `shared-contract`, `file-overlap-order`), computes topological rank, and renders the Mermaid/table human view; output is deterministic with sorted keys and no timestamps or host paths", + "integration" : "`tests/unit/gates/` drift gate (`xtask spec-registry`/`xtask implementation-graph` + `git diff --exit-code`) added to the existing drift-gate set; the graph is consumed by ADR046-streamline-005/006/008/013 and the ready-wave query in `ADR-046-validation-and-delivery` instead of each re-parsing Markdown independently", + "removalProof" : "None — net-new; no prior owner to remove; this is the foundational generator for the remaining items", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "`spec_registry::parse_metadata_table`, `spec_registry::acyclic_dependency_graph`, `xtask_spec_registry_regenerates_clean`, `spec_registry_json_schema_matches_doc`, `implementation_graph::every_spec_and_work_item_mapped_once`, `implementation_graph::acyclic_and_wave_monotonic`, `implementation_graph::parallel_groups_are_file_disjoint`, `implementation_graph_regenerates_clean`", + "workItemId" : "ADR046-streamline-001" + }, + { + "currentSource" : "Decision-register D010/D032/D075/D076/D080 prose is the Tier A source; no existing generated ResourceType schema source until Tier B", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001; `d2b-core` owner (Tier B only)", + "destination" : "`docs/specs/schemas/*.schema.json` (Tier A: hand-authored-once canonical source checked into the tree, matching the frozen D010/D032/D075/D076/D080 decisions exactly); `packages/d2b-core/src/resource_schema/*.rs` (Tier B: the eventual Rust source of truth once ResourceType implementation exists, at which point the Tier A JSON becomes generated from Rust instead of hand-authored)", + "detailedDesign" : "Tier A: author the schema JSON once per primitive directly from the already-frozen decision-register entries (D010, D032, D075, D076, D080); Tier B: `xtask gen-spec-schemas` derives the same JSON from real Rust `#[derive(JsonSchema)]`-equivalent types once they land, replacing the hand-authored Tier A source without changing its consumers Primary reuse disposition: `create`. Preserved source-plan detail: net-new Tier A schema source; later replace source with generated output from real d2b-core ResourceType types without changing consumers.", + "integration" : "Consumed by ADR046-streamline-003 as the validation target and by ADR046-streamline-004's scaffold as the snippet source", + "removalProof" : "Tier A hand-authored JSON is superseded (not deleted) by Tier B generated JSON once real Rust types exist; consumers are unaffected since the file shape is identical", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "`spec_schema_check::canonical_field_set`", + "workItemId" : "ADR046-streamline-002" + }, + { + "currentSource" : "None — net-new spec Markdown/schema lint; no existing fenced ResourceSpec extractor", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001, ADR046-streamline-002", + "destination" : "`packages/xtask/src/bin/spec_schema_check.rs`; wired as a Layer-1 policy lint at `packages/d2b-contract-tests/tests/policy_spec_schema.rs`", + "detailedDesign" : "Reuse the CommonMark parser already available to the Rust toolchain (the same class of dependency `xtask`'s existing `gen-*` commands already use for structured generation) to walk fenced blocks; classify each block by its enclosing heading (`## Nix authoring and configuration cleanup` vs. any other heading) to apply the current-evidence exclusion without a manual per-block marker Primary reuse disposition: `create`. Preserved source-plan detail: net-new; reuse only the established xtask dispatch pattern and Rust Markdown-parser dependency class.", + "integration" : "Added to `tests/tools/tier0-first-pass.sh` as an additional fast pre-check (closes F17) and to the standing `packages/d2b-contract-tests` policy-lint suite", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "`spec_schema_check::extract_fenced_blocks`, `spec_schema_check::canonical_field_set`, `policy_spec_schema` (Type 4/5)", + "workItemId" : "ADR046-streamline-003" + }, + { + "currentSource" : "None — net-new provider-dossier scaffold; canonical snippets come from ADR046-streamline-002", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001, ADR046-streamline-002", + "destination" : "`docs/specs/providers/TEMPLATE.md` (committed, non-normative reference); `packages/xtask/src/bin/new_provider_dossier.rs` (`cargo run -p xtask -- new-provider-dossier --name `)", + "detailedDesign" : "Scaffold generator emits the template pre-filled with the requesting Provider's name, D080 qualification-grammar examples, and the canonical state-Volume snippet; does not attempt to author Provider-specific semantic sections (those remain the dossier author's normative content) Primary reuse disposition: `create`. Preserved source-plan detail: net-new scaffold; reuse canonical schema/snippet source from ADR046-streamline-002.", + "integration" : "Referenced from `docs/specs/README.md` \"Required metadata\" section (a follow-up doc edit outside this task's scope, tracked as a required cross-reference)", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "`xtask_new_provider_dossier_scaffold`; the emitted scaffold must independently pass `spec_schema_check`/`work_item_validator` on first generation", + "workItemId" : "ADR046-streamline-004" + }, + { + "currentSource" : "Decision-register D080/D081/D082/D083/D084/D037 invariants and ADR046-streamline-001 registry output; no existing cross-spec lint", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001", + "destination" : "`packages/d2b-contract-tests/tests/policy_spec_vocabulary.rs`, `policy_spec_ownership.rs`, `policy_spec_finalizer_phase.rs`, `policy_spec_source_policy.rs`", + "detailedDesign" : "Four focused lint files (one invariant family per file, matching the existing `packages/d2b-contract-tests/tests/policy_*.rs` one-concern-per-file convention) each scanning the registry output plus raw Markdown text for the specific violation patterns named in F2/F4/F5/F7/F9 Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lints over generated registry and raw Markdown.", + "integration" : "Standing Layer-1 policy-lint suite; runs on every PR touching `docs/specs/**`", + "removalProof" : "None — net-new; no prior owner to remove; graduates to permanent per [Permanent methodology](#permanent-methodology-after-accepted)", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "`policy_spec_vocabulary`, `policy_spec_ownership`, `policy_spec_finalizer_phase`, `policy_spec_source_policy` (all Type 5)", + "workItemId" : "ADR046-streamline-005" + }, + { + "currentSource" : "None — real Zone resource-store/controller-registration graph not implemented yet; doc-level invariant source is ADR046-streamline-005", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "The Zone resource-store implementation work item (not yet filed; blocked on W0-W10 implementation request per D024); ADR046-streamline-001 for the doc-level invariant source", + "destination" : "`packages/d2b-resource-store-redb/tests/provider_state_graph.rs` (or the eventual crate implementing Zone resource storage)", + "detailedDesign" : "A graph-walk over the real controller-registration/resource-ownership index (not Markdown) asserting the D076/D086/D087 invariants; the doc-level half of this check (dossier text describing the invariant correctly) is covered now by ADR046-streamline-005's `policy_spec_ownership` Primary reuse disposition: `create`. Preserved source-plan detail: net-new future runtime graph checker.", + "integration" : "Runs as a Type 3 integration test against the real resource-store crate once it exists", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "New test asserting: zero `ProviderStateSet` rows in the store; every state Volume corresponds to a declared component namespace; no empty identity-only Volume; no bootstrap state Volume or bootstrap-storage mechanism exists", + "workItemId" : "ADR046-streamline-006" + }, + { + "currentSource" : "Existing policy-broker lint pattern in packages/d2b-contract-tests/tests/policy_broker_dispositions.rs and policy_broker_schema.rs; no Provider crate source exists yet", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "The Provider-toolkit/EffectPort implementation work item (not yet filed; blocked on the W0-W10 implementation request); ADR046-streamline-001 for the doc-level invariant source", + "destination" : "`packages/d2b-contract-tests/tests/policy_effectport_boundary.rs`, `policy_worker_bus_boundary.rs`", + "detailedDesign" : "Static source scan (import-graph/symbol-use analysis) over compiled Provider crates, mirroring the existing `policy_broker_dispositions.rs` pattern already proven against `packages/d2bd`/`packages/d2b-priv-broker` Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing policy-broker lint pattern to Provider/Worker boundary checks.", + "integration" : "Standing Layer-1 policy lint once Provider crates exist", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "New tests asserting zero direct-broker-import matches in any Provider crate; zero disallowed-capability matches in any Worker binary", + "workItemId" : "ADR046-streamline-007" + }, + { + "currentSource" : "None — no existing work-item required-field or unique-ID validator", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001", + "destination" : "`packages/d2b-contract-tests/tests/policy_work_items.rs`", + "detailedDesign" : "Consumes `docs/specs/ADR-046-work-items.json` (generated by ADR046-streamline-001); flags duplicate IDs, missing/placeholder fields, and dangling cross-references Primary reuse disposition: `create`. Preserved source-plan detail: net-new validator consuming ADR046-streamline-001 output.", + "integration" : "Standing Layer-1 policy lint", + "removalProof" : "None — net-new; no prior owner to remove; graduates to permanent", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "`work_item_validator::unique_ids`, `work_item_validator::required_fields`", + "workItemId" : "ADR046-streamline-008" + }, + { + "currentSource" : "None — no existing generated Provider catalog/index", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001", + "destination" : "`docs/specs/ADR-046-provider-catalog.md` (generated, committed)", + "detailedDesign" : "`cargo run -p xtask -- gen-provider-catalog` reads the registry and decision-register D043-D049 rows and renders the table; drift-gated like the existing `gen-schemas`/`gen-migration-ledger` pattern Primary reuse disposition: `create`. Preserved source-plan detail: net-new generator consuming registry and decision-register rows.", + "integration" : "`tests/unit/gates/` drift gate", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "`provider_catalog::frozen_family_membership`, `provider_catalog_matches_frozen_families`", + "workItemId" : "ADR046-streamline-009" + }, + { + "currentSource" : "None — no existing stale-base/current-parent reconcile helper", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001", + "destination" : "`tests/tools/reconcile-stale-base.sh` (reporting only) plus a documented `git town sync`/`git town` restack procedure this report feeds into, since this repository does not yet use Git Town and F1/F11 show plain rebase/cherry-pick was insufficient to prevent duplicate reconciliation", + "detailedDesign" : "`git merge-base ` plus `git diff --name-only` intersection reporting; emits a JSON report (branch, divergence SHA, commits-behind count, file-overlap list) consumed by ADR046-streamline-013's ready/launched/blocker report Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new read-only git report.", + "integration" : "Referenced from `AGENTS.md` once graduated (see [Permanent methodology](#permanent-methodology-after-accepted)); used manually before opening or restacking any future dossier branch", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "`reconcile_helper_reports_stale_base`", + "workItemId" : "ADR046-streamline-010" + }, + { + "currentSource" : "None — no existing agent handoff manifest schema or validator", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "No prerequisite; `packages/xtask` schema/validator owner", + "destination" : "`packages/xtask/src/bin/handoff_manifest.rs` (schema/validator only); manifest instances are per-round artifacts referenced from PR bodies, not committed to the tree (consistent with this repository's \"Screenshot and visual artifact hygiene\"-style external-evidence posture)", + "detailedDesign" : "A small JSON schema (`assigned_files: [string]`, `commit_sha: string`, `test_result: {command, exit_code}`, `base_sha: string`) plus a validator rejecting incomplete records; no attribution field for any AI/tool/model, consistent with this repository's commit/PR-body attribution rule Primary reuse disposition: `create`. Preserved source-plan detail: net-new schema and validator.", + "integration" : "Referenced from `AGENTS.md` \"Worktrees for parallel agents\" once graduated", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "`handoff_manifest_rejects_incomplete_record`", + "workItemId" : "ADR046-streamline-011" + }, + { + "currentSource" : "None — no existing task DB to git consistency importer", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-011 (shares the manifest schema for commit-SHA binding)", + "destination" : "`tests/tools/import-task-db-consistency.sh`", + "detailedDesign" : "Reads the session database's todo table, resolves each `done` row's expected file set against `git log --name-only` for the current branch, and reports mismatches; read-only, no database or git mutation Primary reuse disposition: `create`. Preserved source-plan detail: net-new read-only importer/checker.", + "integration" : "Run manually at the end of each authoring round and before any pre-panel gate", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "New test seeding a synthetic task DB with one orphaned \"done\" row and confirming the importer flags it", + "workItemId" : "ADR046-streamline-012" + }, + { + "currentSource" : "None — no existing anti-serialization ready/launched/blocker report", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001, ADR046-streamline-010", + "destination" : "`tests/tools/anti-serialization-report.sh`", + "detailedDesign" : "Cross-references the spec registry's dependency graph (ADR046-streamline-001) against the set of currently open `adr0046-*` branches and the reconcile helper's (ADR046-streamline-010) staleness report; emits a per-scope ready/launched/blocked classification Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new report combining registry, branch, and staleness data.", + "integration" : "Run at the start of each authoring round and after any foundation-spec change", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "New test seeding a synthetic registry + branch list with one ready-but-unlaunched scope and confirming it is reported", + "workItemId" : "ADR046-streamline-013" + }, + { + "currentSource" : "Existing bounded-parallel test-runner precedent in tests/tools/run-layer.sh and layer1-jobs.py; Provider toolkit fake crates are net-new", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "The Provider-toolkit implementation work item (blocked on the W0-W10 implementation request)", + "destination" : "`tests/tools/run-layer.sh` extension (this repository already has `tests/tools/run-layer.sh` and `layer1-jobs.py` bounded-parallelism precedent) plus fake `EffectPort`/`ResourceClient` stub crates under `packages/d2b-provider-toolkit-fakes/`", + "detailedDesign" : "Extend the existing `layer1-jobs.py` bounded-parallel-shard pattern with a per-Provider-crate shard definition; fake dependency crates implement the same `EffectPort`/`ResourceClient` trait surface with in-memory stand-ins Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt bounded-parallel shard pattern; add net-new fake EffectPort/ResourceClient crates.", + "integration" : "`make test-unit`/`make check` shard addition once Provider crates exist", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "New integration test proving a single Provider crate's test run succeeds with zero other Provider crates built", + "workItemId" : "ADR046-streamline-014" + }, + { + "currentSource" : "Existing packages/xtask gen-* and drift-gate pattern; no existing generated-artifact conflict detector", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001, ADR046-streamline-009, ADR046-streamline-002 (Tier A part)", + "destination" : "Shared `packages/xtask` regeneration-conflict-detection helper consumed by every `gen-*`/`spec-registry` subcommand", + "detailedDesign" : "Each generator hashes its input set before writing; if a concurrent regeneration on a sibling branch (detected via the reconcile helper's file-overlap report) would produce a different hash for the same output path, the generator refuses to overwrite and reports the divergent input set Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing generator/drift-gate pattern with net-new input-hash conflict detection.", + "integration" : "Wired into every drift gate this spec and ADR046-streamline-001/009 introduce", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "New test simulating two divergent input sets producing conflicting output for the same generated file and confirming the conflict is reported, not silently resolved", + "workItemId" : "ADR046-streamline-015" + }, + { + "currentSource" : "D014/docs/specs/README.md zero-open-decision requirement; no existing automated pre-panel gate", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001, ADR046-streamline-008", + "destination" : "`packages/d2b-contract-tests/tests/policy_no_leaked_decision_prefix.rs`; invoked as a required precondition script `tests/tools/pre-panel-gate.sh`", + "detailedDesign" : "Scans registry output plus raw Markdown for `decision-required`, `TBD`, `TODO`, and any decision-ID-shaped token (`[A-Z]+-\\d+`) not matching the canonical `D\\d+` register numbering Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lint and precondition script.", + "integration" : "Required precondition before requesting the ADR 0046 documentation set's panel round (D014)", + "removalProof" : "None — net-new; no prior owner to remove; graduates to permanent panel-gate precondition", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "`policy_no_leaked_decision_prefix`", + "workItemId" : "ADR046-streamline-016" + }, + { + "currentSource" : "This spec's Observed friction evidence command prose; no reusable evidence-command artifact exists yet", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "No prerequisite; `docs/specs` evidence-command documentation owner", + "destination" : "`docs/specs/ADR-046-streamline-evidence-commands.md` (a follow-up artifact outside this task's file scope; tracked here as a required future addition, not authored by this spec)", + "detailedDesign" : "A plain Markdown list of the exact commands used to produce each F1-F17 citation above (already reproduced verbatim in this spec's prose), organized by friction ID, so re-running them is copy-paste rather than re-derivation Primary reuse disposition: `adapt`. Preserved source-plan detail: extract/adapt the already-cited evidence commands into a net-new documentation artifact.", + "integration" : "Referenced from PR bodies as external evidence per this repository's \"PR bodies contain... check-status summaries only... may link to external evidence\" convention", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "Manual reviewer re-run of at least one command per friction ID during panel, confirmed to reproduce the cited count/SHA", + "workItemId" : "ADR046-streamline-017" + }, + { + "currentSource" : "Existing disk-hygiene operator guidance; no ADR 0046 worktree disk report exists yet", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-010 (shares branch-staleness detection)", + "destination" : "`tests/tools/worktree-disk-report.sh`", + "detailedDesign" : "`git worktree list --porcelain` plus `du -sh` per worktree's `packages/target/`; output is a report only, never a `git worktree remove`/`rm -rf` invocation Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing disk-hygiene reporting concepts into a net-new non-mutating script.", + "integration" : "Run manually before requesting garbage collection or worktree removal, consistent with this repository's existing \"Disk hygiene contract\"-equivalent operator guidance for code waves", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "New test confirming the reporting script's exit code and output never include a mutating command string, and confirming it correctly flags a fixture worktree with a real (non-symlink) `packages/target/`", + "workItemId" : "ADR046-streamline-018" + }, + { + "currentSource" : "ADR-046 terminology and current-code migration-map specs; no automated old-to-new terminology helper exists yet", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001", + "destination" : "`packages/xtask/src/bin/terminology_check.rs` (`cargo run -p xtask -- terminology-check`)", + "detailedDesign" : "Parses every \"Current-code fit\" table's \"Current anchor\" cell for symbol/crate names, cross-references against the migration map's disposition rows, and flags any current-source citation absent from the map Primary reuse disposition: `create`. Preserved source-plan detail: net-new terminology checker over existing mapping specs.", + "integration" : "Standing Layer-1 policy lint once wired; also usable ad hoc when authoring a new dossier's evidence section", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "New test seeding a fixture spec citing a current-source symbol absent from the migration map and confirming it is flagged", + "workItemId" : "ADR046-streamline-019" + }, + { + "currentSource" : "D094 test-placement requirement; no existing hermetic test-placement policy lint", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001", + "destination" : "`packages/d2b-contract-tests/tests/policy_test_placement.rs`", + "detailedDesign" : "Scans hermetic-tier Rust sources for banned API surfaces (`std::process::Command`, socket/container/DBus/systemd helpers, `#[ignore]`) and for `integration/`-only markers appearing outside `integration/` Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lint.", + "integration" : "`make test-policy` row; no new top-level `tests/*.sh` gate", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "Fixture crate with an intentional process-spawning hermetic test is rejected naming the file/line; a correct crate passes", + "workItemId" : "ADR046-streamline-020" + }, + { + "currentSource" : "D094 deterministic-clock requirement; no existing wall-clock sleep/retry policy lint", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-020", + "destination" : "`packages/d2b-contract-tests/tests/policy_test_determinism.rs`", + "detailedDesign" : "Scans hermetic sources for banned time/sleep APIs and asserts the deterministic fake-clock/RNG fixtures from the toolkit are used; classified crypto/property exceptions are allow-listed by explicit name with a declared per-test budget Primary reuse disposition: `create`. Preserved source-plan detail: net-new policy lint layered on ADR046-streamline-020.", + "integration" : "`make test-policy` row", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "Fixture hermetic test using `thread::sleep` is rejected; a classified crypto test on the allow-list passes", + "workItemId" : "ADR046-streamline-021" + }, + { + "currentSource" : "ADR046-delivery-007 timing-ledger design; no existing execution-only test-runtime ledger in this spec set", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-delivery-007", + "destination" : "`packages/xtask/src/test_runtime_ledger.rs` (shared with `ADR046-delivery-007`)", + "detailedDesign" : "Parses per-test JSON timings, aggregates per test/crate/shard, compares against pinned budgets and the previous ledger, and fails on regression beyond the threshold Primary reuse disposition: `adapt`. Preserved source-plan detail: share/adapt ADR046-delivery-007 timing-ledger implementation for this gate.", + "integration" : "Consumed by wave entry/exit (`ADR-046-validation-and-delivery` §4/§10.16); `make test-rust` and Layer-1 shards run concurrently", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "Synthetic timing regression fails the gate; ledger output is deterministic and machine-readable", + "workItemId" : "ADR046-streamline-022" + }, + { + "currentSource" : "Current-code migration-map disposition rows and existing gate manifests; no legacy-test retirement generator exists yet", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-008, ADR046-streamline-022", + "destination" : "`packages/xtask/src/bin/legacy_test_retirement.rs` (`cargo run -p xtask -- legacy-test-retirement`)", + "detailedDesign" : "Reads the migration map's disposition rows, cross-references the live `tests/layer1-jobs.json`/gate manifests, and emits the delete set plus an absence assertion; never deletes automatically — it produces the gated checklist and the failing test that proves incomplete retirement Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new generator reading existing migration rows and gate manifests without mutating them.", + "integration" : "`make test-policy`/`make test-drift` row; wired to every current-code migration work item's removal proof", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "Fixture with a replaced behavior whose old selector still appears in `tests/layer1-jobs.json` fails; once removed, the absence assertion passes", + "workItemId" : "ADR046-streamline-023" + }, + { + "currentSource" : "The graph is regenerated by hand today; `ADR-046-spec-set.json` and the test-runtime ledger (ADR046-delivery-007) each have their own emitter — duplicate-generator finding, retained here for reconciliation", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-streamline-001", + "destination" : "`packages/xtask/src/bin/implementation_graph.rs` (`cargo run -p xtask -- implementation-graph`); folds the spec-set and test-runtime emitters into the one `xtask` dispatch", + "detailedDesign" : "Deterministic (sorted keys, no timestamps/host paths); regenerated after any spec or work-item change and after the two manifests; a drift gate runs it and `git diff --exit-code`. Retains the duplicate-generator findings for the spec-set manifest and the test-runtime ledger and reconciles them to the single `xtask` path rather than leaving three independent emitters Primary reuse disposition: `create`. Preserved source-plan detail: net-new generator that consumes the two manifests and the wave topology; folds the duplicate spec-set/test-runtime emitters into one canonical path.", + "integration" : "`tests/unit/gates/` drift gate alongside `xtask spec-registry`; consumed by the ready-wave query in `ADR-046-validation-and-delivery` §3.5.1", + "removalProof" : "The ad hoc separate emitters for spec-set and test-runtime are removed after the single `xtask implementation-graph`/`spec-registry` dispatch reaches parity", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-streamline", + "specPath" : "docs/specs/ADR-046-streamline.md", + "validation" : "`implementation_graph::every_spec_and_work_item_mapped_once`; `implementation_graph::acyclic_and_wave_monotonic`; `implementation_graph::parallel_groups_are_file_disjoint`; `implementation_graph_regenerates_clean`; duplicate-generator reconciliation asserts one canonical emitter path for spec-set and test-runtime", + "workItemId" : "ADR046-streamline-024" + }, + { + "currentSource" : "No canonical v3 equivalent. Adapt the Provider descriptor pattern from `packages/d2b-realm-provider/src/provider.rs` and the bounded audit-envelope pattern from `packages/d2bd/src/daemon_audit.rs`; do not carry forward daemon topology or unsafe-local helper protocol types.", + "dataMigration" : "Full d2b 3.0 reset; no Provider config, handler checkpoint, or audit state is imported.", + "dependencyOwner" : "`ADR046-provider-001`, `ADR046-exec-003`, `ADR046-exec-004`, `ADR046-exec-005`, `ADR046-pstate-012`, `ADR046-telem-001`, and `ADR046-audit-001`; `Provider/system-core` owner", + "destination" : "`packages/d2b-provider-system-core/src/manifest.rs`, `packages/d2b-provider-system-core/src/audit.rs`, and `packages/d2b-provider-system-core/tests/provider_boundary.rs`", + "detailedDesign" : "Compile the system-core Provider manifest, empty closed config schema, Host/User component descriptors, and empty state-namespace declaration. The manifest binds both library handlers to the fixed `d2b-core-controller` bootstrap process without declaring either handler as a Process resource. The audit adapter emits one bounded, redacted `ResourceReconciled` record after each Host/User reconcile. The boundary rejects Provider config fields and proves that handler call paths neither write `Provider.status` nor emit `ProcessEffect`; core-controller infrastructure owns the former and `ADR046-exec-006`/`ADR046-exec-007` own the latter. `ADR046-pstate-012` remains the owner of generic optional-state admission; this item only declares system-core's empty state set. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt descriptor/audit patterns; implement the v3 Provider-specific boundary.", + "integration" : "`ADR046-exec-003` and `ADR046-exec-004` call the audit adapter after reconcile; `ADR046-exec-005` and core-controller infrastructure load the manifest and derive the runtime-owned `Provider/system-core` resource/status.", + "removalProof" : "No independent destination removal. `ADR046-exec-009` owns unsafe-local helper/wire retirement, and `ADR046-telem-008` owns removal proofs for legacy unsafe-local and VM-name telemetry labels.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-system-core", + "specPath" : "docs/specs/providers/ADR-046-provider-system-core.md", + "validation" : "`config_schema_empty_only`, `provider_status_not_written_by_handlers`, `provider_state_set_empty`, `host_no_process_effect_emitted`, `host_resource_reconciled_audit`, and `user_resource_reconciled_audit`; manifest golden vector proves no Process descriptor and no state namespace for either handler.", + "workItemId" : "ADR046-system-core-001" + }, + { + "currentSource" : "`packages/d2b-unsafe-local-helper/src/systemd.rs` — `SystemdUserScopeManager`, `VerifiedScope`; `packages/d2bd/src/supervisor/` — pidfd adoption, restart backoff", + "dataMigration" : "No state migration; controller relists and adopts on restart", + "dependencyOwner" : "`ADR046-process-002`; Process contracts/supervisor owner; effect port interface owner", + "destination" : "`packages/d2b-provider-system-systemd/src/controller.rs` (async reconcile loop), `src/launch.rs` (launch algorithm via effect port), `src/effect_port.rs` (`SystemdProcessEffectPort` trait + test double), `src/adoption.rs` (adoption algorithm), `src/sandbox.rs` (SandboxSpec → unit property compiler)", + "detailedDesign" : "Full §6 launch algorithm (effect port integration); §7 EphemeralProcess; §8 restart/adoption (effect port `locate_by_identity`); §9 drain (effect port `stop`/`kill`); §10 sandbox compilation; §11 bus services; ProviderSupervisor LaunchTicket integration Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Core ProviderDeployment creates controller Process via Provider/system-minijail and creates/deletes one private Volume per component per execution target before/after component Processes (naming: `system-systemd--controller--main-state--`; ownerRef: Provider/system-systemd; kind=state; persistenceClass=persistent; minimal nonzero quota; identity marker; migrationPolicy=none; layout principal: Nix-preprovisioned `User/` or bounded principal pool; no ComponentPrincipal ResourceRef); Provider/volume-local is sole Volume reconciler; system-systemd controller does NOT create, own, or reconcile its Volume; controller consumes only the `controller-rw` view dirfd via its Process mounts; controller watches Process/EphemeralProcess; ProviderSupervisor calls LaunchProcess; effect port implementation injected by core supervisor spec", + "removalProof" : "`VmProcessDag` supervisor roles removed per role disposition table after each succeeds in conformance", + "reuseAction" : "adapt", + "reuseSource" : "Main `a1cc0b2d`: `d2b-session/src/engine.rs`, `d2b-session-unix/src/adapter.rs` (effect port test double session/transport)", + "specId" : "ADR-046-provider-system-systemd", + "specPath" : "docs/specs/providers/ADR-046-provider-system-systemd.md", + "validation" : "`tests/conformance.rs` (shared conformance kit); `tests/identity_binding.rs` (InvocationID/cgroup/MainPID/start-time golden vectors via mock effect port); `tests/adoption.rs` (quarantine/identity-mismatch cases); `tests/restart.rs` (backoff/maxRestarts); latency assertions (p95 ≤5 ms hint→handler, ≤20 ms commit→effect port `start` call)", + "workItemId" : "ADR046-systemd-001" + }, + { + "currentSource" : "`nixos-modules/unsafe-local-helper.nix`; `nixos-modules/processes-json.nix`", + "dataMigration" : "No configuration compatibility path; full reset at v3 cutover", + "dependencyOwner" : "`ADR046-systemd-001`; Nix/package integrator", + "destination" : "`nixos-modules/` (Provider ResourceSpec emission for `system-systemd`); `packages/d2b-provider-system-systemd/` package derivation and catalog entry", + "detailedDesign" : "§16 Nix configuration; `d2b.artifacts.system-systemd` catalog entry; Provider and Process ResourceSpec emission; eval/build validation rules; drift gate update (`xtask gen-nix-options` + `make test-drift`)", + "integration" : "Zone configuration activates Provider/system-systemd; Process resources reference it via `spec.providerRef = \"Provider/system-systemd\"`", + "removalProof" : "`nixos-modules/unsafe-local-helper.nix` removed after user-domain Host/Process parity", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-system-systemd", + "specPath" : "docs/specs/providers/ADR-046-provider-system-systemd.md", + "validation" : "`tests/unit/nix/cases/provider-system-systemd.nix` (eval-time validation); `tests/unit/gates/drift-check.sh` covers generated option schema", + "workItemId" : "ADR046-systemd-002" + }, + { + "currentSource" : "`packages/d2bd/src/supervisor/` (existing process lifecycle tests); `packages/d2b-unsafe-local-helper/src/systemd.rs` (existing scope tests)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "`ADR046-systemd-001`; conformance kit / test infrastructure", + "destination" : "`packages/d2b-provider-system-systemd/tests/conformance.rs`, `tests/fault.rs`, `tests/ephemeral.rs`, `tests/sandbox_compile.rs`; `integration/host_scenario.rs`, `integration/guest_scenario.rs`", + "detailedDesign" : "Full §19 test/integration requirements Primary reuse disposition: `adapt`. Preserved source-plan detail: copy/adapt.", + "integration" : "`cargo test -p d2b-provider-system-systemd`; `make test-integration -- provider-system-systemd`; `make test-host-integration -- provider-system-systemd`", + "removalProof" : "No removal; tests are permanent", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-system-systemd", + "specPath" : "docs/specs/providers/ADR-046-provider-system-systemd.md", + "validation" : "All conformance vectors pass; all fault injection scenarios reach expected phase/condition; all §19 Host and Guest test scenarios pass", + "workItemId" : "ADR046-systemd-003" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/trace_context.rs` (`TraceContext`, `MAX_TRACE_FIELD_LEN`); `packages/d2b-realm-core/src/audit.rs` (`AuditHash`, `AuditHashError`, `AuditChainLink`, `AuditChainRecord`); `packages/d2b-realm-core/src/ids.rs` (`OperationId`, `CorrelationId`); `packages/d2b-realm-codec-protobuf/src/lib.rs` (`encode_trace_context`, `decode_trace_context`); `packages/d2b-contract-tests/tests/policy_observability.rs::startup_tracing_avoids_host_path_fields`", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "W0/W1a; telemetry crate owner", + "destination" : "`packages/d2b-telemetry/src/{trace_context.rs,audit_hash.rs,emitter.rs,meter_registry.rs,metric_label_policy.rs,redaction_guard.rs}`", + "detailedDesign" : "`d2b-telemetry` provides: (1) `TraceContext` / `AuditHash` / `AuditChainLink` extracted unchanged; (2) `BoundedEmitter`: `tracing`-subscriber layer that serializes span/metric events into compact frames and writes them over a private Unix datagram socket to the `observability-otel` Provider — no `opentelemetry_sdk` dependency; (3) the canonical closed `METRIC_LABEL_POLICY`, structural descriptor/data-point/exemplar validator, exact forbidden-key/suffix predicates, and non-serializable resource-identity canary matcher used by emitter defense in depth and the mandatory collector ingress gate; (4) `RedactionGuard` span wrapper that asserts the v3 resource attribute allowlist at span creation. No OTEL SDK in this crate. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract unchanged (`TraceContext`, `AuditHash`, `AuditChainLink`); adapt (`OperationId`/`CorrelationId` for v3 record contract); add bounded emitter.", + "integration" : "Every v3 core process initializes a `BoundedEmitter` pointing at `$ZONE_STATE/telemetry/emitter.sock`; v3 audit records use `AuditHash`/`AuditChainLink` from this crate", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "Unit test for `RedactionGuard` attribute gate; unit test for `BoundedEmitter` ring-full drop and FIFO drain; table tests for `METRIC_LABEL_POLICY` exact keys, suffixes, and `metadata.name`/UID/ResourceRef identity canaries; `policy_telemetry_redaction.rs::startup_tracing_avoids_host_path_fields` port; assert `config_source = \"realm-controllers\"` absent; assert no `opentelemetry_sdk` dependency in `d2b-telemetry` Cargo.toml", + "workItemId" : "ADR046-telem-001" + }, + { + "currentSource" : "`packages/d2bd/src/metrics.rs` (`MetricDescriptor`, `MetricKind`, `VM_START_BUCKETS_SECONDS`, `BROKER_REQUEST_BUCKETS_SECONDS`, `ACTIVATION_PHASE_BUCKETS_SECONDS` — for bucket pattern reference only; the `vm` labels are not reused)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-telem-001 + ADR046-store-001; store owner", + "destination" : "`packages/d2b-resource-store-redb/src/metrics.rs`, `packages/d2b-resource-store-redb/src/tracing.rs`", + "detailedDesign" : "Instrument the store actor, write/read/group-commit paths with the metric inventory from this spec via `d2b-telemetry` `BoundedEmitter`. Emit `d2b.store.*` spans. The p95 `d2b_store_write_duration_seconds` hard target (≤10 ms) feeds the benchmark fixture. No `vm` label; `resource_type` label only from closed catalog. No OTEL SDK in the store crate. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt bucket boundary constants (rename; remove `vm` labels); replace hand-rolled `Registry` with `d2b-telemetry` `BoundedEmitter` meter API.", + "integration" : "Store actor calls `d2b-telemetry` meter/tracer via `BoundedEmitter`; spans linked to API request spans via `TraceContext`", + "removalProof" : "Hand-rolled registry in `d2bd/src/metrics.rs` retained until daemon-level ADR 0046 cutover", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "p95 write ≤10 ms benchmark fixture; metric inventory policy test asserting no `vm` label; assert old `d2b_daemon_vm_state` shape absent", + "workItemId" : "ADR046-telem-002" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/ids.rs` (`OperationId`, `CorrelationId`); `packages/d2b-realm-codec-protobuf/src/lib.rs` (`encode_trace_context`, `decode_trace_context` for v3 codec adaptation); `packages/d2b-realm-router/src/mux_session.rs`, `route_engine.rs` (current implemented-but-unwired generic routing)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-telem-001 + ADR046-session-001 + ADR046-bus-001; session/bus owner", + "destination" : "`packages/d2b-resource-api/src/metrics.rs`, `packages/d2b-session/src/metrics.rs`, `packages/d2b-bus/src/metrics.rs`", + "detailedDesign" : "Instrument resource API verb dispatch, watch delivery, bus route resolution, and session handshake/reconnect per the metric/span catalog in this spec. Propagate `TraceContext` from incoming bus request to store write transaction span as child context. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt `TraceContext` protobuf codec for v3 resource API framing; adapt routing metrics patterns.", + "integration" : "ResourceClient → bus → API → store span chain via `TraceContext`", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "API request metric inventory test; session profile/outcome label cardinality gate; bus direction label gate; assert no `realm` field in span attributes", + "workItemId" : "ADR046-telem-003" + }, + { + "currentSource" : "`packages/d2bd/src/metrics.rs` (`BROKER_REQUEST_BUCKETS_SECONDS` for reference; `d2b_daemon_broker_request_duration_seconds` labels `[\"op\"]` as a cardinality-safe example); `packages/d2b-realm-core/src/allocator_engine.rs` (field `trace: Option` at line 873 — existing trace context wiring pattern)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-telem-001 + ADR046-core-001; core-controller owner", + "destination" : "`packages/d2b-core-controller/src/metrics.rs`, `packages/d2b-core-controller/src/tracing.rs`", + "detailedDesign" : "Emit `d2b.controller.hint` span at the instant the post-commit dispatcher fires; emit `d2b.controller.reconcile` child span at handler entry. Interval = p95 ≤5 ms target. `handler` label from closed set; no resource name labels. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt bucket patterns; adapt trace-context-in-reconcile pattern from `allocator_engine.rs`.", + "integration" : "Post-commit dispatcher creates hint span; handler creates child reconcile span via `TraceContext`", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "`hint_to_handler_latency` benchmark with p95 ≤5 ms assertion; closed `handler` label set gate", + "workItemId" : "ADR046-telem-004" + }, + { + "currentSource" : "`packages/d2bd/src/metrics.rs` (`d2b_daemon_vm_start_duration_seconds` `VM_START_BUCKETS_SECONDS`; `d2b_daemon_vm_shutdown_duration_seconds` `VM_SHUTDOWN_BUCKETS_SECONDS`; `d2b_daemon_vm_shutdown_total` labels `[\"vm\", \"vmm\", \"outcome\"]` — `vmm` = current `RunnerRole` → v3 `provider`); `packages/d2bd/src/supervisor/pidfd.rs` (pidfd adoption/launch call sites); `packages/d2b-contracts/src/broker_wire.rs::RunnerRole` (`CloudHypervisor`, `QemuMedia`, `OtelHostBridge` etc. → v3 `provider` label values)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-telem-001 + ADR046-process-001; Process Provider owner", + "destination" : "`packages/d2b-provider-supervisor/src/metrics.rs`, `packages/d2b-provider-supervisor/src/tracing.rs`", + "detailedDesign" : "`d2b_process_launch_duration_seconds`: start = instant Process controller receives commit-to-Ready hint; end = first OS spawn call (clone3 or systemd unit start). This implements p95 ≤20 ms. `provider` label replaces `vmm`/`RunnerRole` with the closed set `{minijail,systemd}`. No `vm` name label. A separate `d2b_process_ready_duration_seconds` histogram covers launch-attempt → readiness signal (not a hard target). Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt launch histogram bucket constants; rename `vm` label to no label (process identity in resource attributes); rename `vmm`/`RunnerRole` → `provider` closed enum.", + "integration" : "Process Provider controller start handler → supervisor ticket delivery → first spawn call", + "removalProof" : "`d2b_daemon_vm_start_duration_seconds` (with `vm` label) retained in `d2bd/src/metrics.rs` until daemon-level ADR 0046 cutover", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "`commit_to_launch_latency` benchmark with p95 ≤20 ms assertion; assert no `vm` label in process metrics; `vmm→provider` label rename gate", + "workItemId" : "ADR046-telem-005" + }, + { + "currentSource" : "`nixos-modules/components/observability/host.nix` (`otelRuntimeDir = \"/run/d2b/otel\"`, `hostEgressSocket`, ACL `setfacl` pattern, `scrapeJournal`, `hostCfg.identityName`); `nixos-modules/components/observability/stack.nix` (SigNoz stack, `ingressSources`, per-source `vmName`, `receiverGrpcPort`/`receiverHttpPort`, loopback binding, `cfg.signoz.listenPort`); `nixos-modules/components/observability/guest.nix` (`vm.name`/`vm.env`/`vm.role` identity stamping, guest collector); `packages/d2b-host/src/otel_host_bridge_argv.rs` (`OtelHostBridgeArgvInputs`; vsock forwarding); `packages/d2bd/src/otel_host_bridge_readiness.rs` (readiness gate pattern: `OtelHostBridgeReadiness::{Ready,Pending,Failed}`); `packages/d2b-core/src/processes.rs::ProcessRole::OtelHostBridge`; `packages/d2b-contracts/src/broker_wire.rs::RunnerRole::OtelHostBridge`; `packages/d2b-contract-tests/tests/{policy_observability.rs,minijail_relay_otel.rs}`", + "dataMigration" : "Existing SigNoz data not migrated; v3 starts fresh", + "dependencyOwner" : "ADR046-process-001 + ADR046-provider-001; `observability-otel` Provider owner", + "destination" : "`packages/d2b-provider-observability-otel/src/`, `nixos-modules/components/observability/` (adapted files)", + "detailedDesign" : "`Provider/observability-otel` is an **ordinary optional non-bootstrap Process** (not counted toward the ≤64 MiB mandatory core aggregate). It owns: (1) per-Zone datagram receiver socket at `$ZONE_STATE/telemetry/emitter.sock` (drains frames from core emitters) and OTLP/gRPC Unix socket at `$ZONE_STATE/telemetry/otlp.sock`; (2) the full OTEL SDK with OTLP exporter — only this process links `opentelemetry_sdk`; (3) OTel Collector pipeline per Zone and per Host; (4) vsock OTLP forwarding to obs Zone (replaces socat-based `OtelHostBridgeArgvInputs`); (5) D096 import-stream ingest; (6) one structural `METRIC_LABEL_POLICY` gate shared by Unix emitter, OTLP Unix, OTLP/vsock, and import-stream metrics before aggregation, queueing, batching, retry, or export, with bounded non-echoing errors/quarantine/backpressure; (7) SigNoz stack Nix adapted from `stack.nix` with per-Zone `ingressSources` replacing per-VM `vmName`; (8) journald scrape (optional, disabled by default); (9) self-metrics endpoint. Trusted producer identity is stamped only into allow-listed OTEL Resource attributes; audit remains separate. Zone/controller startup does not wait for this Provider. If absent or unready, Zone health is `Degraded` (not `Failed`). Readiness: socket exists and first drain cycle completes successfully. `d2b.observability.host.identityName` option preserved; `vmName` in `ingressSources` populated from Zone name. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt Nix pipeline shape (replace per-VM `vmName` with per-Zone naming); adapt `OtelHostBridgeArgvInputs` vsock forwarding to native OTLP/gRPC-over-vsock; adapt readiness gate pattern (`OtelHostBridgeReadiness::Ready` → Provider phase `Ready`); adapt `ingressSources` per-VM → per-Zone.", + "integration" : "Core process `BoundedEmitter` → `emitter.sock` → observability-otel collector → `otlp.sock` → vsock → obs Zone SigNoz; Zone startup independent of Provider readiness", + "removalProof" : "`otel_host_bridge_argv.rs` socat runner and `otel_host_bridge_readiness.rs` retired after `observability-otel` Provider delivers native OTLP/vsock and passes conformance; `ProcessRole::OtelHostBridge` and `RunnerRole::OtelHostBridge` retired from `d2b-core/src/processes.rs` and `d2b-contracts/src/broker_wire.rs` after Provider migration", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "`emitter_socket_receive`, `emitter_ring_drains_on_socket_available`, `emitter_ring_drop_on_overflow`, table-driven `ingress_metric_policy` across all four ingress adapters, `no_vm_label_in_metrics`, and `zone_startup_proceeds_without_provider` tests; adapted `policy_observability.rs` tests (retain `loki_native_otel_resource_attributes` and SigNoz-only backend assertions); adapted `minijail_relay_otel.rs` shape test for Provider-managed runner", + "workItemId" : "ADR046-telem-006" + }, + { + "currentSource" : "`nixos-modules/components/observability/host.nix` (`scrapeJournal = hostCfg.scrapeJournal`, `journaldStorageDir = \"/var/lib/d2b-host-otel-collector/journald\"`, cgroup-path filtering pattern for host units)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-telem-006; Nix/observability owner", + "destination" : "`packages/d2b-provider-observability-otel/src/nix/journald.nix` (new Nix fragment)", + "detailedDesign" : "`d2b.zones..observability.journald.enable = false` (default). When enabled: journald receiver follows `z-/*` and `s-/*` cgroup filters. Collector applies redaction: drops `MESSAGE` credential/path patterns, `_CMDLINE`, `_EXE`, `INVOCATION_ID`. Current `scrapeJournal` host option is preserved unchanged. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt journald receiver config for per-Zone cgroup filter (`z-/*`, `s-/*`).", + "integration" : "`observability-otel` Provider Nix config → OTel Collector journald receiver → redaction filter → obs Zone", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "Nix eval test: filter expression set when enabled; test that `_CMDLINE` and `INVOCATION_ID` appear in drop list", + "workItemId" : "ADR046-telem-007" + }, + { + "currentSource" : "`packages/d2b-contract-tests/tests/policy_observability.rs` (`loki_native_otel_resource_attributes` allowlist: `[\"deployment.environment\",\"host.name\",\"service.name\",\"service.namespace\",\"source\",\"vm.env\",\"vm.name\",\"vm.role\"]`; `tempo_stack_signoz_backend_and_collector` SigNoz-only assertion; `startup_tracing_avoids_host_path_fields` forbidden fields); `packages/d2b-contract-tests/tests/policy_metrics.rs` (`EXPECTED_METRICS` table parity with `docs/reference/daemon-metrics.md`)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-telem-001; policy/contract-tests owner", + "destination" : "`packages/d2b-contract-tests/tests/policy_telemetry_redaction.rs` (new); updated `policy_observability.rs`; updated `policy_metrics.rs`", + "detailedDesign" : "(1) Extend `loki_native_otel_resource_attributes` allowlist to include `d2b.zone`, `d2b.provider`, `d2b.component`, `service.version`. (2) Add redaction lint: scan all v3 instrumentation call sites for `realm`, `workload_id`, `node_id`, `vm` (as label key), `path`, `socket`, `argv`, `pid`, `exe`. (3) Add structural metric-label policy lint: parse every v3 `MetricDescriptor`, require each label key and value domain to exist in the closed `METRIC_LABEL_POLICY`, reject exact keys `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `network`, `network_name`, and `link_name_hash`, reject resource-name-derived key suffixes `*_name`, `*_name_hash`, `*_name_digest`, and `*_uid`, and prove `metadata.name`, UID, ResourceRef, and resource-identity canaries never enter label values. Fixed semantic labels remain allowed only with closed domains. (4) Prove the observability Provider's Unix-emitter, OTLP-Unix, OTLP/vsock, and import-stream adapters all invoke that validator before queue/batch/export; assert whole-frame rejection, bounded non-echoing error classes/quarantine, and policy-before-capacity backpressure. (5) Assert the `d2b.zone` Resource attribute remains present and audit is unchanged. (6) Add bucket boundary gates for 5 ms and 20 ms. (7) Retain: `startup_tracing_avoids_host_path_fields`; SigNoz-only backend assertion; `tempo_guest_collector_shape`; `config_source = \"realm-controllers\"` absence gate. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt and extend; keep existing tests; add new policy gates.", + "integration" : "Contract-tests run in workspace check and `make test-drift`", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "These tests are their own validation artifact", + "workItemId" : "ADR046-telem-008" + }, + { + "currentSource" : "`nixos-modules/options-observability.nix` (`d2b.observability.host.identityName`, `scrapeJournal`, `otlpIngest.enable`, `signoz.jwtSecretFile`, `signoz.rootPasswordFile`, `signoz.clickhousePasswordFile`, `retention.*`, `sampling.*` — all predecessor options); `nixos-modules/options-realms.nix` (submodule/option-type/assertion pattern); `nixos-modules/components/observability/{host,stack,guest}.nix` (Nix pipeline shape, ACL pattern, `identityName`/`vmName`); `nixos-modules/manifest.nix` (resource-bundle emission pattern); `packages/d2b-contract-tests/tests/policy_observability.rs::loki_native_otel_resource_attributes` (closed allowlist enforcement — adapt as bundle schema policy test)", + "dataMigration" : "Current `d2b.observability.*` options are retained with compat warnings (same pattern as current `retention.*`/`sampling.*` compat options); the v3 `d2b.zones..resources.*` option is the authoritative surface", + "dependencyOwner" : "W0/W1a; Nix integrator (adapts `nixos-modules/options-observability.nix` and `options-realms.nix`)", + "destination" : "`nixos-modules/resources.nix` (uniform `d2b.zones..resources` schema-aware option; `spec.*` option types generated from `ResourceTypeSchema` for each `type`); `nixos-modules/resources-bundle.nix` (ADR-only: sorted integrity-pinned bundle derivation)", + "detailedDesign" : "(1) Implement `d2b.zones..resources = lib.mkOption { type = lib.types.attrsOf (schemaAwareResourceSubmodule); }` where the submodule, given `config.type`, loads the registered `ResourceTypeSchema` and generates `spec.*` option types from it. For `type = \"Provider\"`, `spec.config.*` options are generated from the signed Provider schema for the package identified by `spec.artifactId` (see ADR-046-provider-model-and-packaging). No second bespoke vocabulary; `spec` fields mirror the canonical JSON fields exactly. (2) `resources-bundle.nix` derivation: serialize each resource to canonical sorted JSON (keys alphabetically sorted at every level); sort resources by `(type, name)`; compute generation digest; emit `zone-resources-.json` as Nix store output. Publication handler sets `metadata.managedBy = \"configuration\"` and `metadata.configurationGeneration` on activation — these fields are NOT authored in Nix. (3) `status`, UID, generation, revision, and timestamps are absent from Nix authoring; core fills them. Primary reuse disposition: `adapt`. Preserved source-plan detail: Implement uniform `d2b.zones..resources. = { type; spec; }` option with schema-driven `spec.*` generated option types; adapt option submodule pattern from `options-realms.nix`; adapt pipeline shape from `{host,stack,guest}.nix`; emit canonical sorted ResourceSpec JSON from `resources-bundle.nix`.", + "integration" : "`d2b-core-controller` reads the Nix store path from the activated system closure; secrets never appear in the bundle", + "removalProof" : "`nixos-modules/options-observability.nix` predecessor options retained with compat warnings until `d2b.observability.enable` migration is complete", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "`eval_rejects_unknown_type`, `eval_rejects_invalid_emitter_ring_size`, `eval_rejects_unknown_provider_settings`, `eval_rejects_inline_secret_in_settings`, `eval_rejects_unresolved_credential_ref`, `eval_rejects_duplicate_resource_name` nix-unit cases; `bundle_is_sorted_canonically`, `bundle_digest_is_deterministic`, `bundle_contains_no_secret_values`, `bundle_schema_validates_against_provider_schema` contract tests", + "workItemId" : "ADR046-telem-009" + }, + { + "currentSource" : "`packages/d2b-contract-tests/tests/policy_observability.rs::startup_tracing_avoids_host_path_fields` (forbidden-field pattern enforcement — adapt as bundle forbidden-field gate); `packages/d2b-contract-tests/tests/policy_metrics.rs` (metric inventory policy test pattern); `packages/d2b-priv-broker/src/runtime.rs` (current runtime schema load/verify pattern); `packages/d2b-contracts/src/provider_registry_v2.rs::ProviderBindingV2` (non-exhaustive signed schema contract)", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import", + "dependencyOwner" : "ADR046-telem-009 + ADR046-telem-006 + ADR046-store-001; schema/validation owner", + "destination" : "`nixos-modules/resources-bundle.nix` (build-time validation step 4 in the `resources-bundle` derivation); `packages/d2b-core-controller/src/configuration.rs` (runtime activation checks)", + "detailedDesign" : "Build-time: (1) For each `Provider` resource, fetch the `resourceTypeSchema` output from the package; validate `settings` JSON against the JSON Schema; fail the build on schema mismatch or unknown fields. (2) Assert no resource spec field contains a bare secret/path/argv (forbidden-field pattern from `startup_tracing_avoids_host_path_fields`). Runtime: (3) Core-controller re-validates Provider package identity (per ADR-046-provider-model-and-packaging) against the installed package; resolves Credential refs; checks conflict/bounds; rejects with closed-enum `generation-rejected` reason on any failure; no partial activation. (4) Provider schema mismatch between the bundle's schema and the installed Provider's live schema → reject, emit `generation-rejected{reason=\"package-identity-mismatch\"}`. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt `startup_tracing_avoids_host_path_fields` forbidden-field pattern for bundle schema gate; adapt `ProviderBindingV2` non-exhaustive contract for Provider-specific settings schema fingerprint.", + "integration" : "Nix `resources-bundle.nix` derivation gate + core-controller `configuration.rs` activation path", + "removalProof" : "None — net-new; no prior owner to remove; this is new tooling", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "`bundle_schema_validates_against_provider_schema` bundle contract test; `generation_rejected_emits_audit_record` cleanup contract test with a `schema-validation-failed` reason; add a nix-unit case `eval_rejects_unknown_fields_against_signed_schema` that runs the bundle derivation with a schema mismatch and asserts build failure", + "workItemId" : "ADR046-telem-010" + }, + { + "currentSource" : "`packages/d2bd/src/daemon_audit.rs` (hash-chain `ResourceMutation`-like append pattern — adapt for cleanup audit records); `packages/d2b-priv-broker/src/audit.rs` (`AuditWriteClass::{Standard,Unprivileged}` — cleanup audit records use `Standard` durability); `packages/d2b-realm-core/src/audit.rs::AuditChainLink::new` (hash-chain append for cleanup audit records); `nixos-modules/manifest.nix` (prior-generation retention pattern in the current bundle contract)", + "dataMigration" : "None — the `managedBy`/`configurationGeneration`/`deletionRequestedAt` fields are new; existing resources gain them on first v3 activation", + "dependencyOwner" : "ADR046-telem-009 + ADR046-telem-010 + ADR046-audit-001 + ADR046-store-001; core-controller owner", + "destination" : "`packages/d2b-core-controller/src/{configuration.rs, ownership.rs}`", + "detailedDesign" : "(1) On new generation activation, every stored `managedBy=configuration` resource absent from the new configured set receives `deletionRequestedAt` plus `deletion-pending`; controller/API-managed resources are untouched. (2) Activation returns after durable intent queueing and does not wait for cleanup. (3) The ownership handler drives child-before-parent finalizers. (4) When finalizers clear, one atomic store transaction writes the `Deleted` revision/change event and removes the row and indexes. After commit, the audit subsystem appends `ResourceMutation{event=\"deleted\", trigger=\"config-cleanup\"}` from that revision using a dedup/exactly-once recovery key; audit append is not part of the store transaction. (5) Stall detection sets `cleanup-stalled` without force-removing finalizers. (6) Prior generations use count retention, default 3 and range 1..16, with no TTL. (7) Core sets `managedBy`/`configurationGeneration` in persisted resources; input bundles omit both. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt hash-chain append from `daemon_audit.rs` for `ResourceMutation{trigger=\"config-cleanup\"}` records; adapt prior-generation retention window from `manifest.nix` pattern.", + "integration" : "`d2b-core-controller::configuration.rs` (generation activation); `d2b-core-controller::ownership.rs` (cleanup ordering and atomic final deletion); `d2b-audit` sink (cleanup audit records)", + "removalProof" : "None — net-new; no prior owner to remove; this is new behavior", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-telemetry-audit-and-support", + "specPath" : "docs/specs/ADR-046-telemetry-audit-and-support.md", + "validation" : "All tests in \"Configuration-owned cleanup contract tests\" subsection; additionally: `managedby_configuration_set_on_activated_resources`, `controller_created_resources_have_managedby_controller`, `absent_resource_receives_delete_on_new_generation`, `deletion_sets_deletionrequestedat_not_phase`, `final_deletion_is_atomic`, `cleanup_does_not_touch_controller_children`, `pending_cleanup_condition_set_on_zone`, `zone_is_degraded_not_failed_during_cleanup`, `pending_cleanup_cleared_after_deletion_completes`, `prior_generation_retained_count_based`, `rollback_schedules_delete_for_new_generation_resources`, `audit_segments_preserved_on_provider_delete`, `cleanup_stall_condition_set`, `generation_rejected_emits_audit_record`", + "workItemId" : "ADR046-telem-011" + }, + { + "currentSource" : "`packages/d2b-provider-relay/src/lib.rs` (`RelayEndpoint`, `RelayCredential`, `RelayRole`, `RelayStream`, `connect()`, `listen()`, `mint_sas()`)", + "dataMigration" : "No compatibility with current relay sessions; v3 sessions are independent", + "dependencyOwner" : "W0 shared contract root; ComponentSession transport adapter owner", + "destination" : "`packages/d2b-provider-transport-azure-relay/src/relay_transport.rs`", + "detailedDesign" : "Adapt `RelayStream` as relay transport service process; expose named opaque byte stream on the `transport-service` Unix endpoint; add 2-byte length-prefixed framing; preserve credential redaction; TLS/WebSocket state stays in-process — only Noise record bytes traverse the named stream; register named stream with d2b-bus as `TransportHandle`; transport descriptor: `attachment_support: false`, `locality: Remote`, `atomic: false`; expose `OpenTransport`/`CloseTransport`/`ObserveTransport` interface to core; long-lived service process multiplexes sessions internally Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "The child Zone's core ZoneLink controller calls its same-Zone selected Provider's `OpenTransport(spec.transportSettings, roleCredentialRef)` using the role ref selected from `spec.transportCredentials` → receives named byte stream handle; relay service cannot interpret plaintext bytes; one carriage per call; WebSocket loss closes the named stream; the parent has only sealed allocator/route state", + "removalProof" : "`d2b-provider-relay/src/lib.rs` relay plumbing retained until ACA display migration completes", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-azure-relay", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-azure-relay.md", + "validation" : "`tests/fake_relay_transport.rs`: connect/accept, framing, credential redaction, named stream roundtrip; `tests/listener_sender_conformance.rs`: named stream contract; Noise KK binding; relay identity exclusion", + "workItemId" : "ADR046-transport-relay-001" + }, + { + "currentSource" : "None (new)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-relay-001; Credential KK session; ComponentSession/d2b-bus owner", + "destination" : "`packages/d2b-provider-transport-azure-relay/src/credential_client.rs`", + "detailedDesign" : "Async Credential KK session client for service components (service processes have d2b-bus access, enabling Credential KK; workers do not); child core validates the same-Zone refs in `spec.transportCredentials`, selects the unique listener or sender ref by Credential audience, and supplies it to the child-local Provider service inside its gateway Guest; raw credential bytes are held in zeroizing memory inside the gateway Guest, presented to Azure Relay, then immediately zeroized; no Credential ref or byte crosses a Zone or Guest boundary, and byte delivery between the Credential Provider and consuming service remains inside the protected KK session; the parent allocator receives neither refs nor credentials and keeps only sealed route state; redacted Debug; no credential bytes in logs/audit/OTEL; the child Zone's core ProviderDeployment creates a private persistent Volume (per ADR-046-provider-state) for each component before its Process starts — the transport Provider does not own or create these Volumes; `Provider/volume-local` reconciles them; `migrationPolicy: none` means no migration worker is ever spawned; no relay auth token, WebSocket handle, session key, or credential byte is written to that Volume; all relay session state remains transient in-process memory", + "integration" : "The selected Provider's role service invokes acquisition inside the child gateway Guest; the parent allocator has only sealed route state and no Provider/Credential/ZoneLink resource", + "removalProof" : "N/A; new module", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-azure-relay", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-azure-relay.md", + "validation" : "`tests/credential_redaction.rs`: credential bytes never reach any Debug/log/audit/OTEL path; `src/tests/integration/credential_delivery.rs`: end-to-end credential delivery using injected fake Credential effect port", + "workItemId" : "ADR046-transport-relay-002" + }, + { + "currentSource" : "None (new; core drives reconnect, not the transport Provider)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-relay-001; reconnect contract; child Zone's ZoneLink handler", + "destination" : "`packages/d2b-provider-transport-azure-relay/src/reconnect.rs`", + "detailedDesign" : "Relay service responds to `CloseTransport`+`OpenTransport` cycle from core; core owns reconnect policy and backoff scheduling; relay service tears down the current WebSocket when core calls `CloseTransport` and establishes a new WebSocket connection when core calls `OpenTransport`; relay service does not maintain a backoff state machine or independently retry — it starts a new WebSocket on demand and emits the connect result via `ObserveTransport`; listener and sender are long-lived service processes that do not re-spawn on reconnect", + "integration" : "`ObserveTransport` delivers `TransportObservation::Disconnected` to core; core drives reconnect via `CloseTransport` then `OpenTransport` after applying its own backoff", + "removalProof" : "N/A; new module", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-azure-relay", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-azure-relay.md", + "validation" : "`tests/reconnect_backoff.rs`: relay responds to CloseTransport/OpenTransport cycle; WebSocket starts on demand; ObserveTransport reports connect result; `src/tests/integration/reconnect_scenario.rs`: full reconnect cycle including Credential re-acquisition", + "workItemId" : "ADR046-transport-relay-003" + }, + { + "currentSource" : "`docs/specs/ADR-046-zone-routing.md` transport settings Nix example", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-relay-001; transport settings schema; Nix configuration owner", + "destination" : "`packages/d2b-provider-transport-azure-relay/src/transport_settings.rs`; `docs/reference/schemas/v3/providers/transport-azure-relay.transport-settings.json`", + "detailedDesign" : "`AzureRelayTransportSettings` Rust struct with serde for only `relayNamespaceId` and `relayEntityId`; validation against committed JSON Schema; reject secret-shaped fields/values; generate and admit the exact six-field ZoneLink base; reject legacy provider envelopes and allocator-private fingerprint/capability fields; resolve `spec.transportProviderRef` before schema validation; validate exactly two same-Zone `spec.transportCredentials` refs with one `azure-relay-listen` and one `azure-relay-send` audience; enforce `disabled`/`limits` in child core; xtask `gen-provider-transport-schemas` integration", + "integration" : "`make test-drift` gate: `xtask gen-provider-transport-schemas && git diff --exit-code`", + "removalProof" : "N/A; new contract", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-azure-relay", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-azure-relay.md", + "validation" : "`tests/transport_settings_schema.rs`: valid/invalid schema vectors; `tests/transport_credentials.rs`: exact canonical ZoneLink field set, same-Zone ref/count/audience/scope checks, and rejection of credential refs inside `transportSettings`; eval-time Nix assertion coverage from `nix-unit: transport-settings-secret-key` test (see zone-routing spec)", + "workItemId" : "ADR046-transport-relay-004" + }, + { + "currentSource" : "`packages/d2b-session/src/scheduler.rs`, `streams.rs` (main commit `a1cc0b2d`)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-relay-001; backpressure/credit contract", + "destination" : "`packages/d2b-provider-transport-azure-relay/src/backpressure.rs`", + "detailedDesign" : "Outbound WebSocket send buffer bounded at `MAX_AGGREGATE_NAMED_STREAM_QUEUE_BYTES`; relay WebSocket write backpressure propagates to `FairScheduler` credit; `d2b_relay_transport_backpressure_events_total` counter emitted; no unbounded memory growth under slow relay", + "integration" : "Named stream send on `transport-service` Unix endpoint blocks on relay WebSocket write; d2b-bus `FairScheduler` observes backpressure via credit stall", + "removalProof" : "N/A; new module", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-azure-relay", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-azure-relay.md", + "validation" : "`tests/backpressure_credit.rs`: slow relay writer saturates outbound queue; named-stream credit stalls before unbounded growth; source Zone never buffers beyond aggregate limit", + "workItemId" : "ADR046-transport-relay-005" + }, + { + "currentSource" : "`packages/d2bd/src/metrics.rs` (hand-rolled Prometheus; baseline)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-relay-001 through ADR046-transport-relay-005; telemetry/audit owner", + "destination" : "`packages/d2b-provider-transport-azure-relay/src/{metrics.rs, audit.rs}`", + "detailedDesign" : "Emit all OTEL metrics and audit records listed in §OTEL and §Audit; closed semantic label sets with no Zone/resource-name-derived keys; retain Zone identity only in the `d2b.zone` OTEL resource attribute; never label secret bytes; provider audit covers **carriage authentication and health observations only** — Azure auth events, WebSocket lifecycle, credential acquisition outcomes — and is **separate from resource audit** (resource lifecycle events are owned by core); audit records appended through the Zone runtime audit log interface (no atomicity guarantee with Zone resource state in redb; best-effort delivery per the Zone's audit provider configuration); OTEL via lightweight emitter ring (no direct OTEL SDK dependency in Provider)", + "integration" : "`Provider/observability-otel` receives emitter ring frames; audit log via Zone runtime `d2b.audit.transport` category", + "removalProof" : "N/A; new module", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-azure-relay", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-azure-relay.md", + "validation" : "`tests/credential_redaction.rs` extended to cover audit/OTEL paths; `tests/metric_labels.rs` structurally asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys and that a Zone-name canary never enters label values; `tests/fake_relay_transport.rs` asserts audit record fields against schema", + "workItemId" : "ADR046-transport-relay-006" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "Provider crate owner; integration test owner", + "destination" : "`packages/d2b-provider-transport-azure-relay/src/tests/integration/README`", + "detailedDesign" : "Required content: fake relay server setup and teardown using the injected fake Relay effect port; how to run hermetic integration tests without a live Azure service; how to configure the injected fake Credential effect port for credential delivery tests; the fixture declares compiler-only `k2.parentZone = \"local-root\"` and puts the exact-shape ZoneLink, selected Provider, Network, Credentials, Process, and Endpoint resources only in K2; local-root's store is asserted to contain no reciprocal ZoneLink or Provider; how to run with a real Azure namespace (requires same-child-Zone `Credential` resources listed in `spec.transportCredentials`, not environment-variable credential paths); integration test scenarios and expected outcomes; CI/local execution instructions", + "integration" : "`make test-integration` invokes `tests/integration/containers/` scenarios which inject the fake relay and credential port implementations from `src/tests/integration/fake_relay_server.rs`", + "removalProof" : "N/A; mandatory layout", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-azure-relay", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-azure-relay.md", + "validation" : "File must be present; workspace policy gate enforces `src/tests/integration/README`", + "workItemId" : "ADR046-transport-relay-007" + }, + { + "currentSource" : "`packages/d2b-contracts/src/v2_component_session.rs` (protocol constants, credit-class constants) at main `a1cc0b2d`", + "dataMigration" : "None; v3 constants freeze independently", + "dependencyOwner" : "ADR046-session-001 (v3 ComponentSession contracts); W0 shared contract root", + "destination" : "`packages/d2b-provider-transport-unix/src/credit.rs` (imports `MAX_PACKET_ATTACHMENTS=32`, `RESERVED_CONTROL_FDS=64`, credit-class constants from v3 contract); `src/portal.rs` (imports `MAX_PACKET_ATTACHMENTS` for portal validation)", + "detailedDesign" : "Import credit scope capacities and headroom from `v3_zone_session.rs`; add `MAX_OPEN_TRANSPORTS: usize = 256` local constant for handle table bound. Primary reuse disposition: `adapt`. Preserved source-plan detail: Dependency on ADR046-session-001 output.", + "integration" : "`CreditScopeSet` constructed from imported constants at session setup", + "removalProof" : "No current code imports v3 transport constants; new import", + "reuseAction" : "adapt", + "reuseSource" : "Same file; constants copied into `packages/d2b-contracts/src/v3/zone_session.rs` by ADR046-session-001", + "specId" : "ADR-046-provider-transport-unix", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "validation" : "`tests/credit.rs::ancillary_capacity_is_derived_from_closed_hard_bounds` passes against v3 constants", + "workItemId" : "ADR046-transport-unix-001" + }, + { + "currentSource" : "`packages/d2b-session-unix/src/{adapter,socket,descriptor}.rs`, `tests/unix_session.rs` at main `a1cc0b2d`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-unix-001; d2b-bus transport layer (ADR046-bus-001)", + "destination" : "`packages/d2b-provider-transport-unix/src/{seqpacket,identity,socket}.rs`", + "detailedDesign" : "Copy transport structs verbatim; adapt `PeerIdentityPolicy` to report `PeerCredentials` upward to ComponentSession for subject mapping (not for direct resource lookup — that is core's responsibility); maintain `SO_PASSCRED` setup and first-packet credential extraction as documented; CLOEXEC enforcement uses `rustix` syscall wrappers over `libc` where available Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "`portal.rs::open_transport` calls `SeqpacketSocket::getsockopt(SO_TYPE)` and `setsockopt(SO_PASSCRED)`, constructs `UnixSeqpacketTransport`, hands OwnedTransport FD back to caller", + "removalProof" : "`d2b-realm-transport` seqpacket path retired after ZoneLink sessions migrate to child-local Providers and tests prove no reciprocal parent-store resource or cross-Zone FD transfer remains", + "reuseAction" : "adapt", + "reuseSource" : "Same; `UnixSeqpacketTransport`, `PeerIdentityPolicy`, `UnixAttachmentPayload`, `OwnedUnixAttachment`, `SeqpacketSocket`, `PeerCredentials`, `ObjectIdentity`, `AcceptedAttachment`, `VerifiedPacket`", + "specId" : "ADR-046-provider-transport-unix", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "validation" : "Copy all 12 test functions; add `peercred_reported_to_componentsession_not_resolved_to_subject_here`", + "workItemId" : "ADR046-transport-unix-002" + }, + { + "currentSource" : "`packages/d2b-session-unix/src/adapter.rs` `UnixStreamTransport`, `src/socket.rs` `StreamSocket` at main `a1cc0b2d`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-unix-001", + "destination" : "`packages/d2b-provider-transport-unix/src/{stream,socket}.rs`", + "detailedDesign" : "Copy verbatim; add `attachment_support: false` in `TransportDescriptor` (stream never carries SCM_RIGHTS regardless of route class); `admission.rs::validate_route_class` rejects `attachments_enabled=true` for stream Primary reuse disposition: `adapt`. Preserved source-plan detail: copy unchanged.", + "integration" : "Same path as seqpacket but without SCM_RIGHTS paths", + "removalProof" : "No current stream ZoneLink path exists; stream is net-new", + "reuseAction" : "adapt", + "reuseSource" : "Same", + "specId" : "ADR-046-provider-transport-unix", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "validation" : "`tests/portal.rs::stream_open_transport_forces_no_attachments`; `tests/identity.rs::stream_transport_reassembles_partial_and_coalesced_records`", + "workItemId" : "ADR046-transport-unix-003" + }, + { + "currentSource" : "`packages/d2b-session-unix/src/credit.rs` at main `a1cc0b2d`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-unix-001", + "destination" : "`packages/d2b-provider-transport-unix/src/credit.rs`", + "detailedDesign" : "Copy all five types verbatim; import scope-capacity constants from v3 contract; add `#[derive(Debug)]` with redacted Display (no raw counts in Debug output) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy unchanged.", + "integration" : "`CreditScopeSet` created per active ComponentSession; `CreditBundle` per packet receive; credits released in `UnixAttachmentPayload::close()`", + "removalProof" : "No current code path uses this crate directly; new", + "reuseAction" : "adapt", + "reuseSource" : "Same; `CreditPool`, `CreditScopeSet`, `CreditBundle`, `ProcessCreditLimit`, `CreditScope`", + "specId" : "ADR-046-provider-transport-unix", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "validation" : "Copy all 4 credit test functions; add `credit_released_on_attachment_close` and `emergency_headroom_constant_across_fd_counts`", + "workItemId" : "ADR046-transport-unix-004" + }, + { + "currentSource" : "`packages/d2b-session-unix/src/descriptor.rs` `PidfdIdentityPolicy`, `DescriptorPolicy` at main `a1cc0b2d`", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-unix-002", + "destination" : "`packages/d2b-provider-transport-unix/src/descriptor.rs`", + "detailedDesign" : "Copy verbatim; adapt `DescriptorPolicy::verify` to produce `AcceptedAttachment` carrying `ObjectIdentity` binding for v3 ComponentSession attachment descriptor model; `pid` not stored beyond liveness check Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt.", + "integration" : "Called by seqpacket transport after decrypting attachment descriptor", + "removalProof" : "Broker pidfd-open path in `d2b-priv-broker/src/sys.rs` serves different purpose (process supervision); no removal dependency", + "reuseAction" : "adapt", + "reuseSource" : "Same", + "specId" : "ADR-046-provider-transport-unix", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "validation" : "Copy `pidfd_identity_requires_live_launch_evidence_and_rejects_unrelated_process` and `duplicate_kernel_objects_are_rejected_and_cleaned_up`", + "workItemId" : "ADR046-transport-unix-005" + }, + { + "currentSource" : "No existing v3 socket-kind admission module", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-unix-002", + "destination" : "`packages/d2b-provider-transport-unix/src/admission.rs`", + "detailedDesign" : "`validate_route_class(route_class, socket_kind, attachments_enabled, received_fd)` calls `getsockopt(SO_TYPE)` (blocking adapter) on `received_fd`: `SOCK_SEQPACKET` must match `\"seqpacket\"`, `SOCK_STREAM` must match `\"stream\"`, any other type fails `invalid-socket-fd`; if `route_class == RouteClass::ZoneLink && attachments_enabled == true` fail `attachment-policy-conflict` with detail `cross-zone-attachments-forbidden`; if `socket_kind == \"stream\" && attachments_enabled == true` fail `attachment-policy-conflict`; no Noise profile enforcement (that is ComponentSession's responsibility); returns `Ok(RouteAdmission { route_class, socket_kind, attachments_enabled })`", + "integration" : "Called by `portal.rs::open_transport` before the monitoring dup and handle allocation", + "removalProof" : "No current code has this gate; new path", + "reuseAction" : "adapt", + "reuseSource" : "`getsockopt(SO_TYPE)` pattern widely used; no specific main reuse source", + "specId" : "ADR-046-provider-transport-unix", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "validation" : "`tests/admission.rs::seqpacket_fd_passes_seqpacket_kind`; `stream_fd_passes_stream_kind`; `seqpacket_fd_rejects_stream_kind_declaration`; `zone_link_with_attachments_enabled_fails`; `local_portal_seqpacket_with_attachments_accepted`; `stream_with_attachments_enabled_rejected`", + "workItemId" : "ADR046-transport-unix-006" + }, + { + "currentSource" : "No portal service in v3 baseline; `d2b-provider-toolkit/src/server.rs` `GeneratedProviderServiceServer` dispatch pattern (main `a1cc0b2d`) for service entry pattern", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-unix-002 through 006; ADR046-bus-001 (d2b-bus ComponentSession method dispatch); ADR046-session-001 (named-stream protocol)", + "destination" : "`packages/d2b-provider-transport-unix/src/{portal,service}.rs`", + "detailedDesign" : "`portal.rs`: `PortalHandler` struct owns a bounded `HashMap` (capacity `MAX_OPEN_TRANSPORTS=256`); `open_transport(req, attachment_fd)` validates via `admission.rs`, dups FD, allocates handle, stores `MonitorState { dup_fd, observation_senders: Vec }`; `close_transport(handle)` closes dup FD, half-closes all observation senders, removes entry; `observe_transport(handle)` registers a new `NamedStreamSender` and spawns an async epoll-watcher task on the dup FD; `TransportHandle` is a `[u8; 16]` random token; redacted in all Debug impls; `service.rs` is the binary entry: accepts the allocator-issued portal endpoint FD at launch, runs `GeneratedTransportServiceServer` over it, dispatches to `PortalHandler` Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt dispatch pattern; implement portal methods as new.", + "integration" : "The child Zone's core ZoneLink controller calls the three methods via same-Zone d2b-bus; the selected parent allocator retains its peer endpoint as sealed route state and injects the child endpoint through sealed bootstrap; the child runtime supplies that FD locally at Process spawn, not through a parent Provider, a cross-Zone attachment, or `SD_LISTEN_FDS`", + "removalProof" : "Ad-hoc IPC stubs in `d2bd/src/` retired after portal migration", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d` `d2b-provider-toolkit/src/server.rs` service dispatch pattern", + "specId" : "ADR-046-provider-transport-unix", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "validation" : "`tests/portal.rs::open_transport_zone_link_validates_and_returns_ownedtransport`; `open_transport_uses_child_bootstrap_endpoint_only`; `parent_endpoint_never_enters_provider_portal`; `open_transport_local_portal_seqpacket_with_attachments_accepted`; `open_transport_zone_link_attachments_enabled_rejected`; `close_transport_is_idempotent_after_handle_removed`; `observe_transport_delivers_pollhup_as_peer_disconnected`; `handle_table_rejects_at_max_capacity`; `restart_clears_all_handles`", + "workItemId" : "ADR046-transport-unix-007" + }, + { + "currentSource" : "`packages/d2b-priv-broker/src/` minijail spawn patterns (v3 baseline); `packages/d2b-host/src/` process arg patterns; current package derivations in `flake.nix`", + "dataMigration" : "None (fresh Provider resource)", + "dependencyOwner" : "ADR046-transport-unix-007; ADR046-provider-003 (system Provider framework); Provider/system-minijail (ADR046-provider-003)", + "destination" : "`packages/d2b-provider-transport-unix/` crate Cargo.toml binary target `d2b-transport-unix-service`; Provider component descriptor JSON committed at `packages/d2b-provider-transport-unix/descriptor/unix-transport-service.json`; Nix package derivation at `packages/d2b-provider-transport-unix/`", + "detailedDesign" : "Component descriptor declares: `processClass=service`, `template=unix-transport-service`, `stateNamespaces=[]` (no Provider state Volume; bounded non-secret operational state in status/core ledger, D087), `sandbox.capabilityClasses=[]`, `sandbox.namespaceClasses=[mount]`, `sandbox.seccompClass=strict`, `budget.memory.limit=\"16Mi\"`, `budget.cpu.limit=\"200m\"`, `budget.fds.limit=512`, `endpoints=[{name:portal,transport:unix,purpose:transport-unix-portal}]`, `readiness={class:provider-defined,initialDelay:\"0s\",timeout:\"5s\",failureThreshold:1,successThreshold:1}`, `restartPolicy={class:always,backoffBase:\"2s\",backoffMax:\"60s\",backoffMultiplier:2.0,maxRestarts:10,resetAfter:\"1h\"}`; Provider package bundles descriptor digest; core ProviderDeployment creates the Process with empty `mounts` when `Provider/transport-unix` is installed Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt; no direct symbol copy.", + "integration" : "Provider resource installed → core ProviderDeployment reads component descriptor → creates child `Process/transport-unix-service` (no state-Volume prerequisite) → ProviderSupervisor spawns binary with portal FD in inherited FD table. On delete: Process terminal first → ProviderDeployment finalizer cleared last; the service `status` disappears with the resource row", + "removalProof" : "No current transport-service Process exists; new path", + "reuseAction" : "adapt", + "reuseSource" : "Minijail sandbox semantic class patterns from current v3 broker; Process resource schema from ADR-046-resources-host-guest-process-user", + "specId" : "ADR-046-provider-transport-unix", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "validation" : "`tests/conformance.rs::process_resource_matches_component_descriptor`; `tests/conformance.rs::provider_state_set_is_empty`; `tests/conformance.rs::no_state_volume_mount`; sandbox policy tests against minijail conformance kit", + "workItemId" : "ADR046-transport-unix-008" + }, + { + "currentSource" : "`nixos-modules/options-realms.nix` realm options (v3 baseline); `nixos-modules/assertions.nix`", + "dataMigration" : "`d2b.realms.*` Nix options superseded by `d2b.zones.*`; no compatibility bridge (v3 reset)", + "dependencyOwner" : "ADR046-transport-unix-001; Nix/build integrator", + "destination" : "`docs/reference/schemas/v3/providers/transport-unix.transport-binding.json`; `nixos-modules/assertions.nix` (assertion additions); generated `nixos-modules/generated/options-zones-ZoneLink.nix` `transportSettings` submodule", + "detailedDesign" : "Commit the JSON Schema; run `xtask gen-zone-schemas` and `xtask gen-zone-nix-options` to regenerate committed files; generate the exact six-field ZoneLink base; reject legacy provider envelopes and allocator-private fingerprint/capability fields; add assertions for stream+attachments conflict and sensitive key names; require an empty `transportCredentials` list; reuse the common topology assertions to require the ZoneLink and selected Provider in the same child Zone, self-matching `childZoneName`, compiler-only non-root `parentZone`, and no reciprocal parent resource; `xtask gen-zone-resources` adds Provider-selected `transportSettings` validation", + "integration" : "Build emitter resolves `spec.transportProviderRef`, validates `spec.transportSettings` against that Provider's schema, and validates the empty `spec.transportCredentials` list before computing `generationId`; the topology compiler seals child→parent allocator selection separately and emits only the child-local Provider/ZoneLink resources; drift gate enforces sync", + "removalProof" : "`nixos-modules/options-realms.nix` realm wiring retired after Zone resource bundle activation replaces it", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-unix", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "validation" : "All eval/build tests in the Nix section, including `transport-unix-exact-zonelink-spec`, `transport-unix-child-local-topology`, legacy-field rejection, empty-credential enforcement, and a generated-bundle assertion that the parent store has no reciprocal Provider/ZoneLink row", + "workItemId" : "ADR046-transport-unix-009" + }, + { + "currentSource" : "v3 baseline `tracing` crate patterns; v3 `d2b-realm-router/src/service_v2.rs` audit field shapes", + "dataMigration" : "Existing `d2bd/src/metrics.rs` VM-label metrics superseded by v3 metrics; not migrated", + "dependencyOwner" : "ADR046-transport-unix-007; ADR-046-telemetry-audit-and-support", + "destination" : "`packages/d2b-provider-transport-unix/src/{audit,metrics}.rs`", + "detailedDesign" : "`AuditRecordKind` enum with 6 event kinds from Audit section; `AuditRecord` carries only the fields listed (no uid/gid/pid/path/handle/ZoneLink name); emit via Zone runtime `emit_audit_record()` interface; `MetricCounter`/`MetricHistogram` with closed label types per Metrics section; emit via bounded in-process ring to OTEL Provider datagram socket; `tracing::instrument` spans on `PortalHandler` methods with the 3 permitted span attributes only", + "integration" : "`portal.rs` calls `audit.rs::emit_*` before returning from each portal method; `seqpacket.rs` calls `metrics.rs::record_*` on every accept/packet/attachment", + "removalProof" : "`d2bd/src/metrics.rs` hand-rolled registry retired after metric surface migration", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-unix", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "validation" : "`tests/conformance.rs::audit_records_contain_no_pid_uid_or_handle`; `tests/conformance.rs::metric_labels_are_closed_set`; `tests/conformance.rs::span_attributes_contain_no_sensitive_fields`", + "workItemId" : "ADR046-transport-unix-010" + }, + { + "currentSource" : "No existing integration tests for Unix portal scenarios", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-transport-unix-007 through 010; test orchestration owner", + "destination" : "`packages/d2b-provider-transport-unix/integration/` and `integration/README.md`", + "detailedDesign" : "Four scenarios: `transport_open.rs` (fixture models compiler-only `k1.parentZone = \"local-root\"`; fake selected allocator retains one socketpair endpoint as sealed route state and injects the other into K1 bootstrap; K1 core passes only that child endpoint to its same-Zone Provider → OwnedTransport out; verify socket kind, CLOEXEC, SO_PASSCRED, K1-local Provider/ZoneLink, and absent local-root reciprocal row; p95 latency assertion ≤2 ms); `fd_transfer.rs` (within-Zone local-portal seqpacket `SCM_RIGHTS` transfer, credit accounting, scavenge on error injection; ZoneLink route rejects the same packet); `reconnect.rs` (CloseTransport + re-OpenTransport with a fresh sealed child endpoint, verify previous handle is unknown and monitoring dup closed); `observation_stream.rs` (ObserveTransport stream receives `PEER_DISCONNECTED` when peer closes within 5 ms p95). `integration/README.md` documents prerequisites (no KVM required; all scenarios use in-process socketpairs and fake Zone API endpoint stub), invocation (`cargo test -p d2b-provider-transport-unix --test integration`), environment variables, and expected output", + "integration" : "Invoked by `make test-integration`; no host mutation; each scenario creates its own socketpair, keeps parent endpoint state outside the resource stores, and exposes only the child endpoint to the child-local Provider", + "removalProof" : "Ad-hoc IPC test stubs retired after scenario parity", + "reuseAction" : "adapt", + "reuseSource" : "Test scenario shapes from `d2b-session-unix/tests/unix_session.rs` end-to-end test (main `a1cc0b2d`)", + "specId" : "ADR-046-provider-transport-unix", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-unix.md", + "validation" : "All four scenarios pass in CI; latency assertions enforced using monotonic timestamps; scavenge correctness verified by open-FD count before/after error injection", + "workItemId" : "ADR046-transport-unix-011" + }, + { + "currentSource" : "None — net-new v3 work; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "d2b-contracts crate shape stabilised by shared root contract; d2b-contracts owner", + "destination" : "packages/d2b-contracts/src/usbip_effect_port.rs", + "detailedDesign" : "Define UsbipEffectPort and UsbipGuestEffectPort in d2b-contracts with DeviceUid, NetworkUid, UsbBindingUid, LeaseToken, FirewallToken, FirewallGenerationFence, FirewallObservation, KernelModuleClass, DeviceProbeResult, and UsbipEffectError; export traits/types only with no implementation. `apply_firewall` and `release_firewall` both accept expected Network/Service generations; release also accepts NetworkUid and borrows the token so the controller can retain it until confirmed effect. Keep firewall apply/observe/release Network/busid-scoped, attach/detach Binding-addressed, and all fd/path/busid values private. `TransientDetail` derives `Clone, PartialEq, Eq` while retaining manual redacted Debug/Display so `UsbipEffectError`'s derives compile without disclosure. Primary reuse disposition: `create`. Preserved source-plan detail: net-new trait definition.", + "integration" : "Provider/device-usbip controller depends on this trait for injected semantic effects; the framework core adapter implements it in ADR046-usbip-002.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-usbip", + "specPath" : "docs/specs/providers/ADR-046-provider-device-usbip.md", + "validation" : "d2b-contracts tests for trait object safety, generation-fenced firewall apply/observe/release signatures (release requires NetworkUid and token), `FirewallGenerationMismatch`, `UsbipEffectError: Clone + PartialEq + Eq`, `TransientDetail` clone/equality, redacted Debug/Display behavior, and no implementation leakage.", + "workItemId" : "ADR046-usbip-001" + }, + { + "currentSource" : "packages/d2bd/src/usbip_state_machine.rs, packages/d2bd/src/usbip_reconcile_state.rs, packages/d2b-host/src/usbip_argv.rs, packages/d2b-priv-broker/src/ops/usbip_firewall.rs, usbip_host.rs, and usbip_lock.rs", + "dataMigration" : "Full d2b 3.0 reset; adapter resumes from Service/Binding status and authority owner proofs rather than daemon-coupled snapshots", + "dependencyOwner" : "ADR046-usbip-001, ADR046-zone-control-024; UsbipBindFirewallRule broker op; d2b-host usbip argv support; framework core adapter owner", + "destination" : "packages/d2b-core/src/device_usbip_adapter.rs", + "detailedDesign" : "Implement UsbipEffectPort in the core adapter: signed-bundle busid lookup, same-Zone validation, trusted physical-USB identity resolution, mandatory Core-derived `physical-usb-backing/v1` digest and exact `(Host, physical-usb-backing, opaqueKeyDigest)` claim shared with every security-key/USB Provider, exclusive OFD claim, sole ownership of all USBIP TCP/3240 and exact per-Network/per-busid effects through the existing closed `UsbipBindFirewallRule` request with action enum `Ensure|Remove`, ownership-scoped observe/release, anti-spoof probe, one shared Host module/backend authority, one Core-derived D097 relay Endpoint/firewall authority per Network, D097 authority-index preflight/adoption, and post-effect audit. `Ensure` applies; `Remove` uses the same request, binds expected Network/Service generations, validates exact ownership, succeeds idempotently on validated absence, and fails closed on a foreign marker. No separate release op exists. Complete the shared claim before any open, withhold, bind, module, relay, firewall, or attachment effect; never expose raw busid, identity digest, path, fd, bind address, nftables body, audit structs, or broker wire types. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt into framework-internal adapter.", + "integration" : "Reconcile framework injects the adapter into Provider/device-usbip; D097 authority index gates effects; adapter calls privileged broker and d2b-host argv helpers behind the semantic trait.", + "removalProof" : "Old daemon-coupled adapter call sites are removed by ADR046-usbip-009 after Provider wiring and adapter tests pass.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-usbip", + "specPath" : "docs/specs/providers/ADR-046-provider-device-usbip.md", + "validation" : "Fast packages/d2b-core/tests/device_usbip_adapter.rs covers same-Zone gate, exact shared physical backing tuple derivation, byte-identical keys for USB/security-key views of one fake token, Provider-private-class/digest bypass rejection, `physical-usb-backing-conflict` before effects, separate USBIP module/relay authorities, one-module/one-relay reuse, exact closed `Ensure|Remove` decoding and same-request broker mapping, unknown-action rejection, expected Network/Service generation binding, exact Network/busid firewall scoping, validated-absence `Remove`, foreign-marker failure without mutation, ownership-scoped drift/status, transient retry with retained token/authority, independent per-busid release, no network-local dependency, anti-spoof, redaction, and no digest/busid/path/fd exposure.", + "workItemId" : "ADR046-usbip-002" + }, + { + "currentSource" : "None — net-new Provider crate; no pre-ADR45 baseline equivalent", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-usbip-001, ADR046-provider-004; Provider model crate structure; device-usbip provider owner", + "destination" : "packages/d2b-provider-device-usbip/", + "detailedDesign" : "Create the required crate layout; bind the shared D098 `UsbService`/`UsbBinding` base versions/fingerprints from ADR046-provider-004 and implement only strict USBIP Provider extensions; sign/register extension schemas and advertise explicit export only for authority `UsbService` resources implemented by this Provider; implement validation.rs and compile-checked EffectPort injection. Declare the controller user/User resource in Nix activation. Primary reuse disposition: `create`. Preserved source-plan detail: net-new crate skeleton with contract reuse.", + "integration" : "Workspace manifests, Provider artifact catalog, Nix module, and ProviderDeployment consume the crate and component descriptor.", + "removalProof" : "None — net-new; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-usbip", + "specPath" : "docs/specs/providers/ADR-046-provider-device-usbip.md", + "validation" : "make test-policy passes; Cargo.toml has no d2b-priv-broker dependency; fast schema/manifest tests consume the common fixtures, accept canonical minimal base without `spec.provider`, prove a fake direct-local Provider can implement the same base, and cover Service-only exportability, Binding non-exportability, Core projection ownerRef/base fields with explicit `spec.provider` rejection, D088 status layering, semantic factory-fingerprint stability across Provider/adapter identity changes, strict refs, and trait injection.", + "workItemId" : "ADR046-usbip-003" + }, + { + "currentSource" : "packages/d2bd/src/usbip_state_machine.rs and packages/d2bd/src/usbip_reconcile_state.rs", + "dataMigration" : "Full d2b 3.0 reset; no direct import of d2bd usbip_reconcile_state snapshots", + "dependencyOwner" : "ADR046-usbip-001, ADR046-usbip-002, ADR046-usbip-003, and ADR046-zone-control-024; device-usbip controller owner", + "destination" : "packages/d2b-provider-device-usbip/src/{controller,reconcile,export_import}.rs", + "detailedDesign" : "Reconcile provider-neutral authority/projection `UsbService` and per-Guest `UsbBinding` resources through strict USBIP Provider extensions, consuming UsbipEffectPort, the exact shared Host-global physical USB tuple, USBIP-private D097 authorities, and signed D096 ExportAdapter/ImportAdapter. Enforce same-Zone base and provider refs; ResourceExport authority-Service-only target; same-type ResourceImport-owned projection with `providerRef`, semantic base/import fields, no `spec.provider`, and no physical fields/effects; route selection from the signed local descriptor/import record; semantic factory fingerprint independent of separately authenticated adapter identity; exclusive fair Binding admission; encrypted bounded named-stream control/data; D088 layered status; Service/Binding finalizers; restart adoption; declared/explicit modes; no session/transfer resources. Firewall reconcile maps apply to `UsbipBindFirewallRule` action `Ensure`; finalization maps release to action `Remove` on the same request with expected Network/Service generations. Finalization retains firewall token/status and relay authority until confirmed effect, retries transient failure, refreshes on generation mismatch, and blocks fail-closed on a foreign marker. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt step machine into Provider reconcile loop.", + "integration" : "Controller watches Device/Host/Network/Guest/Endpoint/Export/Import dependencies, calls injected EffectPorts, commits ResourceMutationBatch updates, coordinates children from ADR046-usbip-005, and delegates only semantic export/import admission to the Provider adapter while core owns D096 routing/lifecycle.", + "removalProof" : "packages/d2bd/src/usbip_state_machine.rs and usbip_reconcile_state.rs are deleted by ADR046-usbip-009 once Provider parity tests pass.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-usbip", + "specPath" : "docs/specs/providers/ADR-046-provider-device-usbip.md", + "validation" : "Fast tests/controller_state_machine.rs, service_binding_schema.rs, export_import.rs, authority_conflict.rs, async_loop.rs, finalizer.rs, and wrong_zone.rs cover authority/projection/Binding lifecycle, Service-only export, projection `spec.provider` rejection, semantic fingerprint stability under adapter identity changes plus separate descriptor authentication, D088 `status.resource`/`status.provider` placement, shared physical tuple collision before effects, encrypted fake streams, no physical projection effect, exclusivity, restart, WrongZone degradation, exact `Ensure|Remove` mapping, and firewall status/token/authority retention until `Remove` confirmation.", + "workItemId" : "ADR046-usbip-004" + }, + { + "currentSource" : "None — net-new Process resources; templates derive from the Provider package descriptor", + "dataMigration" : "Full d2b 3.0 reset; old per-env runners become Host/Network authorities and per-Device port-3240 workers are forbidden", + "dependencyOwner" : "ADR046-usbip-003; Process ResourceType schema; device-usbip process lifecycle owner", + "destination" : "packages/d2b-provider-device-usbip/src/reconcile.rs", + "detailedDesign" : "Create/adopt exactly one Host backend Process authority, exactly one D097 Network relay Process/Endpoint/firewall authority bound to TCP 3240 with a Core-derived Network/policy key, and one Binding-owned Guest proxy/private Endpoint per attached Binding. Restrict relay resolution to the Core adapter and exact active-lease Binding proxies; deliver their connected streams by LaunchTicket. Use canonical system-minijail specs, signed templates, bounded budgets/readiness/restart, no argv/path/address/fd fields; attach/detach remains a one-shot EffectPort operation, not a second Process. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt into singleton Host backend, per-Network relay, and per-Binding Guest proxy management.", + "integration" : "Service controller registers physical busids with shared backend/relay; Binding controller creates its Guest proxy/private Endpoint; Process controller launches workers; UsbipGuestEffectPort attaches to the private Endpoint.", + "removalProof" : "Old per-env usbipd autostart and ProcessRole::Usbip paths are removed by ADR046-usbip-009 after Process resource lifecycle tests pass.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-usbip", + "specPath" : "docs/specs/providers/ADR-046-provider-device-usbip.md", + "validation" : "Fast Process/Endpoint shape tests prove one backend per Host, one Core-derived multiplexed TCP 3240 Endpoint/firewall authority per Network, deterministic duplicate conflict, adapter/exact-Binding-only resolution, LaunchTicket-connected stream, no generic Network reader access, Binding ownership/private Guest policy, no per-Device listener, no raw address/argv/path/fd, and readiness before bind/attach.", + "workItemId" : "ADR046-usbip-005" + }, + { + "currentSource" : "packages/d2bd/src/usbip_reconcile_state.rs state fields", + "dataMigration" : "Full d2b 3.0 reset; current d2bd reconcile state is not imported", + "dependencyOwner" : "ADR046-usbip-003; Device provider details plus `UsbService` and `UsbBinding` base/provider status schema owner", + "destination" : "packages/d2b-provider-device-usbip/src/status.rs", + "detailedDesign" : "Define provider-neutral `UsbService` `status.resource` with whole-device availability/access counts, authority-only `physical-usb-backing` claim state, and projection-only common import state; define `UsbBinding.status.resource` with generic attachment phase/queue/generation/timestamps. Define separate strict `status.provider` USBIP details: Device probe, owner backend/relay, USBIP-owned `firewallState`/opaque digest/observed generation, imported-route observations, and Binding proxy/private Endpoint/subphase. During `UsbipBindFirewallRule` action `Remove`, strict status remains `firewallState: releasing`; the token and relay authority remain held until confirmed success, including validated absence. No common access/import/attachment field appears directly under `status`; no USBIP module, Network, Endpoint, proxy, server/client, port, firewall, or busid field enters base status; no raw backing digest, busid, path, fd, address, session/transfer ID, remote identity, or payload appears anywhere. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt state fields to typed status.provider.details.", + "integration" : "Controller writes each extension atomically with its resource's common status; dependency/update propagation is Device/Export → Service/projection → Binding.", + "removalProof" : "Old d2bd USBIP reconcile-state structs are removed by ADR046-usbip-009 after status extension coverage passes.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-usbip", + "specPath" : "docs/specs/providers/ADR-046-provider-device-usbip.md", + "validation" : "Fast tests/status_serde.rs covers generic base status plus three strict USBIP detail schemas, exact `status.resource` placement for access/import/attachment/backing claim, exact `status.provider` placement for relay/firewall implementation observations, drift transition and ownership, `releasing` retention until `Remove` confirmation, rejection of semantic fields directly under `status`, mode-dependent omissions, bounded counts/refs, unknown-field denial, and a deny corpus proving USBIP-only fields are rejected from base and Network status.", + "workItemId" : "ADR046-usbip-006" + }, + { + "currentSource" : "packages/d2b-contract-tests/tests/usbip_policy_network_scoping.rs plus new integration scenarios", + "dataMigration" : "None — docs/tooling only; no runtime state", + "dependencyOwner" : "ADR046-usbip-004 and ADR046-usbip-005; device-usbip integration owner", + "destination" : "packages/d2b-provider-device-usbip/{src,tests,integration/README.md}; tests/host-integration/usbip-service.nix; tests/host-integration/hardware/usbip-service.sh", + "detailedDesign" : "Put provider-neutral Service/Binding base-schema separation, strict USBIP extensions, projection `spec.provider` rejection, D088 layered status, semantic factory-fingerprint independence from Provider/adapter identity, exact shared physical backing tuple/conflict, one Core-derived per-Network relay Endpoint/firewall authority, exact per-busid firewall ownership/drift/release through one `UsbipBindFirewallRule` request with closed `Ensure|Remove`, generation fencing, idempotent validated-absence `Remove`, foreign-marker fail-closed behavior, and status/token/authority retention until confirmation, arbitration, same-type export/import, encrypted fake-stream, and least-privilege process/Endpoint shape coverage in fast Layer-1 Rust tests. Include a fake direct-local Provider proving the same base contract has no USBIP dependency. Reserve runNixOSTest for real Linux usbip_host/vhci_hcd, usbipd, namespaces/nftables, TCP 3240, zero network-local carve-out, and Guest checks; reserve the hardware script for an approved physical device. Use existing Make gates only. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing network-scoping assertion and add new scenarios.", + "integration" : "Layer-2 lanes exercise actual kernel/backend/relay/Guest/device paths and do not duplicate pure controller/schema cases. Cross-Zone protocol logic remains hermetic with fake peers; the runNixOSTest only proves its real-system integration.", + "removalProof" : "Old usbip_policy_network_scoping coverage is retired only after the fast wrong-Zone admission test and `tests/host-integration/usbip-service.nix` successor both pass and the migration ledger is updated.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-usbip", + "specPath" : "docs/specs/providers/ADR-046-provider-device-usbip.md", + "validation" : "`make test-host-integration` runs the non-hardware real-kernel case on a capable host; `make test-hardware` runs the explicit manual device case. No Layer-1 test opens a device, loads a module, creates a namespace, or listens on a socket.", + "workItemId" : "ADR046-usbip-007" + }, + { + "currentSource" : "nixos-modules/components/usbip.nix guest wiring and new Zone resource declarations", + "dataMigration" : "Full d2b 3.0 reset; operators reauthor old per-VM options as Device + authority/projection Service + per-Guest Binding", + "dependencyOwner" : "ADR046-usbip-003, ADR046-zone-control-024; ADR-046-nix-configuration; Nix integrator", + "destination" : "nixos-modules/components/usbip.nix, nixos-modules/options-zones.nix, nixos-modules/assertions.nix", + "detailedDesign" : "Add Provider config; remove the old per-VM option; emit provider-neutral authority `UsbService`, per-Guest `UsbBinding`, strict USBIP Provider envelopes, and optional ResourceExport/ResourceImport authoring shapes; imported same-type projection Services remain Core-created with no `spec.provider`. Assert same-Zone base/provider refs, projection ownerRef/forbidden implementation and physical fields, Service-only export target, one Host backend/Network relay, exact Core-derived Host-global `(Host, physical-usb-backing, opaqueKeyDigest)` exclusion shared by USB/security-key, Provider-private-class bypass rejection, one `UsbipBindFirewallRule` contract with exact actions `Ensure|Remove` and no separate release operation, and retain guest vhci_hcd/tools. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt guest module, remove host-side option surface, and extend eval assertions.", + "integration" : "Nix compiler emits Device plus provider-neutral `UsbService`/`UsbBinding` and explicit D096 resources consumed by core/Provider; guest runtime supplies USBIP proxy/attach tools; generated base and provider-extension schemas/fingerprints remain canonical.", + "removalProof" : "d2b.vms..usbip.yubikey and host-side USBIP module paths are removed at reset once Zone resource emitter coverage passes.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-usbip", + "specPath" : "docs/specs/providers/ADR-046-provider-device-usbip.md", + "validation" : "Fast tests/unit/nix/cases/usbip-*.nix cover schema shape, Core projection `spec.provider` rejection, D088 layered status fixtures including USBIP-owned firewall state and `releasing` retention, all reference/owner/export assertions, byte-identical USB/security-key tuple collision and private-class bypass rejection, one Core-derived 3240 Endpoint/firewall authority per Network, exact `Ensure|Remove` broker action surface with unknown values rejected and no second op, least-privilege consumer policy, absence of network-local 3240 rules, old-option removal, and guest module retention.", + "workItemId" : "ADR046-usbip-008" + }, + { + "currentSource" : "packages/d2bd/src/usbipd_perenv_autostart.rs, packages/d2bd/src/usbip_state_machine.rs, packages/d2bd/src/usbip_reconcile_state.rs, nixos-modules/network.nix USBIP firewall block, and ProcessRole::Usbip in packages/d2b-core/src/processes.rs", + "dataMigration" : "Full d2b 3.0 reset; no daemon-coupled USBIP runtime state import", + "dependencyOwner" : "ADR046-usbip-004 and ADR046-usbip-008; Provider fully wired and validated; daemon cleanup owner", + "destination" : "packages/d2bd/src/, nixos-modules/network.nix, packages/d2b-core/src/processes.rs", + "detailedDesign" : "Remove daemon-coupled USBIP after Provider tests and integration tests pass: delete per-env autostart, state machine, and reconcile state modules after migration; remove USBIP firewall block from network.nix; remove ProcessRole::Usbip; run Layer-1 gates and confirm no d2bd or network.nix references remain outside the adapter and contracts. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete after Provider replacement reaches parity.", + "integration" : "Provider/device-usbip, core D096/D097 adapters, Nix Device plus provider-neutral USB resource emitter, USBIP authority workers, and Binding-owned children are the sole USBIP lifecycle path after deletion.", + "removalProof" : "usbipd_perenv_autostart.rs, usbip_state_machine.rs, usbip_reconcile_state.rs, network.nix USBIP firewall block, and ProcessRole::Usbip are deleted after parity.", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-device-usbip", + "specPath" : "docs/specs/providers/ADR-046-provider-device-usbip.md", + "validation" : "make test-unit and make test-flake plus grep or contract checks for removed symbols and no residual d2bd/network.nix USBIP lifecycle references.", + "workItemId" : "ADR046-usbip-009" + }, + { + "currentSource" : "None — the fixed user-session authority is today ambient prose across the display/audio/clipboard/notification/secret-service dossiers; no named owner exists", + "dataMigration" : "None — full d2b 3.0 reset", + "dependencyOwner" : "ADR046-zone-control-019 (authority index); `Provider/system-systemd` (user manager) + core/user-agent owner", + "destination" : "`packages/d2b-core-controller/src/user_session_authority.rs` (or a core/user-agent per-session agent Process under `Provider/system-systemd`); `AuthorityDescriptor` on the session authority", + "detailedDesign" : "Name and implement the **fixed user-session authority** (D097 desktop/session): `authorityScope: seat` bound to `(Host, User, login-session/seat)`, opaque `authorityKey` (never a raw socket path/XDG_RUNTIME_DIR/DISPLAY/seat name), `cardinality: exactly-one` per `(Host, User, login-session)`, `arbitration: exclusive`, owner = a core/user-agent per-user-session agent Process (NOT a new Provider), adoption by `ownerProof` (agent Process identity + login-session id), `exportability: forbidden`. It is the sole opener of the compositor/PipeWire/session-bus FDs and hands them to desktop Providers only via the EffectPort/LaunchTicket. Core's authority index rejects a duplicate session authority (or a duplicate same-user display portal, clipboard host, notification sink, audio mediator, systemd user manager, Secret Service, or seat-input claimant) with `duplicateConflict` before any FD open; multi-user/seat is admitted only up to the declared per-Host limit. Guest-stop invalidates every session authority/lease bound to that Guest across display/audio/notification/credential/shell in one dependency-aware cascade (D091), with no stale FD surviving. Host input (`wl_seat`/pointer constraints) is an `at-most-one`-per-seat authority under this session authority; pointer-constraint enforcement is a declared boundary until an interaction Provider implements it. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (name and implement the shared user-session authority).", + "integration" : "Core authority index (ADR046-zone-control-019); `Provider/system-systemd` user manager; display/audio/clipboard/notification/secret-service/shell Provider services bind to this single authority for their FDs; D091 Guest-stop cascade", + "removalProof" : "Not applicable (net-new named authority; replaces ambient prose)", + "reuseAction" : "adapt", + "reuseSource" : "`Provider/system-systemd` user-manager scope; D077 EffectPort/LaunchTicket FD handoff", + "specId" : "ADR-046-resources-host-guest-process-user", + "specPath" : "docs/specs/ADR-046-resources-host-guest-process-user.md", + "validation" : "Single session authority per `(Host, User, session)`; duplicate same-user session authority / desktop service rejected with `duplicateConflict`; multi-seat declared-limit enforcement; Guest-stop invalidates all bound desktop/audio/notification/credential/shell authorities and leases (no stale compositor/PipeWire/session-bus FD); seat-input second claimant rejected; adoption by `ownerProof` and quarantine on ambiguity; hermetic with fakes", + "workItemId" : "ADR046-user-session-001" + }, + { + "currentSource" : "`d2b-core/src/storage.rs` (`StoragePathSpec`, `StoragePathKind`, policy enums); `d2b-core/src/sync.rs` (`SyncJson`, `LockSpec`)", + "dataMigration" : "Full v3 reset; no row-level import", + "dependencyOwner" : "ADR046-primitives-001; v3 contracts owner", + "destination" : "`d2b-contracts/src/v3/volume_layout.rs` (LayoutEntry, EntryType, all policy enums, AclGrant, Invariant, SensitivityClass); `d2b-contracts/src/v3/volume_spec.rs` (VolumeSpec, ViewSpec, Attachment, QuotaSpec, SourceKind, `SourcePolicyId` opaque newtype); `d2b-contracts/src/v3/effect_port.rs` (`VolumeEffectPort` trait, opaque ID newtypes `VolumeId`/`LayoutEntryId`/`UserId`/`ViewId`/`SealingPolicyId` each with custom redacted Debug, `VolumeId` and `SealingPolicyId` with the exact `Clone`/serde wire derives and bounded `TryFrom` deserialization required by the canonical request, `VolumeMountToken`, and canonical `RotateSealingKeyRequest`/`Result`/`Error` types)", + "detailedDesign" : "All LayoutEntry fields as documented in this dossier; enum value names preserved from `StoragePathKind`/policy enums with renames where noted; `User/` ACL principal (no numeric UID); `sourcePolicyId` opaque newtype replaces raw `hostPath` in `SourceKind::LocalPath` and `SourceKind::BlockImage`; deny-unknown sealing-rotation request contains only opaque Volume/policy/operation IDs and generation/revision preconditions, with no key bytes/path/handle; its crate-private `VolumeId` and `SealingPolicyId` strings serialize canonically and deserialize only when non-empty, ASCII-graphic, and at most 128 bytes, while retaining manual redacted Debug Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Volume spec and status structs; Provider descriptor component stateNamespace; Nix resource compiler schema validation", + "removalProof" : "`d2b-core/src/storage.rs` StoragePathSpec/policy enums removed only after all Provider descriptor consumers are on v3 Volume spec", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "Schema golden vectors; round-trip serde; ACL principal validation rejects numeric forms; `sourcePolicyId` present; no `hostPath` field in any volume_spec contract; compile-time assertions for the exact `Clone + Serialize + DeserializeOwned` ID bounds and `Serialize + DeserializeOwned` request bounds; sealing-rotation request canonical-string/round-trip and deny-unknown tests; exact redacted-Debug assertions; deserialization rejects empty, over-128-byte, non-ASCII, whitespace, and control-byte IDs without echoing input; compile-time trait conformance includes `rotate_sealing_key`", + "workItemId" : "ADR046-vl-001" + }, + { + "currentSource" : "`d2b-state/src/{atomic,path,lock}.rs` (main `6faa5256`); `d2b-priv-broker/src/ops/swtpm_dir.rs` (marker algorithm)", + "dataMigration" : "New marker written for each Volume at v3 first-boot", + "dependencyOwner" : "ADR046-vl-001; volume-local Provider owner", + "destination" : "Full `packages/d2b-provider-volume-local/` scaffold per §Crate layout: `src/`, `tests/`, `integration/`, `README.md`; crate `Cargo.toml` depends only on `d2b-contracts`, `d2b-provider`, `d2b-provider-toolkit`", + "detailedDesign" : "`AnchoredDir`, `AnchoredResource`, `LeafName`, `RelativePath`; adapted `AtomicFilesystem`/`StateEnvelope`; adapted `LockGuard`/`LockSet`/`OfdTransfer`; marker write/verify/check; `src/effect_port.rs` re-exports `VolumeEffectPort` trait from `d2b-contracts::v3::effect_port` and provides Provider-side opaque ID construction helpers (no adapter implementation; adapter lives in host runtime); `sourcePolicyId` validation against declared policy list; no `openat2`/`setfacl`/`fallocate`/numeric-UID call sites in Provider crate Primary reuse disposition: `adapt`. Preserved source-plan detail: copy-unchanged (`path.rs`); adapt (`atomic.rs`, `lock.rs`); adapt swtpm_dir marker algorithm.", + "integration" : "Controller binary receives `VolumeEffectPort` via ComponentSession injection; adapter calls `provision_marker` when a new Volume first appears in the `providerRef` reconcile queue (ProviderDeployment has already created the resource; volume-local provisions physical state) and `verify_marker` on restart relist", + "removalProof" : "`swtpm_dir.rs` marker implementation retired only after device-tpm Provider Volume is live and marker-check parity is confirmed", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "All `tests/marker.rs`, `tests/state.rs` scenarios; all `integration/provision.rs` scenarios; `cargo deny check` verifies no `d2b-priv-broker`/`d2bd` dependency", + "workItemId" : "ADR046-vl-002" + }, + { + "currentSource" : "`d2b-priv-broker/src/ops/{state_dir,storage_contract}.rs` (broker layout ops); `d2b-core/src/storage_lifecycle.rs` (lifecycle report)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-vl-002; ADR046-reconcile-001; d2b-bus/ComponentSession owner", + "destination" : "`src/controller.rs`, `src/layout.rs`, `src/acl.rs`, `src/source.rs`", + "detailedDesign" : "Async reconcile loop; topological LayoutEntry evaluation; `VolumeEffectPort` semantic op dispatch (no direct broker connection; no `openat2`/`setfacl` call sites); ACL reconciliation cycle via effect port; drift detection; status write with expected revision; `sourcePolicyId` validation against declared `sourcePolicies`; controller watch remains responsive while per-resource effect calls run concurrently; **single watch scope** `providerRef: Provider/volume-local` — physical state reconciliation for all served Volumes (layout/ACL/quota/marker); ProviderDeployment creates/deletes Volume instances; volume-local does not issue create/delete API calls; Nix-preprovisioned `User/` layout principals; no cross-component Volume sharing; each component consumes only its declared view; empty-payload stateNamespace Volumes use `migrationPolicy: none` — no migration EphemeralProcess dispatched", + "integration" : "Controller binary instantiated by Zone runtime after Provider Ready; receives `VolumeEffectPort` implementation and d2b-bus `ResourceClient` via ComponentSession", + "removalProof" : "`d2b-priv-broker/src/ops/storage_contract.rs` `reconcile_storage_scope` retired only after Volume controller parity confirmed", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "`tests/layout_provision.rs`, `tests/layout_repair.rs`, `tests/layout_adopt.rs`, `tests/acl.rs`, `tests/view_rights.rs`, `tests/source.rs`, `integration/provision.rs`", + "workItemId" : "ADR046-vl-003" + }, + { + "currentSource" : "`d2b-host/src/hardlink_farm.rs`; `d2b-priv-broker/src/ops/{store_sync,store_view_posture,store_view_farm,store_sync_audit,store_sync_export}.rs`; `nixos-modules/store.nix`", + "dataMigration" : "None (format preserved; activation changed from Nix to Volume controller)", + "dependencyOwner" : "ADR046-vl-003; runtime-cloud-hypervisor Provider owner", + "destination" : "`src/store_view.rs`; `tests/store_view.rs`; `integration/store_view.rs`", + "detailedDesign" : "Store-view LayoutEntry matrix (see §Same-filesystem hardlink farm); private-NS sync via `run_store_sync` effect op; `StoreSyncComplete` semantic operation; OFD lock semantics; `gcroots/` and `state/` at store-view root; spec-correction enforcement", + "integration" : "`runtime-cloud-hypervisor` Provider declares store-view Volume in its ProviderStateSet; volume-local controller handles sync", + "removalProof" : "`nixos-modules/store.nix` activation and `d2b-priv-broker/src/ops/store_sync.rs` retired only after store-view Volume controller is live and passes all parity tests", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "`tests/store_view.rs` all invariants; `integration/store_view.rs` same-filesystem boundary; private-NS sync with concurrent reader", + "workItemId" : "ADR046-vl-004" + }, + { + "currentSource" : "`d2b-priv-broker/src/ops/swtpm_dir.rs`", + "dataMigration" : "None (full v3 reset; TPM NVRAM must be backed up by operator)", + "dependencyOwner" : "ADR046-vl-002; device-tpm Provider owner", + "destination" : "`src/swtpm_volume.rs`; `tests/swtpm_volume.rs`; `integration/swtpm_marker.rs`", + "detailedDesign" : "TPM LayoutEntry matrix; `create-if-never-provisioned` + fail-closed repair; broker-maintained provisioning marker; ancestor traverse ACL; `previously-provisioned-swtpm-state-missing` fail-closed detection; `secret-adjacent` sensitivity enforcement", + "integration" : "`device-tpm` Provider declares TPM Volume in ProviderStateSet; volume-local handles layout/marker lifecycle", + "removalProof" : "`d2b-priv-broker/src/ops/swtpm_dir.rs` retired only after device-tpm Provider TPM Volume is live and fail-closed tests pass", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "`tests/swtpm_volume.rs` all scenarios; `integration/swtpm_marker.rs` real broker-maintained marker", + "workItemId" : "ADR046-vl-005" + }, + { + "currentSource" : "No equivalent in baseline; new", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-vl-003", + "destination" : "`src/source.rs` (block-image and tmpfs branches); `tests/source.rs`; `integration/block_image.rs`", + "detailedDesign" : "`block-image`: image file create/verify via `provision_block_image` effect op; `fallocate` performed by adapter when `preallocate: true`; FD transfer to Guest runtime via LaunchTicket via `open_volume_mount_token` effect op; `tmpfs`: `mount_tmpfs`/`umount_tmpfs` effect ops; `size=` and `nr_inodes=` derived from quota fields; cleanup via `umount_tmpfs` op", + "integration" : "Guest runtime Provider (cloud-hypervisor) receives block-image FD from volume-local via LaunchTicket; no path crosses the boundary", + "removalProof" : "Not applicable (new)", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "`tests/source.rs` allowlist pass/fail; block-image/tmpfs eval constraints; `integration/block_image.rs` real image lifecycle", + "workItemId" : "ADR046-vl-006" + }, + { + "currentSource" : "`d2b-state/src/atomic.rs` (main); no existing migration/snapshot infrastructure in v3", + "dataMigration" : "None (new protocol)", + "dependencyOwner" : "ADR046-vl-002; ADR046-vl-003; ADR046-pstate-004 through ADR046-pstate-006", + "destination" : "`src/{migration,snapshot,sealing}.rs`; `tests/{migration_unit,snapshot_unit,sealing_unit}.rs`; `integration/{migration,snapshot,sealing}.rs`", + "detailedDesign" : "Schema migration (see §Schema migration) via `volume-migration-worker` EphemeralProcess; Snapshot create/list/expire (see §Snapshots) via `volume-snapshot-worker` EphemeralProcess; controller reports `stateSchemaPhase` and `snapshots` in Volume status. Sealing lifecycle uses the status-first `rotation-pending` transition and only canonical `VolumeEffectPort::rotate_sealing_key`; it persists/resumes the core Operation-ledger fingerprint, classifies exact typed errors, and has no key lease, direct rewrite, generic broker call, or Provider-owned EphemeralProcess worker.", + "integration" : "Controller's reconcile handler dispatches migration/snapshot EphemeralProcess via d2b-bus `ResourceClient`; sealing reconcile/upgrade commits status before effect and maps typed results to `sealed`/`rotation-failed`; volume-local reports state schema, snapshots, and safe sealing generations in Volume status", + "removalProof" : "Not applicable (new)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "All `tests/migration_unit.rs`, `tests/snapshot_unit.rs`, `tests/sealing_unit.rs`, `integration/migration.rs`, `integration/snapshot.rs`, and `integration/sealing.rs` scenarios, including restart/idempotency and status-before-effect assertions", + "workItemId" : "ADR046-vl-007" + }, + { + "currentSource" : "No equivalent; new", + "dataMigration" : "Not applicable", + "dependencyOwner" : "ADR046-vl-003; ADR046-vl-007", + "destination" : "`src/relocation.rs`; `tests/relocation_unit.rs`; `integration/relocation.rs`", + "detailedDesign" : "As documented in §Relocation, §Retention, §Incident hold, §Unclaimed Volume GC, §Destruction", + "integration" : "Controller adds `Relocating` finalizer; creates relocation EphemeralProcess; destruction sequence is ordered leaf-first with `fsync` after each step", + "removalProof" : "Not applicable (new)", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "All `tests/relocation_unit.rs`, `integration/relocation.rs` scenarios; destruction ordering under fault injection", + "workItemId" : "ADR046-vl-008" + }, + { + "currentSource" : "`d2b-state/src/audit.rs` (main `6faa5256`); OTEL cardinality model from `d2b-provider-observability-local/src/` (main `a1cc0b2d`)", + "dataMigration" : "None — full d2b 3.0 reset; no prior state to migrate", + "dependencyOwner" : "ADR046-vl-001; Zone audit stream owner; `observability-otel` Provider owner", + "destination" : "`src/audit.rs`; `src/otel.rs`; `src/error.rs`; `tests/audit_unit.rs`; `integration/audit.rs`", + "detailedDesign" : "Event types and Zone audit emission per §Audit events, including controller rotation start/failure/commit and exactly-once broker `RotateSealingKey` success; OTEL metric definitions per §OTEL metrics with closed semantic labels and no Zone/resource-name-derived dimensions; Zone identity remains in `d2b.zone` resource attributes; error catalog per §Error catalog; no-path/no-key invariant enforced in all outputs", + "integration" : "Every lifecycle transition calls `audit::emit_volume_event`; OTEL metrics exported via `observability-otel` Provider", + "removalProof" : "Not applicable", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "`tests/audit_unit.rs` golden records and structural metric descriptor assertions for exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus resource-name canary absence; rotation audit exact-once/digest-only vectors; `tests/error_messages.rs` bounded messages; `integration/audit.rs` live stream", + "workItemId" : "ADR046-vl-009" + }, + { + "currentSource" : "`nixos-modules/storage-json.nix`; `nixos-modules/store.nix`; `packages/xtask/src/main.rs` (`gen-schemas`)", + "dataMigration" : "`nixos-modules/storage-json.nix` path rows superseded by Volume resources; `nixos-modules/store.nix` activation superseded by store-view Volume", + "dependencyOwner" : "ADR046-vl-001; ADR046-pstate-010; NixOS module owner", + "destination" : "`nixos-modules/zone-resources.nix` (per §ADR046-pstate-010); `root-config.schema.json` in the Provider package", + "detailedDesign" : "`sourcePolicies` in Provider root config (opaque IDs; no raw host paths); path prefix injection by resource compiler into private bundle (never into ResourceSpec or operator-authored Nix); `controllerExecutionRef` in Provider config; all eval-time validation rules per §Nix configuration including `sourcePolicyId` validation; artifact catalog entry; Provider and Volume resource authoring shapes", + "integration" : "NixOS build emits `/etc/d2b/zones//resources.json`; Zone daemon activates bundle and creates Volume resources; volume-local controller reconciles", + "removalProof" : "`nixos-modules/storage-json.nix` and `nixos-modules/store.nix` per-VM rows retired only after Volume resources replace every path row and all consumers complete bundle-format migration", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "All Nix eval-time validation rules; `contentId` determinism; credential-ref guard; unknown Provider config key → build fail", + "workItemId" : "ADR046-vl-010" + }, + { + "currentSource" : "`packages/xtask/src/main.rs` (`gen-schemas`, workspace-policy checks); `tests/unit/gates/drift-check.sh`", + "dataMigration" : "Not applicable", + "dependencyOwner" : "ADR046-vl-002; ADR046-pstate-011; workspace policy owner", + "destination" : "`packages/xtask/src/provider_crate_policy.rs`; `tests/unit/gates/provider-crate-layout-check.sh`", + "detailedDesign" : "`cargo xtask check-provider-crate-layout` gate asserts `src/`, `tests/`, `integration/` (with at least one `.rs` file and a `README.md`), and `README.md` for every `packages/d2b-provider-*` workspace member; fails closed with typed `missing-provider-crate-path` error Primary reuse disposition: `adapt`. Preserved source-plan detail: extend (per ADR046-pstate-011).", + "integration" : "`make test-policy` runs the gate; GitHub CI runs `make test-policy` on every PR", + "removalProof" : "Not applicable (permanent gate)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "Gate detects each missing path; idempotent across re-runs; existing non-provider `d2b-*` crates not flagged", + "workItemId" : "ADR046-vl-011" + }, + { + "currentSource" : "`d2b-priv-broker/src/ops/{state_dir,storage_contract,swtpm_dir,store_sync,store_view_posture}.rs`; `d2b-host/src/hardlink_farm.rs`", + "dataMigration" : "None (adapter replaces direct broker-op call sites)", + "dependencyOwner" : "ADR046-vl-001; ADR046-vl-002; Zone broker/core owner", + "destination" : "`packages/d2b-host/src/volume_effect_adapter.rs` (or the equivalent host-runtime crate designated by the Zone broker owner), implementing the `VolumeEffectPort` trait defined in `d2b-contracts`; planned `d2b-priv-broker/src/ops/rotate_sealing_key.rs` closed operation", + "detailedDesign" : "Adapter holds trusted FD table keyed by `VolumeId`; resolves `SourcePolicyId` to host path prefix from private bundle; calls `openat2(RESOLVE_BENEATH)` anchored at retained FD for all FS ops; calls `setfacl`/`acl_set_fd`, `mount`/`umount`, `fallocate` from within adapter only; authorizes `volume.rotate-sealing-key`, verifies committed proof, recomputes the canonical idempotency key, and maps `rotate_sealing_key` one-to-one to the closed broker `RotateSealingKey` operation. Broker independently resolves opaque `VolumeId`/`SealingPolicyId`, checks policy/generation/preconditions, performs journaled atomic rewrap and roll-forward recovery, and durably emits exactly one success audit before returning. Neither boundary accepts key bytes, credential bytes, key handles, or paths. Other blocking filesystem calls run in the bounded blocking-thread pool Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt into adapter.", + "integration" : "Zone runtime creates adapter with required FD table and bundle reference at provider startup; passes `Arc` to controller via ComponentSession bootstrap", + "removalProof" : "Baseline broker op handlers (`state_dir.rs`, `storage_contract.rs`, `swtpm_dir.rs`, `store_sync.rs`, `store_view_posture.rs`) retired only after Volume controller parity is confirmed and all callers are on the adapter", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "Adapter hermetic tests: each effect op called with mock FD table and bundle; rotation authorization, policy binding, all generation/revision preconditions, canonical idempotency vectors, byte-identical duplicate/retry, different-payload conflict, typed retry classification, and no key/path/handle in wire/Debug/error/audit; broker crash injection at every journal boundary with old-or-target visibility, roll-forward, and exactly-once success audit; anchored-path rejection for RESOLVE_BENEATH violations; `cargo deny check` verifies adapter exposes neither raw paths nor broker implementation to Provider crate; `integration/{provision,sealing}.rs` exercise full adapter paths", + "workItemId" : "ADR046-vl-012" + }, + { + "currentSource" : "No equivalent; new", + "dataMigration" : "None; pre-existing baseline `StorageRoot` rows for the volume-local controller are superseded on v3 reset", + "dependencyOwner" : "ADR046-vl-001; ADR046-vl-012; Zone broker/core owner", + "destination" : "Zone core ProviderDeployment controller-start path (outside `d2b-provider-volume-local`)", + "detailedDesign" : "The volume-local controller declares no Provider state Volume, so there is no bootstrap Volume, no `BootstrapProviderStateVolume` broker op, no pre-provisioned controller Volume, and no bootstrap-storage exception (D086, superseded by D087). On first install and on every daemon restart, core ProviderDeployment starts the volume-local controller Process directly; the controller reaches `Ready` from its own resource `status`, the core Operation ledger, and a resource-store relist. Once Ready, it reconciles every Volume carrying `providerRef: Provider/volume-local` (operator-created Volumes and other Providers' declared state Volumes) as they appear in its `providerRef` watch, re-verifying identity markers against external reality, never creating them itself. A Guest bootstraps its own Guest-local volume-local instance from Guest-local primitives only.", + "integration" : "Core ProviderDeployment spawns the controller Process with no state-Volume prerequisite; the controller's startup relist reconciles served Volumes and re-verifies markers", + "removalProof" : "Not applicable (new)", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-local", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-local.md", + "validation" : "`integration/provider_state.rs`: controller starts and reaches Ready with no state Volume; served Volumes reconciled and markers re-verified after restart; no bootstrap Volume and no bootstrap Provider Process in the resource list", + "workItemId" : "ADR046-vl-013" + }, + { + "currentSource" : "`packages/d2b-core/src/storage.rs`, `sync.rs`; `nixos-modules/storage-json.nix`", + "dataMigration" : "Full d2b 3.0 reset; storage.json rows migrated per table above", + "dependencyOwner" : "W0 shared contract root; `d2b-contracts`", + "destination" : "`packages/d2b-contracts/src/v3/volume.rs`, `volume_layout.rs`, `volume_attachment.rs`", + "detailedDesign" : "Complete Volume ResourceSpec, LayoutEntry, all policy enums (values preserved from baseline), AclGrant, ViewSpec, AttachmentSpec, quota placeholder, strict serde unknown-field rejection, canonicalization, bounds Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "Provider dossiers/controller descriptors bind exact types; Nix resource compiler emits canonical JSON", + "removalProof" : "`d2b-core/src/storage.rs` and `sync.rs` removed only after all Volume-successor consumers are live", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-volume", + "specPath" : "docs/specs/ADR-046-resources-volume.md", + "validation" : "Golden JSON spec vectors; serde unknown-field; path anchor/depth/traversal validators; ACL grant bounds; policy enum coverage", + "workItemId" : "ADR046-volume-001" + }, + { + "currentSource" : "`nixos-modules/storage-json.nix`, `nixos-modules/store.nix`, `packages/d2b-priv-broker/src/ops/swtpm_dir.rs`, `packages/d2b-priv-broker/src/ops/storage_contract.rs` (`reconcile_storage_scope`, `validate_lock_spec`), `packages/d2b-priv-broker/src/ops/store_sync.rs` (`run_store_sync`, `run_store_sync_repair`, `cleanup_store_view`, `prune_gcroots`), `packages/d2b-priv-broker/src/ops/store_sync_audit.rs`, `packages/d2b-priv-broker/src/ops/store_sync_export.rs`, `packages/d2b-priv-broker/src/ops/store_view_posture.rs` (`posture_store_view_matrix_paths`, `plant_live_marker_with_matrix_posture`), `packages/d2b-priv-broker/src/ops/state_dir.rs` (`PrepareStateDir`, `PrepareRuntimeDir`), `packages/d2b-host/src/hardlink_farm.rs` (`build_store_view`, `GenerationMarker`, `gcroots_dir`, `state_dir`), `packages/d2b-core/src/storage_lifecycle.rs` (`StorageLifecycleReport`, `StorageLifecycleIssue`), `packages/d2b-contract-tests/tests/storage_sync_contracts.rs`, `packages/d2bd/src/ownership_preflight.rs`", + "dataMigration" : "Per-VM `storage.json` rows (scoped `\"vm:\"` → `ownerRef: Guest/`) replaced by Volume resources generated by Nix resource compiler; broker continues to own path operations", + "dependencyOwner" : "ADR046-volume-001; volume-local Provider owner", + "destination" : "`packages/d2b-provider-volume-local/src/` (layout engine, store_view.rs, swtpm_volume.rs, broker op adapters); `packages/d2b-provider-volume-local/tests/` (hermetic layout/store-view/swtpm tests); `packages/d2b-provider-volume-local/integration/` (container Host-path and store-view FS fixtures); `packages/d2b-provider-volume-local/README.md`", + "detailedDesign" : "volume-local controller: layout engine (provision/repair/cleanup/adopt per policy), store-view mode (hardlink farm from `hardlink_farm.rs`, private-NS sync, zero-length marker, `gcroots/` and `state/` at store-view root, `sync.lock` OFD), swtpm volume hardening (provisionIfNeverProvisioned + marker + fail-closed repair as in `swtpm_dir.rs`), path-free broker audit ops, storage lifecycle report, opaque BundleOpId contract preserved from `storage_contract.rs` Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "volume-local controller registered under Host/system-core; resource status written via ResourceClient", + "removalProof" : "`nixos-modules/storage-json.nix` removed only after Volume resources replace all path rows and all consumers verified", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-volume", + "specPath" : "docs/specs/ADR-046-resources-volume.md", + "validation" : "`tests/unit/nix/cases/per-vm-state-ownership.nix` adapted to Volume LayoutEntry matrix; `tests/unit/smoke/smoke-eval-tpm.nix` migrated to TPM Volume invariant; `d2b-contract-tests/tests/storage_sync_contracts.rs` parity tests adapted; new: store-view same-filesystem, zero-length marker existence, sync.lock preserve-OFD, gcroots at store-view root (not meta/), state/ dir existence, swtpm fail-closed-on-missing-after-provision, anchored-path validators", + "workItemId" : "ADR046-volume-002" + }, + { + "currentSource" : "`packages/d2b-host/src/virtiofsd_argv.rs` (`VirtiofsdArgvInput`, `generate_virtiofsd_argv`), `nixos-modules/minijail-profiles.nix` (virtiofsdProfiles; principals `d2b--runner`, `d2b--gctlfs`), `nixos-modules/processes-json.nix` (virtiofsdRunner shape; `roStoreSharedDir` sentinel), `packages/d2b-core/src/processes.rs` (`ProcessRole::Virtiofsd`, `VmProcessDag`; the virtiofsd dag node is a `ProcessRole::Virtiofsd` entry in a WorkloadId-keyed `VmProcessDag`), `packages/d2b-priv-broker/src/ops/spawn_runner.rs` (`SpawnRunnerPlan` for virtiofsd; current `SpawnRunnerPlanInput` carries `adr_carve_out` for virtiofsd swtpm path), `packages/d2b-priv-broker/src/sys.rs` (clone3/user-NS pre-establishment), ADR 0021", + "dataMigration" : "Current `processes-json.nix` virtiofsd `VmProcessDag` nodes (keyed by `WorkloadId` = current VM name, role `ProcessRole::Virtiofsd`) replaced by virtiofsd Process resources owned by Volume", + "dependencyOwner" : "ADR046-volume-001; volume-virtiofs Provider owner", + "destination" : "`packages/d2b-provider-volume-virtiofs/src/` (controller, virtiofsd_argv.rs); `packages/d2b-provider-volume-virtiofs/tests/` (hermetic argv/lifecycle/ADR-0021 tests); `packages/d2b-provider-volume-virtiofs/integration/` (virtiofsd launch and guest-mount fixtures); `packages/d2b-provider-volume-virtiofs/README.md`", + "detailedDesign" : "volume-virtiofs controller: attachment lifecycle, owned virtiofsd Process create/update/delete, argv generation (reuse current 14 tests), ADR 0021 invariant (`capabilityClasses: []`, `startRoot: false`, `sandbox: chroot`, user-NS via `userNamespace.mappingClass: process-principal-root`), per-attachment export socket readiness check (`unix-socket-exists` readiness kind; current v2 socket path: `/run/d2b/vms//-virtiofs-.sock`; v3: stable hash-derived private path under Zone runtime directory, never exposed in spec/status/API), guest-mount status observation, finalizer drain Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "volume-virtiofs registered under Host; virtiofsd Process resource owned by Volume; guest-control health integration for mount readiness", + "removalProof" : "`nixos-modules/processes-json.nix` virtiofsdRunner block removed only after virtiofsd Process resources pass parity; `packages/d2bd/src/supervisor/dag.rs` `ProcessRole::Virtiofsd` path removed after controller lifecycle covers all cases", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-volume", + "specPath" : "docs/specs/ADR-046-resources-volume.md", + "validation" : "Migrated `virtiofsd_argv` unit tests (14 tests); `tests/tools/gen-migration-ledger.sh` virtiofsd-argv-shape gate adapted; `minijail-validator-virtiofsd` gate adapted to Process sandbox spec; new: attachment lifecycle (create/ready/delete), ADR 0021 invariant rejection test, multi-attachment isolation, readOnly flag per access mode, store-view shared-dir = store-view/live (never /nix/store)", + "workItemId" : "ADR046-volume-003" + }, + { + "currentSource" : "`nixos-modules/storage-json.nix`, `nixos-modules/store.nix`, `nixos-modules/options-vms.nix` (`d2b.vms..*` — current VM Nix option namespace; virtiofs shares and TPM enable are configured here), `nixos-modules/options-realms-workloads.nix` (`d2b.realms..stateDir` — current realm workload state root), `packages/d2b-realm-core/src/workload.rs` (`WorkloadProviderKind::LocalVm`/`QemuMedia`/`UnsafeLocal` — informs which WorkloadIds need store-view Volumes vs. no Volume)", + "dataMigration" : "`d2b.vms..shares` (virtiofs entries) → Volume attachments; `d2b.vms..tpm.enable` → swtpm Volume", + "dependencyOwner" : "ADR046-volume-001; Nix integrator", + "destination" : "`nixos-modules/resources-volume.nix`, `nixos-modules/options-volumes.nix`", + "detailedDesign" : "Nix resource compiler for Volume/LayoutEntry/View/Attachment from d2b.zones config; strict schema validation; emit canonical JSON per Volume; generate store-view Volume per Guest (from current `d2b.vms.` → future flat `d2b.zones..resources.` with `type = \"Guest\"`) with hardlink-farm layout (gcroots/, state/ at root per `hardlink_farm.rs`); generate swtpm Volume for TPM-enabled Guests; emit volume-virtiofs attachment spec per virtiofs share; migration: store-view stateDir root configuration", + "integration" : "`nixos-modules/default.nix` wires resources-volume.nix; Nix evaluation tests verify canonical output", + "removalProof" : "Old `storage-json.nix` and `store.nix` emitters removed only after rendered Volume JSON passes all drift-check gates", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-volume", + "specPath" : "docs/specs/ADR-046-resources-volume.md", + "validation" : "nix-unit cases for store-view Volume output (gcroots at root), TPM Volume spec, virtiofs attachment spec, anchored-path rejection; render parity with current storage.json path rows; canonical JSON golden vector; Provider schema validation rejection; symlink target validation; bundle digest coverage", + "workItemId" : "ADR046-volume-004" + }, + { + "currentSource" : "N/A (no baseline evidence for block-image, quota enforcement, snapshots, or tmpfs Volume paths)", + "dataMigration" : "Per-sub-item; block-image and tmpfs are new capabilities with no legacy migration required", + "dependencyOwner" : "ADR046-volume-002, ADR046-volume-003; respective Provider owners", + "destination" : "`packages/d2b-provider-volume-local/src/` (block-image, quota, snapshots, tmpfs, ACL reconciliation); `packages/d2b-provider-volume-local/tests/` (hermetic quota/tmpfs/ACL/block-image/snapshot tests); `packages/d2b-provider-volume-local/integration/` (block-image virtio-blk, FS-without-quota fixture, tmpfs memory-budget); `packages/d2b-provider-volume-virtiofs/src/` (single-writer enforcement, shared-write, private socket path contract); `packages/d2b-provider-volume-virtiofs/tests/` (single-writer rejection, shared-write capability gate, socket-path invariant); `packages/d2b-provider-volume-virtiofs/integration/` (shared-write cross-Guest fixture)", + "detailedDesign" : "(1) **block-image SourceKind**: add `SourceKind::BlockImage` to volume-local; manage raw/qcow2 image file lifecycle; emit virtio-blk attachment spec consumed by Guest Provider; `quota.maxBytes` required; add store-overlay.img migration path for current `DiskInit` plan-op. (2) **Quota hard enforcement**: implement `enforcement: hard` capability check in volume-local at Volume creation time; query backing FS for quota/limits support; test with no-project-quota fixture; enforce `maxBytes`/`maxInodes` via xfs project quota or ext4 per-dir quota where available. (3) **Volume snapshots/migrations**: design and implement EphemeralProcess templates in volume-local catalog for snapshot (copy-on-write or rsync capture) and content migration (atomic rename + sync); no CLI-only path; all operations surface through resource API. (4) **Single-writer enforcement**: volume-virtiofs controller tracks active `read-write` attachment count; rejects a second `read-write` attachment while one is active (returns `ResourceConflict` error); `shared-write` mode accepted only if Provider declares `supportsSharedWrite: true`. (5) **tmpfs source**: implement tmpfs mount/unmount lifecycle in volume-local; `maxBytes` → `size=`, `maxInodes` → `nr_inodes=` mount options; charge memory against Host/Guest budget; cleanup unmounts on Volume deletion or restart. (6) **Bounds enforcement**: enforce max 1024 layout entries, 64 Views, 64 attachments at schema validation layer; add corresponding row to API request-size limit table in `ADR-046-resource-api-and-authorization`. (7) **File/symlink first-class lifecycle**: implement independent `createPolicy`/`repairPolicy`/`cleanupPolicy` for `file` and `symlink` entries; implement `target` field validation (relative, no `..`, must resolve within Volume root); `symlink` create writes the target link. (8) **ACL principal ResourceRef**: remove bare `{type,ref}` struct from AclGrant; implement `User/` ResourceRef resolution with User resource watch and re-reconcile on User revision change. (9) **Continuous ACL reconciliation**: implement `foreignChildPolicy: preserve|fail` in broker reconcile loop; re-apply `accessAcl`/`defaultAcl` to all existing entries and children on every repair cycle; emit `ForeignAclViolation` condition when `foreignChildPolicy: fail` and unexpected entries found. (10) **virtiofsd socket path contract**: implement stable hash-derived private socket path in volume-virtiofs (deterministic hash of Zone name + Volume name + attachment executionRef); assert path never appears in public status, spec, audit, or CLI output; validate with a dedicated security invariant test.", + "integration" : "Each sub-item produces a focused spec amendment; resolved decisions already reflected in spec revision 2", + "removalProof" : "None — net-new capabilities; no prior owner to remove", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-volume", + "specPath" : "docs/specs/ADR-046-resources-volume.md", + "validation" : "(1) `VirtioblkArgvInput` unit tests; block-image integration fixture. (2) Quota-enforcement fixture with FS-without-quota; hard-enforcement failure test. (3) EphemeralProcess snapshot lifecycle test; content-migration parity test. (4) Single-writer rejection test; shared-write capability gate test. (5) tmpfs mount/unmount lifecycle test; memory-budget accounting assertion. (6) Schema bound rejection tests (1025 entries, 65 views, 65 attachments). (7) File/symlink independent lifecycle tests; target validation (absolute rejected, `..` rejected, escape rejected). (8) ACL principal ResourceRef validation; numeric form rejected; User revision trigger test. (9) foreignChildPolicy preserve/fail tests; continuous repair cycle test. (10) Socket path invariant test; no-status-leak assertion.", + "workItemId" : "ADR046-volume-005" + }, + { + "currentSource" : "`nixos-modules/storage-json.nix` (current Nix eval-time validation via `lib.asserts`; schema checked by `d2b-contract-tests/tests/storage_sync_contracts.rs`); `packages/d2bd/src/` (current config activation / host-prepare dispatch)", + "dataMigration" : "Full d2b 3.0 reset; no partial import of prior generation state", + "dependencyOwner" : "ADR046-volume-001, ADR046-volume-004; Nix integrator and core-controller config-publication owner", + "destination" : "`nixos-modules/resources-volume.nix` (Nix eval-time schema validation, canonical JSON emission, bundle digest); `packages/d2b-core-controller/src/configuration.rs` (config-publication handler cleanup logic); `packages/d2b-contracts/src/v3/zone_bundle.rs` (bundle index schema)", + "detailedDesign" : "**Nix eval/build validation**: implement all 15 validation steps in §Nix eval/build validation as Nix assertions; abort build on any failure with structured error (Volume name + field path + error class); provider-specific settings schema (`root-config.schema.json`, `attachment.schema.json`) read from the private artifact catalog entry for each Provider's `artifactId` by the resource compiler; validate against `lib.evalModules`-compatible schema; emit canonical sorted JSON with all defaults materialized; compute SHA-256 per resource; emit Zone resource bundle with `generationId` and `bundleDigest`. **Config-publication handler cleanup**: on new bundle activation, diff resources with `metadata.managedBy = \"configuration\"` between new and prior bundle; issue async Delete for resources absent from new bundle; mark deleted resources with `ConfigurationRemoved` condition; track pending-cleanup set in Zone status; Zone status is `Degraded/PendingCleanup` while prior-generation deletions are in progress; activation is immediate but Zone readiness reflects cleanup completion. **Config-owned vs controller-created distinction**: `metadata.managedBy = \"configuration\"` is the authoritative marker set by core at activation; the bundle index carries only digests; controller-created resources have `metadata.managedBy = \"controller\"` and are never touched by the configuration cleanup pass. **Prior generation retention**: retain `priorGenerationCount` prior generations (default 3, range 1..16); no time-based TTL; when count is exceeded prune oldest generation from the store with a tamper-evident audit record. **Generation reactivation**: re-activate any retained prior bundle via `ActivateGeneration` operation; cancel in-flight Deletes for resources being reinstated; issue Deletes for resources added by the aborted new generation.", + "integration" : "`nixos-modules/default.nix` wires resources-volume.nix; `d2b-core-controller` config-publication handler consumes the bundle; all Volume controllers observe `ConfigurationRemoved` condition and respond to finalizer triggers", + "removalProof" : "Old `storage-json.nix` schema assertions removed only after Nix eval-time Volume validation covers all prior `lib.asserts` paths; old config-activation code in `d2bd` removed after config-publication handler is live", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-volume", + "specPath" : "docs/specs/ADR-046-resources-volume.md", + "validation" : "Tests per §Cleanup contract — Tests for removed-resource cleanup table (10 tests); nix-unit: `volume_canonical_json_golden_vector`, `volume_bundle_digest_covers_all_resources`, `provider_schema_validation_rejects_unknown_fields`, `symlink_target_escape_rejected_at_eval`, `tmpfs_without_quota_rejected_at_eval`, `layout_bounds_1025_entries_rejected`, `attachment_bounds_65_rejected`, `conflicting_host_paths_rejected`; integration: cleanup audit redaction, generation reactivation, prior-generation pruning", + "workItemId" : "ADR046-volume-006" + }, + { + "currentSource" : "Evidence class `test-only-or-preview`; baseline has no generic vsock transport Provider or opaque endpoint/binding ID trait.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Title: Implement `VsockEffectPort` trait and `OpaqueEndpointId`/`OpaqueBindingId` newtypes; Phase 1; Priority P0; Depends on ADR046-bus-001 (OwnedTransport in d2b-session); Owner crate `d2b-provider-transport-vsock`.", + "destination" : "`packages/d2b-provider-transport-vsock/src/effect_port.rs`; test fake in `tests/effect_port_mock.rs`; redaction checks in `tests/redaction.rs`.", + "detailedDesign" : "Implement `VsockEffectPort` trait and `OpaqueEndpointId`/`OpaqueBindingId` newtypes. Define `VsockEffectPort` async trait and opaque ID newtypes in `effect_port.rs`; implement `FakeVsockEffectPort` for tests; `redaction.rs` asserts no raw `u32` in any `Debug`/`Display` output of opaque types; no real vsock socket opened. Primary reuse disposition: `create`. Preserved source-plan detail: net-new trait/newtypes with redaction tests; no real vsock socket opened.", + "integration" : "Child Zone's core ZoneLink/delegation controller calls its same-Zone Provider with opaque IDs; Provider calls injected `VsockEffectPort`; live AF_VSOCK child-endpoint resolution remains in child core runtime while the selected parent retains only sealed peer/route state.", + "removalProof" : "None — net-new; no prior owner to remove.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-vsock", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-vsock.md", + "validation" : "Proof type: hermetic unit + redaction test; `tests/effect_port_mock.rs` and `tests/redaction.rs`.", + "workItemId" : "ADR046-vsock-001" + }, + { + "currentSource" : "Evidence class `implemented-but-unwired`; main commit `a1cc0b2da4a08ca3240a770a972fe4da6f912bef` `packages/d2b-session-unix/src/vsock.rs` contains `FramedVsockTransport` framing behavior, not current v3 baseline behavior.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Title: Implement framing utilities and bridge task in Provider crate; Phase 1; Priority P0; Depends on ADR046-vsock-001; Owner crate `d2b-provider-transport-vsock`.", + "destination" : "`packages/d2b-provider-transport-vsock/src/framing.rs` and `src/bridge.rs`; tests in `packages/d2b-provider-transport-vsock/tests/framing.rs`.", + "detailedDesign" : "Implement framing utilities and bridge task in Provider crate. Copy `FramedVsockTransport` framing-only code (length-prefix encode/decode, bounded allocation, EOF/reset) from main `a1cc0b2d` → `framing.rs`; implement bridge task pumping bytes between an opaque `AsyncRead+AsyncWrite` stream from `VsockEffectPort::open` and the named ComponentSession stream; hermetic tests using `FakeVsockEffectPort` (no real socket). Primary reuse disposition: `adapt`. Preserved source-plan detail: copy/adapt framing-only code; exclude raw AF_VSOCK socket calls and ADR 0045 endpoint-role assumptions.", + "integration" : "OpenTransport creates a framed opaque stream, bridge task pumps to a named ComponentSession stream, and d2b-bus consumes it as an `OwnedTransport` without FD attachment support.", + "removalProof" : "None for framing; raw socket portions from the source are deliberately not copied into the Provider crate.", + "reuseAction" : "adapt", + "reuseSource" : "Main `a1cc0b2d` `packages/d2b-session-unix/src/vsock.rs` framing-only code and `packages/d2b-session-unix/tests/unix_session.rs` vsock framing subset.", + "specId" : "ADR-046-provider-transport-vsock", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-vsock.md", + "validation" : "Proof type: hermetic framing tests; `tests/framing.rs` covers partial/coalesced records, oversized frames, EOF/reset classification, and no real socket.", + "workItemId" : "ADR046-vsock-002" + }, + { + "currentSource" : "Evidence class `test-only-or-preview`; no current v3 generic `VsockTransportService` implementation exists.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Title: Implement `VsockTransportService` (OpenTransport/CloseTransport/ObserveTransport); Phase 1; Priority P0; Depends on ADR046-vsock-002 and ADR046-bus-001; Owner crate `d2b-provider-transport-vsock`.", + "destination" : "`packages/d2b-provider-transport-vsock/src/service.rs`; tests `tests/open_close.rs`, `tests/observe.rs`, and conformance kit.", + "detailedDesign" : "Implement `VsockTransportService` (OpenTransport/CloseTransport/ObserveTransport). Child core resolves the exact six-field ZoneLink spec, validates `transportSettings` through the Provider selected by `transportProviderRef`, rejects non-empty `transportCredentials`, and passes only derived opaque IDs/deadline to `service.rs`; `open_close.rs`, `observe.rs`, and topology tests cover the service and reject legacy ZoneLink provider/fingerprint/capability fields; conformance kit passes. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new service implementation over ComponentSession and fake effect port tests.", + "integration" : "Child Zone's core ZoneLink/delegation controller is the only authorized caller; service opens named stream handles for child d2b-bus, releases them on close, and streams transport events for observe; no parent-side Provider or handler exists.", + "removalProof" : "None — net-new; no prior owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-vsock", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-vsock.md", + "validation" : "Proof type: service round-trip plus exact-shape tests; `tests/open_close.rs`, `tests/observe.rs`, `tests/topology.rs::{canonical_zonelink_spec_fields_are_exact,legacy_zonelink_provider_fields_are_rejected,transport_credentials_must_be_empty}`, and provider conformance tests.", + "workItemId" : "ADR046-vsock-003" + }, + { + "currentSource" : "Evidence class `ADR-only`; baseline has guest-control and relay vsock paths, but no allocator-backed `LiveVsockEffectPort` for ZoneLink transport.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import.", + "dependencyOwner" : "Title: Implement `LiveVsockEffectPort` in child Zone runtime; Phase 2; Priority P0; Depends on ADR046-vsock-001 and the Zone allocator (`ADR-046-resources-zone-control`); Owner crate `d2b-core-controller`.", + "destination" : "`d2b-core-controller` child Zone runtime `LiveVsockEffectPort`; child-local Provider receives it by dependency injection at startup.", + "detailedDesign" : "Implement `LiveVsockEffectPort` in the child Zone runtime. It consumes the child's sealed binding for the allocator selected by compiler-only `parentZone`, resolves opaque endpoint/binding IDs only inside the effect adapter, opens the child AF_VSOCK endpoint, and injects an opaque stream into the same-Zone Provider service; the selected parent keeps its peer binding and port registry only as sealed allocator/route state; no raw CID/port is exposed to the Provider and no FD or ResourceRef crosses Zones. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new core adapter; keep raw AF_VSOCK syscalls outside Provider crate.", + "integration" : "Selected parent allocator issues a sealed endpoint/binding allocation without creating resources; child core resolves its local side, opens/accepts the AF_VSOCK endpoint, returns an opaque stream to its Provider service, and excludes reserved ports.", + "removalProof" : "None — net-new core adapter; no prior owner to remove.", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-vsock", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-vsock.md", + "validation" : "Proof type: integration test; `integration/host_guest.rs` exercises live open/close byte round-trip with the injected effect and proves the selected parent has only sealed allocator/route state, with no parent-store Provider/ZoneLink row.", + "workItemId" : "ADR046-vsock-004" + }, + { + "currentSource" : "Evidence class `test-only-or-preview`; no operator-authored v3 state Volume exists for transport-vsock in baseline.", + "dataMigration" : "Full d2b 3.0 reset; no v2 state/config import; state Volume is created fresh with `migrationPolicy: none`.", + "dependencyOwner" : "Title: Child core ProviderDeployment creates/deletes service component state Volume; Phase 1; Priority P0; Depends on the volume-local Provider (`ADR-046-provider-volume-local`); Owner crate `d2b-provider-transport-vsock`.", + "destination" : "ProviderDeployment Volume creation/deletion path plus `packages/d2b-provider-transport-vsock/tests/state_volume.rs`.", + "detailedDesign" : "Child core ProviderDeployment creates/deletes the service component state Volume in the same child Zone. It creates `Volume/transport-vsock--service--empty-state--*` before the component Process and deletes it after the Process finalizer; the transport-vsock service does not own Volume, add Volume to exported ResourceTypes, or create its prerequisite; Volume spec: empty schema, `kind: state`, `persistenceClass: persistent`, `migrationPolicy: none`, `User/d2b-transport-vsock` owner, minimal nonzero `quota.maxBytes`/`quota.maxInodes` with `enforcement: hard`, `private` sensitivity, `broker-maintained` identity marker; `state_volume.rs` tests the canonical schema; installation/removal tests verify marker lifecycle; no operator-authored or parent-store Volume exists; component receives only its child-local dirfd view. Primary reuse disposition: `create`. Preserved source-plan detail: net-new ProviderDeployment/Volume integration and tests.", + "integration" : "Child core ProviderDeployment creates the Volume before Process, volume-local reconciles it, the Provider process receives only a child-local dirfd view, and Provider deletion removes Process before Volume/identity marker; parent state remains allocator/route-only.", + "removalProof" : "Remove the state Volume and its broker-maintained identity marker during Provider deletion; no operator-authored Volume remains.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-vsock", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-vsock.md", + "validation" : "Proof type: unit + integration test; `tests/state_volume.rs` and Provider install/remove integration tests verify schema, user refs, marker lifecycle, and no ComponentPrincipal.", + "workItemId" : "ADR046-vsock-005" + }, + { + "currentSource" : "Evidence class `test-only-or-preview`; existing guest-control compile proof and socat relay tests are not full ZoneLink transport coverage.", + "dataMigration" : "None — docs/tooling only; no runtime state.", + "dependencyOwner" : "Title: Integration test: real vsock socketpair + full ZoneLink open/close; Phase 2; Priority P1; Depends on ADR046-vsock-003 and ADR046-vsock-004; Owner crate `d2b-provider-transport-vsock`.", + "destination" : "`packages/d2b-provider-transport-vsock/integration/host_guest.rs` and `integration/no_fd_transfer.rs`.", + "detailedDesign" : "Integration test: fixture declares compiler-only `k1.parentZone = \"local-root\"`, puts the selected Provider and an exact six-field ZoneLink (`transportProviderRef`, validated `transportSettings`, empty `transportCredentials`, `disabled`, and `limits`, plus self-matching `childZoneName`) only in K1, and gives local-root only sealed allocator/route state; `integration/host_guest.rs` opens a real Linux vsock path through K1's `LiveVsockEffectPort`, then exercises `OpenTransport` + byte round-trip + `CloseTransport` and validates bridge throughput ≥ 512 MiB/s; `no_fd_transfer.rs` structurally rejects attachment packets and asserts no FD/ResourceRef crosses the Zone boundary. Primary reuse disposition: `create`. Preserved source-plan detail: net-new integration coverage with no FD transfer over vsock.", + "integration" : "Test drives K1-local Provider service, child `LiveVsockEffectPort`, d2b-bus `OwnedTransport`, byte bridge, close path, absent local-root reciprocal resource row, and attachment rejection across the integration lane.", + "removalProof" : "None — test coverage net-new; old duplicate vsock tests are retired only after successor assertions migrate.", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-vsock", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-vsock.md", + "validation" : "Proof type: integration test; `make test-integration` runs `host_guest.rs` and `no_fd_transfer.rs`.", + "workItemId" : "ADR046-vsock-006" + }, + { + "currentSource" : "Evidence class `implemented-and-reachable`; legacy sources are `packages/d2b-host/src/vsock_relay_argv.rs` socat OTLP relay and `packages/d2bd/src/guest_control_vsock.rs` CONNECT-proxy guest-control path.", + "dataMigration" : "Full d2b 3.0 reset; no v2 relay or guest-control state/config import.", + "dependencyOwner" : "Title: Delete legacy socat OTLP relay and CONNECT-proxy guest-control vsock; Phase 3; Priority P2; Depends on the observability-otel Provider (`ADR-046-provider-observability-otel`) and the Guest resource lifecycle (`ADR-046-resources-host-guest-process-user`); Owner crates `d2b-host`, `d2bd`.", + "destination" : "Remove legacy paths from `d2b-host` and `d2bd`; replacement lives in `observability-otel` Provider native vsock relay and Guest resource lifecycle/bootstrap.", + "detailedDesign" : "Delete legacy socat OTLP relay and CONNECT-proxy guest-control vsock. Remove `vsock_relay_argv.rs` socat path after `observability-otel` Provider native vsock relay passes parity; remove `guest_control_vsock.rs` CONNECT-proxy after Guest resource lifecycle + guestd vsock bootstrap reach parity; no raw CID or socat vsock path remains. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete after replacement parity; preserve reserved guest-control/OTLP port exclusions until replacements own them.", + "integration" : "Observability-otel owns OTLP vsock relay replacement; Guest lifecycle owns guest-control bootstrap replacement; transport-vsock ZoneLink allocator excludes ports 14317, 14318, and 14319.", + "removalProof" : "Delete `packages/d2b-host/src/vsock_relay_argv.rs` socat path after observability parity, delete `packages/d2bd/src/guest_control_vsock.rs` CONNECT-proxy after Guest lifecycle parity, and prove no raw CID or socat vsock path remains.", + "reuseAction" : "delete-after-cutover", + "reuseSource" : null, + "specId" : "ADR-046-provider-transport-vsock", + "specPath" : "docs/specs/providers/ADR-046-provider-transport-vsock.md", + "validation" : "Proof type: deletion + parity test; parity tests for observability-otel and Guest lifecycle plus redaction checks that no raw CID/socat vsock path remains.", + "workItemId" : "ADR046-vsock-007" + }, + { + "currentSource" : "`packages/d2b-host/src/virtiofsd_argv.rs` (VirtiofsdArgvInput, generate_virtiofsd_argv, 14 unit tests, golden argv.txt); `packages/d2b-host/src/lib.rs` (module declaration)", + "dataMigration" : "v3.0 reset; socket path format changes", + "dependencyOwner" : "ADR046-volume-001 (Volume contract types); ADR046-vvfs-export-001 (Export type); W1; volume-virtiofs Provider owner", + "destination" : "`packages/d2b-provider-volume-virtiofs/src/virtiofsd_argv.rs`; `packages/d2b-provider-volume-virtiofs/tests/argv_golden.rs`", + "detailedDesign" : "Create crate skeleton with mandatory `src/`, `tests/`, `integration/`, `README.md`. Extract `VirtiofsdArgvInput` and `generate_virtiofsd_argv` with these changes: (1) replace `extra_args: Vec` with nothing (removed); (2) replace `socket_path: String` with `socket_path: SocketPath` newtype backed by `socket_path.rs`; (3) add `shared_dir_fd: i32` replacing `shared_dir: String` (FD-based); (4) replace `socket_group: Option` with `socket_group: Option`. Implement `socket_path.rs`: private path using SHA-256 of `\\x00\\x00`, truncated 8 hex chars, formatted as `/vms//vol-.vfd.sock`. Assert path length ≤ 108 bytes. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "volume-virtiofs controller `export.rs` calls virtiofsd_argv.rs at spawn time; LaunchTicket carries resolved socket path as opaque sealed field", + "removalProof" : "`packages/d2b-host/src/virtiofsd_argv.rs` removed only after parity confirmed by argv-shape gate", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-virtiofs", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-virtiofs.md", + "validation" : "`tests/argv_golden.rs`: 14 migrated tests + `no_extra_args_ever_emitted`, `socket_path_is_not_in_args`, `shared_dir_is_fd_path`, `path_length_within_sunpath_limit`; `tests/socket_path_privacy.rs`: `socket_path_not_in_export_status`, `socket_path_not_in_volume_status`, `socket_path_not_in_audit_record`; `tests/schema_conformance.rs`: `process_spec_readiness_class_is_provider_defined`, `process_spec_readiness_has_no_kind_or_period_fields`, `process_spec_budget_cpu_request_limit_nested`, `process_spec_budget_memory_request_limit_nested`, `process_spec_budget_pids_limit_present`, `process_spec_budget_fds_limit_present`, `process_spec_sandbox_no_new_privileges_true`, `process_spec_sandbox_read_only_root_true`, `process_spec_no_host_uid_gid_in_spec`", + "workItemId" : "ADR046-vvfs-001" + }, + { + "currentSource" : "`packages/d2b-priv-broker/src/sys.rs` (`clone3_spawn_runner`, user-NS pre-establishment block); `packages/d2b-priv-broker/src/ops/spawn_runner.rs` (`SpawnRunnerPlanInput.user_namespace`, `RunnerIsolationSpec.user_namespace`); ADR 0021 implementation contract", + "dataMigration" : "v3.0 reset; current `adr_carve_out` field in `SpawnRunnerPlanInput` removed; ADR 0021 path is now the default", + "dependencyOwner" : "ADR046-vvfs-001; ADR046-volume-001; W1; broker/spawn owner", + "destination" : "`packages/d2b-provider-volume-virtiofs/src/user_ns.rs` (conformance kit); `packages/d2b-provider-volume-virtiofs/tests/adr021_invariant.rs`", + "detailedDesign" : "`user_ns.rs` contains only the conformance check and template descriptor assertion: verify that the virtiofsd-worker Process template declares `capabilityClasses: []`, `startRoot: false`, `noNewPrivileges: true`, `readOnlyRoot: true`, and `sandbox.userNamespace.mappingClass: process-principal-root`. `hostUid`/`hostGid` are NOT set by the controller — system-minijail resolves the mapping from the `User/vol--vfd` principal when building the LaunchTicket via the effect port. The conformance check rejects any template mutation that adds host capability classes, sets `startRoot: true`, disables `noNewPrivileges`, or disables `readOnlyRoot`. The user-NS pre-establishment code itself remains in `d2b-priv-broker/src/sys.rs` and is invoked via the system-minijail effect port. Primary reuse disposition: `extract`. Preserved source-plan detail: extract conformance kit only; pre-establishment code stays in broker.", + "integration" : "volume-virtiofs controller calls conformance check before emitting any Process Create; effect port adapter in system-minijail invokes broker spawn path", + "removalProof" : "`adr_carve_out` field and virtiofsd-specific branch in current `SpawnRunnerPlanInput` removed only after v3 LaunchTicket covers all virtiofsd spawn cases", + "reuseAction" : "extract", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-virtiofs", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-virtiofs.md", + "validation" : "`tests/adr021_invariant.rs`: `virtiofsd_capability_classes_must_be_empty`, `virtiofsd_start_root_must_be_false`, `virtiofsd_no_new_privileges_must_be_true`, `virtiofsd_read_only_root_must_be_true`, `process_spec_has_no_host_uid_gid`, `sandbox_namespace_never_emitted`, `user_ns_single_entry_single_uid_mapping`, `uid_map_write_ordering_uid_setgroups_gid`, `child_setuid_in_ns_not_host_uid`, `clone_newns_not_in_clone3_flags`, `child_exits_user_ns_sync_on_pipe_eof`", + "workItemId" : "ADR046-vvfs-002" + }, + { + "currentSource" : "`packages/d2bd/src/supervisor/dag.rs` (ProcessRole::Virtiofsd dag node); `nixos-modules/processes-json.nix` (virtiofsdRunner block; attachment-to-Process mapping)", + "dataMigration" : "Current `ProcessRole::Virtiofsd` dag nodes replaced by Export → Process resource lifecycle", + "dependencyOwner" : "ADR046-vvfs-001, ADR046-vvfs-002, ADR046-vvfs-export-001; ADR046-volume-001; W2; volume-virtiofs controller owner", + "destination" : "`packages/d2b-provider-volume-virtiofs/src/controller.rs`; `packages/d2b-provider-volume-virtiofs/src/export.rs`", + "detailedDesign" : "Implement volume-virtiofs-controller reconcile loop using toolkit ResourceClient. Watch selector: `virtiofs.d2bus.org.Export` resources (all in zone), owned Process resources, owned User resources, Volume resources (read-only for view/vcpu resolution), Guest resources (read-only for vcpu count). On `spec-generation-changed` for an Export: (1) resolve View from Volume; (2) check store-view marker if applicable; (3) resolve threadPoolSize from Guest vcpus; (4) ensure User/vol--vfd; (5) diff against current Process; (6) emit Create/UpdateSpec. On `owned-resource-changed` for a Process: update Export status. On `deletionRequestedAt` for Export: two-phase teardown (§6.2). Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "volume-virtiofs controller registered by core ProviderDeployment; receives owned-resource-changed trigger from Export; emits Process resources consumed by system-minijail", + "removalProof" : "`ProcessRole::Virtiofsd` branch in `d2bd/src/supervisor/dag.rs` removed only after v3 controller passes all lifecycle tests", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-virtiofs", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-virtiofs.md", + "validation" : "`tests/export_lifecycle.rs`: `export_create_spawns_virtiofsd_process`, `export_ready_when_socket_present`, `export_delete_terminates_virtiofsd`, `export_delete_waits_for_guest_mount_absent`, `export_delete_with_guest_unreachable_holds_finalizer_degraded`, `export_proof_of_ns_death_clears_finalizer`; `tests/multi_attachment.rs`: `two_guests_get_separate_exports_and_processes`, `process_failure_does_not_affect_sibling_export`; `tests/schema_conformance.rs`: `provider_state_set_volume_created_on_install`, `provider_state_set_volume_owner_ref_is_provider`, `provider_state_set_volume_layout_principal_is_user_not_component_principal`, `provider_state_set_no_cross_component_volume_sharing`", + "workItemId" : "ADR046-vvfs-003" + }, + { + "currentSource" : "`packages/d2bd/src/vm_readiness.rs` (`ReadinessKind::UnixSocketExists`); guest-control vsock health protocol", + "dataMigration" : "Current `UnixSocketExists` readiness kind adapted to FD-based path resolution", + "dependencyOwner" : "ADR046-vvfs-003; guest-control integration owner; W2", + "destination" : "`packages/d2b-provider-volume-virtiofs/src/readiness.rs`; `packages/d2b-provider-volume-virtiofs/integration/guest_mount_readiness/`", + "detailedDesign" : "`unix-socket-exists` readiness: check file existence at the private socket path via a bounded blocking adapter (e.g., `tokio::task::spawn_blocking` wrapping `fstatat(2)` relative to the zone runtime `OwnedFd`, or an async-safe fd-relative equivalent); no blocking syscall on the async executor thread. Probe period 1 s; timeout 30 s. On socket present → set `Export.status.exportReady: true`. Guest-mount readiness: send `VirtioFsMountReady?` probe to guest-control health endpoint over vsock. Response `MountReady` sets `guestMountReady: true`. Response `MountAbsent` or timeout sets `guestMountReady: false`. The vsock health probe is async-native. If Guest is down, set Export `phase: Unknown`. All readiness probes (unix-socket-exists, guest-mount health) use bounded blocking adapters or async-safe fd-relative equivalents; no blocking I/O on the reconcile executor thread. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt.", + "integration" : "`readiness.rs` called from `controller.rs` reconcile loop; uses toolkit health probe client", + "removalProof" : "Current `UnixSocketExists` readiness path in `d2bd` retired after volume-virtiofs readiness covers all cases", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-virtiofs", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-virtiofs.md", + "validation" : "`tests/export_lifecycle.rs` (extended); `integration/guest_mount_readiness/`: virtiofsd launches, socket appears, guest-control probe returns MountReady, guestMountReady flips to true; probe returns MountAbsent on umount", + "workItemId" : "ADR046-vvfs-004" + }, + { + "currentSource" : "`packages/d2b-host/src/hardlink_farm.rs` (`live_dir()`, marker `live/.d2b-marker-`, zero-length); `nixos-modules/processes-json.nix` (`roStoreSharedDir` sentinel `share.source == \"/nix/store\"` → `store-view/live`); `nixos-modules/store.nix` (per-VM hardlink farm)", + "dataMigration" : "Current `roStoreSharedDir` redirect in `processes-json.nix` replaced by `ro-store` View definition in the store-view Volume resource", + "dependencyOwner" : "ADR046-vvfs-003, ADR046-vvfs-004; ADR046-volume-002 (store-view Volume); W3", + "destination" : "`packages/d2b-provider-volume-virtiofs/src/controller.rs` (pre-launch prerequisite check); `packages/d2b-provider-volume-virtiofs/integration/store_view_readonly/`", + "detailedDesign" : "Before issuing Process Create for a store-view virtiofs Export, check that `live/.d2b-marker-` exists (zero-length, correct mode) via a bounded blocking adapter (e.g., `tokio::task::spawn_blocking` wrapping `fstatat(2)` relative to the zone runtime directory, or an async-safe fd-relative equivalent); no blocking syscall on the async executor thread directly. If absent, requeue with exponential backoff. Assert `--shared-dir` resolves to `store-view/live` (the `ro-store` View path), never to `/nix/store`. Validate in `integration/store_view_readonly/` that virtiofsd serves only paths under `store-view/live`.", + "integration" : "Pre-launch check in controller.rs; store-view Export recognized by `view == \"ro-store\"` and `access == \"read-only\"`", + "removalProof" : "`nixos-modules/processes-json.nix` `virtiofsdRunner` block and `roStoreSharedDir` sentinel removed only after store-view virtiofs Export resources pass parity gate", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-virtiofs", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-virtiofs.md", + "validation" : "`integration/store_view_readonly/`: mount from guest reads closure paths; no host-store path escapes; `tests/argv_golden.rs`: `store_view_shared_dir_is_live_not_nix_store`; `tests/export_lifecycle.rs`: `store_view_launch_waits_for_marker`", + "workItemId" : "ADR046-vvfs-005" + }, + { + "currentSource" : "`nixos-modules/processes-json.nix` (virtiofsdRunner block); `nixos-modules/minijail-profiles.nix` (virtiofsdProfiles); `nixos-modules/options-vms.nix` (`d2b.vms..shares.*`)", + "dataMigration" : "`d2b.vms..shares` virtiofs entries → Volume attachments; `d2b.vms.` store-view auto-emission replaces `nixos-modules/store.nix` virtiofsd portion", + "dependencyOwner" : "ADR046-vvfs-001, ADR046-volume-004; Nix integrator; W3", + "destination" : "`nixos-modules/resources-volume.nix` (store-view and user Volume attachment emission); `nixos-modules/options-volumes.nix` (optional user-facing volume/attachment options)", + "detailedDesign" : "Extend the Nix resource compiler to: (1) auto-emit a store-view Volume (with `ro-store` and `meta` Views, virtiofs ro-store attachment) per Guest that has a VM runtime Provider; (2) emit virtiofs attachment entries for explicitly configured user Volumes; (3) emit `User/vol--vfd` resources for each Volume with virtiofs attachments; (4) emit `Provider/volume-virtiofs` as a Provider resource when any virtiofs attachment is configured. volume-local creates Export resources at runtime (not in Nix bundle); no `virtiofs.d2bus.org.Export` resources appear in the Nix-emitted bundle. All eval validation steps (§16.5) apply.", + "integration" : "`nixos-modules/default.nix` wires resources-volume.nix; nix-unit tests verify canonical output", + "removalProof" : "`nixos-modules/processes-json.nix` virtiofsdRunner block, `nixos-modules/minijail-profiles.nix` virtiofsdProfiles removed only after Nix resource compiler produces parity output and all nix-unit cases pass", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-virtiofs", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-virtiofs.md", + "validation" : "nix-unit: `store_view_volume_auto_emitted_per_guest`, `volume_virtiofs_attachment_canonical_json`, `virtiofs_provider_emitted_when_attachment_configured`, `vfd_user_emitted_per_volume`, `second_read_write_attachment_rejected_at_eval`, `transport_virtiofs_requires_provider_installed`; drift-check gate for `nixos-modules/processes-json.nix` virtiofsdRunner removal", + "workItemId" : "ADR046-vvfs-006" + }, + { + "currentSource" : "No analog; new ResourceType", + "dataMigration" : "None; new type", + "dependencyOwner" : "ADR046-volume-001; W1; volume-virtiofs Provider owner", + "destination" : "`packages/d2b-provider-volume-virtiofs/src/export.rs`; `packages/d2b-contracts/src/v3/virtiofs_export.rs`", + "detailedDesign" : "Declare `virtiofs.d2bus.org.Export` ResourceType in `d2b-contracts`. Base fields: `providerRef`, `volumeRef`, `executionRef`, `view`, `access`, `mountPath`; virtiofs tunables live under `spec.provider.settings` (as in §4.2). Status fields: top-level `phase`/`conditions`, `status.resource.exportReady`, `status.resource.guestMountReady`, and `status.provider.details.workerProcessRef`. Strict serde `deny_unknown_fields`. Implement the conformance test fixture that validates schema fingerprint stability. The Export spec JSON schema and provider status extension schema are signed and included in the Provider package.", + "integration" : "`d2b-contracts` exports the Export DTO; volume-virtiofs controller and volume-local both import it for ResourceClient typed operations", + "removalProof" : "N/A (new type)", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-provider-volume-virtiofs", + "specPath" : "docs/specs/providers/ADR-046-provider-volume-virtiofs.md", + "validation" : "`tests/schema_conformance.rs`: `export_schema_canonical_json_stable`, `export_spec_denied_unknown_fields`, `export_status_exportready_is_boolean_not_path`, `export_owner_must_be_volume`", + "workItemId" : "ADR046-vvfs-export-001" + }, + { + "currentSource" : "main `a1cc0b2d`: `packages/d2b-contracts/src/v2_services.rs` (`MethodSpec{mutating,stream_kind,...}`, `ServiceSpec`, `SERVICE_INVENTORY` covering 20+ services and all provider services, `service_schema_fingerprint`, `public_daemon_schema_fingerprint`, `direct_guest_schema_fingerprint`, `StrictWireMessage`, `decode_strict`, `encode_strict`, `admit_metadata`, `TerminalStreamValidator`, `ServerStreamLease`, `RedactedTerminalFrame`, stream-name validators — lines 204–1004; tests `public_endpoint_fingerprint_binds_both_services_dependencies_and_order`, `public_endpoint_fingerprint_binds_daemon_and_guest_method_descriptors`, `direct_guest_fingerprint_binds_activation_and_remains_separate_from_public_endpoint` — lines 872–1030); `packages/d2b-contracts/src/v2_state.rs` (constants: `STATE_SCHEMA_VERSION=2`, `STATE_SCHEMA_GENERATION=1`, `MAX_JSON_DOCUMENT_BYTES=1_048_576`, `MAX_INVENTORY_ROWS=4096`, `MAX_LOCKS=1024`, `MAX_LOCK_DEPENDENCIES=32`, `MAX_DISCOVERY_OBSERVATIONS=4096`, `MAX_AUDIT_RECORD_BYTES=8192`, `MAX_AUDIT_RECORDS_PER_SEGMENT=16384`, `MAX_AUDIT_SEGMENT_BYTES=64*1024*1024`, `MAX_AUDIT_RETENTION_DAYS=14`, `MAX_LOCK_DEADLINE_MS=300_000`; types: `Digest`, `Generation`, `OwnershipEpoch`, `SafeJsonInteger`, `StorageCategory`, `StateEnvelope`, `CanonicalPayloadVerifier`, `AtomicWritePhase`, `RunnerEvidence`, audit types incl. `AuditRecord/Segment/Checkpoint/Gap`, `detect_audit_gap`, `AuditRetentionPolicy`, lock types: `LockClass`, `LockSpec`, `SyncInventory`, `LeaseRecord`); `packages/d2b-contracts/src/v2_identity.rs` (`IdentityError` 13 variants, canonical name rules `^[a-z][a-z0-9-]*$` max 63 bytes start-lowercase-letter, `RealmPath` label/separator rules, `ProviderType::ALL` 11 types + `as_str()` — lines 11–250); `packages/d2b-contracts/src/v2_provider.rs` (bounded opaque IDs `[a-z][a-z0-9-]{0,63}`: `ImplementationId`, `OperationId`, `IdempotencyKey`, `PlanId`, `HandleId`, `LeaseId`, `TransferId`, `PROVIDER_CONTRACT_FINGERPRINT`, `ProviderContractError` 34 variants, `Fingerprint` 64 lowercase-hex chars — lines 18–219); `packages/d2b-contracts/src/generated_v2_services/` (all 40+ generated ttrpc client/server stubs for v2 services)", + "dataMigration" : "v2 and v3 wire type namespaces coexist in the same contracts crate; no migration", + "dependencyOwner" : "ADR046-zone-control-013", + "destination" : "`packages/d2b-contracts/src/v3/{services,state,identity,provider}.rs` (new v3 namespace); generated stubs regenerated from v3 proto files in `packages/d2b-contracts/proto/v3/`", + "detailedDesign" : "**Selected**: `MethodSpec`/`ServiceSpec`/fingerprinting infrastructure → v3 Zone API service schema fingerprinting; `StrictWireMessage`/`decode_strict`/`encode_strict`/`admit_metadata` → v3 wire decode/admit for all resource API requests; `CanonicalPayloadVerifier` payload-digest binding → v3 resource store integrity checks; audit chain types `AuditRecord`/`AuditSegment`/`AuditCheckpoint`/`detect_audit_gap` + `MAX_AUDIT_RETENTION_DAYS=14`/`MAX_AUDIT_RECORD_BYTES=8192`/`MAX_AUDIT_SEGMENT_BYTES=64MiB` → v3 Zone audit (§13.3); lock types `LockSpec`/`SyncInventory`/`LeaseRecord` + `MAX_LOCKS=1024`/`MAX_LOCK_DEADLINE_MS=300_000` → v3 resource store lock layer; bounded opaque ID pattern `[a-z][a-z0-9-]{0,63}` → v3 `OperationId`/`HandleId`/`PlanId`/`LeaseId`; `Fingerprint` 64-hex-char → v3 Provider `spec.configFingerprint`; `ProviderContractError` 34 variants → v3 Provider operation error taxonomy; canonical name constraint `^[a-z][a-z0-9-]*$` max 63 bytes → same as `ids.rs::is_label()` shared validator (§16.2). **Excluded ADR45 assumptions**: `RealmLabel`/`WorkloadName`/`RealmPath` identity types: ADR45 workload/realm address format; replaced by `metadata.name` + Zone `ResourceRef`. `ProviderType::ALL` fixed 11-type closed enum: v3 Provider type is an open string field in the Provider resource spec. `STATE_SCHEMA_VERSION=2`/`STATE_SCHEMA_GENERATION=1`: v3 store schema uses redb table versioning, not a JSON schema version field. v2 service fingerprint tests reference ADR45-specific proto files; v3 fingerprints use different proto inputs but the same `service_schema_fingerprint` seeding mechanism. Generated stubs in `generated_v2_services/` are v2-specific and excluded; v3 uses regenerated stubs from `proto/v3/`.", + "integration" : "v3 Zone API service layer uses `MethodSpec.mutating` + `StrictWireMessage` for admission and fingerprinting; `v3/state.rs` `Digest`/`StateEnvelope` integrate with redb store writes; `CanonicalPayloadVerifier` validates resource payloads loaded from store; audit types feed §13.3 Zone audit segment; lock types wire into ADR 0034 lock lifecycle", + "removalProof" : "`d2b-contracts/src/v2_{services,state,identity,provider}.rs` and `v2_component_session.rs` deprecated in contracts crate after all v2 clients decommissioned", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`v3-canonical-name-matches-ids-is-label-regex`, `v3-service-fingerprint-changes-on-method-mutation` (behavior ported from `public_endpoint_fingerprint_binds_daemon_and_guest_method_descriptors`), `v3-audit-gap-detection-covers-missing-segment`, `v3-strict-wire-rejects-unknown-fields`, `v3-state-envelope-digest-mismatch-rejected`, `v3-canonical-payload-verifier-binding-holds-under-mutation`", + "workItemId" : "ADR046-wire-001" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/ids.rs` (`RealmId`, `LABEL_PATTERN = \"^[a-z][a-z0-9-]*$\"`, `is_label()`, `MAX_ID_LEN = 128`, `IdError` — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-realm-core/src/realm.rs` (`RealmPath`, `MAX_REALM_LABELS = 16`, `RealmControllerPlacement`, `EntrypointMode` — `implemented-and-reachable`, baseline `b5ddbed6`); Zone resource schema: `ADR-only`", + "dataMigration" : "Destructive d2b 3.0 reset; no v2/Realm Zone resource import", + "dependencyOwner" : "ADR046-object-001 (resource envelope); ADR046-store-001 (redb store); ADR046-identities-001 (types)", + "destination" : "`packages/d2b-contracts/src/v3/zone.rs`; `packages/d2b-core-controller/src/zone.rs`", + "detailedDesign" : "Zone ResourceType schema with `spec = {}`; self-resource enforcement; store_meta binding checks; phase/conditions; cardinality-1 enforcement; `zone-drain` finalizer; canonical JSON schema. Nix Zone options include compiler-only `parentZone`: required for every non-root Zone, forbidden on `local-root`, and never emitted into `Zone.spec` Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`RealmId`→`ResourceName` validators, `RealmPath` depth bound→ZoneLink depth, `RealmControllerPlacement`→Provider component placement); new (Zone resource schema/store table/handler).", + "integration" : "Zone runtime open/upgrade verifies `Zone/` self resource; configuration publication handler creates/reconciles Zone; resource API rejects cross-Zone refs", + "removalProof" : "`d2b-realm-core` Realm struct removed only after Zone resource integration is live in Zone runtime", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`zone-self-resource-enforced`, `zone-uid-mismatch-quarantine`, `zone-name-mismatch-rejected`, `zone-cardinality-one`, `zone-cross-zone-ref-rejected`, `zone-owner-rejected`, `zone-deletion-only-on-drain`", + "workItemId" : "ADR046-zone-control-001" + }, + { + "currentSource" : "`packages/d2b-realm-router/src/session.rs` (`PeerSession` — `implemented-but-unwired`, baseline `b5ddbed6`; no live call sites in d2bd or d2b — only reachable through `realm_stubs.rs` dead code in d2bd); `packages/d2b-realm-router/src/secure_session.rs` (`SecurePeerSession`, Noise-based — `implemented-but-unwired`); `packages/d2b-realm-router/src/mux_session.rs` (`MuxSession` — `implemented-but-unwired`); `packages/d2b-realm-router/src/session_lifecycle.rs` (`SessionLifecycle`, `SessionPhase` — `implemented-but-unwired`); `packages/d2b-realm-core/src/route_engine.rs` (`RouteTreeEngine`, `admit_advertisement()`, `decide_route()`, `RoutePruneReport` — `implemented-but-unwired`; exported from `d2b_realm_core::lib.rs:122` but zero call sites in live daemon or CLI code, tests only); `packages/d2b-realm-core/src/identity_store.rs` (`RealmIdentityStore`, `EnrollmentRecord`, `ChildKeyPin` — `implemented-and-reachable`); `packages/d2b-realm-core/src/realm.rs` (`RealmPath`, `MAX_REALM_LABELS = 16` — `implemented-and-reachable`); `packages/d2b-realm-core/src/access.rs` (`RealmTransportBinding::{LocalUnixSocket,RemoteRealmTransport,ProviderRealmTransport}` — `implemented-and-reachable` in CLI routing path); ZoneLink resource schema and cursor tables: `ADR-only`", + "dataMigration" : "Destructive reset; no v2 Realm peer migration", + "dependencyOwner" : "ADR046-zone-control-001; ADR046-zone-control-018 (ComponentSession)", + "destination" : "`packages/d2b-contracts/src/v3/zone_link.rs`; `packages/d2b-core-controller/src/zone_link.rs`", + "detailedDesign" : "Child-local ZoneLink schema with self-matching `spec.childZoneName` plus same-Zone `spec.transportProviderRef`, `spec.transportSettings`, and `spec.transportCredentials`; at most one uplink resource per non-root Zone and none in local root; child-store cursor/intent state; reconnect loop with exponential backoff; local intent queue (max 256 entries); local child UID change detection; drain finalizer; no reciprocal parent-store resource; compiler-only `parentZone` selects the one allocator owner, while that allocator owns privileged listener/placement/route allocation effects and exposes only a sealed bootstrap/allocation interface Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`SecurePeerSession` Noise model → ComponentSession Noise KK; `SessionLifecycle`/`SessionPhase` → ZoneLink session reconnect loop and connection detail fields; `Connecting`/`Established` current evidence phases drive `status.connected` and `status.phase` transitions to `Pending`/`Ready`, not direct phase values; `RouteTreeEngine.decide_route()` → cursor tracking; `RealmIdentityStore` enrollment → ZoneLink child key-pin).", + "integration" : "child core-controller zone_link handler; child redb `zone_link_cursors` table; Nix-compiled `parentZone` bootstrap topology; selected parent allocator and route engine through sealed allocation authority; d2b-bus transport resolver; ComponentSession lifecycle", + "removalProof" : "`realm_stubs.rs` (`ApiFrontend`, `PeerOperationRouter`, `TargetResolver`) removed after ComponentSession integration (ADR046-zone-control-018); `realm_access_resolver.rs` module removed after ZoneLink replaces entrypoint-table resolution; gateway `PeerSession`/`SecurePeerSession` session types remain as dead code in d2b-realm-router until Provider session migration wave", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `packages/d2b-session/src/lifecycle.rs`, `d2b-session-unix/src/adapter.rs` for reconnect/transport precedents", + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`zonelink-reconnect-child-uid-change`, `zonelink-disconnect-unknown-phase`, `zonelink-intent-queue-limit`, `zonelink-disabled-no-reconnect`, `zonelink-child-auth-denied-failed`, `zonelink-drain-closes-session`, `zonelink-child-name-matches-store`, `zonelink-one-child-local-uplink`, `zonelink-parent-bootstrap-binding`, `zonelink-parent-has-no-reciprocal-row`", + "workItemId" : "ADR046-zone-control-002" + }, + { + "currentSource" : "`packages/d2b-realm-provider/src/provider.rs` (`HostSubstrateProvider`, `RuntimeProvider`, `WorkloadProvider`, `DurableExecutionProvider`, `InfrastructureProvider`, `NodeProvider` traits — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-realm-provider/src/capabilities.rs` (`RuntimeCapabilitySet`, `WorkloadCapabilitySet`, `NodeCapabilitySet` — `implemented-and-reachable`); `packages/d2b-realm-provider/src/conformance.rs` (`workload_lists_and_advertises`, `display_fails_closed_when_unsupported` — `implemented-and-reachable`); `packages/d2b-realm-provider/src/error.rs` (`ProviderError`, `ErrorKind`, `RetryHint`, `ProviderDiagnostic` — `implemented-and-reachable`); `packages/d2b-realm-core/src/ids.rs` (`ProviderId`, label-shaped — `implemented-and-reachable`); `packages/d2b-core/src/processes.rs` (`ProcessRole::{CloudHypervisorRunner, Virtiofsd, Swtpm, GpuRenderNode, Audio, Video, QemuMediaRunner, VsockRelay, OtelHostBridge, Usbip}`, `VmProcessDag`, `ProcessNode` — `implemented-and-reachable`); Provider resource schema and API catalog: `ADR-only`", + "dataMigration" : "Full reset; Provider packages recompiled and re-registered per new schema", + "dependencyOwner" : "ADR046-zone-control-001; ADR046-api-001; ADR046-zone-control-017", + "destination" : "`packages/d2b-contracts/src/v3/provider.rs`; `packages/d2b-core-controller/src/provider_lifecycle.rs`; `packages/d2b-core-controller/src/api_catalog.rs`", + "detailedDesign" : "Provider resource schema with all spec fields from §4.3, including resolved component bounds (max 8 controllers, 8 services, 32 worker templates, 16 ResourceTypes per controller); trust/conformance/config validation; component descriptor validation; dependency alias resolution; API binding with permission intersection; lifecycle policies; Nix Provider installation options; Provider crate layout enforcement per §4.8 (see ADR046-pkg-001) Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`workload_lists_and_advertises`/`display_fails_closed_when_unsupported` conformance behavior; `RuntimeCapabilitySet`/`WorkloadCapabilitySet`/`NodeCapabilitySet` → Provider component `supportedCapabilities`; `ProviderError`/`RetryHint` → Provider lifecycle error schema; `ProviderId` → Provider `metadata.name` validator; `ProcessRole` variants → Provider component type identifiers).", + "integration" : "Zone config publication installs Provider resources; API catalog handler binds exported ResourceTypes; Provider/system-core and Provider/system-minijail are bootstrap exceptions with pre-created records", + "removalProof" : "`d2b-realm-provider` trait crate removed per ADR046-zone-control-017 after Provider resource integration", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: any `d2b-provider-toolkit` registry/descriptor patterns named in ADR046-zone-control-017 sub-items", + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "All §15.3 Provider tests including the resolved bounds checks", + "workItemId" : "ADR046-zone-control-003" + }, + { + "currentSource" : "`packages/d2bd/src/admission.rs` (`PeerRole::{Admin, Launcher, HostShutdown}`, `PeerIdentity`, `authorize_peer()`, `verb_requires_admin()`, `verb_allowed_for_host_shutdown()` — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-daemon-access/src/lib.rs` (`LocalUnixAllowlistRole::{Admin, Launcher}`, `DaemonAccessPolicyRole::RealmAdmin`, `DaemonAccessDecision`, `MappedDaemonAccessPrincipal`, `map_local_unix_daemon_access()` — `implemented-and-reachable`); Role resource schema and RBAC index: `ADR-only`", + "dataMigration" : "Initial Roles generated from Nix config; no v2 Role resource import", + "dependencyOwner" : "ADR046-zone-control-001; ADR046-api-002", + "destination" : "`packages/d2b-contracts/src/v3/role.rs`; `packages/d2b-core-controller/src/authz.rs`", + "detailedDesign" : "Role resource schema with rule schema from §5.3 and resolved bounds (32 rules, 16 resourceTypes per rule, 64 resourceNames, 32 executionRefs); separate closed resource/session verb enums including `relay`; core-generated ZoneLink ownership, exact adjacent-Zone enrollment selector, exact target bounds, and explicit admin-policy exception admission; explicit wildcard enforcement; index builder; phase/conditions; `role-binding-drain` finalizer; generated Nix Role option help; audit/OTEL instrumentation Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`verb_requires_admin()` verb table → Role verb enum; `DaemonAccessDecision` error types → Role admission errors; `MappedDaemonAccessPrincipal` → subject identity model).", + "integration" : "Authorization evaluator (ADR046-api-002) reads Role index entries; core `authz` handler owns reconcile loop", + "removalProof" : "`daemon-access` capability enum removed after RBAC Role engine covers all access decisions", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "All §15.4 Role tests including closed-enum, relay origin/scope/target-verb, and resolved-bounds checks", + "workItemId" : "ADR046-zone-control-004" + }, + { + "currentSource" : "`packages/d2b-daemon-access/src/lib.rs` (`DaemonAccessDecision::Authorized { role: DaemonAccessPolicyRole }`, `DaemonAccessAdmissionSource`, `map_remote_daemon_access()` — `implemented-and-reachable`, baseline `b5ddbed6`; these implement the current coarse binding decision per-connection); RoleBinding resource schema: `ADR-only`", + "dataMigration" : "Initial RoleBindings generated from Nix config", + "dependencyOwner" : "ADR046-zone-control-004", + "destination" : "`packages/d2b-contracts/src/v3/role_binding.rs`; `packages/d2b-core-controller/src/authz.rs` (shared with Role handler)", + "detailedDesign" : "RoleBinding resource schema with no expiry field and max 128 subjects; subject resolution and UID binding; external principal selector; scope narrowing intersection; revocation; immediate deletion with cache invalidation; Nix RoleBinding options Primary reuse disposition: `adapt`. Preserved source-plan detail: new (RoleBinding resource schema/store table/handler); adapt (`DaemonAccessAdmissionSource` identity fields → subject selector; `map_remote_daemon_access()` logic → subject UID-binding behavior).", + "integration" : "Subject resolution uses store owner index; authorization evaluator reads combined Role+narrowing entry; revocation/update/delete flow uses the normal resource lifecycle", + "removalProof" : "Not applicable; new type", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "All §15.5 RoleBinding tests, including the 128-subject admission bound and no-expiry lifecycle model", + "workItemId" : "ADR046-zone-control-005" + }, + { + "currentSource" : "`packages/d2bd/src/admission.rs` (`authorize_peer()`, `verb_requires_admin()`, `verb_allowed_for_host_shutdown()`, `PeerRole::{Admin, Launcher, HostShutdown}` — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-daemon-access/src/lib.rs` (`LocalUnixAllowlistRole`, `DaemonAccessDecision::Authorized/Denied`, `DaemonAccessAdmissionSource`, `MappedDaemonAccessPrincipal`, `map_local_unix_daemon_access()` — `implemented-and-reachable`); `packages/d2bd/src/lib.rs` (`LoadedRealmControllersConfig`, `LoadedRealmIdentityConfig` startup state — `implemented-and-reachable`); compiled bootstrap constant policy: `ADR-only`", + "dataMigration" : "Bootstrap is always freshly compiled; no migration", + "dependencyOwner" : "ADR046-zone-control-004, ADR046-zone-control-005; Zone runtime", + "destination" : "`packages/d2b-resource-api/src/bootstrap_authz.rs`; Zone runtime startup path", + "detailedDesign" : "Compiled bootstrap authorization as described in §9; exact subjects (system-core, system-minijail); closed verb table; non-configurable enforcement; atomic supersession after stored RBAC publishes; out-of-band reset path Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt (`authorize_peer()`/`verb_requires_admin()` verb table → bootstrap constant policy verb table; `PeerRole` two-variant model → system-core/system-minijail bootstrap subjects; `map_local_unix_daemon_access()` SO_PEERCRED mapping → bootstrap subject derivation; `LoadedRealmControllersConfig` startup path → Zone runtime startup bootstrap init sequence).", + "integration" : "Resource API authorization layer checks bootstrap policy before stored RBAC; supersession is triggered by first `IndexBuilt=True` event from authorization handler", + "removalProof" : "`daemon-access` bootstrap stub removed after Zone runtime bootstrap authz integrates", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "All §15.6 bootstrap tests", + "workItemId" : "ADR046-zone-control-006" + }, + { + "currentSource" : "`nixos-modules/options-realms.nix` (`providerKind = \"^[a-z][a-z0-9-]*$\"` label regex matching `LABEL_PATTERN` in `ids.rs`; `providerType` submodule with `enable`/`kind`/`placement`/`freeformType` fields; `d2b.realms..providers.*` attrset — `generated-or-eval-contract`, baseline `b5ddbed6`); `nixos-modules/options-realms-workloads.nix` (`d2b.realms..workloads.*` submodule shape — `generated-or-eval-contract`); `d2b.zones.*` Nix options: `ADR-only`", + "dataMigration" : "Full reset; Nix realm options (`d2b.realms.*`) remain until purge wave", + "dependencyOwner" : "ADR046-identities-002; ADR046-zone-control-001; ADR046-zone-control-002; ADR046-zone-control-003; ADR046-zone-control-004; ADR046-zone-control-005; ADR046-zone-control-009; ADR046-zone-control-010", + "destination" : "`nixos-modules/options-zones.nix`; `nixos-modules/resources-zone-control.nix`; extended `nixos-modules/index.nix`", + "detailedDesign" : "`d2b.zones..*` Nix options for Zone/ZoneLink/Provider/Role/RoleBinding/Quota/EmergencyPolicy authoring; compiler-only scalar `parentZone` required on non-root Zones and forbidden on `local-root`; Nix eval-time validation of parent existence, one resolved parent, self/cycle rejection, 16-name ancestry depth, ResourceRefs, separate resource/session verb enums (including session-only `relay`), relay origin/scope restrictions, digest format, subject types, child-local ZoneLink self-name/one-uplink/local transport-ref constraints, and the no-expiry RoleBinding model; generated help carries the relay semantics; canonical JSON serialization; generation-bound resource bundle output; the compiler seals the validated parent map into allocator bootstrap topology without emitting `parentZone` into `Zone.spec` or reciprocal resources Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (`providerType` submodule → Provider option submodule; label regex `^[a-z][a-z0-9-]*$` retained unchanged; `placement` option → Provider component placement; `d2b.realms.*` option namespace → `d2b.zones.*` option namespace).", + "integration" : "Nix compiler → sealed allocator topology for `parentZone`; resource compiler → configuration publication handler → Zone store; bootstrap Provider records auto-generated", + "removalProof" : "`nixos-modules/options-realms.nix` and related realm options removed only after Zone resource Nix integration is live", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "nix-unit vectors for each Zone control type schema; closed resource/session verb and relay restriction vectors; cross-field constraint tests; rendered JSON contract tests (`make test-drift`)", + "workItemId" : "ADR046-zone-control-007" + }, + { + "currentSource" : "`d2b-realm-core/src/workload.rs:13,27,83` (`WorkloadProviderKind::UnsafeLocal`, `IsolationPosture::UnsafeLocal`, `WorkloadExecutionPosture` with canonical unsafe-local tuple at lines 206–211: `isolation=unsafe-local`, `environment=systemd-user-manager-ambient`, `executionIdentity=authenticated-requester-uid`, `sessionPersistence=user-manager-lifetime` — `implemented-and-reachable`, baseline `b5ddbed6`); `d2b-core/src/unsafe_local_workloads.rs:16,47,106,150` (`UnsafeLocalWorkloadsJson`, `UnsafeLocalWorkload`, `UnsafeLocalShellPolicy`, `MAX_UNSAFE_LOCAL_SHELL_SESSIONS=64`, `MAX_UNSAFE_LOCAL_WORKLOADS=256` — `implemented-and-reachable`); `d2bd/src/unsafe_local_helper.rs` (`HelperRegistry`, `bind_helper_socket`, `dispatch_launch` — `implemented-and-reachable`, live in `d2bd/src/lib.rs:1346–1468`); `nixos-modules/options-realms.nix:346` (`policy.allowUnsafeLocal` — `generated-or-eval-contract`); `nixos-modules/options-realms-workloads.nix:221,233` (`kind = \"unsafe-local\"`, doc \"no isolation boundary\" — `generated-or-eval-contract`)", + "dataMigration" : "Destructive reset; `unsafe-local-workloads.json` bundle artifact and `HelperRegistry` replaced by Host resource store entries + Process Provider launch", + "dependencyOwner" : "ADR046-zone-control-001 (Zone resource); ADR046-zone-control-003 (Provider/system-core installed)", + "destination" : "`packages/d2b-contracts/src/v3/host.rs` (Host resource schema, user-domain variant); `packages/d2b-core-controller/src/host_user.rs` (reconciler owned by Provider/system-core); Nix Host authoring via `d2b.zones..resources. = { type = \"Host\"; spec = { ... }; };` (validated per ResourceTypeSchema; no separate `options-zones-hosts.nix` submodule)", + "detailedDesign" : "Host ResourceType schema for user-domain variant: `spec.defaultDomain=user`, `spec.allowedDomains=[user]`, `spec.defaultUserRef=User/`, `spec.shellPolicy` (adapted from `UnsafeLocalShellPolicy`), `spec.launcherItems` (adapted from `UnsafeLocalLauncherItem`). No-isolation posture recorded in `status.observedPosture`. A dedicated Host admission gate blocks unsafe-local Host creation without opt-in. Mandatory no-isolation warning in Host `status.conditions[0].message` and CLI/UI output for all Host commands. No-isolation posture included in audit record body (`isolation=no-isolation`); it is never emitted as an OTEL metric label value or span attribute. Child processes use standard Process Providers — no Provider resource with name or kind `unsafe-local` is created. Cardinality bounds: max 256 user-domain Hosts per Zone, max 64 launcher items per Host, max 64 shell sessions per Host. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt (`WorkloadExecutionPosture` unsafe-local posture tuple → Host `status.observedPosture`; posture details in audit record body, not OTEL labels; `UnsafeLocalShellPolicy.{defaultName,maxSessions}` → Host `spec.shellPolicy`; `HelperRegistry.dispatch_launch` → Zone runtime Host process launch broker; `policy.allowUnsafeLocal` → dedicated Host admission gate); new (Host ResourceType user-domain schema with `defaultDomain`, `allowedDomains`, `defaultUserRef`, `shellPolicy`, cardinality bounds).", + "integration" : "Provider/system-core bootstrap exception creates Host resources from Zone config publication; Host controller reconciles `defaultUserRef` via User resource lookup; `HelperRegistry` in Zone runtime becomes per-uid launch broker for Host Process launch; Host admission layer enforces the `allowUnsafeLocal` gate before Host creation", + "removalProof" : "`d2bd/src/unsafe_local_helper.rs` `HelperRegistry` removed after Zone runtime Host/Process broker integration; `unsafe-local-workloads.json` bundle artifact removed after Host resource store replaces it; `kind = \"unsafe-local\"` Nix enum value removed in purge wave", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`host-user-domain-no-isolation-warning-required`, `host-user-only-disallows-system-domain`, `host-allowUnsafeLocal-gates-creation`, `host-defaultUserRef-user-type-required`, `host-shell-policy-max-sessions-bound`, `host-launcher-item-max-count-bound`, `host-audit-body-isolation-label-present`, `host-otel-no-posture-label`, `host-cli-no-isolation-warning-present`", + "workItemId" : "ADR046-zone-control-008" + }, + { + "currentSource" : "`packages/d2b-realm-core/src/ids.rs` (`LABEL_PATTERN`, `MAX_ID_LEN` — ResourceName validation for quotaRef); `packages/d2b-core/src/unsafe_local_workloads.rs:16–164` (`MAX_UNSAFE_LOCAL_WORKLOADS=256`, etc. — bound evidence for quota ceiling defaults); no current quota ResourceType exists (`ADR-only`)", + "dataMigration" : "Full reset; no prior Quota resources exist", + "dependencyOwner" : "ADR046-zone-control-001; Zone store (ADR046-store-001); Quota handler owner", + "destination" : "`packages/d2b-contracts/src/v3/quota.rs`; `packages/d2b-core-controller/src/quota.rs`; `packages/d2b-resource-api/src/quota_gate.rs`", + "detailedDesign" : "Quota resource schema with all spec/status fields from §7; ceiling bounds enforcement at admission (hard policy: reject over-quota with `quota-exceeded`; soft policy: warn); usage index built from resource scan with `quotaRef` field; per-ResourceType ceiling in `perTypeCeilings`; `core.quota-drain` finalizer that blocks Quota deletion until all dependent resources with `spec.quotaRef` pointing to this Quota are reassigned or deleted by authorized owners/operators — the controller never issues spec-updates to clear `quotaRef` on other resources; `dependentCount` status field updated from dependency index; audit event `quota-check` per admission; Nix Quota options per §7.7", + "integration" : "Resource API admission gate (`packages/d2b-resource-api/src/quota_gate.rs`) called for every `create` verb; Zone controller triggers quota reconcile on resource-created/deleted/quotaRef-changed events; quota handler registered in core-controller process", + "removalProof" : "Additive; no existing code removed", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`quota-ceiling-hard-reject`, `quota-ceiling-soft-warn`, `quota-ceiling-pertype`, `quota-drain-blocks-on-dependents`, `quota-over-quota-status`, `quota-nix-eval-bounds`, `quota-nix-build-pertype-unknown-type`", + "workItemId" : "ADR046-zone-control-009" + }, + { + "currentSource" : "`packages/d2bd/src/lib.rs` admission gate (`authorize_peer()` — `implemented-and-reachable`, baseline `b5ddbed6`); `packages/d2b-daemon-access/src/lib.rs` (`DaemonAccessDecision::Denied` — basis for admission rejection pattern); no EmergencyPolicy ResourceType exists (`ADR-only`)", + "dataMigration" : "Full reset; no prior EmergencyPolicy resources exist", + "dependencyOwner" : "ADR046-zone-control-001; Zone store (ADR046-store-001); EmergencyPolicy handler owner", + "destination" : "`packages/d2b-contracts/src/v3/emergency_policy.rs`; `packages/d2b-core-controller/src/emergency_policy.rs`; `packages/d2b-resource-api/src/emergency_gate.rs`", + "detailedDesign" : "EmergencyPolicy resource schema from §8; union semantics: multiple `enabled=true` policies allowed simultaneously; effective scope = OR of all enabled policies' scope flags; effective `drainDeadlineSeconds` = minimum across enabled policies; scope flag evaluation: `stopNewAdmissions` signal to API admission gate, `disconnectZoneLinks` graceful signal to ZoneLink handler, `stopProviderProcesses` suppresses Process launch and sends stop signal to running Provider component Processes (does NOT set `deletionRequestedAt` on Process resources; reconciliation resumes on deactivation), `drainOngoingOperations` deadline drain; `core.emergency-drain` finalizer for enabled policies; audit events `emergency-policy-activated` / `emergency-policy-deactivated`; `reason` field stored in spec and included in audit record body (never in OTEL metric label values, structured log labels, or status fields); Nix EmergencyPolicy options per §8.7", + "integration" : "API admission gate checks union of enabled EmergencyPolicies before every admitted request; ZoneLink handler subscribes to EmergencyPolicy watch triggers; Provider process lifecycle listens for effective `stopProviderProcesses` and resumes launch on deactivation", + "removalProof" : "Replaces the inline `emergencyDisable` field from the proposed Zone.spec option B; that option was not implemented", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`emergency-policy-activates-gate`, `emergency-policy-disconnects-zonelinks`, `emergency-policy-union-most-restrictive`, `emergency-policy-multi-enabled-combined-scope`, `emergency-policy-stop-processes-no-delete`, `emergency-policy-deactivation-restores-gate`, `emergency-policy-drain-finalizer`, `emergency-nix-eval-drain-deadline-bound-tightest`", + "workItemId" : "ADR046-zone-control-010" + }, + { + "currentSource" : "main `a1cc0b2d`: `packages/d2b-session/src/lifecycle.rs` (`SessionPhase::{Established,Disconnected,Reconnecting,Closing,Closed}`, `KeepaliveAction::{None,SendPing,Close}`, `SessionLifecycle::new/phase/on_activity/poll_keepalive/receive_pong/disconnect/begin_reconnect/reconnect_established/close` — lines 9–195); `packages/d2b-session/src/engine.rs` (`SessionEngine`, `SessionEvent`, `establish_initiator/responder`, `reconnect_initiator/responder`, `call/complete_call/cancel_call`, `send_ttrpc/receive`, `open/send/grant_credit/close/reset_named_stream`, `send_attachments`, `drive_keepalive`, `fail_closed`, encode/decode helpers for keepalive/cancel/close/stream-control/attachment — lines 33–995); `packages/d2b-session/src/driver.rs` (`ComponentSessionDriver` trait 18 methods, `SessionDriverHandle`, `DriverQueues` 4 event queues + named-send FIFO, `DRIVER_COMMAND_CAPACITY=128`, `DRIVER_EVENT_CAPACITY=128`, backpressure on enqueue and dequeue, test `cancelled_immediate_receiver_restores_queued_event` — lines 20–718); `packages/d2b-session/src/streams.rs` (`StreamId`, `StreamPhase::{Open,HalfClosedLocal,HalfClosedRemote,Closed,Reset}`, `StreamEvent::{Data,RemoteClosed,Reset}`, `NamedStreamMux::open/reserve_send/grant_send_credit/refund_send_credit/receive_data/close_local/receive_close/reset/remove_terminal/active` — lines 7–237); `packages/d2b-session/src/transport.rs` (`OwnedTransport` trait, `TransportDescriptor`, `TransportPacket`, `TransportError::{Disconnected,Truncated,LimitExceeded,InvalidAttachment,Other}` — lines 9–106); `packages/d2b-session/src/error.rs` (`SessionError`, all `From<>` mappings covering `ContractError`, `BinaryError`, `SequenceError`, `FragmentSequenceError`, `HandshakeRejectReason`, `TransportError` — lines 10–143); supporting modules: `attachment.rs`, `bootstrap.rs`, `cancellation.rs`, `deadline.rs`, `fragmentation.rs`, `metrics.rs`, `record.rs`, `scheduler.rs`, `server.rs`", + "dataMigration" : "Not applicable; new crate", + "dependencyOwner" : "ADR046-identities-001; ADR046-store-001", + "destination" : "`packages/d2b-bus/src/{lifecycle,engine,driver,streams,transport,error}.rs` (new crate `d2b-bus`)", + "detailedDesign" : "**Selected**: full `SessionLifecycle` FSM including keepalive ping-nonce/timeout close (`poll_keepalive` lines 81–124), nonce-exhaustion close (`NonceExhausted` remediation `ReplaceGeneration`), reconnect attempt counting + window expiry (`begin_reconnect` lines 147–174), generation increment on reconnect; `ComponentSessionDriver` 18-method trait as the d2b-bus driver contract; `SessionEngine` full frame encode/decode, keepalive, named-stream credit/half-close/reset; `NamedStreamMux` credit model with send/receive credit, phase transitions, terminal removal; `DriverQueues` backpressure (`QueueBackpressure`) on both deliver and receive paths; all `SessionError`/`From<>` mapping chains. **Excluded ADR45 assumptions**: `establish_initiator_with_generation_discovery()` (lines 102–123): ADR45 initiator-probes-server for current generation; in v3 the generation lives in the Zone resource store — use `establish_initiator()` directly. OTEL labels in `metrics.rs` reference ADR45 realm/workload dimensions — replace with Zone/ZoneLink labels per §13.2. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "integration" : "`d2b-bus` crate is the sole session-transport dependency for ZoneLink (ADR046-zone-control-018), Provider component Processes (ADR046-zone-control-017), and the Zone resource API service layer (ADR046-api-001); `ComponentSessionDriver` is the interface ZoneLink uses to send/receive ttrpc frames and named streams", + "removalProof" : "`d2b-realm-router/src/{session.rs,secure_session.rs,mux_session.rs,session_lifecycle.rs}` removed after d2b-bus integration wave", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`session-lifecycle-reconnect-attempts-exhausted`, `session-lifecycle-keepalive-timeout-closes`, `session-lifecycle-nonce-exhausted-close-with-replace-generation-remediation`, `session-driver-cancelled-receiver-restores-event` (ported from test at line 707), `named-stream-credit-half-close-then-remote-close-transitions-closed`, `named-stream-reset-cancels-pending-send`", + "workItemId" : "ADR046-zone-control-011" + }, + { + "currentSource" : "main `a1cc0b2d`: `packages/d2b-session-unix/src/adapter.rs` (`UnixSeqpacketTransport::new`, `UnixStreamTransport::new`, `PeerIdentityPolicy::{Pathname{verifier},InheritedSocketpair{expected_peer}}`, `OwnedUnixAttachment::file`, `UnixAttachmentPayload`, `consume_peer_credentials()` credential strip+verify, `map_validation_error`/`map_transport_error`, tests `inherited_first_packet_requires_credentials`/`inherited_first_packet_rejects_wrong_credentials` — lines 23–852); `packages/d2b-session-unix/src/socket.rs` (`AncillaryCapacity::from_policy`, `OutboundPacket::new`, `SeqpacketSocket`, `prearmed_seqpacket_pair()`, `SendBurst`/`PacketBurst`/`SentPacket`, `recv_one` with `DONTWAIT|CMSG_CLOEXEC`, `send_one` with `DONTWAIT|NOSIGNAL`, SCM_RIGHTS+SCM_CREDENTIALS, fairness-budget burst loops, tests `raw_control_scanner_rejects_unknown_and_partial_headers`/`raw_control_scanner_accepts_exact_rights_shape` — lines 35–698); `packages/d2b-session-unix/src/pidfd.rs` (`PidfdEvidence::new`, `ProcPidfdIdentityVerifier::verify` with double-read race guard, `ProcSelfFdInfoSource::read_pid_from_fdinfo`, `DigestEvidenceCallback` — lines 8–159); `packages/d2b-session-unix/src/credit.rs` (`CreditPool::new/reserve`, `CreditScope::{Packet,Request,Operation,Session,Process,Host}`, `CreditScopeSet::reserve/reserve_ingress`, `CreditBundle::acquire_dispatch`, `ProcessCreditLimit::derive` from `RLIMIT_NOFILE` — lines 13–286); `packages/d2b-session-unix/src/descriptor.rs` (`ReceivedPacket::verify/verify_first_packet_credentials`, `DescriptorPolicy`, `ObjectIdentity`, `PeerCredentials`, `FirstPacketCredentials`, sealed memfd `F_SEAL_WRITE|GROW|SHRINK|SEAL` — lines 21–617); `packages/d2b-session-unix/src/error.rs` (`UnixSessionError` 21 variants, Display strings — lines 4–92); `packages/d2b-session-unix/src/systemd.rs` (`ActivatedSeqpacketListener`, `SystemdActivationError`)", + "dataMigration" : "Not applicable; new crate", + "dependencyOwner" : "ADR046-zone-control-011", + "destination" : "`packages/d2b-bus-unix/src/{adapter,socket,pidfd,credit,descriptor,error,systemd}.rs` (new crate `d2b-bus-unix`)", + "detailedDesign" : "**Selected**: `UnixSeqpacketTransport` seqpacket adapter implementing `OwnedTransport`; `PeerIdentityPolicy` SO_PEERCRED verification (pathname + inherited socketpair); `prearmed_seqpacket_pair()` for bootstrap inherited-socketpair path; `CreditPool`/`CreditScopeSet` six-scope credit model with per-packet/request/operation/session/process/host reservation; `ProcessCreditLimit::derive()` from `RLIMIT_NOFILE` minus `RESERVED_CONTROL_FDS`; `ProcPidfdIdentityVerifier` double-read race guard on `/proc/self/fdinfo/` with executable + cgroup digest callbacks; sealed memfd four-seal enforcement; `AncillaryCapacity` derived from `AttachmentPolicy`; `UnixSessionError` 21-variant Display strings preserved verbatim for audit log compatibility; `consume_peer_credentials()` strips-and-verifies SCM_CREDENTIALS exactly once on first packet. **Excluded ADR45 assumptions**: `vsock.rs` (`FramedVsockTransport`, `NativeVsockListener`, `NativeVsockTransport`): ADR45 guest-control vsock transport; v3 ZoneLink uses Unix sockets for host-local; vsock for Guest connections is separate work. Specific socket paths (`PUBLIC_SOCKET_PATH`, `BROKER_SOCKET_PATH`) are replaced by Zone-resource-managed paths. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "integration" : "`d2b-bus-unix` provides `OwnedTransport` impl consumed by `d2b-bus` `SessionEngine`; Zone runtime passes accepted seqpacket socket to `UnixSeqpacketTransport::new`; Process pidfd identity verifier integrates with Host/Process broker launch", + "removalProof" : "`d2b-realm-router/src/secure_session.rs` `SecurePeerSession` seqpacket layer removed after d2b-bus-unix provides the replacement", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`seqpacket-inherited-missing-credentials-rejected` (ported from `inherited_first_packet_requires_credentials`), `seqpacket-inherited-wrong-credentials-rejected` (ported from `inherited_first_packet_rejects_wrong_credentials`), `pidfd-double-read-race-guard-detects-pid-reuse`, `sealed-memfd-partial-seal-rejected`, `credit-scope-six-levels-ordered`, `ancillary-capacity-from-disabled-policy-is-zero`, `raw-control-unknown-header-rejected` (ported from `raw_control_scanner_rejects_unknown_and_partial_headers`), `raw-control-valid-rights-parsed` (ported from `raw_control_scanner_accepts_exact_rights_shape`)", + "workItemId" : "ADR046-zone-control-012" + }, + { + "currentSource" : "main `a1cc0b2d`: `packages/d2b-contracts/src/v2_component_session.rs` (all protocol constants: `PREFACE_LEN=16`, `PREFACE_MAGIC=b\"D2BCS2\\r\\n\"`, `COMPONENT_SESSION_MAJOR=2`, `COMPONENT_SESSION_MINOR=0`, `MAX_HANDSHAKE_OFFER_BYTES=16384`, `HANDSHAKE_OFFER_CANONICAL_LEN=148`, `ENDPOINT_POLICY_IDENTITY_CANONICAL_LEN=140`, `MAX_PROTECTED_CIPHERTEXT_BYTES=65535`, `NOISE_TAG_BYTES=16`, `RECORD_LENGTH_BYTES=2`, `MAX_PROTECTED_PLAINTEXT_BYTES=65519`, `MAX_LOGICAL_MESSAGE_BYTES=1_048_576`, `MAX_ACTIVE_NAMED_STREAMS=128`, `MAX_PACKET_ATTACHMENTS=32`, `MAX_REQUEST_ATTACHMENTS=64`, `MAX_OPERATION_ATTACHMENTS=128`, `MAX_SESSION_ATTACHMENTS=256`, `MAX_PROCESS_ATTACHMENT_CREDITS=2048`, `MAX_HOST_ATTACHMENT_CREDITS=8192`, named-stream/control queue limits, credential size/flag constants; closed enums: `EndpointPurpose` 19 tags, `PurposeClass` 3 tags, `EndpointRole` 19 tags, `ServicePackage` 15 tags, `NoiseProfile` 3 tags, `IdentityEvidenceRequirement` 3 tags, `Locality` 4 tags, `TransportClass` 7 tags, `AttachmentPolicyKind`; structs: `ComponentSessionPreface::parse/encode`, `HandshakeOffer` 11 fields, `HandshakeAccept`, `HandshakeReject`, `HandshakeRejectReason` 21 tags, `LimitProfile` 15 fields with `local_default()`, `AttachmentPolicy::validate/disabled`, `BoundedVec` — throughout); `packages/d2b-session/src/handshake.rs` (`HandshakeRole::{Initiator,Responder}`, `HandshakeCredentials::{Nn,Kk,IkPsk2Initiator,IkPsk2Responder}`, `NegotiatedOffer`, `NoiseHandshake::new/write_next/read_next/finish`, `EstablishedHandshake::transcript_hash/generation`, `encode_offer/negotiate_offer`, generation-discovery magics `b\"D2BGD2Q\\n\"`/`b\"D2BGD2A\\n\"`, `encode_generation_discovery_request/accept/response/decode_generation_discovery_response` — lines 39–433)", + "dataMigration" : "Additive: new v3 tags appended to existing closed enums; ADR45 v2 tag values remain valid during coexistence", + "dependencyOwner" : "ADR046-identities-001", + "destination" : "`packages/d2b-contracts/src/v3/component_session.rs` (new v3 namespace in existing contracts crate)", + "detailedDesign" : "**Selected**: all numeric constants verbatim; all 21 `HandshakeRejectReason` tags; `ComponentSessionPreface::parse/encode` with exact magic/length layout; `HandshakeOffer` 11-field shape; `AttachmentPolicy::validate` transport constraints (packet-atomic requires seqpacket or inherited-socketpair); `LimitProfile` 15-field profile; `NoiseHandshake` for Nn/Kk/IkPsk2 profiles; `EstablishedHandshake::transcript_hash`; `BoundedVec`. **Excluded ADR45 assumptions**: `EndpointPurpose` 19 tags and `ServicePackage` 15 tags encode ADR45-specific service families — v3 will append new tags for Zone API endpoints without renumbering existing ones. `IdentityEvidenceRequirement` + `GuestSessionCredentialV1` / `GuestBootstrapPsk` / `GUEST_SESSION_CREDENTIAL_MAGIC`: ADR45 guest bootstrap credential formats, excluded until v3 Guest bootstrap work item. Generation-discovery protocol (`encode_generation_discovery_request` lines 138–149): ADR45 initiator probes server for current generation; v3 generation lives in Zone resource store — generation-discovery excluded from initial d2b-bus copy. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "integration" : "All d2b-bus, d2b-bus-unix, Provider registry, and client packages import v3 protocol constants; v3 Zone API service layer uses `EndpointPurpose`/`ServicePackage` tags for Zone API endpoints", + "removalProof" : "`d2b-contracts/src/v2_component_session.rs` ADR45-specific tags retired after all v2 sessions decommissioned", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`preface-magic-exact-16-bytes`, `preface-offer-len-zero-rejected`, `preface-offer-len-over-max-rejected`, `preface-offer-len-canonical-accepted`, `handshake-21-reject-reasons-all-covered`, `attachment-policy-packet-atomic-requires-seqpacket-or-inherited`, `limit-profile-local-default-all-fields-positive`", + "workItemId" : "ADR046-zone-control-013" + }, + { + "currentSource" : "`nixos-modules/options-realms.nix` (option schema/assertion conventions); `nixos-modules/bundle-artifacts.nix` (bundle emit pattern); `nixos-modules/provider-registry-v2-json.nix` (provider-registry JSON emit); `nixos-modules/assertions.nix:730` (assertion style for unsafe-local); `packages/d2b-realm-core/src/ids.rs` (`LABEL_PATTERN`, `MAX_ID_LEN`, `is_label()` — eval-time name validation target)", + "dataMigration" : "Replace `nixos-modules/options-realms.nix`-derived option trees once Zone controller is live and has reached parity", + "dependencyOwner" : "ADR046-zone-control-001, ADR046-routing-011; Nix module owner", + "destination" : "`nixos-modules/options-zones.nix`, `nixos-modules/generated/resource-types.nix`, `nixos-modules/generated/options-zones-.nix`, `nixos-modules/resource-type-validators.nix`", + "detailedDesign" : "Consume the generator and registry established by ADR046-routing-011 to declare the unified `d2b.zones..resources.` option tree plus the Zone-level compiler-only `parentZone` scalar. The generated standard registry is exactly the canonical 19 types (`Zone`, `ZoneLink`, `Provider`, `Host`, `Guest`, `Process`, `EphemeralProcess`, `Network`, `Volume`, `Credential`, `Device`, `Endpoint`, `User`, `Role`, `RoleBinding`, `Quota`, `EmergencyPolicy`, `ResourceExport`, `ResourceImport`), and every standard type has a strict generated `spec` submodule carrying the schema's Nix types, defaults, bounds, and documentation. Installed Provider artifacts may append only signed qualified ResourceTypes whose strict schema has been verified and generated into the evaluated option set. `type` is selected from that closed combined registry: an unknown standard string, an unqualified extension, or a qualified type without an installed verified schema fails evaluation; there is no unrestricted string or free-form `spec` fallback. Require `parentZone` on every non-root Zone and forbid it on `local-root`; resolve it against declared Zone keys, reject self-parent/conflicting module definitions/cycles, and cap each ancestry path at 16 Zone names. Compile the validated map into sealed allocator bootstrap topology; never emit it into the resource bundle or `Zone.spec`. Declare the global `d2b.artifacts.` catalog with `package` (types.package, required) and `type` (closed enum, required). Provider `spec.artifactId` is a plain catalog ID; the derivation is not a `spec` field. Implement the Phase 1 cross-resource assertions (§14.10 Phase 1 table); retain `credentialRef`, `resourceRef`, and `closedEnum` helpers; reject operator-authored `Zone`; and enforce child-local ZoneLink topology. The runtime creates `Zone/`, `Provider/system-core`, and `Provider/system-minijail` with `managedBy=controller`; none is emitted or inferred from the configuration bundle. `allowUnsafeLocal` maps to the dedicated Host admission gate. Provider manifest-derived fields (`spec.exports`, `spec.components`, `spec.dependencies`, `spec.permissionClaims`, `spec.upgradePolicy`, `spec.restartPolicy`) are read-only and setting one is an eval error.", + "integration" : "ADR046-routing-011 supplies the one canonical 19-type registry and generated option family; validated `parentZone` feeds the allocator bootstrap sealer; the closed `d2b.zones..resources.*` tree is consumed by ADR046-zone-control-015; the Zone controller (ADR046-zone-control-001) reads the resulting bundle; Provider package conventions come from ADR046-zone-control-003", + "removalProof" : "`nixos-modules/options-realms.nix`, `nixos-modules/realm-controller-config-json.nix`, `nixos-modules/realm-identity-config-json.nix` deleted after Zone controller and resource compiler reach full parity; `nixos-modules/assertions.nix` lines referencing `allowUnsafeLocal`/realm names removed after Host admission validation replaces them", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "All Phase 1 eval tests in §15.8 (`nix-eval-name-regex-enforced`, `nix-eval-verb-closed-enum`, `nix-eval-session-verb-closed-enum`, `nix-eval-relay-session-verb-known`, `nix-eval-roleref-format`, `nix-eval-subject-type-restricted`, `nix-eval-no-duplicate-subjects`, `nix-eval-bootstrap-provider-rejected`, `nix-eval-provider-missing-artifact-id`, `nix-eval-artifact-id-not-in-catalog`, `nix-eval-artifact-wrong-type`, `nix-eval-artifact-id-format`, `nix-eval-credentialref-declared`, `nix-eval-dollar-key-rejected`, all five `nix-eval-parent-zone-*` vectors, `nix-eval-zonelink-child-name-mismatch-rejected`, `nix-eval-zonelink-second-uplink-rejected`, `nix-eval-zonelink-limits-maxpendingintents-bound`); Phase 2 runs `nix-build-relay-scope-restricted`; drift test asserts the standard registry and generated option modules cover exactly all 19 canonical types; negative evals reject unknown strings, unqualified extensions, unsigned or uninstalled qualified types, and unknown `spec` fields; a positive fixture admits an installed signed qualified type and validates its strict generated schema", + "workItemId" : "ADR046-zone-control-014" + }, + { + "currentSource" : "`nixos-modules/bundle-artifacts.nix` (artifact emit pattern); `nixos-modules/processes-json.nix` (canonical JSON serialization conventions); `packages/xtask/src/main.rs` `gen-schemas` subcommand (schema-from-derivation pattern); `packages/d2b-core/src/bundle.rs` (current bundle DTO shape for adaptation reference)", + "dataMigration" : "Full reset; no prior bundle state exists to carry forward", + "dependencyOwner" : "ADR046-zone-control-014; resource compiler owner", + "destination" : "`packages/d2b-resource-compiler/src/{main,bundle,schema,validator,digest,sort,secret_lint,generation}.rs`; exposed as `pkgs.d2b-resource-compiler`; called from `nixos-modules/resource-compiler.nix`", + "detailedDesign" : "Implement all Phase 2 build-time checks (§14.10 Phase 2 table): dispatch on `type` to look up ResourceTypeSchema; validate each resource's `spec` canonical JSON against the committed schema (build validation compares canonical rendered JSON against ResourceTypeSchema for each core type); compile the `d2b.artifacts.*` catalog: for each entry, build/include the derivation, verify `type` is a recognized value, compute `digest` over the derivation output, extract and hash manifest and config schema files, validate signature chain and conformance attestation; detect duplicate artifact IDs; for each Provider resource, look up `spec.artifactId` in the compiled catalog (build failure if absent or wrong type), verify `configSchemaDigest` matches schema SHA-256, validate operator `spec.config` against loaded JSON Schema using a pure-Rust validator bundled in the derivation, verify `manifestDigest` and signature chain, load manifest-derived fields (`exports`, `components`, `dependencies`, `permissionClaims`, `upgradePolicy`, `restartPolicy`) into the bundle envelope; emit private integrity-pinned artifact catalog (ID → type/digest/closure metadata) as a separate private file (never merged into the public resource bundle); check `spec.rules[*].resourceTypes` against installed Provider catalogs in the bundle (Role); verify `spec.roleRef` names an existing Role in the bundle (RoleBinding); verify `spec.subjects[*]` names resolve in bundle (RoleBinding); check ResourceType short-name collision across all Zone Providers; RFC 8785 canonical JSON serialization; per-resource `digest` computation; `bundleDigest` computation over sorted `resources` array; inline-secret heuristic lint (`--strict-secrets` flag); `generation` counter persistence in Nix module state; emit `zone-resources.json` bundle with all fields per §14.9", + "integration" : "Reads from `d2b.zones..resources.*` (ADR046-zone-control-014); emits bundle consumed by ADR046-zone-control-001 configuration publication handler; generation counter stored as Nix module derivation input hash (hermetic) or in a NixOS state file (impure) — exact mechanism is implementation decision", + "removalProof" : "No current equivalent; additive only; no prior code removed", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "All Phase 2 build tests in §15.8 (`nix-build-artifact-id-missing-from-catalog`, `nix-build-artifact-wrong-type-rejected`, `nix-build-duplicate-artifact-id`, `nix-build-artifact-store-path-absent-from-bundle`, `nix-build-artifact-store-path-absent-from-config`, `nix-build-config-schema-failure`, `nix-build-schema-digest-mismatch`, `nix-build-manifest-digest-mismatch`, `nix-build-resourcetype-collision`, `nix-build-bundle-sorted`, `nix-build-bundle-digest-stable`, `nix-build-per-resource-digest-correct`, `nix-build-credential-ref-survives-build`, `nix-build-inline-secret-lint-warning`, `nix-build-inline-secret-strict-failure`)", + "workItemId" : "ADR046-zone-control-015" + }, + { + "currentSource" : "`packages/d2bd/src/lib.rs` lines 1408 and 16741 (`RealmControllersJson` load — live active generation load pattern); `nixos-modules/realm-controller-config-json.nix` (current config bundle emit to `/etc/d2b/`); `packages/d2b-realm-core/src/allocator_engine.rs` (generation/activation pattern); `packages/d2b-core/src/unsafe_local_workloads.rs:16–164` (`MAX_UNSAFE_LOCAL_WORKLOADS=256`, etc. — bounds reference for Credential/Host cleanup)", + "dataMigration" : "Full reset; no prior bundle activation state exists to carry forward", + "dependencyOwner" : "ADR046-zone-control-015; ADR046-zone-control-001; configuration publication handler owner", + "destination" : "`packages/d2b-core-controller/src/configuration.rs` (Phase 3 activation, diff, delete dispatch); `packages/d2b-core-controller/src/cleanup.rs` (pending tracking, status, stuck detection, rollback verb handler)", + "detailedDesign" : "Implement all Phase 3 runtime activation checks (§14.10 Phase 3 table); `bundleDigest` integrity verify; `zoneUid` consistency check; `generation` monotone check; per-resource `digest` re-verify; atomic generation advance in `store_meta`; diff computation (resources with `managedBy=configuration` absent from new bundle → async Delete; `managedBy=controller`/`managedBy=api` resources untouched); `managedBy` and `configurationGeneration` field maintenance on resources in redb store; `cleanupPendingCount` and `generationCleanupPending` maintenance; Zone.status.phase → Degraded while cleanup pending, reverts on completion; `GenerationCleanupPending`/`GenerationCleanupFailed` condition management; stuck-cleanup `GenerationCleanupFailed=True` at `cleanupStuckThreshold` (default 5 min) with exponential backoff retry; prior generation bundle retention and pruning up to configured `retainedPriorGenerationCount` (default 3, range 1..16); audit emission for all four cleanup audit kinds (§14.11); `zone.config-rollback` verb handler Primary reuse disposition: `adapt`. Preserved source-plan detail: extract exponential backoff from `begin_reconnect`.", + "integration" : "Zone store / redb (ADR046-store-001); core-controller watch/trigger bus (ADR046-zone-control-011); Zone status writer (ADR046-zone-control-001); audit emitter (§13.2); Credential revocation hook (triggered when `deletionRequestedAt` is set on a Credential and `core.credential-revoke` finalizer is present; revocation completes before finalizer clearance)", + "removalProof" : "`d2bd/src/lib.rs` config-load at lines 1408 and 16741 removed after Zone configuration publication handler reaches parity; `realm-controller-config-json.nix` and `realm-identity-config-json.nix` Nix bundle-emit removed after resource compiler reaches parity", + "reuseAction" : "adapt", + "reuseSource" : "main `a1cc0b2d`: `packages/d2b-session/src/lifecycle.rs` `begin_reconnect` exponential backoff logic (cleanup retry); `packages/d2b-state/` lock/lease types (ADR046-store-001 dependency for bundle file locking)", + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "All Phase 3 runtime and cleanup tests in §15.8 (`nix-runtime-bundledigest-integrity`, `nix-runtime-generation-monotone`, `nix-runtime-zoneuid-mismatch-rejected`, `nix-runtime-zonename-mismatch-rejected`, `nix-runtime-activation-nonblocking`, `nix-runtime-provider-config-invalid-continues`, all `cleanup-*` and `rollback-*` tests)", + "workItemId" : "ADR046-zone-control-016" + }, + { + "currentSource" : "main `a1cc0b2d`: `packages/d2b-provider/src/registry.rs` (`RegistryLimits`, `AdmissionOptions`, `ProviderRegistryBuilder::new/limits/register_factory/register_instance/register_constructed/finish`, `ProviderRegistry::lifecycle/snapshot/instance/admit/shutdown`, `InFlightPermit`, `AdmittedProvider`, `ProviderRegistryManager::new/current/publish` — lines 33–568; tests `shutdown_closes_final_permit_notify_race`, `finish_drain_closes_final_permit_notify_race` — lines 683–691); `packages/d2b-provider/src/rpc.rs` (`SessionIdentity`, `ProviderClock`/`SystemProviderClock`, `RpcCall`, `RpcPayload`, `RpcResponse`, `AuthenticatedProviderRpc`, `RpcProviderProxy::new/preflight/call*`, `session_identity_matches_placement` — lines 27–895; test `provider_and_user_agent_session_identities_are_placement_exact` — lines 923–959); `packages/d2b-provider-toolkit/src/adapter.rs` (`ProviderAgentAdapter::new/invoke_session/invoke` — lines 18–194); `packages/d2b-provider-toolkit/src/conformance.rs` (`ConformanceError`, `check_descriptor_conformance`, `check_provider_conformance` — lines 8–126); `packages/d2b-provider-toolkit/src/fixture.rs` (`DeterministicClock`, `Fixture::new/from_descriptor/operation/request/call_context/session_identity`, `FakeProvider`, `sample_lease_request` — lines 39–262); `packages/d2b-provider-toolkit/src/registration.rs` (`ToolkitError`, `register_exact_instances` — lines 12–107); `packages/d2b-provider-toolkit/src/server.rs` (`GeneratedProviderServiceServer::from_session_handle/new/shutdown/generated_services` — lines 59–176; test `rpc_statuses_retain_closed_actionable_reasons`); `packages/d2b-provider-toolkit/src/values.rs` (`ProviderValues::new/descriptor/health/plan/handle_from_request/handle_from_plan/observation` — lines 18–192); `packages/d2b-provider-toolkit/src/redaction.rs` (`Redacted`, `Secret` — lines 3–37)", + "dataMigration" : "Provider descriptors re-registered on Zone store bootstrap; no v2 provider state migration", + "dependencyOwner" : "ADR046-zone-control-011, ADR046-zone-control-013; Provider registry/toolkit foundation owner", + "destination" : "`packages/d2b-provider/src/{registry,rpc}.rs` (new v3 Provider package); `packages/d2b-provider-toolkit/src/{adapter,conformance,fixture,registration,server,values,redaction}.rs` (new v3 toolkit)", + "detailedDesign" : "**Selected**: `ProviderRegistry` lifecycle `Accepting→Draining→Retired` with drain-waiter notify-race safety (ported from `shutdown_closes_final_permit_notify_race` / `finish_drain_closes_final_permit_notify_race` tests); `InFlightPermit` + global + per-provider in-flight quota; `ProviderRegistryManager::publish` validates snapshot before swap; `RpcProviderProxy::preflight` cancellation-check, deadline-check, method/capability match, session-identity/placement exactness; `ProviderAgentAdapter` rejects attachments with missing descriptors or non-increasing indexes (lines 79–99); `GeneratedProviderServiceServer::new` single-service-per-agent requirement + shutdown via atomic accept-flag + idle notify + timeout; `check_provider_conformance` health/inspection/observability check sequence; `Fixture`/`FakeProvider`/`DeterministicClock` as conformance harness; `Redacted`/`Secret` retained unchanged. **Excluded ADR45 assumptions**: `TrustedFirstPartyInProcess` as the only accepted placement (in `session_identity_matches_placement` lines 577–598): v3 Provider resources support multiple placements per `RealmControllerPlacement` mapping (§16.2); in-process placement is retained but not exclusive. Generated ttrpc stubs in `d2b-contracts/src/generated_v2_services/` are v2-service-specific; v3 Provider processes compile their own stubs from v3 proto files. ACA workload adapter (`d2b-gateway-runtime/src/aca_workload.rs`) excluded. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy + adapt.", + "integration" : "Provider/system-core and Provider/system-minijail use `ProviderRegistryBuilder::register_constructed` as bootstrap exceptions; other Providers register instances from Process-spawned agents via `ProviderAgentAdapter`; Zone runtime hosts one `ProviderRegistry` per installed Provider component; `ProviderRegistryManager::publish` swaps registry on Provider update", + "removalProof" : "`d2b-realm-provider` trait crate removed after v3 Provider resource + registry integration (§16.5)", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`provider-registry-drain-waiter-race-safe` (ported from both notify-race tests), `provider-registry-publish-validates-snapshot-before-swap`, `provider-rpc-proxy-placement-exact` (ported from `provider_and_user_agent_session_identities_are_placement_exact`), `provider-agent-adapter-rejects-non-monotone-attachment-indexes`, `provider-server-shutdown-drains-in-flight-requests`, `provider-conformance-health-inspection-observability-sequence`", + "workItemId" : "ADR046-zone-control-017" + }, + { + "currentSource" : "main `a1cc0b2d`: `packages/d2b-session/src/lifecycle.rs` (`SessionPhase` 5-phase FSM, `begin_reconnect` window/attempt bounds — lines 9–195); `packages/d2b-realm-router/src/service_v2.rs` (`RealmSessionAuthority::local_controller/gateway_peer`, authority validation rejecting invalid combinations — lines 53–123; `RealmServiceServer::call` wire/request/generation/lifetime/attachment validation — lines 415–497; `RealmServiceLimits` 15 fields — lines 147–181; `RealmAuditEvent`/`RealmAuditOutcome` — lines 236–245); `packages/d2b-realm-router/src/session_lifecycle.rs` (`SessionPhase::{Allocating,TokenMinting,RelayConnecting,DisplayOpening,Running,Stopping,Stopped}`, `fail`/`stop`/`finish_stop`, tests `forward_sequence_reaches_running_then_refuses_to_advance`, `failure_mid_establishment_rolls_into_teardown_and_records_phase`, `stop_is_idempotent`, `finish_stop_without_stopping_is_a_no_op` — lines 31–220); `packages/d2b-client/src/session.rs` (`ComponentSessionConnector` trait — lines 79–86; `NamedStream` lifecycle/close/reset/send/receive — lines 426–576); `packages/d2b-daemon-access/src/component_session.rs` (`connect_component_session()` peer-UID verify + `HandshakeCredentials::Nn` + `TransportKind::LocalUnix` — lines 53–85)", + "dataMigration" : "Not applicable; new implementation", + "dependencyOwner" : "ADR046-zone-control-011, ADR046-zone-control-012, ADR046-zone-control-013; ZoneLink session/admission foundation owner", + "destination" : "`packages/d2b-core-controller/src/zone_link.rs` (ZoneLink handler); `packages/d2b-resource-api/src/admission.rs` (request admission)", + "detailedDesign" : "**Selected**: `SessionPhase` 5-phase FSM (from main `d2b-session`) drives ZoneLink session state; `Established` state → `status.connected=true` + ZoneLink `status.phase=Ready`; `Disconnected`/`Reconnecting` states → `status.connected=false` + ZoneLink `status.phase=Pending` (or `Degraded` if degraded capability); session-internal phases are not exposed as `ZoneLink.status.phase` values — only the common Resource phases (`Pending|Ready|Degraded|Failed|Unknown`) appear in `status.phase` (§3.5); `begin_reconnect` window/attempt logic → ZoneLink reconnect loop; `RealmSessionAuthority` local vs gateway pattern → ZoneLink authority types for host-local vs transport-bridge sessions; `RealmServiceServer::call` wire validation (generation, request lifetime, attachment) → Zone API request admission; `RealmServiceLimits` 15 fields → ZoneLink `spec.limits`; `connect_component_session()` Nn peer-UID path → Zone runtime bootstrap ComponentSession; `NamedStream` lifecycle → ZoneLink named-stream operations; session-lifecycle tests ported as ZoneLink phase regression tests. **Excluded ADR45 assumptions**: `RealmSessionAuthority::gateway_peer()` (lines 72–87): gateway custody and `Locality::Remote` + `CredentialCustody::GatewayGuest` are ADR45 realm-gateway patterns; v3 ZoneLink transport is bound by the resolved `spec.transportProviderRef`, `spec.transportSettings`, and `spec.transportCredentials` contract instead. Realm 7-phase `SessionLifecycle` (`Allocating→…→Running→Stopping→Stopped`) is the ADR45 realm-specific lifecycle; ZoneLink uses the 5-phase d2b-session model. `GuestBootstrapPsk`/`GuestSessionCredentialV1`: ADR45 guest bootstrap, excluded. `realm_stubs.rs` `ApiService`/`ApiFrontend` dead code excluded (§16.2).", + "integration" : "Zone runtime startup creates bootstrap ComponentSession using `HandshakeCredentials::Nn` + local domain socket + peer-UID verification (adapted from `connect_component_session`); the child-local ZoneLink handler opens its allocator-bound uplink session using d2b-bus `SessionEngine`, and the parent routes child calls over that session; resource API admission validates requests using the `RealmServiceServer::call` pattern", + "removalProof" : "`d2b-realm-router/src/service_v2.rs` `RealmServiceServer` removed after Zone API service layer replaces realm v2 service; `d2b-realm-router/src/session_lifecycle.rs` removed after d2b-bus lifecycle replaces realm-specific one", + "reuseAction" : "adapt", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "`zonelink-reconnect-child-uid-change`, `zonelink-disconnect-unknown-phase`, `zonelink-intent-queue-limit`, `zone-session-phase-forward-sequence-refuses-repeat` (ported from `forward_sequence_reaches_running_then_refuses_to_advance`), `zone-session-failure-records-phase` (ported from `failure_mid_establishment_rolls_into_teardown_and_records_phase`), `zone-session-stop-is-idempotent` (ported), `zone-bootstrap-session-nn-peer-uid-verified`", + "workItemId" : "ADR046-zone-control-018" + }, + { + "currentSource" : "None — net-new ADR 0046 cross-Zone sharing model (D096); no pre-ADR45 baseline equivalent", + "dataMigration" : "None — full d2b 3.0 reset; no prior cross-Zone sharing state", + "dependencyOwner" : "ADR046-zone-control-001, ADR046-provider-004; ADR046-zonelink owner; `d2b-core-controller` + `d2b-contracts` owners", + "destination" : "`packages/d2b-contracts/src/v3/{resource_export,resource_import}.rs` (base schemas); `packages/d2b-core-controller/src/export_import.rs` (core ZoneLink export/import routing controller); shared adapter trait in `packages/d2b-provider/src/share_adapter.rs` (`ExportAdapter`/`ImportAdapter` signed-capability traits)", + "detailedDesign" : "Implement the `ResourceExport` and `ResourceImport` standard ResourceTypes per §8A plus signed Provider `ProjectionFactory` metadata binding qualified Service type, qualified Binding type, allowed owner-Service backing refs, allowed Binding target refs, projection schema/fingerprint, and aggregate factory fingerprint. Admission accepts only an owner Service as `ResourceExport.resourceRef`; matches export/import/local-factory type and fingerprints; and creates exactly one same-qualified-type projection Service (`ownerRef: ResourceImport/`). It never projects Device/Endpoint/Binding and never creates Binding. Binding spec is desired consumer intent only; observations belong only in status. No cross-Zone Ref, FD, secret, path, locator, or resource grant crosses a Zone; payload bytes use bounded encrypted named streams and high-churn sessions/streams remain internal. Export removal/ZoneLink loss revokes leases and degrades the projection Service; reconnect revalidates generation and both fingerprints. D091 currency propagates Service → export → import → projection Service → authored Binding → children. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (extend ZoneLink controller).", + "integration" : "Zone store/redb (ADR046-store-001); shared D098 semantic base catalog (ADR046-provider-004); ZoneLink reconcile (§3); ComponentSession bounded encrypted named streams; signed projection factories/adapters for audio-pipewire, device-security-key, observability-otel, and policy-gated device-usbip; CLI graph rendering", + "removalProof" : "Not applicable (new surface)", + "reuseAction" : "adapt", + "reuseSource" : "ZoneLink reconcile/handler scaffolding (§3); `packages/d2b-session/src/streams.rs` `NamedStream` credit/backpressure (bounded encrypted stream carriage)", + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "§8A.7: fast hermetic factory absent/mismatch/tamper, Service-only export target, exactly-one same-type projection Service, no Device/Endpoint/Binding projection, no auto-Binding, intent-only spec/status-only observations, backing/target allowlists, finalizer/update propagation, Provider classification, canonical Nix stability including compiler-only `d2b.zones.work.parentZone = \"local-root\"`, child-local `ZoneLink/work-uplink` in `d2b.zones.work.resources`, local `zoneLinkRef` resolution, quotas/reconnect/revoke, and no FD/secret/path tests; slower real encrypted-stream integration for audio/security-key/observability/policy-gated USBIP", + "workItemId" : "ADR046-zone-control-019" + }, + { + "currentSource" : "None — net-new ADR 0046 cross-Zone sharing model (D096); the core owner/child reconcile machinery (§11) is prior-art design context only", + "dataMigration" : "None — full d2b 3.0 reset", + "dependencyOwner" : "ADR046-zone-control-019; `d2b-core-controller` owner", + "destination" : "`packages/d2b-core-controller/src/export_import_projection.rs` (local qualified Service projection lifecycle owned by `ResourceImport`)", + "detailedDesign" : "Core creates exactly one same-qualified-type projection Service per `ResourceImport` and keeps it synchronized with the remote Service lease. Operators/Nix separately author same-Zone matching Binding resources with `serviceRef` plus an allowed Guest/User/Zone target; Binding specs hold desired intent only and Binding controllers write observations only to status while owning Process/Endpoint children. On revoke, mark the projection draining/revoked and let Binding controllers stop children. On delete, wait for Bindings to be deleted/retargeted (`BindingReferencesRemain`), release the lease, delete only the projection Service/provider-owned children, then clear the import finalizer. Never create/delete Binding or project Device/Endpoint.", + "integration" : "ADR046-zone-control-019 controller; owner/dependency reconcile (§11, ADR046-reconcile-*); local semantic Provider import adapter", + "removalProof" : "Not applicable (new surface)", + "reuseAction" : "create", + "reuseSource" : null, + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "Exactly one same-type Service projection owned by import; no Device/Endpoint/Binding projection; Binding never auto-created/deleted; Binding target allowlist; intent-only spec/status-only observations; owned Process/Endpoint child cleanup; pending finalizer while Binding refs remain; reconnect only after generation/factory/schema revalidation; hermetic fake-adapter + real-stream integration tiers", + "workItemId" : "ADR046-zone-control-020" + }, + { + "currentSource" : "`packages/d2bd/src/` process-global statics `USBIP_BACKGROUND_RECONCILE_ACTIVE`, `FORCE_SHUTDOWN_GENERATIONS`, and `activation_locks()`; current per-VM configuration staging symbols; ZoneLink cursor persistence in `zone_link_cursors`", + "dataMigration" : "Full d2b 3.0 reset; no process-global state persisted across the cutover", + "dependencyOwner" : "ADR046-zone-control-001, ADR046-zone-control-016; `d2b-core-controller` owner", + "destination" : "`packages/d2b-core-controller/src/{coordinator,configuration,zonelink}.rs`", + "detailedDesign" : "Per D097 core-audit findings (§8B.2): move the process-global `USBIP_BACKGROUND_RECONCILE_ACTIVE`, `FORCE_SHUTDOWN_GENERATIONS`, and `activation_locks()` state into **per-Zone** provider/resource status or a per-Zone coordinator keyed by the authority index (no process-global singletons that ignore Zone boundaries). Migrate the configuration publisher's per-VM staging symbols to **per-Zone** staging under the single configuration-publisher authority. Make ZoneLink cursor persistence and restart adoption an authority owned by the ZoneLink handler (`ownerProof`; ambiguity quarantines). All coordinated through the core authority index; no direct broker path, no process-global lock. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` (move process-global state to per-Zone status/coordinator).", + "integration" : "Core authority index (ADR046-zone-control-019); ZoneLink reconcile (§3); configuration publication handler (ADR046-zone-control-016)", + "removalProof" : "The process-global statics and per-VM staging symbols are deleted after the per-Zone coordinator reaches parity; confirmed by `cargo check` and a no-process-global lint", + "reuseAction" : "adapt", + "reuseSource" : "ZoneLink handler/cursor scaffolding (§3); core coordinator patterns", + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "Two Zones on one host do not share `USBIP_BACKGROUND_RECONCILE_ACTIVE`/`FORCE_SHUTDOWN_GENERATIONS`/activation-lock state; per-Zone configuration staging isolation; ZoneLink cursor adoption by `ownerProof` and quarantine on ambiguity; hermetic with fakes", + "workItemId" : "ADR046-zone-control-021" + }, + { + "currentSource" : "None — net-new D097 admission (Provider cardinality)", + "dataMigration" : "None — full d2b 3.0 reset", + "dependencyOwner" : "ADR046-zone-control-019, ADR046-api-001; `d2b-core-controller` + resource API owners", + "destination" : "`packages/d2b-core-controller/src/authority.rs`; resource API admission hook", + "detailedDesign" : "Admission enforces **Provider controller cardinality** via the core authority index: most Providers are `exactly-one` per Zone; the observability Provider is `at-most-one` (zero-or-one). A `Create`/activation that would install a second controller for an `exactly-one` Provider (or a second observability Provider) is rejected with `duplicateConflict` naming the incumbent owner digest before any effect; config activation goes `Degraded`.", + "integration" : "Core authority index; resource API admission; configuration activation", + "removalProof" : "Not applicable (new surface)", + "reuseAction" : "adapt", + "reuseSource" : "Core authority index (ADR046-zone-control-019); resource API admission (`ADR-046-resource-api-and-authorization`)", + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "Second Provider controller for an `exactly-one` Provider rejected with `duplicateConflict`; second observability Provider rejected; single controller admitted; `Degraded` config activation names the incumbent digest; hermetic", + "workItemId" : "ADR046-zone-control-022" + }, + { + "currentSource" : "None — `Quota` and `EmergencyPolicy` scope-uniqueness are specified (§7, §8) but not implemented/tested at baseline", + "dataMigration" : "None — full d2b 3.0 reset", + "dependencyOwner" : "ADR046-zone-control-019; `d2b-core-controller` + `d2b-contracts` owners", + "destination" : "`packages/d2b-core-controller/src/{quota,emergency_policy}.rs`; `packages/d2b-contracts/src/v3/{quota,emergency_policy}.rs`", + "detailedDesign" : "Implement `Quota` and `EmergencyPolicy` scope-uniqueness as `exactly-one`-per-scope authorities in the core authority index: a second `Quota`/`EmergencyPolicy` claiming the same scope is a `duplicateConflict`. Add the scope-uniqueness admission, status, and the test matrix (per D094 fast hermetic tests). Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (implementation + tests).", + "integration" : "Core authority index (ADR046-zone-control-019); resource API admission", + "removalProof" : "Not applicable (new implementation)", + "reuseAction" : "adapt", + "reuseSource" : "`Quota`/`EmergencyPolicy` schemas (§7, §8); core authority index", + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "Duplicate-scope `Quota`/`EmergencyPolicy` rejected with `duplicateConflict`; single-scope admitted; union/individual scope flags honored; fast hermetic tests", + "workItemId" : "ADR046-zone-control-023" + }, + { + "currentSource" : "None — net-new D097 hardware-audit contract; today physical/kernel backings are guarded per-Zone or per-process, not Host-global", + "dataMigration" : "None — full d2b 3.0 reset", + "dependencyOwner" : "ADR046-zone-control-019, ADR046-zone-control-022; `d2b-core-controller` owner", + "destination" : "`packages/d2b-core-controller/src/authority.rs` (Host-global index scope + hardware admission)", + "detailedDesign" : "Extend the core authority index so `host`, `physical-device`, `seat`, and `external-service` authorities are keyed **Host-global** (`(Host, authorityClass, opaqueKeyDigest)`), admitting exactly one owner across all Zones on the host, while `zone`-scoped authorities stay Zone-local. Enforce the §8B.3 hardware rows: GPU full-device exclusive vs render-node shared; per-Guest swtpm and physical TPM exclusive (state never wiped); one Core-derived `physical-usb-backing/v1` identity digest claimed through the exact `(Host, physical-usb-backing, opaqueKeyDigest)` tuple by every USB or security-key implementation before effects, plus the separate host-global `usbip-host` module and the Host-global `Provider/device-usbip` relay `Endpoint`, exactly one per Core-derived Network UID/signed-policy-port digest with multiplexed arbitration and `usbip-network-relay-authority-conflict` on a second owner; macvtap/NIC `parentInterface` `passthru` globally exclusive across all Zones; host-shared `/dev/kvm` and `/dev/vhost-vsock` as `Provider/system-core` grants (no 28th Provider, no `kvm` busClass); globally-unique vsock CID; other fixed listener ports as `Endpoint`s; host store + per-Guest store-view writer; Network TAP/bridge. A second Zone claiming the same physical backing receives `physical-usb-backing-conflict` before any open, bind, withhold, module, relay, or attachment effect; restart adopts by `ownerProof`; Guest-stop drains dependent leases. GPU-owned `udmabuf`/video and per-session `vhost-vsock` tokens stay authority subresources/DeviceGrants (not resources/Providers). Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (Host-global index scope for host/physical-device authorities).", + "integration" : "Core authority index (ADR046-zone-control-019); Provider cardinality admission (ADR046-zone-control-022); `Provider/system-core` KVM/vhost-vsock grant; `Provider/device-*` and `Network` authorities", + "removalProof" : "Not applicable (new surface)", + "reuseAction" : "adapt", + "reuseSource" : "Core authority index (ADR046-zone-control-019)", + "specId" : "ADR-046-resources-zone-control", + "specPath" : "docs/specs/ADR-046-resources-zone-control.md", + "validation" : "Two Zones on one host cannot both claim one GPU/TPM/USB/`/dev/kvm`/passthru NIC/vsock CID/fixed port — second is `duplicateConflict`; security-key and USB implementations resolving the same physical token submit byte-identical `physical-usb-backing` tuple keys and the loser receives `physical-usb-backing-conflict` before any effect; a second USBIP relay `Endpoint` for one Core-derived Network UID/signed-policy-port digest receives `usbip-network-relay-authority-conflict`, while multiple admitted Services share the multiplexed owner and no `Network` authority owns the listener/firewall; Provider-private authority classes/digests cannot bypass the collision; render-node shared admits bounded holders; per-Guest swtpm exclusive and marker never wiped; host-global adoption by `ownerProof`; hardware D096 exportability (GPU/KVM/TPM/store/macvtap non-exportable; semantic USB policy-gated); fast hermetic with fakes", + "workItemId" : "ADR046-zone-control-024" + } + ], + "schemaVersion" : 1, + "status" : "Proposed" +} diff --git a/docs/specs/ADR-046-zone-routing.md b/docs/specs/ADR-046-zone-routing.md new file mode 100644 index 000000000..248ac5eba --- /dev/null +++ b/docs/specs/ADR-046-zone-routing.md @@ -0,0 +1,2628 @@ +# ADR 0046 Zone routing + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-zone-routing` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-zone-routing`, `d2b-bus` Zone route engine, core-controller ZoneLink handler | +| Depends on | `ADR-046-terminology-and-identities`, `ADR-046-componentsession-and-bus`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-object-model`, `ADR-046-resource-reconciliation` | +| Supersedes | `RealmEntrypointTable`/`EntrypointMode`/`RealmControllerPlacement` enum (→ compiler-only `parentZone` topology + ZoneLink transport + private Zone runtime bootstrap placement); `RouteTreeEngine`/`routing.rs` route contracts (adapted, not deleted); `RemoteNodeRegistry` (→ ZoneLink controller); `OperationRouter` (→ ZoneLinkIdempotencyKey in d2b-bus); `realm_access_resolver` module (→ ZoneEntrypointResolver); `realm-controllers.json`/`realm-identity.json` bundle artifacts (→ Nix-authored Zone settings/ZoneLink resources); `d2b.realms.*` Nix option namespace (→ `d2b.zones.*`); `WorkloadId` routing targets (→ typed Guest/Host resource refs); `CapabilitySet`-only routing authz (→ per-hop RBAC narrowing + allocator-issued capability scope) | + +## Source and reuse policy + +The pre-ADR-0045 v3 baseline has a mature pure tree-routing engine in +`d2b-realm-core/src/route_engine.rs` and a matching routing contract in +`routing.rs`. These are the primary baselines. The `d2b-realm-router` +operation router owns principal/capability/idempotency and is the second +primary baseline. + +Main commit `a1cc0b2da4a08ca3240a770a972fe4da6f912bef` contains an +in-progress ADR 0045 ComponentSession and Provider implementation. It is +**not** current v3 behavior — it lives on main and not in the pre-ADR45 +baseline — but it is the primary reuse source for bus/session/transport/ +client/provider work. The per-crate inventory follows. Each entry records +exact file/symbol/test, selected behavior, v3 destination, and the ADR45 +assumptions that are excluded. + +## Main commit reuse inventory + +All sources in this section are from main commit +`a1cc0b2da4a08ca3240a770a972fe4da6f912bef`. They are NOT currently present +in the pre-ADR45 v3 baseline. Do not cite them as v3 baseline behavior. + +### d2b-session — ComponentSession v2 runtime + +**Crate**: `packages/d2b-session/` + +| Module | Key symbols | Selected behavior | +| --- | --- | --- | +| `src/handshake.rs` | `NoiseHandshake`, `HandshakeCredentials` (Nn/Kk/IKpsk2), `EstablishedHandshake`, `NegotiatedOffer`, `encode_offer`, `negotiate_offer`, generation discovery functions | Three Noise profiles (25519·ChaChaPoly·SHA256); prologue = preface‖canonical‑offer binds offer to transcript; INIT/ACCEPT authenticated payloads; generation discovery pre-handshake (SHA256 binding); fail-closed on all key/payload/step errors | +| `src/lifecycle.rs` | `SessionLifecycle`, `SessionPhase` (Established/Disconnected/Reconnecting/Closing/Closed), `KeepaliveAction` | Keepalive ping/pong with nonce tracking; reconnect up to `MAX_RECONNECT_ATTEMPTS=10` within `MAX_RECONNECT_WINDOW_MS=300000`; generation increment on each reconnect; deterministic close record with `CloseReason`/`Remediation` | +| `src/streams.rs` | `NamedStreamMux`, `StreamId`, `StreamPhase` (Open/HalfClosedLocal/HalfClosedRemote/Closed/Reset), `StreamEvent` | Credit-based named-stream state machine; max `MAX_ACTIVE_NAMED_STREAMS=128`; per-stream send/receive credit; half-close, reset, remove-terminal; send-credit reservation/refund; receive-credit release | +| `src/cancellation.rs` | `Cancellation` (Arc+AtomicBool+Notify), `RequestRegistry` | Per-generation request registry; cancel-before/after-dispatch distinction; `CancelResult` (5 variants); `cancel_all` on session close; zero allocation `cancelled()` async wait | +| `src/attachment.rs` | `OwnedAttachment`, `AttachmentPayload` (trait: `validate_descriptor`/`close`/`as_any`/`into_any`), `AttachmentValidationError` | Transport-observed properties validated against authenticated descriptor post-decryption; close exactly once (Drop impl guards double-close); unbound/pre-bound states; transfer without close via `into_payload` | +| `src/driver.rs` | `ComponentSessionDriver` (trait, 20 async methods), `SessionDriverHandle` (clonable Arc wrapper) | Object-safe async trait; ttrpc send/receive; inbound call registration/completion/removal; attachment send/receive; named stream open/send/receive/credit/close/reset; keepalive drive; control receive; session close; handle serializes requests via `Arc` | +| `src/engine.rs` | `SessionEngine`, `SessionEvent` | Full async drive loop over `OwnedTransport`; reassembles fragments; dispatches record kinds (ttrpc, named-stream, attachment, keepalive, cancel, close); integrates scheduler/mux/protector | +| `src/transport.rs` | `OwnedTransport` (trait), `TransportPacket`, `TransportDescriptor`, `TransportError` | Transport-agnostic packet send/receive/close; packet carries bytes + `Vec`; descriptor declares class/locality/atomic/attachment-support | +| `src/scheduler.rs` | `FairScheduler`, `QueueClass` (Control/Named/Attachment), `OutboundFrame` | Round-robin interleaving across named-stream queues; credit-gated named sends; control queue bypass; bounded queues per `LimitProfile` | +| `src/record.rs` | `RecordProtector`, `ProtectedRecord` | AEAD protect/unprotect with directional sequence number; replay cache (1024 entries); sequence window enforcement | +| `src/bootstrap.rs` | `Secret32` (zeroizing), `BootstrapPsk`, `AdmittedBootstrapPsk`, `BootstrapAdmission` | Single-use PSK admission; binding to operation; consumed-once checked at runtime | +| `src/metrics.rs` | `MetricEvent`, `MetricsSink`, `NoopMetrics` | Transport/session/scheduler metric events; injected via generic; default no-op | + +**Tests** (`packages/d2b-session/tests/component_session.rs`, +`tests/noise_vectors.rs`): + +| Test function | Covers | +| --- | --- | +| `fixed_negotiation_and_all_noise_profiles_are_strict` | Offer/policy negotiation; schema fingerprint binding | +| `public_daemon_handshake_rejects_a_guest_only_schema_peer` | Cross-purpose handshake rejection | +| `protected_records_are_directional_sequenced_and_replay_safe` | Record protect/unprotect/replay | +| `protected_record_boundaries_and_tampering_fail_closed` | Truncation/tamper detection | +| `fragmentation_is_bounded_and_rejects_reordering` | Fragment reassembly ordering | +| `deadline_intersects_wall_monotonic_and_ttrpc_budgets` | DeadlineBudget wall/mono/ttrpc intersection | +| `cancellation_is_generation_bound_and_shared` | RequestRegistry per-generation; cancel before/after dispatch | +| `lifecycle_keepalive_close_and_reconnect_change_generation` | SessionLifecycle state transitions | +| `named_stream_state_and_scheduler_have_independent_credit_and_fairness` | NamedStreamMux + FairScheduler credit | +| `bootstrap_is_operation_bound_expiring_single_use_and_redacted` | BootstrapAdmission single-use | +| `local_generation_discovery_establishes_the_authenticated_generation` | Pre-handshake generation discovery | +| `engine_drives_fragmented_ttrpc_and_request_cancellation` | SessionEngine ttrpc + cancel | +| `driver_handle_is_clonable_object_safe_and_leaves_ttrpc_correlation_to_adapters` | ComponentSessionDriver object safety | +| `driver_fragments_one_mib_logical_stream_under_256_kib_credit` | 1 MiB fragmentation | +| `driver_withholds_logical_delivery_credit_until_grant` | Named-stream credit backpressure | +| `engine_binds_acknowledges_and_releases_owned_attachments` | Attachment bind/ack/close | +| `engine_reconnect_rehandshakes_with_the_next_generation` | Reconnect + generation increment | +| `every_canonical_w2_vector_verifies_exactly_with_snow_0_10` | Noise test vectors (canonical) | + +**V3 destination**: `packages/d2b-bus/src/session/` — adapt as d2b-bus +ComponentSession layer; rename where ADR45-specific names appear. + +**Excluded ADR45 assumptions**: +- `serve_ttrpc_services` binds the ADR45 fixed 4-unit local-root endpoint + set; v3 uses allocator-issued sockets per Zone. The function is excluded; + the socket-plumbing pattern is adapted per Zone transport Provider. +- `EndpointPurpose::GuestBootstrap`/`GuestDirect` are ADR45 guest bootstrap + paths; v3 guest enrollment goes through Zone resource model. Purposes are + extended/renamed in v3 contracts; see ADR046-routing-009. +- `GUEST_SESSION_CREDENTIAL_*` constants and credential embedding in the + handshake offer are ADR45 guest bootstrap wire; excluded from v3 + ZoneLink session bootstrap. + +--- + +### d2b-contracts/src/v2_component_session.rs — wire contract + +**File**: `packages/d2b-contracts/src/v2_component_session.rs` + +| Symbol class | Key symbols | Selected behavior | +| --- | --- | --- | +| Protocol constants | `PREFACE_LEN=16`, `PREFACE_MAGIC`, `COMPONENT_SESSION_MAJOR/MINOR=2.0`, `MAX_ACTIVE_NAMED_STREAMS=128`, `MAX_PACKET_ATTACHMENTS=32`, `MAX_REQUEST_ATTACHMENTS=64`, `MAX_OPERATION_ATTACHMENTS=128`, `MAX_SESSION_ATTACHMENTS=256`, `MAX_PROCESS_ATTACHMENT_CREDITS=2048`, `MAX_HOST_ATTACHMENT_CREDITS=8192`, `RESERVED_CONTROL_FDS=64`, `MAX_NAMED_STREAM_QUEUE_BYTES=262144`, `MAX_AGGREGATE_NAMED_STREAM_QUEUE_BYTES=4194304`, `MAX_LOGICAL_MESSAGE_BYTES=1048576`, `LOCAL_HANDSHAKE_DEADLINE_MS=5000`, `REMOTE_HANDSHAKE_DEADLINE_MS=15000`, `MAX_RECONNECT_ATTEMPTS=10`, `MAX_RECONNECT_WINDOW_MS=300000`, `MAX_REQUEST_LIFETIME_MS=900000` | Hard-coded limits carried as const; all validation is fail-closed against these | +| Preface | `ComponentSessionPreface` (`PREFACE_MAGIC` + 2.0 + offer_len); `PrefaceError` | 16-byte fixed-length prefix before canonical offer; version check fail-closed | +| Offer / policy | `HandshakeOffer` (canonical 148-byte encoding), `EndpointPolicy`, `EndpointPolicyIdentity` (140 bytes), `NoiseProfile` (Nn/Kk/IKpsk2), `LimitProfile` (14 fields), `AttachmentPolicy`, `TransportBinding` | Offer must round-trip through canonical binary encoding; prologue = preface‖canonical-offer binds to Noise transcript | +| Enumerations | `EndpointPurpose`, `PurposeClass`, `EndpointRole`, `ServicePackage`, `Locality`, `TransportClass`, `AttachmentPolicyKind` | Closed enumerations with stable u8 tags; `closed_enum!` macro enforces exhaustive matching | +| Attachment | `AttachmentDescriptor`, `AttachmentKind`, `KernelObjectType`, `AttachmentAccess`, `AttachmentCreditClass`, `AttachmentPacket` | Descriptor is encrypted inside the protected record; attachment indexes are packet-sequence-bound; credit classes are Packet/Request/Operation/Session/Process/Host | +| Channel / record | `ChannelId` (control=0, named≥0x0100), `RecordHeader` (24 bytes), `FragmentHeader` (24 bytes), `RecordKind` | Named-stream channel IDs start at 0x0100; record header binds kind/channel/sequence/length | +| Session control | `SessionErrorCode` (20 codes), `CloseReason`, `Remediation`, `CancelRequest/Ack/Result`, `KeepaliveRecord` | Wire-stable error codes; remediation advises Retry/ReplaceGeneration/Permanent | +| Misc | `BoundedVec`, `ContractError`, `BinaryError` | Deserialize-time bounds enforcement; fail-closed BinaryError propagation | + +**V3 destination**: `packages/d2b-contracts/src/v3/zone_session.rs` — copy +constants and types; re-freeze protobuf field numbers separately for v3; +add v3-specific `ServicePackage` variants (`d2b.zone.v3`, `d2b.resource.v3`); +rename `EndpointRole` variants where ADR45 names appear. + +**Excluded ADR45 assumptions**: +- `GUEST_SESSION_CREDENTIAL_*` constants/types are ADR45 guest bootstrap + wire; excluded from v3 ZoneLink sessions. +- `GUEST_BOOTSTRAP_CREDENTIAL_*` types excluded. +- Protobuf field numbers for existing v2 services must NOT be reused; + v3 service IDs freeze independently. +- `EndpointPurpose` tag values for ADR45 purposes (e.g. `GuestBootstrap`, + `ChildRealmController`) are excluded; v3 purposes freeze new tags. + +--- + +### d2b-session-unix — Unix transport and FD credit + +**Crate**: `packages/d2b-session-unix/` + +| Module | Key symbols | Selected behavior | +| --- | --- | --- | +| `src/adapter.rs` | `UnixSeqpacketTransport`, `UnixStreamTransport`, `PeerIdentityPolicy` (Accepted/Pathname/InheritedSocketpair), `UnixAttachmentPayload`, `OwnedUnixAttachment` | Seqpacket: atomic packet + ancillary FDs; stream: framed with 2-byte record-length prefix; peer credentials verified on first packet for inherited sockets; pathname transport verifies provenance | +| `src/credit.rs` | `CreditPool`, `CreditScopeSet`, `CreditBundle`, `ProcessCreditLimit`, `CreditScope` (6 variants: Packet/Request/Operation/Session/Process/Host) | Multi-scope FD credit reservation with rollback; process limit derived from observed open-FD count; emergency headroom reserved for Process/Host scopes | +| `src/descriptor.rs` | `PeerCredentials`, `PidfdIdentityPolicy`, `DescriptorPolicy`, `VerifiedPacket`, `ObjectIdentity`, `AcceptedAttachment` | Pidfd identity: requires live launch evidence (`/proc//fdinfo`); same-kernel-object check via `st_dev`/`st_ino`/`file_type`; descriptor validated post-authentication | +| `src/socket.rs` | `SeqpacketSocket`, `StreamSocket` | Async wrappers over OS sockets | +| `src/systemd.rs` | `InheritedSocketTransport` | SD_LISTEN_FDS activation (ADR45 path — see exclusions) | +| `src/vsock.rs` | vsock transports | NativeVsock/CloudHypervisorVsock (ADR45/Provider transport — see exclusions) | + +**Tests** (`packages/d2b-session-unix/tests/unix_session.rs`): + +| Test function | Covers | +| --- | --- | +| `ancillary_capacity_is_derived_from_closed_hard_bounds` | CreditScopeSet capacity derivation | +| `process_limit_preserves_emergency_headroom` | ProcessCreditLimit headroom | +| `failed_multiscope_reservation_rolls_back_every_prior_scope` | CreditBundle rollback | +| `staged_credit_reservations_release_once_at_each_scope` | Release idempotency | +| `inherited_passcred_is_verified_but_never_repaired` | SO_PASSCRED passthrough | +| `first_packet_has_exact_directional_credentials` | First-packet credential check | +| `seqpacket_transfer_is_atomic_cloexec_and_object_exact` | FD transfer atomicity | +| `duplicate_kernel_objects_are_rejected_and_cleaned_up` | Duplicate FD rejection | +| `owned_transport_adapters_transfer_packets_and_owned_files_end_to_end` | End-to-end seqpacket | +| `stream_transport_reassembles_partial_and_coalesced_records` | Stream framing | +| `pidfd_identity_requires_live_launch_evidence_and_rejects_unrelated_process` | Pidfd identity | +| `payload_and_control_truncation_scavenge_received_files` | FD scavenge on truncation | + +**V3 destination**: `packages/d2b-bus/src/transport/unix.rs` — copy +`UnixSeqpacketTransport`/`UnixStreamTransport`/credit modules verbatim; +adapt `PeerIdentityPolicy` for v3 Zone principal model. + +**Excluded ADR45 assumptions**: +- `src/systemd.rs` (`InheritedSocketTransport`, SD_LISTEN_FDS) is tied to + the ADR45 fixed 4-unit PID1 socket activation. In v3, Zone local sockets + are pre-bound by the allocator and handed as FDs to the Zone runtime; + they are inherited-socket, not systemd-SD_LISTEN_FDS. Adapt + `InheritedSocketTransport` to receive the allocator-issued FD directly + instead of from the SD_LISTEN_FDS environment variable. +- `src/vsock.rs` vsock transports are transport-Provider-specific paths in + v3; they are not hardcoded as Zone 0 transport. Adapt as a vsock + `TransportProvider` implementation. + +--- + +### d2b-client — async typed client + +**Crate**: `packages/d2b-client/` + +| Module | Key symbols | Selected behavior | +| --- | --- | --- | +| `src/client.rs` | `Client`, `ConnectedClient`, `CallOptions`, `CancellationToken`, `RetryPolicy`, `MetadataInput`, `Response` | Generic over `TargetResolver`+`ComponentSessionConnector`+`WallClock`; `connect()` returns `ConnectedClient`; `invoke` / `invoke_with_attachments` with retry and cancellation; named-stream helper `named_stream`; relative timeout from wall clock + request lifetime | +| `src/service.rs` | `ServiceHandle`, `ServiceKind` (25 variants), `GeneratedClient` (25 variants), `MethodHandle` | Service inventory driven by `SERVICE_INVENTORY`; generated ttrpc client per service kind; raw ttrpc invocation with timeout | +| `src/session.rs` | `ConnectedSession`, `ComponentSessionConnector` (trait), `SessionFailure`, `StreamDispatcher`, `SessionCall`, `SessionReply` | Session connector trait; stream dispatcher owns per-stream inbox/outbox with credit accounting; `SessionFailure` (BeforeDispatch/Retryable/Ambiguous/Disconnected/Deadline/Cancelled/Protocol) | +| `src/target.rs` | `ServiceOwner`, `TargetInput`, `TransportKind`, `ResolvedTarget`, `RouteRecord`, `RouteTable`, `TargetResolver` (trait), `TransportSelection` | Static route table resolver; exact-transport selection; fail-closed on missing/ambiguous routes | +| `src/daemon_service.rs` | `DaemonClient`, `DaemonMethod`, `DaemonLifecycleRequest`, `DaemonTerminal` | Typed daemon service proxy | +| `src/guest_service.rs` | `GuestClient`, `GuestOperation`, `GuestInspectCall`, `GuestCancelCall`, `GuestRetainedLogCall` | Typed guest service proxy | +| `src/host_socket.rs` | `HostSocketConnector`, `local_daemon_endpoint_identity`, ttrpc pump | Local Unix daemon connection; MAX_IN_FLIGHT_REQUESTS=128; ttrpc bridge over ComponentSession driver | +| `src/error.rs` | `ClientError`, `RemoteErrorKind`, `RetryClass` | Typed error hierarchy; retry classification | + +**Tests** (`packages/d2b-client/tests/client.rs`): + +| Test function | Covers | +| --- | --- | +| `daemon_guest_proxy_reuses_the_authenticated_session` | Session reuse for guest proxy | +| `absent_daemon_guest_proxy_fails_closed_without_reconnecting` | Fail-closed on missing proxy | + +**V3 destination**: `packages/d2b-resource-client/` — adapt as v3 typed +`ResourceClient` and `ZoneServiceClient`; rename `ServiceOwner/TargetInput` +variants from `Realm/Workload` to `Zone/Guest/Host`; add +`TargetInput::ZoneService(ZonePath, ZoneServiceKind)` variant; +replace `ServiceKind` with v3 service inventory. + +**Excluded ADR45 assumptions**: +- `ServiceOwner::LocalRoot/Realm/Workload/Provider` ADR45 naming → v3 + uses `Zone/ZonePath/Guest/Host`; the variants are renamed, not reused. +- `ServiceKind` list contains ADR45-specific services (`Realm`, `Broker`, + `RuntimeSystemdUser`, `Shell`, etc.). V3 extends with `Resource`, + `Zone`, and `ZoneLink` service kinds; the existing 25 variants are + evaluated case-by-case. The service inventory serialization format + (tag assignment) must be re-frozen for v3. +- `HostSocketConnector` derives the daemon peer uid from `d2bd` system + username; v3 Zone sockets use allocator-issued FDs with authenticated + static-key pinning, not uid-based trust. +- `CONTROLLER_PIDFD_ATTACHMENT_INDEX`/`BROKER_PIDFD_ATTACHMENT_INDEX` + attachment conventions are ADR45 pidfd supervision wire; excluded from + v3 Zone session bootstrap. + +--- + +### d2b-provider/src — Provider registry and RPC proxy + +**Crate**: `packages/d2b-provider/src/` + +| Module | Key symbols | Selected behavior | +| --- | --- | --- | +| `registry.rs` | `ProviderRegistry`, `ProviderRegistryBuilder`, `ProviderRegistryManager`, `RegistryLimits` (total_in_flight, per_provider_in_flight), `AdmissionOptions`, `InFlightPermit`, `AdmittedProvider` | Registry with lifecycle (Accepting→Draining→Retired); per-provider and total in-flight concurrency caps; `admit()` returns `InFlightPermit` RAII guard; async `shutdown()` waits for drain; `ProviderRegistryManager` handles live registry swap | +| `rpc.rs` | `AuthenticatedProviderRpc` (trait: `session_identity`/`invoke`), `RpcProviderProxy`, `RpcCall`, `RpcResponse`, `RpcOperation`, `RpcPayload`, `SessionIdentity` | Client-side proxy: translates `RpcCall` → typed Provider operation → ttrpc; validates session identity against descriptor; validates capability, health, observation, handle, mutation, lease, plan before dispatch | +| `instance.rs` | `ProviderInstance` (variants: Runtime/Infrastructure/Transport/Substrate/Credential/Display/Network/Storage/Device/Audio/Observability) | Sum type over all provider trait objects | +| `context.rs` | `OwnedOperationContext`, `ProviderCallContext` | Per-call context (metadata, idempotency, deadline, principal) | +| `error.rs` | `ProviderResult`, `ProviderFailure`, `ProviderRuntimeError` | Typed provider error hierarchy | + +**V3 destination**: `packages/d2b-provider/` (largely retained and adapted) +— `ProviderRegistry`/`ProviderRegistryBuilder`/`ProviderRegistryManager` +adapt directly; `RpcProviderProxy` adapts for v3 session identity +(ZonePath instead of RealmId); `ProviderInstance` variants extend for +v3 Provider ResourceType model. + +**Excluded ADR45 assumptions**: +- `SessionIdentity` carries `provider_id: ProviderId` and + `provider_type: ProviderType`; v3 providers are identified by + `Provider/` resource path; `ProviderId` is renamed/reclassified. +- `AdmissionOptions::peer_role: EndpointRole` uses ADR45 role enumeration; + v3 maps to Zone principal + RBAC binding. +- `PROVIDER_BUNDLE_VERSION=13` (in `d2bd/src/provider_registry.rs`) is + the ADR45 bundle version; v3 provider catalogs use a separate versioning + scheme rooted in Zone resource generation. + +--- + +### d2b-provider-toolkit — Provider agent server and conformance + +**Crate**: `packages/d2b-provider-toolkit/src/` + +| Module | Key symbols | Selected behavior | +| --- | --- | --- | +| `server.rs` | `GeneratedProviderServiceServer` | ttrpc service dispatch for all Provider types; routes by service/method; validates call context, capability, attachment indexes; emits capability/observation/failure responses; concurrency limited via `Semaphore` | +| `adapter.rs` | `ProviderAgentAdapter` (implements `AuthenticatedProviderRpc`) | Client-side adapter for a provider running in a remote agent process; translates `RpcCall` → serialized ttrpc frame → agent ComponentSession | +| `conformance.rs` | `check_descriptor_conformance`, `check_provider_conformance`, `ConformanceError` | Structural conformance check against descriptor; live `inspect` call conformance | +| `registration.rs` | `register_exact_instances`, `ToolkitError` | One-shot factory→registry registration | +| `fixture.rs` | Test fixture helpers | Used in conformance tests | +| `redaction.rs` | Log/metric redaction helpers | Strips sensitive provider data from log/metric surfaces | + +**V3 destination**: `packages/d2b-provider-toolkit/` (adapted) — `GeneratedProviderServiceServer` +serves the v3 Provider agent process; `ProviderAgentAdapter` is the +d2b-bus proxy for a Provider running in a Guest/Host Process; `conformance` +module becomes the Provider resource conformance kit. + +**Excluded ADR45 assumptions**: +- `run_registered`/`run` standalone entrypoints use ADR45 process + registration via `SD_LISTEN_FDS` + priv-broker handshake; v3 provider + agents receive their ComponentSession FD via Zone allocator-issued + bootstrap binding. +- `ProviderAgentProcess::from_registry` uses ADR45 `RealmId` routing; + v3 uses ZonePath-rooted service identity. +- The ttrpc service/method name strings (`d2b.provider.runtime.v2.*` etc.) + are ADR45 protobuf service names; v3 re-freezes names under `d2b.provider.*.v3`. + +--- + +### d2b-realm-router/src/service_v2.rs — RealmServiceServer + +**File**: `packages/d2b-realm-router/src/service_v2.rs` + +| Symbol | Description | +| --- | --- | +| `RealmServiceServer` | `d2b.realm.v2.RealmService` ttrpc handler; methods: `bootstrap`, `enroll`, `resolve_route`, `authorize_shortcut`, `revoke_shortcut`, `report_shortcut_close`, `inspect`, `cancel` | +| `RealmServiceProcess` | Drive loop: serves with `MAX_DISPATCH_IN_FLIGHT=64` concurrent requests | +| `RealmSessionAuthority` | Per-session identity: realm, peer_role, locality, purpose, credential custody | +| `CredentialCustody` | `None` (host-local) / `GatewayGuest` (relay-backed) | +| `RealmServiceLimits` | `max_bindings=256`, `max_shortcuts=256`, `max_mutation_records=1024`, `audit_capacity=1024` | +| `RealmAuditEvent`, `RealmMethod`, `RealmAuditOutcome` | Per-method audit records | +| `BootstrapBinding`, `EnrollmentBinding`, `ShortcutBinding` | Per-session state tracked in `RealmState` | +| `MutationRecord` | Idempotent mutation dedup record | + +Constants: `REALM_SERVICE_NAME="d2b.realm.v2.RealmService"`, +`DEFAULT_MAX_REALM_BINDINGS=256`, `DEFAULT_MAX_SHORTCUTS=256`, +`DEFAULT_MAX_MUTATION_RECORDS=1024`, `DEFAULT_AUDIT_CAPACITY=1024`, +`MAX_CONFIGURED_BOUND=4096`, `MAX_DISPATCH_IN_FLIGHT=64`, +`SHUTDOWN_TIMEOUT=5s`. + +**V3 destination**: `packages/d2b-zone-routing/src/service.rs` — adapt +`RealmServiceServer` as `ZoneServiceServer` serving `d2b.zone.v3.ZoneService`; +rename methods (bootstrap→zone-bootstrap, enroll→zone-enroll, +resolve_route→resolve-zone-route, authorize_shortcut→authorize-zone-shortcut); +replace `RealmSessionAuthority` with Zone principal + RBAC binding; +replace `BootstrapBinding`/`EnrollmentBinding` with v3 ZoneLink enrollment +records; add independent `relay` and target-verb checks per forwarding hop; +adapt shortcut model to ZonePath. + +**Excluded ADR45 assumptions**: +- `CredentialCustody::GatewayGuest` maps to the ADR45 constellation gateway + pattern where a gateway guest VM terminates auth and proxies; v3 ZoneLink + sessions use direct KK between adjacent Zone controllers. +- `RealmSessionAuthority::gateway_peer` with `Locality::Remote` and + `CredentialCustody::GatewayGuest` is the ADR45 relay path; excluded. + V3 ZoneLink sessions are always direct controller-to-controller. +- `REALM_SERVICE_NAME` and protobuf package `d2b.realm.v2` are ADR45 wire + identifiers; v3 uses `d2b.zone.v3.ZoneService`. +- Bootstrap/enrollment credential embedding in `BootstrapBinding` uses + ADR45 `GuestSessionCredential` wire; v3 Zone bootstrap uses + allocator-issued ZoneLink bootstrap PSK. + +--- + +### d2bd/src/provider_registry.rs + provider_effects.rs — host Provider composition + +**Files**: `packages/d2bd/src/provider_registry.rs`, +`packages/d2bd/src/provider_effects.rs` + +| Symbol | File | Description | +| --- | --- | --- | +| `compose_host_provider_registry` | provider_registry.rs | Construct `ProviderRegistry` from `HostProviderComposition` (bindings + effects) | +| `compose_agent_provider_registry` | provider_registry.rs | Construct `ProviderRegistry` from `AgentProviderComposition` | +| `HostProviderBinding`, `HostProviderComposition` | provider_registry.rs | Per-binding factory dispatch for 9 implementation IDs | +| `AgentProviderBinding`, `AgentProviderComposition` | provider_registry.rs | Per-binding factory dispatch for agent/relay providers | +| `StartupProviderRegistry` | provider_registry.rs | `registry()`, `runtime_route()`, `lifecycle_dispatch()`, `begin_lifecycle_invocation()` | +| `DaemonEffectAdapters` | provider_effects.rs | 9 typed effect adapters (runtime/transport/substrate/display/network/storage/device/audio/observability) | +| `ProviderLifecycleDispatch`, `ProviderLifecycleInvocationHandle` | provider_effects.rs | Per-invocation lifecycle tracking with `MAX_TRACKED_LIFECYCLE_MUTATIONS=256` | + +**V3 destination**: `packages/d2b-core-controller/src/providers.rs` and +`packages/d2b-core-controller/src/provider_effects.rs` — +`compose_host_provider_registry` adapts as the fixed core-controller +Provider lifecycle handler; `DaemonEffectAdapters` pattern adapts as the +Zone-local Provider effect port set. `ProviderLifecycleDispatch` logic +adapts as the Zone controller's Provider lifecycle handler. + +**Excluded ADR45 assumptions**: +- `PROVIDER_BUNDLE_VERSION=13`, `PROVIDER_BUNDLE_SCHEMA_VERSION="v2"` are + ADR45 bundle version constants; v3 Provider catalogs use Zone-resource- + generation-bound versioning. +- `AZURE_VM_IMPLEMENTATION_ID` and Azure-specific binding paths are + ADR45 ACA provider paths; v3 ACA becomes an `InfrastructureProvider` + implementation under the standard Provider registry, not a special case. +- `validate_host_descriptor` validates against a closed set of + implementation IDs; v3 Provider resource `spec.implementationId` is + open-set validated by the Provider factory registry. +- `CLOUD_HYPERVISOR_IMPLEMENTATION_ID`/`QEMU_MEDIA_IMPLEMENTATION_ID` + paths are v2 substrate runner IDs; v3 runners use Process resources + with typed `executionRef`. + +--- + +Unrelated ADR 0045 assumptions excluded from all reuse: + +- ADR45 child-realm spawn contracts (`child_realm_controller_bootstrap.rs`, + realm-spawn pidfd protocols, `d2b-contracts/src/generated_v2_services/realm.rs` + realm-controller child-spawn wire). +- ADR45 delivery seals (xtask delivery wave / panel / seal process). +- ADR45 fixed 4-unit PID1 endpoint inventory (`d2bd.socket`, `d2bd.service`, + `d2b-priv-broker.socket`, `d2b-priv-broker.service` as invariants); v3 + Zone runtime sockets are allocator-issued, not PID1-owned. +- ADR45 `d2b-contracts/src/generated_v2_services/realm.rs` protobuf field + assignments; v3 re-freezes independently. +- ADR45 controller static key credential path + (`d2b-controller-static-v2` systemd credential); v3 controller keys come + through Zone bootstrap resource enrollment. +- ADR45 `d2b-state/` advisory-lock and audit-segment model; v3 inherits + the storage lifecycle contract from ADR 0034 but does not embed ADR45 + lock-file paths. + +## Baseline Realm architecture and mapping + +The pre-ADR-0045 v3 baseline uses `Realm`/`RealmId`/`RealmPath` throughout. +`Zone` has zero code matches in that baseline; the implementation term is +`Realm`. This section documents the **architecture** behind each baseline +symbol before mapping it to the v3 Zone target. Several mappings are +**not** textual renames; they require schema, contract, or runtime changes. + +### Identifier and path model + +`RealmPath` is a `Vec` written most-specific-first (DNS-style): +`payments.work` = child `payments` under parent `work`. Internally the +`storage_form()` is slash-separated parent-first (`work/payments`). +`RealmPath` grammar: max 16 labels, max 255 rendered bytes, lowercase +`^[a-z][a-z0-9-]*$` labels. + +`RealmId` is one label of a `RealmPath`. +`WorkloadId` is the label identifying a workload (VM, session, sandbox) +within a `Realm`. + +**ADR 0046 mapping:** +- `RealmPath` → `ZonePath`. The label grammar is preserved. The public + _target address_ changes from the DNS-form `workload.realm.d2b` string + to the v3 `Zone/` resource reference model. +- `RealmId` (one label) → `ZoneLabelId`. +- `WorkloadId` → **split**. VM/sandbox workloads become `Guest` resource + name; bare-metal/local execution becomes `Host` resource name. Semantic + classification per workload is required; there is no mechanical rename. +- `NodeId` → `Host` resource name or implicit in Zone-local addressing; + not separately surfaced in the routing contract. + +### Placement model + +`RealmControllerPlacement` is an enum with six variants: + +| Current variant | ADR 0046 Zone mapping | +| --- | --- | +| `HostLocal` | Private Zone runtime bootstrap: local controller, no external transport needed | +| `GatewayVm` | Private Zone runtime bootstrap: gateway-VM host; child identity via the child-local uplink ZoneLink plus allocator binding | +| `CloudFullHost` | Private Zone runtime bootstrap: cloud/remote host; child identity via the child-local uplink ZoneLink | +| `ProviderController { provider }` | Private Zone runtime bootstrap: Provider-managed controller; child identity via the child-local uplink ZoneLink | +| `ProviderAgent { provider }` | Private Zone runtime bootstrap: Provider agent; child identity via the child-local uplink ZoneLink | +| `ProviderSpecific { provider, placement }` | Private Zone runtime bootstrap: provider-schema-validated placement; child identity via the child-local uplink ZoneLink | + +This is an **architectural change**: the static placement enum is replaced by +private per-Zone bootstrap configuration plus two explicit Nix inputs: +compiler-only `d2b.zones..parentZone` selects the allocator owner, while +the child-local uplink ZoneLink's `childZoneName` and `transportProviderRef` +supply transport and local route/session state. The selected parent allocator +binds the ZoneLink UID and child identity to that private parent edge; no +reciprocal parent-store ZoneLink exists. Neither placement nor `parentZone` is +a public field in `Zone.spec`. `Zone.spec` is `{}` — it carries no authored +fields. + +`EntrypointMode` (`HostResident`/`GatewayBacked`) is used in the CLI +target router (`d2b/src/target_routing.rs`) to classify routing decisions. +In v3 this mode enum is subsumed into ZoneLink `spec.transportProviderRef`; +the child alone reads that local transport binding. Parent-side CLI routing and +inspection consume the sealed compiler topology and authenticated +route/projection status, never a parent-local ZoneLink resource. + +### Provider trait model + +`d2b-realm-provider` defines a family of traits (`WorkloadProvider`, +`DisplayProvider`, `RuntimeProvider`, `PersistentShellProvider`, etc.) +with capability advertising and rate-limit plumbing. These traits exist, +have mock/conformance implementations, and are tested, but: + +- `d2b-host-providers` implements some traits (`HostSubstrateProvider`, + `RuntimeProvider`, `DisplayProvider`) but no production binary depends + on `d2b-host-providers`. +- `d2b-provider-aca` (`AcaWorkloadProvider`) and `d2b-provider-relay` + ARE used in d2bd, but for the **ACA gateway display session path** + (`new_gateway_display_runtime_from_config` at `d2bd/src/lib.rs:1396`), + not for general workload routing. + +**ADR 0046 mapping:** +The provider trait family becomes `Provider` ResourceType with +controller/service/worker Processes. The capability advertisement fields +(`CapabilitySet`, `WorkloadCapabilitySet`, etc.) survive in the v3 +allocator-issued capability-scope model. The trait boundary itself is replaced by typed +ResourceSpec/Status fields on the Provider resource. + +### CapabilitySet model + +Current routing authz is entirely `CapabilitySet`-based: the route engine +propagates allocated capability scopes downward and checks that a requested +capability is covered. There is no per-hop RBAC subject/verb check. + +**ADR 0046 mapping:** +v3 adds a **RBAC layer above the allocated capability scope**: at each hop, the +intermediate Zone evaluates both the target verb and a separate `relay` verb +against the authenticated adjacent-Zone subject's RoleBinding. +Capability-scope propagation from `RouteNamespaceAllocation` is preserved. +The dual RBAC check is genuinely new; it does not exist anywhere in the current +baseline. + +### Realm config artifacts + +Two bundle artifacts are loaded at d2bd and priv-broker startup but are +explicitly inert for routing: + +- `realm-controllers.json` (from `realm-controller-config-json.nix`): + loaded at `d2bd/src/lib.rs:1408`; logs "runtime routing remains + inert". Carries `RealmControllersJson` with placement, socket paths, + provider metadata. +- `realm-identity.json` (from `realm-identity-config-json.nix`): + loaded at `d2bd/src/lib.rs:1425`; logs "runtime trust sessions remain + inert". Carries per-realm key refs/fingerprints. + +Both artifacts ARE valid bundle artifacts (installed root:d2bd 0640, +validated at startup). Their contents are **data** that v3 Zone/ZoneLink +resources will supersede; the daemon/broker never act on routing or +trust-session operations based on them today. + +**ADR 0046 mapping:** +- `realm-controllers.json` → superseded by compiler-only `parentZone` topology, + Nix-authored child-local ZoneLink resources, and the runtime-created Zone + self-resource. The `RealmControllersJson` schema retires when sealed allocator + topology owns placement/path and ZoneLinks own transport/route state. +- `realm-identity.json` → superseded by the allocator-sealed ZoneLink bootstrap + identity and the child-local `spec.transportCredentials` references defined + by the canonical ZoneLink schema. The `RealmIdentityConfigJson` schema + retires when ComponentSession enrollment handles key pinning. + +### Realm access resolver + +`d2bd/src/realm_access_resolver.rs` is a complete implementation of +`resolve_local_root_realm_access()` — it maps a `RealmAccessResolverRequest` +(target string + alias bindings + client capabilities) to a +`RealmAccessResolverResponse` (socket path, controller generation, +placement, capability preflight). The module is declared `pub mod` at +`d2bd/src/lib.rs:117` but **has no callers** in the running daemon; +it is implemented-but-unwired. + +**ADR 0046 mapping:** +The access resolver logic is replaced by `ZoneEntrypointResolver` in +work item ADR046-routing-003. The domain expands from "local host-local +socket lookup" to "ZoneLink-based multi-hop routing decision". + +### WorkloadTargetIndex (implemented-and-reachable) + +`d2bd/src/workload_target_index.rs` (`WorkloadTargetIndex`) builds a +canonical-target→VM-name reverse-lookup index from `realm-controllers.json` +and IS called from `d2bd/src/lib.rs:16745` in `PublicRequestArtifacts` +construction (the request handler path). This is a **live bridge** between +the Realm workload metadata and the legacy VM-name-based dispatch. + +**ADR 0046 mapping:** +`WorkloadTargetIndex` retires when Guest/Host resource lookups replace the +legacy VM-name dispatch. + +### Wire protocol + +`d2b-realm-core/src/frame.rs` (`ConstellationFrame`, `Handshake`, +`OperationRequest`, `OperationResponse`, `StreamOpen`, `StreamData`, +`StreamFlow`, etc.) is the current wire protocol. `d2b-realm-codec-protobuf` +serializes it. `d2b-realm-router` (`PeerSession`, `SecurePeerSession`, +`MuxSession`) implements sessions over this protocol. + +The session/mux layer IS used internally within `d2b-realm-router` tests +but is NOT imported by `d2bd` or the CLI (except for the display-session +path in `d2b-gateway-runtime` which uses individual frame types directly). + +**ADR 0046 mapping:** +`ConstellationFrame` variants map to v3 d2b-bus frame types. The KK +handshake from `SecurePeerSession` is adapted as the ComponentSession +Noise KK profile. The codec is adapted; the protobuf field assignments must +be re-frozen for v3. + +### Gateway display vs. general routing + +`d2b-gateway` and `d2b-gateway-runtime` use `RealmPath`, `WorkloadId`, +`RealmId`, `AuthzDecision`, `OperationId`, and `PrincipalId` from +`d2b-realm-core` — but only for the **display-session HMAC handshake +and ACA/Azure Relay Wayland session** orchestration. They do NOT +implement general realm routing. `AcaWorkloadProvider` (from +`d2b-provider-aca`) is instantiated in d2bd for the ACA gateway path +only (`d2bd/src/lib.rs:4127`). + +The `RouteRealmClass` variant `GatewayBacked` is a metric label for +existing gateway-backed routing; it is not an implementation of the +gateway routing path itself. + +## Overview + +Zone routing is the mechanism by which one Zone's resource-plane clients +and runtime service callers reach resources, services, and controllers +homed in a different Zone. The d2b v3 model is intentionally constrained: + +- Every resource belongs to exactly one Zone. Resources never migrate. +- Ordinary `*Ref` fields never cross Zone boundaries. +- Every non-root Zone declares exactly one compiler-only scalar `parentZone`; + `local-root` declares none. The value is a plain declared Zone name, not a + ResourceRef, and compiles into the sealed allocator-bootstrap topology. +- Every non-root child Zone declares at most one local `ZoneLink/` + uplink resource, enabled or disabled. It supplies transport and local + route/session state; the allocator selected by `parentZone` binds that + resource to the direct parent/child edge in sealed bootstrap state. +- A parent calls the child Zone's `d2b.resource.v3` service through an + authenticated ZoneLink ComponentSession; it does not obtain a database + handle, process credential, host path, or cross-Zone resource reference. +- Cross-Zone routing traverses the Zone tree through sessions established by + each child's local uplink. No direct lateral (sibling-to-sibling) route is + provisioned without going through a common ancestor. +- The d2b-bus resolves the outbound route for every service call and + enforces native RBAC at each hop. + +``` +Nix topology: k1.parentZone = local-root; k2.parentZone = k1 + +Zone/local-root (K0 parent allocator/route state) + -> Zone/k1 (child): child-local ZoneLink/k1-uplink + -> Zone/k2 (grandchild): child-local ZoneLink/k2-uplink +``` + +A call from a process in K0 that targets K2 traverses K0→K1→K2. Each +hop uses a separate KK-authenticated ComponentSession; no hop receives +authority from prior hops beyond what its own enrolled RoleBinding grants. + +## ZoneLink resource + +### Authoritative schema and routing interpretation + +[`ADR-046-resources-zone-control.md` §3](ADR-046-resources-zone-control.md) +is the sole normative ZoneLink ResourceType schema. A ZoneLink `spec` has +exactly these six top-level fields: + +1. `childZoneName` +2. `transportProviderRef` +3. `transportSettings` +4. `transportCredentials` +5. `disabled` +6. `limits` + +Unknown fields are rejected. This document does not define a second routing +schema. It only adds these routing interpretations: + +- `childZoneName` identifies the enclosing child Zone and must equal + `metadata.zone`; local root has no uplink and a non-root Zone has at most one + ZoneLink, enabled or disabled. +- `transportProviderRef` resolves to a Ready Provider in that same child Zone. + The Provider validates `transportSettings`, while `transportCredentials` + contains only same-Zone `Credential/` refs. +- `disabled: true` closes the session, withdraws admitted route projections, + and suppresses reconnect until re-enabled. +- `limits.maxPendingIntents`, `limits.maxActiveStreams`, + `limits.reconnectMaxAttempts`, and `limits.reconnectWindowSecs` bound routing + queues, streams, and reconnect activity. Transport-specific backoff remains + Provider-internal and bounded by those limits. +- The allocator-selected parent supplies the sealed ComponentSession identity, + route namespace, and allowed capability scope. None is an additional + ZoneLink spec field. +- The routing wire contract has one fixed, bounded hop budget; it is not + operator-configurable per ZoneLink. + +The canonical D088 status shape is also owned by +`ADR-046-resources-zone-control.md` §3.4. Universal `ResourceStatus` fields +remain directly under `status`; ZoneLink-common observations are under +`status.resource`; optional implementation observations are under +`status.provider`. Routing consumes the universal base plus +`status.resource`, including `childZoneUid`, `connected`, connection +timestamps, revision cursors, `linkEpoch`, `pendingLocalIntents`, and +`childAuthorized`. It never introduces another status container. + +Status phases: + +- `Pending`: link resource created, session not yet established or child + not yet reachable. +- `Ready`: KK session established, child Zone API probed, route + advertisement current. +- `Degraded`: session is up but one or more conditions are impaired (e.g., + route renewal overdue or intent queue high-watermark reached). +- `Failed`: session cannot be established and retry policy is exhausted. +- `Unknown`: controller cannot currently prove session state. + +The child-local core-controller ZoneLink handler owns status. It never writes +`Ready` until the parent allocator has authenticated the child, acknowledged +the route allocation, and successfully probed the child's `d2b.resource.v3` +service within the current link epoch. The parent keeps private +allocation/route state, not a second resource row. + +## Zone tree identity and prefix naming + +Each Zone has exactly one authoritative `Zone/` resource in +its own store. The name is the Zone's local self-name. A non-root child +authors its local `ZoneLink/` uplink; the link name need not equal +the child's self-name, while `spec.childZoneName` must. The provisioning +parent allocator assigns the private tree edge and verifies the child self-name +during KK enrollment. The acknowledged UID is recorded as +`status.resource.childZoneUid`. + +Zone tree positions are described as ordered label paths from the local +root, most-specific first, matching the `RealmPath` grammar from +`d2b-realm-core/src/realm.rs` (max 16 labels, max 255 rendered bytes, +lowercase `^[a-z][a-z0-9-]*$` labels). The routing engine uses this +parent-first storage form internally (`work/payments` for child `payments` +under parent `work`); the public v3 addressing form is the Zone resource +path (`Zone/work`, `Zone/payments` under `Zone/work`), not the DNS-target +form (`payments.work.d2b`). The routing keys inside the `ZoneRouteEngine` +use the ordered-label path model from the baseline. + +``` +k0 # local root +k1.k0 # child k1 under parent k0 +k2.k1.k0 # grandchild k2 +``` + +Each Zone runtime maintains a local `RouteTreeEngine` (adapted from +`d2b-realm-core/src/route_engine.rs`) keyed by these Zone tree paths. It +tracks: + +- **Parent entries**: for each child Zone, the immediate parent path, + optional route id, allocated capability scope, and expiry. +- **Route entries**: for each descendant Zone reachable through this + node, the advertising Zone, next-hop child label, route id, capability + set, and expiry. + +This engine is the single authoritative source for route decisions. It +performs no I/O; callers supply all metadata. + +### Tree path constraints (retained from baseline) + +- Max 32 hops per decision path (`MAX_ROUTE_PATH_HOPS`). +- Max 4096 parent entries per engine (`MAX_PARENT_ENTRIES`). +- Max 4096 route entries per engine (`MAX_ROUTE_ENTRIES`). +- Max 16 Zone names per compiler-authored ancestry path + (`MAX_REALM_LABELS`; local root counts as one). +- Sibling/parent route advertisements are rejected with + `sibling-or-parent-route-advert`. +- Nix rejects missing/unknown/self parents, cycles, and over-depth topology + before sealing bootstrap state. +- Conflicting `parentZone` scalar definitions fail through normal Nix module + merging; runtime advertisements that claim a conflicting parent are rejected + with `multi-parent`. + +## Nix configuration + +The Nix authoring shape mirrors the canonical ResourceSpec schema. Every +resource is declared as `d2b.zones..resources. = { type = +""; spec = { }; };`. The `spec` object +uses exactly the same field names, nesting, and types as the canonical +`spec` object in the JSON ResourceSpec for that ResourceType. There is no +second bespoke Nix vocabulary, no field renaming, and no additional nesting +beyond what the canonical schema has. `status` is omitted — it is +read-only and filled by the Zone runtime. + +`d2b.zones..parentZone` is the one deliberate Zone-level compiler input, +not part of that schema mirror. It is a plain Zone attrset key: required for +every non-root Zone, forbidden on `local-root`, and never emitted into a +ResourceSpec or `Zone.spec`. The compiler canonicalizes the resolved +child→parent rows, validates the complete graph, and seals them into the private +allocator-bootstrap topology. A parent change updates that private topology and +forces release/reallocation of the affected edge even though the Zone resource +bundle remains unchanged. + +### Metadata derivation + +The Nix emitter serializes core-derived metadata (`name`, `zone`, `apiVersion`) +plus the optional authored fields below. Management metadata +(`managedBy`, `configurationGeneration`, `uid`, etc.) is absent from the +bundle and set only by the configuration service/core — never by the emitter. + +| JSON field | Source | +| --- | --- | +| `metadata.name` | `` attribute key in `resources.` | +| `metadata.zone` | `` attribute key in `zones.` | +| `apiVersion` | Fixed: `"resources.d2bus.org/v3"` | +| `metadata.managedBy` | Absent from bundle; set only by configuration service/core when activating the bundle | +| `metadata.configurationGeneration` | Absent from bundle; set only by configuration service/core on Create/UpdateSpec | +| `metadata.uid`, `metadata.generation`, `metadata.revision`, timestamps | Assigned by the Zone runtime on first Create; absent from the bundle | +| `metadata.ownerRef` | Optional authored field; typed `ResourceRef`; omit if none | +| `metadata.labels` | Optional authored field; key-value map; omit if none | +| `metadata.annotations` | Optional authored field; key-value map; omit if none | + +### Option schema + +The base Nix option type for every resource is structural: + +```nix +# nixos-modules/options-zones.nix (structural base; type-specific options are +# generated by xtask gen-zone-nix-options from ResourceTypeSchema JSON) +let + # Generated from ADR-046-resource-object-model's canonical 19-type registry + # plus installed qualified Provider schemas. The drift test asserts that the + # standard subset is exactly the canonical registry. + registeredResourceTypes = import ./generated/resource-types.nix; +in { + d2b.zones..parentZone = mkOption { + # No default. Required for non-root Zones and forbidden on local-root. + type = types.strMatching "^[a-z][a-z0-9-]*$"; + description = '' + Compiler-only parent Zone name. This is not a ResourceRef and is emitted + only into sealed allocator bootstrap topology, never Zone.spec. + ''; + }; + + d2b.zones..resources. = { + type = mkOption { + type = types.enum registeredResourceTypes; + description = "ResourceType for this resource."; + }; + spec = mkOption { + # Freeform at the structural level. xtask gen-zone-nix-options emits + # a type-specific submodule for each ResourceType so that the spec + # subfields carry proper types, defaults, and docs from the committed + # ResourceTypeSchema JSON. Build-time validation then compares the + # canonical rendered JSON against the same schema. + type = types.attrs; + default = {}; + description = '' + Spec fields for this ResourceType. Field names, types, and defaults + must match the canonical ResourceTypeSchema for `type`. Secrets must + appear only as Credential/ refs; no inline key material. + ''; + }; + # Optional authored metadata sub-fields. + # managedBy, configurationGeneration, uid, generation, revision, + # and timestamps are set only by the configuration service/core; + # they must not appear in the authored option tree. + metadata = { + ownerRef = mkOption { + type = types.nullOr types.attrs; + default = null; + description = "Optional ResourceRef of the owning resource, if any."; + }; + labels = mkOption { + type = types.attrsOf types.str; + default = {}; + description = "Optional key-value label map."; + }; + annotations = mkOption { + type = types.attrsOf types.str; + default = {}; + description = "Optional key-value annotation map."; + }; + }; + }; +} +``` + +`xtask gen-zone-nix-options` derives `generated/resource-types.nix` from the +same committed ResourceTypeSchema catalog used by API and bundle validation. +Its drift test compares the standard subset byte-for-byte with the canonical +19-type registry and rejects an omission, addition, duplicate, or reordered +entry; qualified Provider types are appended only from installed signed +schemas. + +`xtask gen-zone-nix-options` reads `docs/reference/schemas/v3/.schema.json` +and emits a generated `nixos-modules/generated/options-zones-.nix` for each +ResourceType, overlaying typed submodule options onto `spec`. These generated files +are committed and kept in sync by `make test-drift`. Field-level type errors (wrong +enum value, out-of-range integer, malformed ResourceRef, etc.) are therefore caught +at `nix eval` time via the generated option type, not by explicit assertions. + +#### Generated ZoneLink spec options (illustrative excerpt) + +```nix +# nixos-modules/generated/options-zones-ZoneLink.nix (generated; do not hand-edit) +{ + d2b.zones..resources..spec = { + childZoneName = mkOption { + type = types.strMatching "^[a-z][a-z0-9-]*$"; + description = "Self-reported name of the child Zone. Verified during KK enrollment."; + }; + transportProviderRef = mkOption { + # Required; no default. Must always be explicitly declared. + type = types.strMatching "^Provider/[a-z][a-z0-9-]*$"; + description = "Provider/ resource that owns the transport session for this link. Always explicit; no default or inference."; + }; + transportSettings = mkOption { + # Freeform; validated at build time against the transport Provider's + # transportSettingsSchema. No socketPath, hostPath, password, token, + # or key top-level keys permitted. + type = types.attrs; + default = {}; + }; + transportCredentials = mkOption { + type = types.listOf (types.strMatching "^Credential/[a-z][a-z0-9-]*$"); + default = []; + description = "Same-Zone Credential refs resolved for ComponentSession establishment."; + }; + disabled = mkOption { + type = types.bool; + default = false; + }; + limits = { + maxPendingIntents = mkOption { + type = types.ints.between 0 1024; + default = 256; + }; + maxActiveStreams = mkOption { + type = types.ints.between 1 128; + default = 32; + }; + reconnectMaxAttempts = mkOption { + type = types.ints.positive; + default = 10; + }; + reconnectWindowSecs = mkOption { + type = types.ints.positive; + default = 300; + }; + }; + }; +} +``` + +This is an exact mirror of the six-field schema owned by +`ADR-046-resources-zone-control.md` §3. The generator rejects any additional +ZoneLink field rather than extending this excerpt. + +### Eval-time assertions + +These invariants require cross-resource context and cannot be expressed as +generated per-field option types. They live in `nixos-modules/assertions.nix`. +Field-level type, bounds, and enum assertions are handled by the generated +option types from `xtask gen-zone-nix-options` and are not repeated here. + +| Assertion | Error message | +| --- | --- | +| `` key matches `^[a-z][a-z0-9-]*$` | `zones: zone key must match ^[a-z][a-z0-9-]*$` | +| `` key not `sys-*` or `launcher` | `zones: zone key uses reserved prefix or exact name` | +| `parentZone` omitted on `local-root` and defined once on every other Zone | `zones.: parentZone is required for non-root Zones and forbidden on local-root` | +| `parentZone` resolves to a declared Zone and does not equal `` | `zones..parentZone: parent must exist and differ from child` | +| Complete `parentZone` graph is acyclic and each ancestry path contains at most 16 Zone names | `zones: parentZone topology has a cycle or exceeds depth 16` | +| `` key matches `^[a-z][a-z0-9-]*$` | `zones..resources: resource key must match ^[a-z][a-z0-9-]*$` | +| No operator-authored `type = "Zone"` under `resources` | `zones..resources.: Zone self-resource is runtime-created` | +| For `type = "ZoneLink"`: `spec.childZoneName` equals ``; at most one uplink resource exists in a non-root Zone; local root has none | `zones.: ZoneLink must be the sole child-local uplink and childZoneName must equal its Zone` | +| For `type = "ZoneLink"`: `spec.transportProviderRef` resolves to a declared `Provider` resource in the same `` | `zones..resources.: transportProviderRef does not resolve to a declared Provider resource` | +| Total `d2b.zones` keys ≤ 64 | `zones: zone count exceeds host limit of 64` | +| `resources` count per `` ≤ 1024 | `zones..resources: resource count exceeds zone limit of 1024` | +| `spec.transportSettings` for `type = "ZoneLink"` has no top-level key named `socketPath`, `hostPath`, `password`, `token`, or `key` | `zones..resources.: transportSettings must not contain host paths, socket paths, or secret material` | + +### Example configurations + +**K0 with local Unix-transport child K1 (K0 = Host, K1 = Guest)**: + +```nix +# K0 is the distinguished local-root Zone. parentZone is forbidden here. +# The runtime creates Zone/local-root with spec = {}; it is not bundle-authored. +d2b.zones.local-root = {}; + +# Unix transport Provider is local to child K1 because the ZoneLink and all of +# its refs resolve in K1. +d2b.zones.k1.parentZone = "local-root"; + +d2b.zones.k1.resources.transport-unix = { + type = "Provider"; + spec = { + kind = "transport-unix"; + description = "Allocator-issued Unix socket transport for local child zones"; + }; +}; + +# K1's local uplink supplies transport/route state for the allocator selected +# by k1.parentZone. +d2b.zones.k1.resources.k1-uplink = { + type = "ZoneLink"; + spec = { + childZoneName = "k1"; + transportProviderRef = "Provider/transport-unix"; + transportSettings = {}; # allocator-issued FD; no path config + transportCredentials = []; + disabled = false; + limits = { + maxPendingIntents = 256; + maxActiveStreams = 32; + reconnectMaxAttempts = 10; + reconnectWindowSecs = 300; + }; + }; +}; +``` + +**K2 child-local uplink to K1 via Azure Relay transport**: + +```nix +# Compiler-only topology makes K1 the one allocator owner for K2. The runtime +# creates Zone/k2 with spec = {}; parentZone is not emitted there. +d2b.zones.k2.parentZone = "k1"; + +# Azure Relay transport Provider is local to K2. +d2b.zones.k2.resources.transport-azure-relay = { + type = "Provider"; + spec = { + kind = "transport-azure-relay"; + description = "Azure Relay transport for K1→K2 link"; + }; +}; + +# Credential ref for the relay SAS token — no inline secret. +d2b.zones.k2.resources.relay-sas-k2 = { + type = "Credential"; + spec = { + kind = "opaque"; + description = "Azure Relay SAS token for d2b-k2 relay"; + # The secret itself is stored in a separate Credential store; only the ref lives here + }; +}; + +# K2's local uplink supplies transport/route state for the allocator selected +# by k2.parentZone. +d2b.zones.k2.resources.k2-uplink = { + type = "ZoneLink"; + spec = { + childZoneName = "k2"; + transportProviderRef = "Provider/transport-azure-relay"; + transportSettings = { + relayNamespaceId = "relns-d2b-prod"; + relayEntityId = "hc-d2b-k2"; + }; + transportCredentials = [ "Credential/relay-sas-k2" ]; + disabled = false; + limits = { + maxPendingIntents = 128; + maxActiveStreams = 32; + reconnectMaxAttempts = 10; + reconnectWindowSecs = 300; + }; + }; +}; +``` + +The relay SAS token is referenced only through +`spec.transportCredentials`; `transportSettings` contains non-secret Provider +configuration. The build emitter rejects settings annotated `"secret": true` +and resolves every credential ref in the same child Zone. + +### Build-time validation + +The Nix build phase runs `xtask gen-zone-resources` which: + +1. **Topology validation and compilation**: resolves every non-root + `parentZone` against declared Zones; rejects a value on `local-root`, + missing, unknown, self, conflicting, cyclic, or over-16-name ancestry; and + canonicalizes the child→parent map into sealed private allocator-bootstrap + topology. `parentZone` is never copied into the resource bundle. + +2. **Schema validation**: validates every emitted resource object against + the committed JSON Schema files: + - `docs/reference/schemas/v3/Zone.schema.json` + - `docs/reference/schemas/v3/ZoneLink.schema.json` + - `docs/reference/schemas/v3/.schema.json` for any other declared type + The `make test-drift` gate enforces `xtask gen-zone-schemas && git diff + --exit-code` so schema, Rust types, and generated Nix option modules stay + in sync. A separate `xtask gen-zone-nix-options && git diff --exit-code` + step ensures the generated Nix option modules match the current schema. + +3. **Provider binding validation**: for each `ZoneLink`, the emitter fetches + the transport Provider's `transportSettingsSchema` (signed, committed under + `docs/reference/schemas/v3/providers/.transport-binding.json`) + and validates `spec.transportSettings` against it. Unknown keys are rejected. + Fields annotated `"secret": true` are rejected; those annotated + `"credentialRef": true` must be `"Credential/"` strings. + +4. **Ref resolution**: all `*Ref` and `*ProviderRef` values, including every + `transportCredentials` entry, are resolved + against resources declared in the same `d2b.zones..resources` and + fail the build if unresolvable. + +5. **Conflict check**: duplicate `(type, zone, name)` tuples across the + entire emitted bundle fail the build. + +### Canonical ResourceSpec JSON shapes + +The `spec` object in Nix and in the emitted JSON are identical in field +names, nesting, and defaults. The emitter does not rename or restructure +any spec field. Resources are sorted by `(type, zone, name)` for +determinism before the integrity digest is computed. + +**Zone resource** — runtime-created from the declaration +`d2b.zones.local-root = {};`, not emitted in the resource bundle: + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Zone", + "metadata": { + "name": "local-root", + "zone": "local-root" + }, + "spec": {} +} +``` + +The runtime gives this self-resource core-populated fields such as +`metadata.managedBy`, `metadata.uid`, and `metadata.generation`. +`metadata.configurationGeneration` is absent because the resource is +controller-created. Compiler-only `parentZone` is absent from both this object +and the Nix-rendered resource bundle. + +**ZoneLink resource** — emitted from +`d2b.zones.k1.resources.k1-uplink = { type = "ZoneLink"; spec = { ... }; };`: + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "ZoneLink", + "metadata": { + "name": "k1-uplink", + "zone": "k1" + }, + "spec": { + "childZoneName": "k1", + "disabled": false, + "limits": { + "maxActiveStreams": 32, + "maxPendingIntents": 256, + "reconnectMaxAttempts": 10, + "reconnectWindowSecs": 300 + }, + "transportCredentials": [], + "transportProviderRef": "Provider/transport-unix", + "transportSettings": {} + } +} +``` + +The canonical form sorts all object keys lexicographically. Array ordering +depends on the array's schema classification: order-significant arrays +(e.g. `transportCredentials`, named-stream lists) are preserved as authored; +schema-declared set-like arrays (e.g. `resourceTypes` in capability specs) are +sorted lexicographically. All fields with defaults are always emitted; no key +omission. The `status` object is absent from the emitted bundle entirely. + +### Zone resource bundle format + +The build installs `/etc/d2b/zones//resource-bundle.json` (root:d2bd +0640). For child K1, the local bundle contains its Provider and uplink. The +controller-created Zone self-resource is not emitted: + +```json +{ + "schemaVersion": 1, + "generationId": "", + "resourceCount": 2, + "resources": [ + { "type": "Provider", "metadata": { "name": "transport-unix", "zone": "k1" }, "...": "..." }, + { "type": "ZoneLink", "metadata": { "name": "k1-uplink", "zone": "k1" }, "...": "..." } + ], + "integrity": "sha256-" +} +``` + +Bundle rules: + +- `generationId` is the SHA-256 (lower hex) of the UTF-8 bytes of the + canonical sorted `resources` array JSON (not the envelope fields). + Two identical Nix configurations always produce the same `generationId` + regardless of host name or wall-clock time. +- `integrity` is computed by serializing the bundle with `integrity` set + to the all-zeros placeholder `"sha256-"`, computing SHA-256 of the + result, encoding as base64url without padding, then replacing the + placeholder with the final value. +- The bundle is the single source of truth for what Nix owns. The Zone + runtime detects changes by comparing `generationId` on startup and on + SIGHUP. +- Transport Provider binding schemas referenced in build validation are + committed separately under `docs/reference/schemas/v3/providers/` and + are not inlined in the bundle. + +## Configuration ownership and cleanup contract + +### Management classification + +The Zone runtime tracks cleanup authority through two canonical metadata +fields set by the emitter or runtime, never authored in the Nix option tree: + +| Field | Set by | Meaning | +| --- | --- | --- | +| `metadata.managedBy` | Configuration service/core; set when activating the validated bundle | Identifies the management agent that owns the resource's lifecycle. Core-defined value; absent from bundle JSON. | +| `metadata.configurationGeneration` | Configuration service/core; set only by core on Create/UpdateSpec | Absent from bundle JSON; absent on controller-created or API-created resources. | + +Cleanup requires **both** conditions: `managedBy` must equal the configuration +service's value **and** `configurationGeneration` must match a known bundle +generation. A resource whose `managedBy` is `controller` or `api` is never +touched by the diff even if it coincidentally shares a name with a bundle +resource; core fails closed on any attempt to seize such a resource without +an explicit mutation. + +### New generation activation flow + +When a new bundle is installed (e.g. after `nixos-rebuild switch`): + +``` +1. Zone runtime reads `/etc/d2b/zones//resource-bundle.json` and verifies integrity. +2. Computes diff against the active generation by generationId. + If generationId is unchanged, no action required. + +3. For each resource in the new bundle: + a. Absent from store → queue Create intent (sets configurationGeneration). + b. Present, spec changed → queue UpdateSpec intent (updates configurationGeneration). + c. Present, spec unchanged → no-op (configurationGeneration refreshed in place). + +4. For each resource in the prior bundle whose configurationGeneration matches + the prior generationId and that is absent from the new bundle: + → queue Delete intent (asynchronous, non-blocking). + Core sets `deletionRequestedAt` on the resource immediately and adds + a Pending condition to signal cleanup is in progress. + +5. Activation completes synchronously once all intents are queued. + The Zone runtime begins applying intents asynchronously. + +6. Zone Resource phase after diff: + - Pending → while any Create or UpdateSpec intent is in-flight. + - Degraded → all Creates/Updates done; one or more Delete intents pending. + - Ready → all intents complete; no pending cleanup. + +7. Prior generation bundles are retained in a capped ring + (default 3, range 1..16, no TTL) until explicitly pruned or rolled back. + See "Prior generation retention and rollback" below. +``` + +### Degraded / pending-cleanup status + +New generation activation is **non-blocking**: the Zone runtime begins +serving the new configuration immediately. Clients accessing a resource +with `deletionRequestedAt` set receive `resource-pending-cleanup` on +Create/UpdateSpec calls; Get/List/Watch continue to succeed until +deletion completes and core atomically removes the row and index. + +Status fields on the active generation resource in the runtime store: + +```yaml +status: + generationId: + phase: Degraded # Pending while creates/updates run; Ready when clean + conditions: + - type: ConfigApplied + status: "True" + reason: all-creates-and-updates-done + - type: CleanupComplete + status: "False" + reason: pending-delete-intents + message: "2 resources pending deletion" + pendingCleanup: + - { type: ZoneLink, name: old-link, zone: k1, deletionRequestedAt: 2026-07-22T21:00:00Z } + - { type: Zone, name: removed-zone, zone: removed-zone, deletionRequestedAt: 2026-07-22T21:00:00Z } + priorConfigurationGeneration: + lastGenerationChange: 2026-07-22T21:00:00Z +``` + +Per-resource cleanup tracking fields (set by core on the pending-delete resource): + +| Field | Type | Meaning | +| --- | --- | --- | +| `deletionRequestedAt` | RFC3339 | When core queued the Delete intent; presence signals pending deletion | +| `cleanupConfigGeneration` | string | The `configurationGeneration` that triggered the Delete intent | +| `cleanupError` | string? | Last error from the Delete attempt, if any | +| `cleanupAttempt` | u32 | Attempt count | + +### Delete lifecycle for removed resources + +A resource absent from the new bundle and matching the prior bundle's +`configurationGeneration` receives an async Delete intent: + +1. **Finalizer drain**: the runtime checks for registered finalizers. If + any finalizer is present, core sets `deletionRequestedAt` on the resource + (adding a Pending condition) and notifies each finalizer holder to + release it. Deletion proceeds only after all finalizers clear. + +2. **Controller-child cascade**: before completing deletion of a parent + resource (Zone or ZoneLink), the runtime notifies the owning controller. + The ZoneLink controller closes its session, withdraws its advertisements, + and deletes its own route entries. Zone controllers signal their Process + and EphemeralProcess children for graceful stop. These are + controller-initiated deletions, not bundle-diff deletions. + +3. **Atomic store commit**: after all controller-created children acknowledge + teardown, core executes one store transaction: writes the `Deleted` + revision/change event and removes the resource row and all index entries. + Once the transaction commits, the authoritative audit record + (`zone-resource-cleanup`) is appended from the committed revision with + dedup/exactly-once recovery; the audit append is not part of the store + transaction itself. + +4. **Failure**: if a Delete intent exhausts retries, the generation status + is `Failed` with a Degraded condition `CleanupFailed: resource-delete-failed`. + Prior generation bundles are retained until the failure is resolved. + +### Boundary invariants + +- A generation diff **never** deletes a resource that has no + `configurationGeneration` field (controller-created or API-created). +- A generation diff **never** deletes a resource whose `managedBy` does not + equal the configuration service's value, even if `configurationGeneration` + appears to match (collision guard — core fails closed without mutation). +- A generation diff **never** deletes a resource whose + `configurationGeneration` does not match the prior bundle's `generationId` + (guards against races between concurrent generation switches). +- A generation diff **never** deletes a resource that still has a live + controller-created child that has not acknowledged teardown. +- Dynamic children (route entries, ephemeral Processes) are deleted by their + owning controller when the parent is deleted — not by the generation diff. + +### Prior generation retention and rollback + +The Zone runtime keeps a capped ring of prior generation bundles on disk +under `/var/lib/d2b/zones//configuration/prior/`: + +``` +/etc/d2b/zones//resource-bundle.json ← active input bundle +/var/lib/d2b/zones//configuration/prior/.json +/var/lib/d2b/zones//configuration/prior/.json +/var/lib/d2b/zones//configuration/prior/.json ← up to retentionCount +``` + +| Parameter | Default | Range | +| --- | --- | --- | +| `retentionCount` | 3 | 1..16 | +| TTL | none | — | + +Retention is count-only: the oldest bundle is pruned when a new generation +is added and the count would exceed `retentionCount`. No time-based expiry. + +- **Rollback**: writing a retained bundle back to + `/etc/d2b/zones//resource-bundle.json` (e.g. via `nixos-rebuild switch` + to a previous NixOS generation) triggers a reverse diff. Resources with + `deletionRequestedAt` set have the field cleared and their Pending + condition removed (Delete intent cancelled if not yet executed); + resources added in the now-superseded generation receive Delete intents. +- **Pruning on cleanup failure**: prior bundles are never forcibly pruned + while a Delete intent originating from their configurationGeneration is + still in flight. + +### Cleanup audit events + +| Audit event kind | Fields | Trigger | +| --- | --- | --- | +| `zone-generation-activate` | `generationId`, `priorGenerationId`, `resourceCount`, `creates`, `updates`, `deletes` | New bundle processed | +| `zone-resource-cleanup` | `type`, `name`, `zone`, `configurationGeneration`, `durationMs` | Appended from committed `Deleted` revision with dedup/exactly-once recovery after the store transaction that removes row/index commits | +| `zone-resource-cleanup-failed` | `type`, `name`, `zone`, `configurationGeneration`, `reason`, `attempt` | Delete intent failure | +| `zone-generation-ready` | `generationId`, `durationMs` | Generation reaches Ready | +| `zone-generation-failed` | `generationId`, `reason` | Generation reaches Failed | +| `zone-generation-rollback` | `generationId`, `priorGenerationId` | Rollback initiated | + +All audit events are emitted to the Zone runtime's audit log under category +`zone-config` and carried in OTEL spans under `d2b.zone.config.generation`. + +### Eval and build tests + +Required before ADR046-routing-011 through ADR046-routing-013 are complete: + +| Test ID | Kind | What it proves | +| --- | --- | --- | +| `nix-unit: zone-name-regex` | nix-unit eval | Zone name regex and reserved-name assertions fire | +| `nix-unit: zone-parent-required-root-forbidden` | nix-unit eval | Every non-root Zone requires `parentZone`; `local-root` rejects it | +| `nix-unit: zone-parent-resolves` | nix-unit eval | Unknown and self-valued `parentZone` settings reject | +| `nix-unit: zone-parent-one-parent` | nix-unit eval | Conflicting scalar definitions reject through normal Nix module merging | +| `nix-unit: zone-parent-cycle` | nix-unit eval | Two-node and longer `parentZone` cycles reject before bootstrap publication | +| `nix-unit: zone-parent-depth` | nix-unit eval | Sixteen-name ancestry succeeds and seventeen-name ancestry rejects | +| `nix-unit: zone-link-credential-ref` | nix-unit eval | Malformed or cross-Zone `transportCredentials` refs rejected | +| `nix-unit: zone-link-child-name` | nix-unit eval | `childZoneName` unequal to enclosing child Zone rejected | +| `nix-unit: zone-link-one-uplink` | nix-unit eval | Second uplink (even disabled) and any local-root uplink rejected | +| `nix-unit: zone-link-closed-spec` | nix-unit eval | Any ZoneLink top-level spec field outside the canonical six is rejected | +| `nix-unit: zone-link-limits` | nix-unit eval | Queue, stream, reconnect-attempt, and reconnect-window bounds enforced | +| `nix-unit: transport-binding-secret-key` | nix-unit eval | Binding with `key =` rejected at eval | +| `drift: zone-resource-schema` | `make test-drift` | `xtask gen-zone-schemas && git diff --exit-code` passes | +| `build: zone-bundle-deterministic` | flake check | Two identical configs produce identical `generationId` | +| `build: transport-binding-unknown-field` | flake check | Unknown Provider binding key fails build | +| `build: allocator-capability-scope` | flake check | Route capability scope wider than the sealed parent allocation fails build | +| `build: missing-transport-provider` | flake check | Unresolvable `transportProviderRef` fails build | +| `build: parent-topology-sealed` | flake check | Valid sorted `parentZone` rows compile only into the sealed allocator bootstrap input and never into `Zone.spec` or resource bundles | +| `host-integration: cleanup-removed-zonelink` | NixOS test | Switch removes ZoneLink; assert `deletionRequestedAt` set; store transaction commits `Deleted` revision and removes row/index; audit record appended from committed revision with exactly-once recovery; generation reaches Ready; dynamic route entries deleted by ZoneLink controller teardown, not by generation diff | +| `host-integration: rollback-restores-zonelink` | NixOS test | After cleanup switch, rollback re-activates ZoneLink; generation diff reverses | +| `host-integration: dynamic-child-not-deleted` | NixOS test | Parent allocator/route entries are NOT diff-owned resources; removing the child-local ZoneLink invokes controller teardown and allocation release | +| `host-integration: zonelink-no-reciprocal-row` | NixOS test | Activating a child-local uplink creates no ZoneLink row in the parent store | + +## Authenticated advertisements, withdrawal, and renewal + +The child-local ZoneLink handler signs each advertisement with its enrolled KK +key. The parent validates the signature before admitting it to the parent's +`RouteTreeEngine`; this creates authenticated in-memory route projection state, +not a parent-store ZoneLink row. The v3 contract adapts the existing +`RouteAdvertisement` / `RouteNamespaceAllocation` types. + +### Advertisement envelope (v3 adaptation) + +```text +ZoneLinkRouteAdvertisement { + advertisingZone: ZonePath // self-path of the advertising Zone + treeEdge: { parent: ZonePath, child: ZonePath } + controllerGeneration: // bound to the child controller lease + routes: [ + { descendant: ZonePath, nextHopChild: ZoneLabelId, routeId: , + capabilities: CapabilitySet } // narrowed by allocator policy + ] // 1–64 routes + issuedAtUnixSeconds: u64 + expiresAtUnixSeconds: u64 // > issuedAt; max 7200 s + signature: { + algorithm: "ed25519-blake3" + keyRole: zone-controller-routing + signingKeyFingerprint: + signatureRef: // opaque; no key bytes + } +} +``` + +Admission rules (adapted from `RouteTreeEngine::admit_advertisement`): + +1. `treeEdge.child == advertisingZone`. +2. Routes non-empty; at most 64 routes. +3. `expiresAt > issuedAt`; current time < `expiresAt`; current time >= + `issuedAt`. +4. `treeEdge.parent` must equal the local root or have an existing + non-expired parent entry. +5. Replay: signature ref + advertising zone + controller generation + + issuedAt must not duplicate a non-expired replay-window entry. +6. Namespace check: `treeEdge` and `controllerGeneration` must match the + allocated `ZoneLinkNamespaceAllocation`; route count ≤ `maxRoutes`. +7. Each route's `descendant` must be a strict descendant of + `advertisingZone`; next-hop label must be the immediate child of + `advertisingZone` toward `descendant`. +8. Each route's `capabilities` must be a subset of `allowedCapabilities` + in the private `ZoneLinkNamespaceAllocation`. +9. Capacity check: projected physical entries after admission must not + exceed `MAX_PARENT_ENTRIES` / `MAX_ROUTE_ENTRIES`; if admission would + overflow, prune expired entries first; fail closed with + `queue-full-drop-new` if pruning is insufficient. + +Admitted advertisements update parent and route entries atomically. A +replay-window key is recorded. Admission is a pure in-memory operation; +the parent Zone store is not mutated. + +### Withdrawal + +A withdrawal message removes one or more specific `routeId` values +from the engine. It must be: + +- signed by the same controller generation as the advertisement it + withdraws; +- issued at a time ≥ the advertisement's `issuedAt`; +- carries the exact `routeId` set to remove. + +The engine removes matching route entries immediately and emits a +`route-withdrawn` audit event. Partial withdrawal is allowed; routes +not named in the withdrawal remain. A withdrawal for an already-expired +or unknown `routeId` is silently accepted (idempotent). + +### Renewal + +Advertisements expire. The child's ZoneLink controller issues a renewal +advertisement before expiry using the bounded protocol scheduler. Renewal +timing is internal routing behavior, not a ZoneLink spec field. A +renewal carries: + +- a new `issuedAt` / `expiresAt` window; +- a new `signatureRef` (distinct from prior); +- optionally updated `capabilities` (may narrow but not widen beyond the + parent's private allocation). + +The engine admits the renewal exactly as a fresh advertisement, replacing +the old entry for each `routeId` named in the renewal. The prior replay +key is superseded and removed. If the renewal arrives after expiry, the +route is treated as a new advertisement. + +### Namespace allocation + +A parent allocates a `ZoneLinkNamespaceAllocation` when the child-local +ZoneLink requests activation and whenever the parent allocator's private +route policy changes: + +```text +ZoneLinkNamespaceAllocation { + treeEdge: { parent: ZonePath, child: ZonePath } + allocatedToGeneration: + allowedPrefixes: [ZonePath] // child zone or descendants; 1–16 + maxRoutes: u32 // 1–64 + allowedCapabilities: CapabilitySet +} +``` + +The child controller must sign advertisements that exactly match the +allocated edge, generation, allowed prefixes, and capability scope. +Allocation changes (e.g. capability narrowing) require the child to issue +a new advertisement under the new generation. + +The `ZoneLinkNamespaceAllocation` above **is** the explicit ZoneLink range +capacity/quota (D097 hardware-audit finding): `allowedPrefixes` (1–16) and +`maxRoutes` (1–64) are the bounded per-edge capacity; a child exceeding either +bound is rejected, so the parent allocator's route namespace cannot be +exhausted by one child edge. + +### Global vsock CID and fixed-port authority (D097) + +vsock CID allocation is a **Host-global** authority (keyed by `(Host, …)` in the +core authority index): every CID is globally unique across all Zones on the host +and a CID never crosses a Zone boundary. The historical hardcoded host-CID +assumption (`CID = 2`) is migrated to this global allocation authority — no +component assumes a fixed CID; the allocator assigns and the transport resolves +the CID under authorization, never as a public locator. Fixed listener ports +(vsock/Unix/TCP) are modeled as `Endpoint` resources with an `exactly-one`-per +Host-global port authority (see `ADR-046-resources-zone-control` §8B.3); a second +binder of the same fixed port is a `duplicateConflict`. + +## ResourceExport advertisement and ResourceImport routing (D096) + +`ResourceExport` and `ResourceImport` (defined in +[`ADR-046-resources-zone-control.md` §8A](ADR-046-resources-zone-control.md)) +use the mechanisms above unchanged; this section states how they compose. No new +transport, cross-Zone reference, or FD-forwarding path is introduced. + +- **Advertisement.** The owner Zone's core export/import controller advertises a + `ResourceExport` to the exact `consumerZonePolicy` selector over the existing + authenticated advertisement envelope, carrying only the bounded `exportKey`, + qualified semantic/provider-neutral Service type, signed projection-schema + and factory fingerprints, the closed operation set, arbitration, and + capability ceiling — never Provider/adapter identity, `spec.provider`, the local + owner-Service `resourceRef`, its Device/Endpoint/backend refs, a path, address, + secret, or bytes. Withdrawal and renewal reuse the withdrawal/renewal + machinery; export removal or ceiling narrowing issues a new generation. +- **Import matching.** The consumer Zone's `ResourceImport` names only its local + `zoneLinkRef` plus `exportKey`; core matches `exportKey` + + `expectedServiceType` + the projection-schema/factory fingerprints against the + advertisement and local installed Provider factory. Missing metadata, a + mismatch, unauthorized Zone, or absent advertisement fails closed. This + preserves the "No cross-Zone resource references" invariant. The consumer's + local `providerRef` independently selects a conformant implementation; route + matching preserves the semantic Service type exactly and never copies the + owner's implementation extension. Core creates the projection with semantic + base/import fields only and rejects `spec.provider`; routing derives from the + signed local Provider descriptor, `providerRef`, and ResourceImport record, + while implementation observations may appear only in `status.provider`. +- **Capability/RBAC ceiling.** Every hop applies the ceiling-propagation and + RBAC-narrowing rules; `requestedCapabilities` is clamped to the export + capability ceiling and to the ZoneLink allocation. No import can exceed the + advertised operation set. +- **Payload carriage.** Shared bytes flow only over the bounded encrypted named + streams described in "Named streams over ZoneLink", with a per-import session + generation, credits/backpressure, cancel, deadline, and idempotency; + intermediate controllers see ciphertext. The "No FD, credential, or host path + forwarding" invariant holds — no device FD, socket, or token crosses a Zone. +- **Projection and lifecycle.** The export target is always the qualified owner + `*Service`, never a Device, Endpoint, or `*Binding`. Core owns exactly one + same-qualified-type local projection Service per import + (`ownerRef: ResourceImport/`). Operator/Nix-authored same-Zone Bindings + reference that Service and a consuming Guest/User/Zone; their Provider + controller owns Process/Endpoint children. Binding spec is desired intent + only; all observations belong in status. Import never creates or exports a + Binding. Link failure, revocation, or withdrawal degrades the projection Service; + reconnect revalidates the remote generation and both fingerprints. D091 + currency propagates owner Service → export → import → projection Service → + Binding → children. +- **Single authority (D097).** The exported backing has exactly one authority + owner in the owner Zone (its signed `AuthorityDescriptor`, tracked in that + Zone's core authority index). Cross-Zone import never creates a second + authority or duplicate open in the consumer Zone — the projection Service is + an explicitly non-authoritative route to the owner. `exportability` and signed + projection-factory presence gate whether a Service may be shared at all + (`forbidden` authorities such as the audit chain, broker, and resource store + are never advertised over a ZoneLink). + +High-churn leases, sessions, ceremonies, transfers, named streams, and stream +handles remain controller/session-internal records; routing never promotes them +to resources or advertises them. + +The frozen routeable pairs are `audio.d2bus.org.AudioService` + +`audio.d2bus.org.AudioBinding`, `security-key.d2bus.org.SecurityKeyService` + +`security-key.d2bus.org.SecurityKeyBinding`, +`telemetry.d2bus.org.TelemetryService` + +`telemetry.d2bus.org.TelemetryBinding`, and the policy-gated +`usb.d2bus.org.UsbService` + `usb.d2bus.org.UsbBinding`. PipeWire, CTAPHID, +OTEL, and USBIP are implementation details and never route keys, base fields, +conditions, errors, or advertised status. + +## Nearest-common-ancestor (NCA) algorithm + +The route decision algorithm is a direct adaptation of +`RouteTreeEngine::build_path_at` (baseline +`packages/d2b-realm-core/src/route_engine.rs`). It is a pure in-memory +tree walk: + +``` +Input: source Zone tree path S, target Zone tree path T, current time +Output: TreeRoutePath or RouteFailClosedReason + +1. Both S and T must be known (local root, or have a non-expired + parent entry or route entry). +2. Compute the nearest common ancestor (NCA) of S and T: + a. Build ancestor sets for S and T by walking up the parent chain. + b. The NCA is the deepest label position shared by both chains. + c. Return UnknownParent if no common ancestor is found. +3. Build the upward hops from S to NCA: + a. Follow parent entries from S, recording each hop. + b. Detect cycles (visited set); return Loop if a cycle is found. + c. Return UnknownParent if any parent entry is missing or expired. +4. Build the downward hops from NCA to T: + a. Walk from NCA toward T using descendant route entries. + b. Verify each step's parent entry; return MultiParent if ambiguous. + c. Return UnknownParent if any step is missing or expired. +5. Return the concatenated hop list with NCA recorded. +``` + +After a path is built, the required `CapabilitySet` for the requested +operation kind is checked against the target Zone's advertised +capabilities: + +- If the required capability is absent: `MissingCapability`. +- If the target Zone has no route entry: `UnknownParent`. + +The algorithm returns a `ZoneRoutePath`: + +```text +ZoneRoutePath { + sourceZone: ZonePath + targetZone: ZonePath + nearestCommonAncestor: ZonePath + hops: [ ZoneRouteHop ] // max 32 +} + +ZoneRouteHop { + from: ZonePath + to: ZonePath + edge: { parent: ZonePath, child: ZonePath } + direction: up-to-parent | down-to-child + routeId: ? +} +``` + +The result is immutable route metadata. It carries no transport socket, +relay endpoint, credential, or host path. d2b-bus uses the hop list to +compose the sequence of ComponentSession calls that forward the request. + +## Capability and RBAC narrowing + +### Allocated capability propagation + +The parent allocator's private `ZoneLinkNamespaceAllocation` declares +`allowedCapabilities`, the maximum `CapabilitySet` the parent will route to +the child. It is not part of the ZoneLink ResourceSpec. The scope is enforced +at two points: + +1. **Advertisement admission**: the child's advertised `capabilities` for + each route must be a subset of `allowedCapabilities` in the namespace + allocation (`CapabilitySet::is_subset_of`). A route advertising + capabilities beyond the allocation is rejected with + `namespace-violation`. +2. **Bus route decision**: the `required_capability` for the operation is + checked against the route's advertised capabilities at the target Zone. + A missing capability returns `MissingCapability`. + +A parent cannot route a call to a child for an operation type the child +did not advertise (even if the parent itself holds that capability). This +makes capability propagation opt-in downward. + +### RBAC narrowing + +When the parent d2b-bus forwards a resource API call to the child Zone, +the call is authorized using the child's own native RBAC engine with a +subject mapped from the ZoneLink's enrolled identity: + +``` +child-local subject = child RoleBinding that: + - matches the parent Zone's enrolled link principal with a trusted exact + externalPrincipalSelector + - grants only the verbs/resourceTypes/names declared in the binding + - has a capability scope no wider than the allocator-issued scope +``` + +The parent cannot self-assert a subject, verb, or resource name in the +child. The child's ResourceAPI authorization evaluates +`AuthenticatedSubjectContext.subjectRef` which is set from the KK-enrolled +parent identity, not from the forwarded request payload. + +Authorization at each hop is independent: + +- The source Zone authorizes the caller's target verb before selecting a route. +- Each intermediate Zone authenticates the inbound adjacent-Zone transport + subject, then independently authorizes both `relay` and the immutable target + verb using its own RBAC. +- The target child authenticates its inbound adjacent-Zone subject and + authorizes the final target verb using child-local RBAC. + +A child that has not bound a matching parent RoleBinding refuses the call +with `authorization-denied`. A forwarding Zone without the separate `relay` +grant returns `relay-denied`. `relay` is core-generated/ZoneLink-scoped and +permits only one route-selected next hop; it grants no CRUD, identity mapping, +capability widening, attachment/credential access, or local lifecycle +authority. No ambient cross-Zone authority exists. + +### Capability floor at local root + +The local Zone root (`Zone/`) advertises its own capabilities +to parent Zones. These reflect the installed Provider catalog, active +Hosts/Guests, and runtime state. A capability absent from the local root's +advertised set is not reachable from any ancestor Zone. + +## Resource API forwarding + +### Forwarding model + +A parent ResourceClient targeting a child Zone routes through d2b-bus: + +``` +Parent ResourceClient + -> parent d2b-bus (route decision: local or allocator-bound child edge) + |-- local Zone: direct to local d2b.resource.v3 + |-- child Zone: ComponentSession represented by child's local ZoneLink + -> intermediate Zone d2b-bus (hop relay) + -> child d2b.resource.v3 +``` + +At each intermediate hop, d2b-bus: + +1. Verifies the inbound ComponentSession's `AuthenticatedSubjectContext`. +2. Evaluates local RBAC for the forwarded target verb with the immutable + ResourceType/service and target Zone. Named methods retain one exact resource + name. Nameless `List`/`Watch` retain an exact non-empty authorized + `resourceNames` set and bounded filters whose possible results are a subset + of that set. +3. Separately evaluates local RBAC for the `relay` session verb against the + authenticated inbound Zone transport subject, governing ZoneLink, exact + target bounds, and route-selected next hop. +4. Fails closed if either check or policy state is missing; no grant is inferred + from a prior hop. +5. Decrements the hop counter; refuses if zero. +6. Opens the allocator-bound ComponentSession represented by the next child + Zone's local uplink. +7. Re-serializes the request with the decremented hop counter; preserves + the named target or nameless selector, all filters/pagination/watch cursor, + and the original operation/idempotency/correlation/trace IDs unchanged. +8. Returns the child response to the inbound caller. + +Relay is a distinct RBAC verb. Intermediate Zones may deny relay without +blocking local resource calls. A relay allow never supplies the target-verb +allow. + +### No cross-Zone resource references + +A resource returned from the child Zone contains only child-Zone +`*Ref` values. The parent sees the resource opaquely. The parent may call +`Get`, `List`, `Watch`, `UpdateSpec`, `UpdateStatus`, `Delete`, and +`CommitBatch` on child resources, but: + +- cannot create a parent-local `*Ref` pointing at a child resource; +- cannot use a child resource's `metadata.uid` in a parent resource spec; +- cannot resolve `ResolveRef` across Zone boundaries; +- cannot inject ownerRef cycles that span Zones. + +An ownerRef in a child resource must resolve to a resource in the same +child Zone. An attempt to set `ownerRef` to a parent-Zone resource is +rejected at child admission with `resource-ref-invalid`. + +### Watch forwarding and revision cursor resync + +A parent that opens a `Watch` on a child Zone resource type uses the +child's own revision cursors: + +1. Parent sends `Watch(resourceType, filters, afterRevision)` over the + ZoneLink ComponentSession. +2. Child streams `ResourceWatchEvent` items, each carrying the child's + revision token. +3. If the ZoneLink session disconnects, parent route-session state records the + last seen child revision; no parent-store ZoneLink row is created. +4. On reconnect, the parent re-issues `Watch` with the identical ResourceType, + authorized name set, and filters plus `afterRevision=`. +5. If the child reports `revision-expired` (cursor too old), the parent + re-issues `List` with that identical selector/filter set to obtain a fresh + snapshot, then re-opens `Watch` with those same filters after the snapshot + revision. + +The parent may not merge child revisions with its own Zone revision +namespace. Parent-local watchers for child resources must be driven +separately from parent-local watches. Watch delivery uses the same +named-stream credit mechanism as local watches; backpressure on the +parent-side does not stall child-local delivery. + +### Batch forwarding + +`CommitBatch` targeting a single child Zone is forwarded atomically: +the child commits all mutations in one Zone transaction. A batch that +spans multiple Zones is rejected at the parent d2b-bus before any +forwarding; the caller must split it. + +## Runtime service calls over ZoneLink + +Runtime service calls (ComponentSession connect/invoke/open-stream and the +exact diagnostic methods authorized by `audit-export` or `support-bundle`) may +be forwarded through a ZoneLink if: + +- the target service is declared in a child Zone Provider's service + descriptor; +- the call's `purpose` class is `remote-zone`; +- RBAC at each forwarding hop grants both `relay` for the authenticated + adjacent-Zone subject and the target session verb (`connect`, `invoke`, + `open-stream`, `audit-export`, or `support-bundle`) for the exact + service/method/stream. The diagnostic verbs remain bound only to + `d2b.audit.v3.AuditService/Export` and + `d2b.support.v3.SupportService/GenerateBundle` and grant no resource + authority; +- the hop count does not exceed the fixed protocol limit of 16. + +The forwarded session carries: + +- original `AuthenticatedSubjectContext` digest (opaque; child cannot + expand it); +- operation, idempotency, correlation, and trace IDs unchanged; +- decremented hop count in the session prologue; +- no raw credential, FD, or host path in any forwarded frame. + +Named streams opened through a ZoneLink follow the same credit-forwarding +model as local named streams. Each hop maintains independent backpressure. +A blocked intermediate hop cannot cause unbounded memory growth at the +originating Zone. + +## Operation lifecycle, idempotency, cancellation, and pinned reverse path + +### Idempotency + +Every mutating resource API call forwarded through a ZoneLink carries a +`ZoneLinkIdempotencyKey`: + +``` +ZoneLinkIdempotencyKey { + operationId: + idempotencyKey: + sourceZonePath: ZonePath + targetZonePath: ZonePath + operationKind: ResourceApiMethodKind + principalDigest: +} +``` + +The dedup namespace is the full 6-tuple; the same opaque +`idempotencyKey` reused under a different source/target/operation/ +principal cannot collide. The child-Zone resource API dedup engine is the +single dedup owner for calls that reach it. Intermediate hop relays do +not deduplicate; they forward. + +Retention window: completed dedup records are retained for 15 minutes +(matching `DEFAULT_RETENTION` in `d2b-realm-router/src/lib.rs`). A +tombstone is kept for 60 minutes after that window (`DEFAULT_NO_REUSE_HORIZON`) +to ensure post-retention reuse fails closed. + +States: +- `InProgress`: same key/request is still running; returns original + `operationId`. +- `Replay`: same key/request completed; returns recorded result. +- `Conflict`: same key, different request fingerprint; refuses. +- `Expired`: key reused after retention window. + +### Cancellation + +A cancel message sent over the ZoneLink for a specific `operationId`: + +1. Parent d2b-bus sends `Cancel(operationId)` downstream on the pinned + reverse path. +2. Each intermediate hop relays the cancel to the next hop. +3. The target Zone's resource API cancels the outstanding operation if it + is still running. +4. Cancellation is best-effort: a completed operation is not un-committed. +5. Cancel delivery failure does not extend the caller's deadline. + +### Pinned reverse path + +The route decision that admitted the operation produces a `ZoneRoutePath`. +This path is pinned for the lifetime of the operation: + +- reply traffic from the child follows the same hops in reverse + (`down-to-child` hops become `up-to-parent`); +- cancel and status-poll messages use the same pinned path; +- intermediate hops may not reroute in-flight traffic; +- if a hop on the pinned path fails, the operation fails with + `zone-link-disconnected`; it is not silently rerouted. + +The pinned path metadata carries no transport socket or credential; it is +a sequence of Zone tree edges plus the session generation bound to each +hop. If a hop's session generation changes (reconnect), the path is +invalidated and the operation fails with `zone-link-disconnected`. The +caller may retry with a new idempotency key after reconnect. + +## Named streams and direct shortcuts + +### Named streams over ZoneLink + +A named stream opened through a ZoneLink occupies one stream slot on every +ComponentSession in the hop chain. Credit is independently managed at each +hop: + +``` +Source Zone Intermediate Zone Target Zone + stream writer + |-- ZoneLink CS1 ---> relay stream + | (credit: parent) |-- ZoneLink CS2 ---> target stream + | | (credit: relay) (credit: child) +``` + +Backpressure from the target propagates inward hop by hop; no hop grants +more credit than its downstream grants it. A source that exceeds its hop-1 +credit budget blocks rather than buffering unboundedly. + +Named streams inherit the `purpose`, `service`, and `schema fingerprint` +of the originating session. At every hop the adjacent ZoneLink transport +subject is authenticated by that hop's KK ComponentSession and local RBAC +separately requires `relay` plus `open-stream` for the immutable target. No +forwarded payload may self-assert the subject or either authorization result. + +### Direct shortcuts + +A direct shortcut allows two Zones with an established path to bypass +the hop relay for subsequent operations, using a pre-authorized shortcut +ComponentSession established directly between source and target. + +Shortcuts are authorized using `ZoneRouteEngine::decide_direct_shortcut` +(adapted from `RouteTreeEngine::decide_direct_shortcut` in +`d2b-realm-core/src/route_engine.rs`): + +1. A `ZoneLinkShortcutAuthorizationRequest` is issued with a bounded + expiry (max 1 hour). +2. The NCA Zone (the common ancestor on the tree path) authorizes the + shortcut if: + - the existing tree path is currently `Allowed`; + - the requesting policy rule explicitly permits shortcuts + (`allowDirectShortcut: true`); + - the shortcut metadata does not carry transport endpoints or + credentials (opaque `shortcutId` only). +3. The authorized `shortcutId` is presented to the target Zone's transport + provider to establish a direct ComponentSession outside the tree relay + chain. +4. The shortcut expiry and capability set cannot exceed those of the + authorized tree path. +5. Shortcut teardown reasons: `completed`, `expired`, `policy-revoked`, + `link-failure`. + +A shortcut is optional optimization metadata. Absence of a shortcut does +not break routing; the tree relay path remains authoritative. Shortcuts +are not provisioned without explicit policy authorization. +Shortcut establishment does not convert `relay` into target authority: every +tree hop must admit the setup under its bounded relay and target scope, and +every operation on the resulting direct session is authorized for its target +verb at the destination. + +## No FD, credential, or host path forwarding + +Zone routing deliberately excludes the following from all inter-Zone +frames: + +- **File descriptors (FDs)**: SCM_RIGHTS is local Unix only. A ZoneLink + ComponentSession uses vsock or an Azure Relay transport; neither carries + FDs. A request that requires FD transfer is rejected at the source + d2b-bus with `attachment-not-permitted-over-zone-link`. +- **Credentials and secrets**: no token, session PSK, private key, + bearer token, enrollment secret, or credential lease byte may appear in + a forwarded frame payload, routing metadata, or named stream content. + Credential resources contain only opaque `leaseId` handles, never + credential material. +- **Host paths**: no filesystem path, socket path, device path, or + store path may appear in routing metadata. Transport bindings reference + only provider-schema-validated binding descriptors without raw paths. +- **PIDs, pidfds, and broker ops**: not propagated across Zones. A + child Zone's Process lifecycle is managed entirely by child-local + controllers. + +Violation of these constraints is a structural check failure at the +serialization boundary, not a runtime policy decision. + +## Link failure, restart, revocation, and loop and hop limits + +### Link failure and restart + +When a child-local ZoneLink ComponentSession disconnects: + +1. The ZoneLink controller sets the link's `SessionEstablished` condition + to `False` and phase to `Degraded` (or `Unknown` if no session was ever + established this generation). +2. In-flight operations with a pinned path through this link fail + immediately with `zone-link-disconnected`. +3. Child-to-parent intents may accumulate in the child store up to + `spec.limits.maxPendingIntents`; parent-to-child operations fail + immediately. +4. The child-local controller schedules bounded Provider-internal reconnect + attempts, limited by `spec.limits.reconnectMaxAttempts` within + `spec.limits.reconnectWindowSecs`. +5. On reconnect: + - a new KK handshake is performed against the allocator-sealed enrolled + identity and resolved `spec.transportCredentials`; + - a new link epoch is assigned; + - all pinned-path tracking for the old epoch is cleared; + - queued intents are replayed (with original idempotency keys if the + queued mutation is younger than the retention window, or as new + operations if older); + - the child-local ZoneLink handler re-issues parent route/export + advertisement watches from the last known parent revision or triggers + `List` + reopen if the cursor expired. +6. After reconnect, `status.resource.linkEpoch` is incremented and + `status.resource.lastConnectedAt` is updated atomically with the cursor + fields. + +During disconnected recovery, the ZoneLink controller updates only its own +local status and outbound intent queue. It does not infer or mutate other +child-local resources from stale parent state, and it performs no remote +cleanup or status correction without a live session. + +### Revocation + +A ZoneLink may be administratively revoked by setting +`spec.disabled: true` and then deleting the resource. + +Revocation sequence: + +1. Spec update: `disabled` set to `true`; the child handler stops reconnecting. +2. Child-local ZoneLink controller issues a route withdrawal for all advertised + `routeId` values. +3. The parent's `RouteTreeEngine` removes all entries for the child. +4. d2b-bus immediately returns `zone-link-revoked` for any new call + targeting the child Zone or any descendant reachable only through + this link. +5. Long-lived streams on this link receive `zone-link-revoked` and close. +6. In-flight operations are cancelled (best-effort); already-committed + child-Zone operations are not rolled back. +7. The child-local ZoneLink resource deletion proceeds through normal finalizer + policy and releases the parent allocator binding; no reciprocal parent + resource requires deletion. + +Authorization lease revocation: when a Role or RoleBinding governing +cross-Zone access changes, the parent's d2b-bus authorization engine +invalidates cached decisions immediately. New forwarded calls require +fresh authorization; outstanding long-lived streams receive a short +reauthorization deadline. + +### Loop detection + +The NCA algorithm detects loops using a `visited` set during the upward +walk from source to NCA. If the same Zone path appears twice in the +walk, the decision returns `loop`. + +Additionally, the parent's `RouteTreeEngine` enforces: + +- A route for `descendant` D must name exactly one `next_hop_child`; if + two advertisements claim different next-hop children for the same + descendant, the later one is rejected with `multi-parent`. +- A parent entry for child C must have `parent == local_root` or a + non-expired parent entry for the intermediate path. A chain that + refers back to its own `advertisingZone` is rejected at admission. + +The hop counter enforced at source d2b-bus provides a belt-and-suspenders +limit independent of the tree walk. The protocol-wide initial budget is 16 +and is not configurable in ZoneLink spec. + +### Hop limits + +Hop counter enforcement: + +- Source d2b-bus decrements the counter before forwarding. +- An operation that arrives at d2b-bus with `remainingHops == 0` is + refused with `hop-limit-exceeded`. +- The remaining-hops field is verified at each intermediate hop; a frame + that claims more remaining hops than it arrived with is rejected with + `malformed-hop-count`. +- The counter is scoped per-call, not per-session. + +## Local intents while disconnected + +The child-local ZoneLink handler may enqueue `UpdateSpec`, `Create`, and +`Delete` intents directed from the child to parent/ancestor resources while +the uplink is disconnected. Parent-to-child mutations are not queued in the +child and fail at the parent route boundary. +Behavior: + +- Intents are stored in the child Zone as bounded `ZoneLinkIntent` records, not + resource mutations. No parent resource state is assumed. +- `spec.limits.maxPendingIntents` limits the queue. When the queue is full, + new intents fail with `zone-link-intent-queue-full`. +- Intent records carry the original operation/idempotency/correlation IDs + and a `queuedAt` timestamp. +- On reconnect, intents are replayed in order with their original + idempotency keys if within the retention window; older intents are + replayed with fresh operation IDs and marked as `late-replay`. +- A replayed intent that receives `resource-conflict` (stale revision) is + not retried automatically; the caller is notified with the conflict. +- A `Get`, `List`, or `Watch` call traversing a disconnected uplink always + returns `zone-link-disconnected` immediately; it is never queued. +- If `maxPendingIntents` is zero, mutating calls fail immediately with + `zone-link-disconnected`; intents are never silently dropped. + +Local intent queueing does not constitute a claim of parent-Zone state. +The ZoneLink handler does not infer parent resource phase, condition, or +status from locally queued intents. + +## Topology diagrams + +### K0 / K1 / K2 example (Host/Guest terminology) + +``` +K0: parent Zone (Host-based, runs on physical host) + Host/host-system + sealed topology/route state only (no ZoneLink resource or handler) + +K1: child Zone (Guest VM, cloud-hypervisor) + ZoneLink/k1-uplink -> K0 allocator (transport: unix socket) + Guest/dev-vm + Host/host-system (accessible only within K1) + +K2: grandchild Zone (nested VM or ACA container) + ZoneLink/k2-uplink -> K1 allocator (transport: vsock CID 5) + Guest/work-container + Process/web-server (K2-local only) +``` + +Route tree from K0's perspective: + +``` +K0 (local root allocator) + └── K1 (child-local ZoneLink/k1-uplink; advertises itself + K2) + └── K2 (child-local ZoneLink/k2-uplink; next-hop K1) +``` + +A call from K0 to `Process/web-server` in K2: +1. K0 d2b-bus: NCA=K0, path K0→K1→K2, 2 hops. +2. K0 opens the allocator-bound ComponentSession represented by K1's local + `ZoneLink/k1-uplink`. +3. K1 d2b-bus: hop count decremented (1 remaining), relays to K2. +4. K2 d2b-bus: hop count decremented (0 remaining), dispatches to + local `d2b.resource.v3`. +5. Reply returns K2→K1→K0 on the pinned reverse path. + +RBAC at each hop: + +``` +K0: subject=User/alice verb=get resourceType=Process zone=K2 + -> K0 Role admits the target operation before route selection +K1: subject=Zone/k0 sessionVerbs=[relay] verbs=[get] resourceType=Process + resourceNames=[web-server] zone=K2 + -> K1 RoleBinding independently allows one-hop forwarding and target get +K2: subject=Zone/k1 verb=get resourceType=Process name=web-server + -> K2 Role allows the authenticated adjacent Zone to get Process/web-server +``` + +### Simple K0/K1 example (local Host + remote Guest) + +``` +K0 (Host/host-system, local Zone) + sealed topology/route state only (no ZoneLink resource or handler) + +K1 (Guest/workvm, cloud-hypervisor VM) + ZoneLink/k1-uplink -> K0 allocator + Guest/workvm + Process/wayland-proxy + Process/shell-session +``` + +A watch from K0 on K1 Processes: + +``` +1. K0 ResourceClient: Watch(Process, + resourceNames=[wayland-proxy,shell-session], zone=K1, + afterRevision=none) +2. K0 d2b-bus: route to K1 via the allocation represented by K1's + ZoneLink/k1-uplink +3. K1: List with the identical resourceNames filter → revision R1; + Watch with that same filter and afterRevision=R1 +4. K1 streams events to K0 named-stream (credit-bounded) +5. Disconnect: K0 records lastRevision=R1 +6. Reconnect: K0 re-opens Watch with the identical filter and afterRevision=R1 +7. K1 delivers matching events after R1 or returns revision-expired → K0 + relists with the identical filter +``` + +## Audit, OTEL, errors, and security + +### Audit records + +Zone routing emits the following audit event types (adapted from +`RouteAuditEventKind` in `routing.rs`): + +| Event | Contents | +| --- | --- | +| `zone-route-allowed` | sourceZone path digest, targetZone path digest, NCA digest, operation kind, hop count, policy rule id, session generation, correlation id | +| `zone-route-denied` | same as above + `RouteFailClosedReason` | +| `zone-advertisement-accepted` | advertising zone digest, controller generation ref, route count, expiry, replay window id | +| `zone-advertisement-denied` | same as above + reason | +| `zone-advertisement-withdrawn` | advertising zone digest, withdrawn route ids, issuer generation ref | +| `zone-link-session-established` | link name digest, child zone name digest, session generation | +| `zone-link-session-failed` | link name digest, reason code | +| `zone-link-intent-queued` | link name digest, operation kind, intent count | +| `zone-link-shortcut-authorized` | shortcut id, NCA digest, source/target zone digests | +| `zone-link-shortcut-torn-down` | shortcut id, teardown reason | +| `zone-link-revoked` | link name digest, revocation trigger | +| `zone-link-relay-admitted` | hop source/target zone digests, operation kind, session generation | +| `zone-link-relay-denied` | hop source/target zone digests, reason | + +Audit records exclude: + +- raw Zone tree paths (replaced by opaque digests); +- transport endpoints, socket paths, or relay addresses; +- resource payloads, spec/status content, or Provider diagnostics; +- credentials, tokens, or key material; +- host paths, process identities, or PIDs. + +### OTEL metrics + +Metric labels use closed low-cardinality sets: + +| Metric | Labels | +| --- | --- | +| `d2b.zone_route.decision.total` | `outcome` (allowed/denied), `operation_kind`, `hop_count_bucket`, `reason_code` | +| `d2b.zone_route.advertisement.total` | `outcome` (accepted/denied/withdrawn/replayed), `reason_code` | +| `d2b.zone_link.session.state` | `phase` (pending/ready/degraded/failed/unknown) | +| `d2b.zone_link.reconnect.total` | `reason` | +| `d2b.zone_link.intent.queued` | (none) | +| `d2b.zone_link.relay.total` | `outcome`, `operation_kind` | +| `d2b.zone_route.shortcut.total` | `outcome` (authorized/denied/torn-down), `teardown_reason` | + +Prohibited labels: Zone names/UIDs, ZoneLink names or hashes/digests derived +from them, resource names, subject refs, provider diagnostics, host paths, +session keys, or advertisement payload. Zone identity remains the `d2b.zone` +OTEL resource attribute; ZoneLink identity remains available in authorized +audit records, never metric labels. + +### OTEL spans + +Spans are emitted for: + +- per-call route decision (leaf span); +- per-hop relay (child span); +- ZoneLink session establish/reconnect (span with generation attribute); +- advertisement admission (span with replay-window depth attribute). + +Span attributes include operation kind, hop count, outcome code, and +correlation/trace IDs. No resource payload, zone name, or endpoint is +included in span attributes. + +### Stable error codes + +| Error code | Meaning | +| --- | --- | +| `zone-link-disconnected` | ZoneLink session not established; call rejected | +| `zone-link-revoked` | ZoneLink was administratively revoked | +| `zone-link-intent-queue-full` | Intent queue reached `spec.limits.maxPendingIntents` | +| `hop-limit-exceeded` | Forwarded call has no remaining hops | +| `malformed-hop-count` | Hop counter in inbound frame claims more hops than allowed | +| `relay-denied` | Intermediate Zone lacks the exact ZoneLink-scoped relay grant | +| `authorization-denied` | A source, intermediate, or target Zone denied the forwarded operation's target verb | +| `attachment-not-permitted-over-zone-link` | FD attachment rejected on ZoneLink transport | +| `zone-route-not-found` | No ZoneLink path exists to the target Zone | +| `zone-route-capability-denied` | Required capability absent from route | +| `zone-route-loop` | NCA algorithm detected a cycle | +| `zone-route-multi-parent` | Route table has conflicting parents for one child Zone | +| `zone-advertisement-namespace-violation` | Advertisement exceeds allocated namespace or capability scope | +| `zone-advertisement-replay` | Duplicate advertisement (replay rejection) | +| `zone-advertisement-expired` | Advertisement received after expiry | +| `zone-advertisement-malformed` | Advertisement fails structural invariants | +| `zone-shortcut-denied` | NCA did not authorize the shortcut | +| `zone-shortcut-expired` | Shortcut token expired | + +Error messages are bounded, UTF-8/control-character validated, and must +not contain resource payloads, zone paths, transport endpoints, +credentials, or provider diagnostics. + +### Security invariants + +1. A Zone never grants authority beyond its own Role/RoleBinding evaluation. + Forwarded calls require the target verb at each hop and a separate `relay` + allow at each forwarding hop. +2. A child Zone's resource status is not inferred from local intents; the + parent only learns child state from authenticated responses. +3. The KK handshake verifies the allocator-sealed enrolled identity and + resolved transport credentials; a different identity is refused before + any resource exchange. +4. Allocated capability scopes narrow monotonically downward; a child cannot + advertise capabilities beyond what its parent allocated. +5. No FD, credential, or host path is forwarded. Transport bindings are + provider-schema-validated opaque values. +6. Loop detection and hop limits bound resource consumption; a malformed + route tree cannot cause unbounded forwarding. +7. Route advertisements are signed and expiring. A replay of a prior + valid advertisement fails the replay-window check. +8. Zone tree paths in audit records are replaced by opaque digests; + raw path strings never enter low-cardinality metrics. + +## Current-code fit + +The following table classifies every Realm-related current baseline symbol. +Evidence classes: **A** = implemented-and-reachable from production binary, +**B** = implemented-but-unwired, **C** = generated-or-eval-contract, +**D** = test-only. + +### Core routing types + +| Symbol | File | Class | Production callers | ADR 0046 mapping | +| --- | --- | --- | --- | --- | +| `RouteTreeEngine` (NCA, `admit_advertisement`, `decide_route`) | `d2b-realm-core/src/route_engine.rs` | **B** | None in d2bd/CLI — inline test suite at line 1202 (45 functions) | → `ZoneRouteEngine` (adapt/rename) | +| `RouteAdvertisement`, `RouteNamespaceAllocation`, `RealmTreeEdge`, `DescendantRoute` | `routing.rs` | **B** | Only in `route_engine.rs` tests | → v3 advertisement envelope with ZonePath | +| `TreeRoutePath`, `TreeRouteHop` | `routing.rs` | **B** | Same | → `ZoneRoutePath`/`ZoneRouteHop` (rename + ZonePath) | +| `RouteFailClosedReason` | `routing.rs` | **B** | Same | → preserved + extended | +| `DirectShortcutAuthorizationRequest/Decision/Teardown` | `routing.rs` | **B** | Same | → ZoneLinkShortcutAuthorization* (adapt) | +| `RouteAuditEventKind` (12 variants) | `routing.rs` | **B** | Same | → Zone-prefixed audit events | +| `RouteRealmClass` (`LocalRoot`…`EphemeralDiscovered`) | `routing.rs` | **B** | Same | → `ZoneClass` metric label (variants map to private Zone runtime bootstrap placement types) | +| `RouteId`, `ControllerGenerationId` | `ids.rs` | **A** | Used in gateway, d2bd, CLI via `d2b_realm_core` | → `ZoneRouteId`, `ZoneLinkControllerGeneration` | + +### Identifier types + +| Symbol | File | Class | Production callers | ADR 0046 mapping | +| --- | --- | --- | --- | --- | +| `RealmId` (one label) | `ids.rs` | **A** | d2bd, d2b CLI, d2b-gateway, priv-broker | → `ZoneLabelId` | +| `RealmPath` (Vec) | `realm.rs` | **A** | d2bd, d2b CLI, d2b-gateway | → `ZonePath`; label grammar identical; public address format changes | +| `WorkloadId` | `ids.rs` | **A** | d2bd, d2b CLI, d2b-gateway, realm-router | → split: `Guest/` or `Host/` per semantic classification | +| `NodeId` | `ids.rs` | **A** | d2b-realm-core, d2b-gateway | → Host resource name or Zone-local implicit address | +| `ProviderId`, `ExecutionId`, `StreamId`, `PrincipalId`, etc. | `ids.rs` | **A** | Throughout | → preserved with v3 names; secret-marker reject logic retained | +| `EntrypointMode` (`HostResident`/`GatewayBacked`) | `realm.rs` | **A** | `d2b/src/target_routing.rs` | → subsumed into ZoneLink `spec.transportProviderRef` | +| `RealmControllerPlacement` (6 variants) | `realm.rs` | **A** | `d2b-realm-core`, bundle artifacts | → private Zone runtime bootstrap placement; compiler-only `parentZone` selects the allocator owner while child-local uplink identity/transportProviderRef supplies transport state (none is a public Zone.spec parent field) | +| `TargetName` (DNS-form target string) | `target.rs` | **A** | d2bd, d2b-gateway | → v3 resource ref path | +| `RealmTarget` (workload + realm path) | `target.rs` | **A** | d2bd/realm_access_resolver.rs | → Zone-local resource lookup | + +### Access resolver and CLI routing + +| Symbol | File | Class | Production callers | ADR 0046 mapping | +| --- | --- | --- | --- | --- | +| `RealmAccessBinding`, `RealmTransportBinding`, `RealmAccessResolverRequest/Response/Error` | `access.rs` | **B** | `realm_access_resolver.rs` only (which is itself unwired) | → ZoneAccessBinding; ZoneEntrypointResolver | +| `resolve_local_root_realm_access()` | `d2bd/src/realm_access_resolver.rs` | **B** | `pub mod` declared at `d2bd/src/lib.rs:117`; no callers in running daemon | → ZoneEntrypointResolver (routing-003) | +| `RealmEntrypointTable`, `DispatchTarget` | `d2b-realm-router/src/target_resolver.rs` | **A** | `d2b/src/lib.rs:5240` (`load_realm_entrypoint_table()`); `d2b/src/target_routing.rs` | → ZoneEntrypointResolver (routing-003) | +| `Route::Local`/`Route::GatewayBacked` dispatch | `d2b/src/target_routing.rs` | **A** | CLI live routing path | → sealed topology + authenticated `ZoneRouteEngine` projection routing | +| `realm list`, `realm inspect` CLI commands | `d2b/src/lib.rs:5942` | **A** | CLI; reads `realm-entrypoints.json` | → `zone list`/`zone inspect` from compiler topology joined with authenticated route/projection status; never a parent ZoneLink list | + +### Operation routing and session layer + +| Symbol | File | Class | Production callers | ADR 0046 mapping | +| --- | --- | --- | --- | --- | +| `OperationRouter`, `DEFAULT_RETENTION/NO_REUSE_HORIZON/MAX_DEDUP_RECORDS` | `d2b-realm-router/src/lib.rs` | **B** | Only `d2bd/src/realm_stubs.rs` (`#[allow(dead_code)]`, declared `pub mod` at `d2bd/src/lib.rs:249`) | → ZoneLinkIdempotencyKey 6-tuple in d2b-bus (routing-005) | +| `RemoteNodeRegistry`, `RemoteNodeEntry`, `RemoteNodeAvailability` | `d2b-realm-router/src/remote_node.rs` | **B** | Same dead_code seam only | → ZoneLink controller handler (routing-004) | +| `SessionLifecycle`, `SessionPhase` | `d2b-realm-router/src/session_lifecycle.rs` | **B** | Same dead_code seam only | → ZoneLink session state machine (routing-004) | +| `MuxSession` stream/operation forwarding | `d2b-realm-router/src/mux_session.rs` | **B** for zone relay; **D** for test suite | No zone relay callers; only used in realm-router internal tests and display transport | → d2b-bus Zone relay path (routing-005) | +| `PeerSession`, `SecurePeerSession` | `d2b-realm-router/src/session.rs`, `secure_session.rs` | **B** (from d2bd); reachable within realm-router tests | No d2bd/CLI import | → ComponentSession/Noise KK (d2b-bus) | +| `DisplayTransportBinding`, `verify_display_preface` | `d2b-realm-router/src/display_transport.rs` | **A** within realm-router | Display-session path only | Not Zone routing; remains display-specific | + +### Wire protocol + +| Symbol | File | Class | Production callers | ADR 0046 mapping | +| --- | --- | --- | --- | --- | +| `ConstellationFrame` (all variants), `Handshake*`, `OperationRequest/Response`, `StreamOpen/Data/Flow/Close/Resume` | `d2b-realm-core/src/frame.rs` | **B** from d2bd; **A** within realm-router and d2b-gateway-runtime (individual types) | `d2b-gateway-runtime` uses individual frame types; d2b-realm-codec-protobuf serializes via realm-router | → v3 d2b-bus frame variants; codec protobuf numbers refrozen | +| `ProtobufCodec` | `d2b-realm-codec-protobuf/src/lib.rs` | **A** within realm-router; **B** from d2bd | `d2b-realm-router` session/mux layer only | → v3 codec (routing-002 + bus spec) | +| `StreamMux` (open stream table) | `d2b-realm-core/src/mux.rs` | **B** from d2bd; **A** within realm-router tests | realm-router internal tests | → v3 named-stream multiplexing | + +### Enrollment and identity + +| Symbol | File | Class | Production callers | ADR 0046 mapping | +| --- | --- | --- | --- | --- | +| `EnrollmentRecord`, `KeyRotationPlan`, `RevocationRecord`, `SessionTeardownDirective` | `d2b-realm-core/src/enrollment.rs` | **B** | `RealmIdentityStore` only (itself unwired) | → ZoneLink controller session/revocation lifecycle | +| `RealmIdentityStore` | `d2b-realm-core/src/identity_store.rs` | **B** | No production callers found | → ZoneLink controller enrollment state machine | +| `RealmIdentityConfigJson` (schema v2) | `d2b-realm-core/src/identity_config.rs` | **A** | Loaded at d2bd startup (lib.rs:1425) and priv-broker startup (runtime.rs:704); logs "runtime trust sessions remain inert" | → allocator-sealed ComponentSession enrollment identity plus ZoneLink `spec.transportCredentials`; artifact retires | + +### Allocator engine + +| Symbol | File | Class | Production callers | ADR 0046 mapping | +| --- | --- | --- | --- | --- | +| `LocalRootAllocatorEngine`, `AllocatorAllocationDecision`, `FakeAllocatorLedger` | `d2b-realm-core/src/allocator_engine.rs` | **B** | No production callers outside crate | → Zone resource allocation engine (core-controller) | +| `LeaseAllocationRequest`, `AllocatorLease`, `GrantedHostResource` | `d2b-realm-core/src/allocator.rs` | **B** | No production callers outside crate | → Zone/Provider resource spec fields | +| `AllocatorJson`, `AllocatorRealm`, `RealmPlacement` | `d2b-core/src/allocator_config.rs` | **C** | Loaded by bundle resolver; read by priv-broker/d2bd as eval contract | → Zone resource spec in Nix | + +### Provider traits + +| Symbol | File | Class | Production callers | ADR 0046 mapping | +| --- | --- | --- | --- | --- | +| `WorkloadProvider`, `DisplayProvider`, `RuntimeProvider`, `PersistentShellProvider` traits | `d2b-realm-provider/src/provider.rs` | **B** | `d2b-host-providers` implements (itself unwired); `d2b-provider-aca`/`relay` implement and ARE wired for display session only | → Provider ResourceType controller/service processes | +| `AcaWorkloadProvider` | `d2b-provider-aca/src/lib.rs` | **A** (display session path) | d2bd `new_gateway_display_runtime_from_config` (lib.rs:4165); ACA display gateway only | Not Zone routing; display path remains display-specific | +| `CapabilitySet`, `WorkloadCapabilitySet`, `DisplayCapabilitySet` | `d2b-realm-provider/src/capabilities.rs` | **B** | Traits only; d2b-gateway-runtime uses `DisplayCapabilitySet` | → v3 private route-allocation capability fields | +| `ProviderCircuitBreaker` | `d2b-realm-provider/src/rate_limit.rs` | **B** | `d2b-provider-aca` only (display path) | → Provider rate-limit policy | + +### Bundle artifacts and Nix options + +| Symbol | File | Class | Production callers | ADR 0046 mapping | +| --- | --- | --- | --- | --- | +| `RealmControllersJson` (schema v2) | `d2b-core/src/realm_controller_config.rs` | **C** | Loaded at d2bd startup (lib.rs:1408) and priv-broker (runtime.rs:687); logs "runtime routing remains inert" | → compiler-only `parentZone` bootstrap topology + runtime Zone self-resource + child-local ZoneLink resources; artifact retires | +| `WorkloadTargetIndex` | `d2bd/src/workload_target_index.rs` | **A** | d2bd `lib.rs:16745` in `PublicRequestArtifacts`; maps canonical targets → VM names for exec dispatch | → Guest/Host resource lookup; retires with legacy VM dispatch | +| `realm-workloads-launcher-v2.json` | `nixos-modules/realm-workloads-launcher-v2-json.nix` | **C** | Active bundle artifact (installed root:d2bd 0640); consumed by launcher clients | → v3 workload catalog from Zone resources | +| `realm-workloads-launcher.json` (v1) | `nixos-modules/realm-workloads-launcher-json.nix` | **C** | **NOT installed** by `bundle.nix`; declared artifact but dead | Retire | +| `d2b.realms.*` Nix options | `nixos-modules/options-realms.nix`, `options-realms-workloads.nix`, `options-realms-network.nix` | **C** | Eval-time; drives JSON artifact emission | → `d2b.zones.*` with compiler-only `parentZone` plus schema-mirrored resources | + +### Architecture change summary + +The following transitions are NOT simple textual renames: + +1. **`RealmControllerPlacement` enum → private Zone runtime bootstrap placement + explicit parent topology + child-local uplink identity**: 6 variants collapse into per-Zone bootstrap configuration; compiler-only `parentZone` selects the allocator owner, and the child-local ZoneLink's `childZoneName`/`transportProviderRef` supplies transport state. The compiler seals the parent edge into allocator state. Placement and `parentZone` are not public `Zone.spec` fields; `Zone.spec` is `{}`. +2. **`WorkloadId` → Guest/Host split**: VM/sandbox workloads become `Guest`; local/bare-metal become `Host`. Classification is semantic, not mechanical. +3. **`CapabilitySet`-only authz → RBAC + allocated capability scope**: current engine checks a route allocation scope only. Independent per-hop target-verb and `relay` RBAC is new. +4. **`RealmPath` DNS target form → Zone resource path**: grammar preserved; wire address format changes. +5. **`EntrypointMode` enum → child-local ZoneLink transport plus topology projection**: `HostResident`/`GatewayBacked` mode is replaced by the child's transport spec; parent routing and CLI inspection use sealed topology and authenticated route projection state. +6. **`realm-controllers.json`/`realm-identity.json` → sealed parent topology + child-local Zone/ZoneLink state**: data is loaded today but routing/trust sessions are explicitly inert. +7. **`d2b-realm-provider` trait family → Provider ResourceType**: trait dispatch is replaced by typed resource controllers. + +| Item | Treatment | +| --- | --- | +| Behavior retained | Pure in-memory NCA tree-walk; loop/multi-parent detection; advertisement replay-window; allocated capability-scope propagation; idempotency dedup full 6-tuple `(realm, principal, node, operation_kind, idempotency_key)`; fail-closed on unknown realm/route; bounded audit label cardinality; `TreeRoutePath`/`TreeRouteHop` already exist (rename to ZoneRoutePath/ZoneRouteHop); `DirectShortcut*` machinery already exists; `RouteAuditEventKind` event set already exists | +| Required delta | Consume the canonical ZoneLink ResourceType spec/status/intent contract from ADR046-zone-control-002; compiler-only validated `parentZone` map sealed into allocator bootstrap topology; independent target-verb plus canonical `relay` RBAC checks per intermediate hop; watch cursor resync over ZoneLink; named-stream credit forwarding; hop counter byte in wire frames; no-FD/credential structural rejection at serialization boundary; private Zone runtime bootstrap placement (replaces placement enum, not a public spec field); per-hop subject narrowing | +| Reuse path | Copy and adapt `RouteTreeEngine` (rename RealmPath→ZonePath, RouteId→ZoneRouteId); copy `RouteAdvertisement`/`RouteNamespaceAllocation`/`TreeRoutePath`/`TreeRouteHop`/`RouteFailClosedReason`/`DirectShortcut*`/`RouteAuditEventKind`; copy `OperationRouter` idempotency dedup; extract `RealmEntrypointTable` suffix-match into ZoneEntrypointResolver; adapt KK handshake from `SecurePeerSession`/`PeerSession` | +| Replacement/deletion | `RealmEntrypointTable`/`RouteTreeEngine` on RealmPath types retire after ZoneRouteEngine live; `RemoteNodeRegistry` retires when ZoneLink controller live; `WorkloadTargetIndex` retires when Guest/Host resource lookup live; CLI `Route::GatewayBacked` retires when ZoneLink handles all cross-Zone routing; `realm-controllers.json`/`realm-identity.json` retire when sealed `parentZone` topology, runtime Zone identity, and ZoneLink transport state replace them | +| Feasibility proof | `route_engine.rs` inline test suite (45 functions at line 1202) proves NCA, advertisement, loop, capability, replay, DirectShortcut; `target_resolver.rs` tests prove suffix match; `lib.rs` tests prove idempotency dedup namespace | +| Future owner | Work items below | + +## Implementation work items + +### ADR046-routing-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-001` | +| Dependency/owner | W0/W1a; zone routing contract owner | +| Current source | `packages/d2b-realm-core/src/routing.rs`: `RouteAdvertisement`, `RouteNamespaceAllocation`, `RealmTreeEdge`, `DescendantRoute`, `TreeRoutePath`, `TreeRouteHop`, `RouteFailClosedReason`, `DirectShortcutAuthorizationRequest`, `DirectShortcutAuthorizationDecision`, `DirectShortcutTeardown`, `DirectShortcutTeardownReason`, `RouteAuditEventKind`, `RouteRealmClass`, `RoutePlacementClass`, `RouteAuditEventMetadata`, all route newtypes; `packages/d2b-realm-core/src/realm.rs`: `RealmPath`, `MAX_REALM_LABELS`, `MAX_REALM_PATH_BYTES`, `RealmControllerPlacement`, `EntrypointMode`; `packages/d2b-realm-core/src/ids.rs`: `RealmId`, `RouteId`, `ControllerGenerationId`, `WorkloadId`, `NodeId`, `ProviderId` (evidence: **A** for ids.rs — used in production; **B** for routing.rs — types exist with tests but no production daemon routing callers) | +| Reuse source | Same v3 baseline commit `b5ddbed6` | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/zone_routing.rs` | +| Detailed design | Rename RealmPath → ZonePath, RealmId → ZoneLabelId, RouteId → ZoneRouteId, ControllerGenerationId → ZoneLinkControllerGeneration; preserve all bounds/validation/serde; add ZoneLink-specific advertisement envelope fields (v3 schema version and private allocated-capability field); preserve `RouteFailClosedReason` + add `zone-link-disconnected`, `hop-limit-exceeded`, `relay-denied`, `attachment-not-permitted-over-zone-link`; freeze v3 protobuf numbers separately from v2 Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | d2b-bus zone route engine and ZoneLink controller consume these types | +| Data migration | Full reset; no v2 Realm route compatibility | +| Validation | Golden advertisement/path/failure vectors shared by Rust/Nix; property tests for NCA/loop/allocated-capability narrowing; replay-window tests; hop-count tests | +| Removal proof | v3 old `RealmPath` route types retired after zone-routing engine is live and all callers switched | + +### ADR046-routing-002 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-002` | +| Dependency/owner | ADR046-routing-001; zone route engine owner | +| Current source | `packages/d2b-realm-core/src/route_engine.rs`: full `RouteTreeEngine` struct and impl; `RouteInventoryEntry`, `RoutePruneReport`, `DirectShortcutAuthorizationRequest/Decision/Teardown`; all helper functions | +| Reuse source | Same v3 baseline commit `b5ddbed6` | +| Reuse action | adapt | +| Destination | `packages/d2b-zone-routing/src/engine.rs` | +| Detailed design | Adapt RouteTreeEngine to ZoneRouteEngine using ZonePath/ZoneRouteId/ZoneLinkControllerGeneration from ADR046-routing-001; preserve all NCA/loop/multi-parent/capability/replay/capacity logic; add `decide_route` enforcement of the fixed protocol hop budget; add hop-counter decrement in relay path; expose `ZoneRoutePath` in v3 types Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | d2b-bus calls `ZoneRouteEngine::decide_route` for every cross-Zone ResourceClient call; ZoneLink controller calls `admit_advertisement`/`admit_withdrawal` | +| Data migration | None (pure in-memory engine) | +| Validation | Copy exact `route_engine.rs` test suite adapted to ZonePath; add relay/hop-count/RBAC-narrowing/shortcut integration tests | +| Removal proof | `RouteTreeEngine` on v3 RealmPath types retired after ZoneRouteEngine is exercised in all bus routing paths | + +### ADR046-routing-003 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-003` | +| Dependency/owner | ADR046-routing-001, ADR046-routing-002; Zone route resolver owner | +| Current source | `packages/d2b-realm-core/src/routing.rs`: `RouteNamespaceAllocation`; `packages/d2b-realm-core/src/access.rs`: `RealmAccessResolverRequest/Response/Error`, `RealmAccessBinding`, `RealmTransportBinding`, `RealmAccessClientContract`, `UnixSocketPath`, `AccessBindingRef`, all access types (evidence: **B** — complete implementation, no production callers); `packages/d2bd/src/realm_access_resolver.rs`: `resolve_local_root_realm_access()`, `local_root_realm_access_client_contract()` (evidence: **B** — `pub mod` at `d2bd/src/lib.rs:117`, no callers from running daemon); `packages/d2b-realm-router/src/target_resolver.rs`: `RealmEntrypointTable`, `DispatchTarget`, `RealmEntrypoint`, `ResolveError` (evidence: **A**); `packages/d2b/src/lib.rs:5240`: `load_realm_entrypoint_table()` (evidence: **A**); `packages/d2b/src/target_routing.rs`: `Route::Local`/`Route::GatewayBacked` dispatch (evidence: **A**); `packages/d2b-realm-core/src/realm.rs`: `EntrypointMode`, `RealmControllerPlacement` (evidence: **A** as types; routing use **B**); `packages/d2b-core/src/realm_controller_config.rs`: `RealmControllersJson` (evidence: **C**); `nixos-modules/realm-controller-config-json.nix` (evidence: **C**) | +| Reuse source | Same v3 baseline commit `b5ddbed6` | +| Reuse action | adapt | +| Destination | `packages/d2b-zone-routing/src/resolver.rs` (ZoneEntrypointResolver) | +| Detailed design | Consume the canonical ZoneLink spec/status/intent types owned by ADR046-zone-control-002; ZoneLinkNamespaceAllocation is issued by the exact parent allocator selected in sealed Nix-compiled `parentZone` topology; ZoneEntrypointResolver uses longest-suffix match over ZonePath (adapted from `RealmEntrypointTable::resolve`) and is driven only by sealed `{ childZone, parentZone }` rows plus authenticated admitted route projections; no reciprocal parent-store resource or parent ZoneLink handler; fail closed on unknown topology, absent/stale projection, or unauthenticated route Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Child core-controller ZoneLink handler manages child-store ZoneLink resources; parent d2b-bus feeds sealed topology and authenticated `ZoneRouteEngine` projection state to ZoneEntrypointResolver for per-call dispatch | +| Data migration | None; ZoneLink resources created from Nix configuration at v3 reset | +| Validation | Longest-suffix match vectors over sealed topology; child-local ZoneLink spec validation; resolver rejects unknown/stale/withdrawn/unauthenticated route projections; parent-store fixture contains no ZoneLink row or handler | +| Removal proof | `RealmEntrypointTable` retired after all host-daemon routing paths use ZoneEntrypointResolver | + +### ADR046-routing-004 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-004` | +| Dependency/owner | ADR046-routing-003; core-controller ZoneLink handler owner | +| Current source | `packages/d2b-realm-router/src/remote_node.rs`: `RemoteNodeRegistry`, `RemoteNodeEntry`, `RemoteNodeAvailability`, `RemoteNodeErrorKind`, `RemoteRetryAction`, `ensure_remote_execution_generation` (evidence: **B** — only in `d2bd/src/realm_stubs.rs` dead_code seam); `packages/d2b-realm-router/src/session_lifecycle.rs`: `SessionLifecycle`, `SessionPhase` (evidence: **B** — same seam); `packages/d2bd/src/realm_stubs.rs`: compile-only seam (`#[allow(dead_code)]`, declared at `d2bd/src/lib.rs:249`); `packages/d2b-realm-core/src/enrollment.rs`: `EnrollmentRecord`, `EnrollmentStatus`, `KeyRotationPlan`, `RevocationRecord`, `SessionTeardownDirective`, `RecoveryProcedure`, `IdentityAuditEventKind` (evidence: **B** — consumed by `RealmIdentityStore` which itself has no production callers); `packages/d2b-realm-core/src/identity_store.rs`: `RealmIdentityStore` (evidence: **B** — no production callers); `packages/d2b-realm-core/src/identity_config.rs`: `RealmIdentityConfigJson` (evidence: **A** — loaded at d2bd/priv-broker startup, routing inert); `nixos-modules/realm-identity-config-json.nix` (evidence: **C**); `packages/d2bd/src/workload_target_index.rs`: `WorkloadTargetIndex` (evidence: **A** — called at `d2bd/src/lib.rs:16745`; this is the live bridge from realm metadata to VM-name dispatch; retires with Guest/Host resource lookups) | +| Main reuse source | `packages/d2b-session/src/lifecycle.rs` (`SessionLifecycle`, `SessionPhase`, `KeepaliveAction`, `poll_keepalive`, `disconnect`, `begin_reconnect`, `reconnect_established`, `close`; limits: `MAX_RECONNECT_ATTEMPTS=10`, `MAX_RECONNECT_WINDOW_MS=300000`; test: `lifecycle_keepalive_close_and_reconnect_change_generation`) — adapt as the ZoneLink session state machine inside the ZoneLink handler; generation-increment logic maps to `status.resource.linkEpoch`; reconnect bounds come from `spec.limits.reconnectMaxAttempts` and `spec.limits.reconnectWindowSecs` | +| Reuse source | Same v3 baseline commit `b5ddbed6` | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/zone_links.rs` | +| Detailed design | Child-local ZoneLink handler in core-controller: consumes the exact six-field ZoneLink schema from ADR046-zone-control-002 and manages local ResourceSpec→allocator-bound session→advertisement lifecycle; session state machine (Pending/Established/Disconnected/Reconnecting/Revoked); Provider-internal reconnect backoff bounded by `spec.limits`; advertisement issuance/renewal/withdrawal using enrolled KK ComponentSession; child-store route cursor and bounded outbound intent queue; private allocator capability-scope changes; D088 `status.resource` writer; aggregate metrics use only closed semantic phase/reason/outcome labels and never `link_name_hash` or another ZoneLink/Zone/resource identity label; Nix-compiled `parentZone` selects the parent allocator, which alone owns privileged listeners, placement, and route namespace and creates no reciprocal resource. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Child core-controller process → local transport Provider → sealed binding for the allocator selected by `parentZone` → d2b-bus ComponentSession; child ZoneLink handler exchanges advertisements while that parent ZoneRouteEngine admits/withdraws them | +| Data migration | New ZoneLink resources from Nix configuration; no prior enrollment compatibility | +| Validation | Session lifecycle tests; reconnect/disabled/revocation/allocator-policy-change; intent queue drain; cursor resync; advertisement renewal timing; fake-child tests; structural metric descriptor test asserts `vm`, `zone`, `zone_id`, `zone_uid`, and `link_name_hash` are absent and a ZoneLink-name canary never enters label values | +| Removal proof | `RemoteNodeRegistry` retired after all enrolled peer routing moves to ZoneLink handler | + +### ADR046-routing-005 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-005` | +| Dependency/owner | ADR046-routing-002, ADR046-routing-007 (from ComponentSession spec); d2b-bus owner | +| Current source | `packages/d2b-realm-router/src/lib.rs`: `OperationRouter`, `RouteDecision`, `DEFAULT_RETENTION`, `DEFAULT_NO_REUSE_HORIZON`, `DEFAULT_MAX_DEDUP_RECORDS` (evidence: **B** — only in dead_code seam); `packages/d2b-realm-router/src/mux_session.rs`: `MuxSession` stream/operation forwarding (evidence: **B** for zone relay — stream-forwarding path for Zone relay does not exist yet; **D** within realm-router display-session tests); `packages/d2b-realm-core/src/frame.rs`: `ConstellationFrame`, `Handshake*`, `OperationRequest/Response`, `StreamOpen/Data/Flow/Close/Resume`, `OperationKind` (evidence: **B** from d2bd routing perspective; **A** within realm-router + `d2b-gateway-runtime` for individual types); `packages/d2b-realm-router/src/session.rs`: `PeerSession` (evidence: **B** from d2bd; **A** within realm-router tests); `packages/d2b-realm-router/src/secure_session.rs`: `SecurePeerSession`, `SecureSessionKey`, `NonceReplayGuard` (evidence: **B** from d2bd; reachable within realm-router) | +| Main reuse source | `packages/d2b-session/src/cancellation.rs` (`Cancellation`, `RequestRegistry`, `cancel_generated`, `CancelResult` 5 variants; test: `cancellation_is_generation_bound_and_shared`) — copy for cross-Zone cancellation forwarding; generation-bound registry maps to per-hop session generation; `packages/d2b-session/src/streams.rs` (`NamedStreamMux`, `StreamId`, credit model; tests: `named_stream_state_and_scheduler_have_independent_credit_and_fairness`, `driver_fragments_one_mib_logical_stream_under_256_kib_credit`) — credit state machine for ZoneLink named-stream forwarding; `packages/d2b-realm-router/src/lib.rs` `OperationRouter`/`DEFAULT_RETENTION=15min`/`DEFAULT_NO_REUSE_HORIZON=60min`/`DEFAULT_MAX_DEDUP_RECORDS=65536` idempotency dedup constants — adapt full 6-tuple dedup namespace `(zone, principal, node, operation_kind, idempotency_key)` as `ZoneLinkIdempotencyKey` | +| Reuse source | Same v3 baseline commit `b5ddbed6` | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/zone_route.rs` (cross-Zone bus routing), `packages/d2b-bus/src/relay.rs` (per-hop relay handler) | +| Detailed design | Cross-Zone routing path in d2b-bus: ZoneEntrypointResolver consumes sealed topology plus authenticated route projections → ZoneRouteEngine::decide_route → admitted ComponentSession established by each next-hop child's local ZoneLink; hop-counter decrement and enforcement; independent target-verb plus canonical ZoneLink-scoped `relay` checks at each intermediate hop; idempotency key namespace (full 6-tuple) in ZoneLinkIdempotencyKey; pinned reverse path tracking; cancellation forwarding; watch cursor forwarding and revision-expired handling; no-FD/credential structural rejection at serialization boundary. No parent route step performs Resource API Get/List/Watch on ZoneLink Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | ResourceClient → d2b-bus → ZoneLink CS → intermediate zone → target zone; cancel/watch/stream all use the same routing path | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | End-to-end K0→K1→K2 resource call; relay-missing, target-verb-missing, wildcard/self-asserted relay, hop-limit, and FD-rejection tests; prove relay alone grants no CRUD/local lifecycle; idempotency namespace collision tests; cancellation delivery tests; watch resync tests | +| Removal proof | Old direct-dispatch and gateway-backed paths retired per bus routing parity | + +### ADR046-routing-006 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-006` | +| Dependency/owner | ADR046-routing-002; benchmark owner | +| Current source | `packages/d2b-realm-core/src/route_engine.rs` inline `#[cfg(test)] mod tests` block at line 1202 (45 test functions covering NCA, advertisement admission/withdrawal, loop/multi-parent detection, capability ceiling, replay window, DirectShortcut authorization/teardown; evidence: **implemented-but-unwired** — tests are in-file, not in a separate `tests/` directory; no external test crate at `packages/d2b-realm-core/tests/*.rs`) | +| Reuse source | Same v3 baseline commit `b5ddbed6` | +| Reuse action | adapt | +| Destination | `packages/d2b-zone-routing/tests/route_engine_vectors.rs`, `packages/d2b-zone-routing/benches/route_decision.rs` | +| Detailed design | Copy exact advertisement/NCA/loop/capability/replay test vectors adapted to ZonePath; add K0/K1/K2 topology scenarios; add hop-count boundary tests; benchmark: p95 route decision for 1/10/100 active Zone tree entries <= 1 ms | +| Integration | Zone route engine correctness gate; bus relay integration tests | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | All v3 baseline route_engine test cases must pass; p95 benchmark gate | +| Removal proof | Not applicable | + +### ADR046-routing-007 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-007` | +| Dependency/owner | W0; d2b-bus owner | +| Current source | None in v3 pre-ADR45 baseline. | +| Main reuse source | `packages/d2b-session/` (commit `a1cc0b2d`): `SessionEngine`, `ComponentSessionDriver` (20 async methods), `SessionDriverHandle`, `NoiseHandshake` (Nn/Kk/IKpsk2), `EstablishedHandshake`, `NegotiatedOffer`, `SessionLifecycle`/`SessionPhase`, `NamedStreamMux`/`StreamId`/`StreamPhase`, `Cancellation`/`RequestRegistry`, `OwnedAttachment`/`AttachmentPayload`, `FairScheduler`/`QueueClass`/`OutboundFrame`, `RecordProtector`/`ProtectedRecord`, `BootstrapAdmission`, `OwnedTransport`/`TransportPacket`/`TransportDescriptor`, `MetricsSink`/`NoopMetrics`; `packages/d2b-contracts/src/v2_component_session.rs`: all protocol constants and wire types; `packages/d2b-session/tests/component_session.rs` (all 18+ test functions) and `tests/noise_vectors.rs` (canonical Noise vectors). | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/session/` | +| Detailed design | Copy `d2b-session` crate wholesale into `d2b-bus/src/session/`; adapt `EndpointPurpose`/`EndpointRole`/`ServicePackage` closed-enum tags for v3 purposes; strip `GUEST_SESSION_CREDENTIAL_*` types; strip `serve_ttrpc_services` fixed-endpoint binding (replaced by allocator-issued FD bootstrap); adapt `SessionEngine` as ZoneLink session drive loop; keep all Noise profiles (Nn/Kk/IKpsk2), generation discovery, record/fragment/keepalive/credit/cancellation/attachment logic verbatim Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | ZoneLink controller instantiates one `ComponentSessionDriver` per ZoneLink, typed as Kk for enrolled peers and Nn for initial bootstrap; d2b-bus routes ResourceClient calls through these drivers | +| Data migration | None (new infrastructure) | +| Validation | Port all `component_session.rs` tests; port `noise_vectors.rs`; add ZoneLink-specific KK enrollment test; add ZoneLink reconnect/revocation integration test | +| Removal proof | Not applicable (new crate) | + +### ADR046-routing-008 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-008` | +| Dependency/owner | ADR046-routing-007; transport-provider owner | +| Current source | None in v3 pre-ADR45 baseline (vsock and seqpacket paths are provider-specific in v3). | +| Main reuse source | `packages/d2b-session-unix/` (commit `a1cc0b2d`): `UnixSeqpacketTransport`, `UnixStreamTransport`, `PeerIdentityPolicy`, `UnixAttachmentPayload`, `OwnedUnixAttachment`; `CreditPool`/`CreditScopeSet`/`CreditBundle`/`ProcessCreditLimit`/`CreditScope` (6 scopes); `PeerCredentials`/`PidfdIdentityPolicy`/`DescriptorPolicy`/`VerifiedPacket`/`ObjectIdentity`; `SeqpacketSocket`/`StreamSocket`; `tests/unix_session.rs` (all 12+ test functions). | +| Reuse action | adapt | +| Destination | `packages/d2b-bus/src/transport/unix.rs`, `packages/d2b-bus/src/transport/credit.rs` | +| Detailed design | Copy `UnixSeqpacketTransport`/`UnixStreamTransport`/credit modules verbatim; adapt `PeerIdentityPolicy` for v3 Zone principal model; adapt `InheritedSocketTransport` to receive allocator-issued FD directly (not SD_LISTEN_FDS); vsock paths adapted as transport-Provider-specific implementations (not hardcoded); `PidfdIdentityPolicy` adapted for v3 Process resource pidfd model Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | Unix transport Provider instantiates `UnixSeqpacketTransport`; vsock transport Provider instantiates vsock transports; both implement `OwnedTransport` consumed by `SessionEngine` in ADR046-routing-007 | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Port all `unix_session.rs` tests; add allocator-issued FD handoff test; add inherited-socket no-SD-listen test | +| Removal proof | Not applicable (new infrastructure) | + +### ADR046-routing-009 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-009` | +| Dependency/owner | ADR046-routing-007; contracts owner | +| Current source | None in v3 pre-ADR45 baseline. | +| Main reuse source | `packages/d2b-contracts/src/v2_component_session.rs` (commit `a1cc0b2d`): all protocol constants (`PREFACE_LEN=16`, `MAX_ACTIVE_NAMED_STREAMS=128`, all limit constants), `ComponentSessionPreface`, `HandshakeOffer`/`EndpointPolicy`/`EndpointPolicyIdentity`, `NoiseProfile`, `LimitProfile`, `AttachmentPolicy`/`AttachmentDescriptor`/`AttachmentKind`/`AttachmentCreditClass`, `ChannelId`/`RecordHeader`/`FragmentHeader`, `SessionErrorCode`/`CloseReason`/`Remediation`, `BoundedVec`, `BinaryError`/`ContractError`; `closed_enum!` macro; `v2_component_session` test coverage via `noise_vectors.rs`. | +| Reuse action | adapt | +| Destination | `packages/d2b-contracts/src/v3/zone_session.rs` | +| Detailed design | Copy all protocol constants verbatim; copy `BoundedVec`, `BinaryError`, `ContractError`; copy `ComponentSessionPreface`, `LimitProfile`, `AttachmentDescriptor`/`AttachmentKind`/`AttachmentCreditClass`/`ChannelId`/`RecordHeader`/`FragmentHeader`/`SessionErrorCode`/`CloseReason`/`Remediation` verbatim; extend `ServicePackage` closed-enum with `ZoneV3`/`ResourceV3`/`ZoneLinkV3` variants at new tag values; extend `EndpointRole` with `ZoneController`/`ZoneRelay`/`ZoneBootstrap` variants; extend `EndpointPurpose`/`PurposeClass` with v3 Zone purposes; strip `GUEST_SESSION_CREDENTIAL_*` constants and types; re-freeze protobuf field numbers for v3 services independently from v2 assignments Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | All d2b-bus session/transport code imports from `d2b-contracts::v3::zone_session` | +| Data migration | None (new contract module) | +| Validation | Updated `negotiate_offer`/`validate_exact` round-trip tests for v3 purposes; canonical encoding stability test; closed-enum exhaustiveness tests | +| Removal proof | v2 contracts remain; v3 module is additive | + +### ADR046-routing-010 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-010` | +| Dependency/owner | ADR046-routing-007, ADR046-routing-009; resource-client owner | +| Current source | None in v3 pre-ADR45 baseline. | +| Main reuse source | `packages/d2b-client/` (commit `a1cc0b2d`): `Client`, `ConnectedClient`, `CallOptions`/`CancellationToken`/`RetryPolicy`/`MetadataInput`, `Response`; `ServiceHandle`/`ServiceKind`/`GeneratedClient`/`MethodHandle`; `ConnectedSession`/`ComponentSessionConnector`/`SessionFailure`/`StreamDispatcher`; `ServiceOwner`/`TargetInput`/`TransportKind`/`ResolvedTarget`/`RouteRecord`/`RouteTable`/`TargetResolver`/`TransportSelection`; `DaemonClient`/`GuestClient`; `HostSocketConnector`/`local_daemon_endpoint_identity`; `ClientError`/`RemoteErrorKind`/`RetryClass`; `tests/client.rs` (all test functions). | +| Reuse action | adapt | +| Destination | `packages/d2b-resource-client/` | +| Detailed design | Copy `Client` generic structure; rename `ServiceOwner::Realm(RealmId)` → `ServiceOwner::Zone(ZonePath)`, `ServiceOwner::Workload{realm,workload}` → `ServiceOwner::Guest{zone,guest}`, `ServiceOwner::LocalRoot` → `ServiceOwner::ZoneLocal`; rename `TargetInput` variants to match; add `TargetInput::ZoneService(ZonePath, ZoneServiceKind)` for cross-Zone service targeting; replace `ServiceKind` (25 ADR45 variants) with v3 service inventory (`Resource`, `Zone`, `ZoneLink`, `Provider`, plus retained guest/daemon variants); adapt `RouteTable` to route by `ZonePath`; replace `HostSocketConnector` uid-based trust with allocator-issued FD + KK static key pinning; keep `SessionFailure`/retry/cancellation/`MetadataInput`/`RetryPolicy` logic verbatim Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | Zone runtime uses `ResourceClient` for all cross-Zone ResourceType calls; d2b-bus wraps `ComponentSessionDriver` (ADR046-routing-007) as the underlying session | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Port `client.rs` tests; add ZonePath routing test; add cross-Zone K0→K1 end-to-end test; add retry/cancellation forwarding test | +| Removal proof | v2 `d2b-client` package remains for ADR45 callers; v3 `d2b-resource-client` is additive | + +### ADR046-routing-014 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-014` | +| Dependency/owner | ADR046-routing-007; Provider resource owner | +| Current source | None in v3 pre-ADR45 baseline (provider traits exist in `d2b-realm-provider` but are unregistered). | +| Main reuse source | `packages/d2b-provider/src/` (commit `a1cc0b2d`): `ProviderRegistry`/`ProviderRegistryBuilder`/`ProviderRegistryManager`; `RegistryLimits`/`AdmissionOptions`/`InFlightPermit`/`AdmittedProvider`; `AuthenticatedProviderRpc` (trait)/`RpcProviderProxy`/`RpcCall`/`RpcResponse`/`RpcOperation`/`RpcPayload`/`SessionIdentity`; `ProviderInstance` (11 variants); `OwnedOperationContext`/`ProviderCallContext`; `ProviderResult`/`ProviderFailure`/`ProviderRuntimeError`; `packages/d2b-contracts/src/v2_provider.rs`: `ProviderDescriptor`/`ProviderCapabilitySet`/`ProviderHealth`/`ProviderAuthority`/`ProviderPlacement`/`AgentPlacementBinding`/`ProviderOperationContext`/`ProviderCallContext` etc.; inline registry tests (`prove_final_drop_between_check_and_await_completes`, `shutdown_closes_final_permit_notify_race`). | +| Reuse action | adapt | +| Destination | `packages/d2b-provider/src/` (adapted in place) | +| Detailed design | Retain `ProviderRegistry`/`ProviderRegistryBuilder`/`ProviderRegistryManager` lifecycle verbatim; adapt `SessionIdentity` to carry `ZonePath` instead of `RealmId`; adapt `AdmissionOptions::peer_role` to v3 Zone principal + RBAC binding; adapt `ProviderDescriptor` schema version to v3; `RegistryLimits` unchanged; `RpcProviderProxy` field adaptations to match v3 session identity; `ProviderInstance` variants retain all 11 types Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | Zone runtime `ProviderComposition` builds a `ProviderRegistry` per Zone; Provider resource controller admits calls through `ProviderRegistry::admit()` | +| Data migration | None (pure runtime) | +| Validation | Port inline registry lifecycle/drain/shutdown tests; add v3 ZonePath routing test; prove provider admission cannot self-assert relay and each forward requires relay plus the target verb | +| Removal proof | Provider registry is v3 core infrastructure; no retirement | + +### ADR046-routing-015 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-015` | +| Dependency/owner | ADR046-routing-014; Provider agent process owner | +| Current source | None in v3 pre-ADR45 baseline. | +| Main reuse source | `packages/d2b-provider-toolkit/src/` (commit `a1cc0b2d`): `GeneratedProviderServiceServer`/`ProviderAgentProcess`; `ProviderAgentAdapter`; `check_descriptor_conformance`/`check_provider_conformance`/`ConformanceError`; `register_exact_instances`/`ToolkitError`; redaction helpers; `packages/d2b-gateway-runtime/src/provider_agent.rs`: `ProviderAgentProcess::from_registry`/`from_registry_with`, `MAX_DISPATCH_IN_FLIGHT=64`, `DEFAULT_AUDIT_CAPACITY=1024`, `ProviderAgentAuditEvent`/`ProviderAgentError`; `run_registered`/`run`; test `audit_capacity_is_closed_and_bounded`. | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-toolkit/src/` (adapted in place) | +| Detailed design | Retain `GeneratedProviderServiceServer` ttrpc dispatch verbatim; adapt `ProviderAgentProcess::from_registry` to receive ComponentSession FD from Zone allocator bootstrap instead of SD_LISTEN_FDS; adapt audit event types for v3 Zone principal; `ProviderAgentAdapter` (client-side proxy) adapted for v3 ZoneLink session; conformance kit extended for v3 Provider resource conformance checks; redaction helpers unchanged Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | Provider Processes (EphemeralProcess or Process resources with `executionRef`) spawn the provider agent entrypoint; Zone bus instantiates `ProviderAgentAdapter` as the proxy inside the Zone runtime | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | Port `audit_capacity_is_closed_and_bounded`; add v3 bootstrap-via-allocator test; add conformance test for new Provider ResourceType schema | +| Removal proof | Provider toolkit is v3 core infrastructure; no retirement | + +### ADR046-routing-016 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-016` | +| Dependency/owner | ADR046-routing-007, ADR046-routing-002, ADR046-routing-004, ADR046-routing-012; Zone service owner | +| Current source | `packages/d2b-realm-router/src/service_v2.rs` (v3 baseline `b5ddbed6`): `RealmServiceServer` (bootstrap/enroll/resolve_route/authorize_shortcut/revoke_shortcut/report_shortcut_close/inspect/cancel), `RealmServiceProcess`, `RealmSessionAuthority`, `CredentialCustody`, `RealmServiceLimits`, `RealmAuditEvent`/`RealmMethod`/`RealmAuditOutcome`, `BootstrapBinding`/`EnrollmentBinding`/`ShortcutBinding`/`MutationRecord`; constants `DEFAULT_MAX_REALM_BINDINGS=256`, `DEFAULT_MAX_SHORTCUTS=256`, `DEFAULT_MAX_MUTATION_RECORDS=1024`, `DEFAULT_AUDIT_CAPACITY=1024`, `MAX_CONFIGURED_BOUND=4096`, `MAX_DISPATCH_IN_FLIGHT=64`, `SHUTDOWN_TIMEOUT=5s` (evidence: v3 baseline, not main; see Baseline section — **B** from d2bd/CLI perspective, **A** within realm-router display-session use) | +| Main reuse source | `packages/d2b-realm-router/src/service_v2.rs` (commit `a1cc0b2d`): same symbols, unchanged from v3 baseline in the main commit. All evidence class notes apply equally to main. | +| Reuse action | adapt | +| Destination | `packages/d2b-zone-routing/src/service.rs` | +| Detailed design | Rename `RealmServiceServer` → `ZoneServiceServer`; service wire name `d2b.realm.v2.RealmService` → `d2b.zone.v3.ZoneService`; rename methods (bootstrap→zone-bootstrap, enroll→zone-enroll, resolve_route→resolve-zone-route, authorize_shortcut→authorize-zone-shortcut, revoke_shortcut→revoke-zone-shortcut, report_shortcut_close→report-zone-shortcut-close, inspect→zone-inspect) and add list/watch topology-projection methods. The read-only projection starts from the sealed sorted `{ childZone, parentZone }` compiler input and joins only authenticated, admitted `ZoneRouteEngine` route/projection status. It exposes no ZoneLink resource name, UID, spec, status, Provider ref, fingerprint, transport setting, or handle. Replace `RealmSessionAuthority` with Zone principal + RBAC binding; replace `BootstrapBinding` with allocator-issued PSK binding associated with the child's local ZoneLink; replace `EnrollmentBinding` with the corresponding KK enrollment record; add independent target-verb plus canonical `relay` RBAC checks per forwarding hop; adapt shortcut model to ZonePath addressing; `RealmServiceLimits` defaults preserved; `MAX_DISPATCH_IN_FLIGHT=64`, `SHUTDOWN_TIMEOUT=5s` preserved; `CredentialCustody::GatewayGuest` excluded (all ZoneLink sessions are direct KK) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | Zone runtime instantiates one `ZoneServiceServer` per Zone; d2b-bus routes `d2b.zone.v3.ZoneService` calls to this server; CLI uses `ZoneServiceClient` (from ADR046-routing-010) for topology list/inspect/watch, enrollment, and route resolution | +| Data migration | None; v3 Zone service is new; no v2 realm-service compatibility | +| Validation | Bootstrap/enroll/resolve-route/shortcut integration tests against a child-local fake ZoneLink; topology list/inspect/watch golden vectors contain exact `{ childZone, parentZone }` rows plus authenticated status and no ZoneLink fields; stale/withdrawn/unauthenticated projection tests; parent-store no-row/no-handler test; relay-plus-target-verb RBAC tests; KK enrollment test; shortcut ZonePath addressing test; concurrent dispatch bound test (64 in-flight) | +| Removal proof | `RealmServiceServer` on `d2b.realm.v2` retires after `ZoneServiceServer` handles all routing; display-session path migrates separately as part of Provider resource work | + +### ADR046-routing-011 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-011` | +| Dependency/owner | ADR046-routing-001; Nix module owner | +| Current source | `nixos-modules/options-realms-workloads.nix` (v3 baseline `b5ddbed6`): `d2b.realms..*` option declarations (evidence: **C** — eval contract; bespoke field names that do NOT mirror canonical ResourceSpec); `nixos-modules/assertions.nix`: realm-name regex, platform-gate, CIDR assertions (evidence: **A**); `nixos-modules/realm-controller-config-json.nix`: `realmControllersJson` emitter, bundle.nix:59 wiring (evidence: **C**) | +| Reuse source | Same v3 baseline `b5ddbed6`; `assertions.nix` pattern reused for Zone assertions; `realm-controller-config-json.nix` is the structural template | +| Reuse action | adapt | +| Destination | `nixos-modules/options-zones.nix` (new structural base), `nixos-modules/generated/resource-types.nix` (generated registry), `nixos-modules/generated/options-zones-.nix` (generated per ResourceType by `xtask gen-zone-nix-options`), `nixos-modules/assertions.nix` (new Zone assertions) | +| Detailed design | Declare compiler-only scalar `d2b.zones..parentZone` plus structural `d2b.zones..resources. = { type = ...; spec = {}; }` as specified in the "Option schema" section above. `parentZone` has no default, is required for every non-root Zone, forbidden on `local-root`, resolves to one declared Zone, and never enters a ResourceSpec. Wire `options-zones.nix` and all `generated/options-zones-*.nix` files into `nixos-modules/default.nix`. Add a new `xtask gen-zone-nix-options` command that reads `docs/reference/schemas/v3/.schema.json` for each ResourceType, derives `generated/resource-types.nix`, and emits a generated submodule overlaying typed spec options (types, bounds, enum constraints, defaults, docs) onto `d2b.zones..resources..spec`. The generated registry's standard subset must equal the canonical 19-type registry from `ADR-046-resource-object-model` exactly; installed signed Provider schemas may append qualified types. These generated modules are committed and kept in sync by `xtask gen-zone-nix-options && git diff --exit-code` wired into `make test-drift`. The ZoneLink module is generated from the exact six-field schema owned by ADR046-zone-control-002 and rejects any seventh field. Because the generated options carry field-level type constraints, field-level eval errors (wrong enum, out-of-range int, malformed ref) are caught without explicit assertions. Explicit assertions in `nixos-modules/assertions.nix` cover cross-resource invariants only: zone/resource key name regex, reserved names, `parentZone` required/forbidden/existence/self/cycle/16-name-depth constraints, child-local `childZoneName == zone`, at most one uplink resource per non-root Zone and none in local root, ref resolution, count limits, and transportSettings secret-key exclusion (listed in the eval-time assertions table). Conflicting parent scalar definitions fail through standard Nix module merging. Each new assertion must have a matching case in `tests/unit/nix/cases/zone-assertions.nix` (nix-unit auto-discovered). `d2b.realms` option namespace is NOT removed in this work item. Primary reuse disposition: `adapt`. Preserved source-plan detail: new module following same pattern. | +| Integration | ADR046-routing-012 consumes the validated `parentZone` map for private allocator bootstrap sealing and iterates `d2b.zones..resources.*` for resource-bundle emission | +| Data migration | None; Zone options are new; Realm options retained until migration PR | +| Validation | `nix-unit: zone-name-regex`, all five `nix-unit: zone-parent-*` vectors, `nix-unit: zone-link-credential-ref`, `nix-unit: zone-link-child-name`, `nix-unit: zone-link-one-uplink`, `nix-unit: zone-link-closed-spec`, `nix-unit: zone-link-limits`, `nix-unit: transport-settings-secret-key`; add `drift: standard-resource-type-registry` asserting the generated standard subset is exactly all 19 canonical types with no omission/addition/duplicate/reordering, plus `drift: zone-nix-options` (`xtask gen-zone-nix-options && git diff --exit-code`); run `make nix-unit-pin` after adding eval cases | +| Removal proof | `nixos-modules/options-realms-workloads.nix` `d2b.realms` namespace retires after all hosts migrate to `d2b.zones` | + +### ADR046-routing-012 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-012` | +| Dependency/owner | ADR046-routing-011, ADR046-routing-001; bundle emitter owner | +| Current source | `nixos-modules/realm-controller-config-json.nix` (v3 baseline `b5ddbed6`): `builtins.toJSON` emitter for `realm-controllers.json` (bundle.nix:59); `nixos-modules/bundle-artifacts.nix`: install table (root:d2bd 0640); `nixos-modules/bundle.nix`: artifact wiring (evidence: **C**); `packages/xtask/src/main.rs` `gen-schemas` subcommand (evidence: **A** — wired into `make test-drift`) | +| Reuse source | `realm-controller-config-json.nix` structural template; `xtask gen-schemas` extension point (main `a1cc0b2d` unchanged in this area) | +| Reuse action | adapt | +| Destination | `nixos-modules/zone-resources-json.nix` (new), private local-root allocator bootstrap compiler/sealer input (not a ResourceSpec or public bundle), `nixos-modules/bundle-artifacts.nix` (new row for per-Zone `resource-bundle.json`), `packages/xtask/src/main.rs` (`gen-zone-schemas` subcommand emitting `docs/reference/schemas/v3/.schema.json` for Zone and ZoneLink; `gen-zone-nix-options` subcommand emitting `nixos-modules/generated/options-zones-.nix`) | +| Detailed design | `zone-resources-json.nix` iterates `d2b.zones..resources.*` to produce the canonical sorted resource list: for each entry, render `{ apiVersion, type, metadata: { name, zone, ownerRef: , labels: , annotations: }, spec: }`. Separately canonicalize sorted `{ childZone, parentZone }` rows from the compiler-only topology and seal them into the private allocator bootstrap input; `parentZone` never enters a resource bundle or `Zone.spec`, and a topology digest change releases/reallocates affected edges independently of resource `generationId`. Per-Zone generation is strict: local root's generated bundle contains no ZoneLink; a non-root Zone's enabled uplink and referenced transport Provider appear together only in that child's bundle; no emitter copies either resource into the selected parent's bundle. The bundle JSON omits `managedBy` and `configurationGeneration`; the configuration service/core sets those fields when activating the validated bundle. Sort all resources by `(type, zone, name)`. Compute `generationId` as SHA-256 (lower hex) of the UTF-8 bytes of the sorted `resources` array JSON. Compute `integrity` as SHA-256 (base64url, no padding) of the full bundle JSON with integrity field zeroed. Install at `/etc/d2b/zones//resource-bundle.json` root:d2bd 0640. Canonical form: all object keys sorted lexicographically; order-significant arrays preserved; schema-declared set-like arrays sorted lexicographically; all optional fields emitted with defaults; no field renaming or restructuring. Build-time validation runs in a Nix derivation: (1) validate the complete parent map (non-root required, local-root forbidden, declared target, one scalar parent, not self, acyclic, max 16 names); (2) validate each resource against the committed JSON Schema, including the exact six-field ZoneLink schema from ADR046-zone-control-002; (3) validate `transportSettings` for each child-local ZoneLink against its same-Zone Provider's `transportSettingsSchema` — `transportProviderRef` is always explicit, never inferred or defaulted; (4) resolve every same-Zone `transportCredentials` ref; (5) verify `childZoneName == metadata.zone`, at most one uplink resource per non-root Zone, and no local-root uplink; (6) check for duplicate `(type, zone, name)` tuples. Private route capability policy is sealed in allocator bootstrap state and is not a ZoneLink ResourceSpec field. Providers MUST commit their `transportSettingsSchema` before any ZoneLink can reference them. Drift gates: `xtask gen-zone-schemas && git diff --exit-code` and `xtask gen-zone-nix-options && git diff --exit-code` both wired into `make test-drift`. Add `checks.${system}.zone-schema-drift` to `flake.nix`. Primary reuse disposition: `adapt`. Preserved source-plan detail: extend and adapt. | +| Integration | The local-root allocator consumes sealed parent topology independently of resource bundles; `nixos-modules/bundle-artifacts.nix` installs each per-Zone `resource-bundle.json`; ADR046-routing-013 Zone runtime reads it on startup | +| Data migration | None; new artifact file | +| Validation | `drift: zone-resource-schema`, `drift: zone-nix-options`, `build: zone-bundle-deterministic`, `build: parent-topology-sealed`, `build: child-local-zonelink-bundle` (K0 has no ZoneLink; K1 contains its self-matching ZoneLink and same-Zone transport Provider; neither is copied to K0), `build: zone-link-exact-six-fields`, `build: transport-settings-unknown-field`, `build: transport-credential-ref`, `build: missing-transport-provider`; run `make flake-matrix-pin` after adding flake checks | +| Removal proof | `realm-controllers.json` artifact retires after Zone runtime is live and all hosts migrated | + +### ADR046-routing-013 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-routing-013` | +| Dependency/owner | ADR046-routing-012, ADR046-routing-003; `d2b-core-controller` owner (ADR-046-core-controllers) | +| Current source | `packages/d2b-realm-core/src/realm_controller_config.rs`: `RealmControllersJson`, `RealmControllerRow`, `RealmControllerConfigError` (evidence: **C** — loaded but routing inert); `packages/d2bd/src/realm_access_resolver.rs`: `resolve_local_root_realm_access()`, `RealmAccessResolverRequest`, `RealmAccessBinding` (evidence: **B** — pub mod at lib.rs:117, no callers); `packages/d2b-state/src/` (both baselines): atomic state, OFD locks, lease primitives (evidence: **A** for locks/leases, **B** for realm-specific storage); `nixos-modules/host-daemon.nix:220–221`: bundle artifact install paths, daemon SIGHUP wiring (evidence: **A**) | +| Main reuse source | `packages/d2b-state/src/` (main `a1cc0b2d`): atomic state, audit segment primitives adapted for generation tracking | +| Reuse action | adapt | +| Destination | `packages/d2b-core-controller/src/configuration.rs` (defined by ADR-046-core-controllers); shared bundle DTOs may live in `packages/d2b-core/` | +| Detailed design | Implement the configuration ownership and cleanup contract from the "Configuration ownership and cleanup contract" section. `configuration.rs` owns: (1) reading and integrity-verifying `/etc/d2b/zones//resource-bundle.json` on startup and SIGHUP; (2) diffing against active generation by `generationId` (no-op if unchanged); (3) queuing Create/UpdateSpec/Delete intents — core sets `configurationGeneration` and `managedBy` when applying Create/UpdateSpec; Delete targets only resources where BOTH `managedBy` equals the configuration service's value AND `configurationGeneration` matches the prior bundle — resources with `managedBy=controller` or `managedBy=api` are never seized; (4) setting `deletionRequestedAt` on pending-delete resources immediately and adding a Pending condition; (5) writing the prior bundle into the capped ring at `/var/lib/d2b/zones//configuration/prior/.json` (default retentionCount=3, range 1..16, no TTL; prune oldest when count would exceed limit); (6) enforcing boundary invariants (no diff-delete for absent `configurationGeneration`, `managedBy` collision guard, live controller-child teardown guard); (7) driving finalizer drain + controller-child cascade before completing a Delete; (8) on successful deletion: one store transaction writes the `Deleted` revision/change event and removes the resource row and all index entries; the authoritative audit record (`zone-resource-cleanup`) is appended from the committed revision with dedup/exactly-once recovery and is NOT part of the store transaction; (9) tracking `deletionRequestedAt`/`cleanupConfigGeneration`/`cleanupError`/`cleanupAttempt` per resource; (10) on rollback: clearing `deletionRequestedAt` and Pending condition for revived resources; (11) never pruning a prior bundle while a Delete intent from its `configurationGeneration` is in flight. OFD lock on the bundle file prevents concurrent activation races. Generation state persisted atomically at `/var/lib/d2b/zones//configuration/generation.json` (root:d2bd 0640). The `spec` object comparison for UpdateSpec detection uses the canonical JSON form so two identical specs always compare equal regardless of Nix rendering order. Resource phase transitions: Pending while Create/UpdateSpec in-flight; Degraded while cleanup pending; Ready when clean; Failed on permanent error. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | `d2b-core-controller` configuration service activates on bundle install and SIGHUP; zone-controller reconcile loops in `d2b-core-controller` consume the queued intents; d2b-bus resource API exposes `status.phase` and `pendingCleanup` via Get/Watch on the active generation resource | +| Data migration | None; new runtime component | +| Validation | `host-integration: cleanup-removed-zonelink`, `host-integration: rollback-restores-zonelink`, `host-integration: dynamic-child-not-deleted`, `host-integration: zonelink-no-reciprocal-row`; unit tests: deterministic generationId, no-op on same generationId, cross-ownership invariant enforcement, prior-bundle write/prune cycle, UpdateSpec canonical comparison, store-transaction-then-audit-append ordering, exactly-once audit dedup | +| Removal proof | `realm_access_resolver.rs` (B) retires after `d2b-core-controller` configuration tracking is live; `RealmControllersJson` (C) retires after all hosts migrated to Zone bundles | diff --git a/docs/specs/README.md b/docs/specs/README.md new file mode 100644 index 000000000..81ac1aab1 --- /dev/null +++ b/docs/specs/README.md @@ -0,0 +1,432 @@ +# ADR specification sets + +The files in this directory are focused normative specifications attached to an +architecture decision record. They keep implementation-level contracts +reviewable without turning the parent ADR into one monolithic document. + +## ADR 0046 set + +Every ADR 0046 specification is named: + +```text +ADR-046-.md +``` + +The parent decision is +[`docs/adr/0046-d2b-3-provider-control-plane.md`](../adr/0046-d2b-3-provider-control-plane.md). +The parent and every manifest-listed spec form one atomic normative set: + +- all files move from `Proposed` to `Accepted` together, and every member is + `Proposed` today; this is a documentation-only set under user review; +- a content change to any member invalidates validation and panel evidence for + the set; +- no spec may silently override another spec; +- cross-spec dependencies must name existing member Spec IDs and stay acyclic; + the parent ADR is not itself a spec dependency; +- one spec owns each serialized contract, state machine, ResourceType, + controller, Provider dossier, process model, and security invariant. + +### Member index (55 specs) + +The set has **55 normative member specs**: 28 foundation, resource, +cross-cutting, and closing specs, plus 27 Provider dossiers. The parent ADR, +this `README.md`, the `providers/README.md` index, the generated `*.json` +manifests, and the generated implementation-graph artifacts +(`ADR-046-implementation-graph.json`/`.md`) are **not** members. + +**Foundation and platform (15):** + +- [`ADR-046-decision-register`](ADR-046-decision-register.md) — resolved + decisions (through D098) +- [`ADR-046-terminology-and-identities`](ADR-046-terminology-and-identities.md) +- [`ADR-046-resource-object-model`](ADR-046-resource-object-model.md) +- [`ADR-046-resource-store-redb`](ADR-046-resource-store-redb.md) +- [`ADR-046-resource-api-and-authorization`](ADR-046-resource-api-and-authorization.md) +- [`ADR-046-resource-reconciliation`](ADR-046-resource-reconciliation.md) +- [`ADR-046-primitive-resource-composition`](ADR-046-primitive-resource-composition.md) +- [`ADR-046-componentsession-and-bus`](ADR-046-componentsession-and-bus.md) +- [`ADR-046-zone-routing`](ADR-046-zone-routing.md) +- [`ADR-046-provider-model-and-packaging`](ADR-046-provider-model-and-packaging.md) +- [`ADR-046-provider-state`](ADR-046-provider-state.md) +- [`ADR-046-core-controllers`](ADR-046-core-controllers.md) +- [`ADR-046-components-processes-and-sandbox`](ADR-046-components-processes-and-sandbox.md) +- [`ADR-046-nix-configuration`](ADR-046-nix-configuration.md) +- [`ADR-046-current-code-migration-map`](ADR-046-current-code-migration-map.md) + +**Resource catalog (6)** — the 19 standard ResourceTypes (`Zone`, `ZoneLink`, +`Provider`, `Role`, `RoleBinding`, `Quota`, `EmergencyPolicy`, `Host`, `Guest`, +`Process`, `EphemeralProcess`, `Volume`, `Network`, `Device`, `User`, +`Credential`, `Endpoint`, `ResourceExport`, `ResourceImport`) have the +following exclusive ResourceType owners. Foundation specs define shared +contracts but do not co-own these types: + +- [`ADR-046-resources-zone-control`](ADR-046-resources-zone-control.md) — + `Zone`, `ZoneLink`, `Provider`, `Role`, `RoleBinding`, `Quota`, + `EmergencyPolicy`, `ResourceExport`, `ResourceImport` +- [`ADR-046-resources-host-guest-process-user`](ADR-046-resources-host-guest-process-user.md) — + `Host`, `Guest`, `Process`, `EphemeralProcess`, `User`, `Endpoint` +- [`ADR-046-resources-volume`](ADR-046-resources-volume.md) — `Volume` +- [`ADR-046-resources-network`](ADR-046-resources-network.md) — `Network` +- [`ADR-046-resources-device`](ADR-046-resources-device.md) — `Device` +- [`ADR-046-resources-credential`](ADR-046-resources-credential.md) — `Credential` + +**Cross-cutting (3):** + +- [`ADR-046-cli-and-operations`](ADR-046-cli-and-operations.md) +- [`ADR-046-telemetry-audit-and-support`](ADR-046-telemetry-audit-and-support.md) +- [`ADR-046-security-and-threat-model`](ADR-046-security-and-threat-model.md) + +**Closing (4):** + +- [`ADR-046-reset-and-cutover`](ADR-046-reset-and-cutover.md) +- [`ADR-046-feasibility-and-spikes`](ADR-046-feasibility-and-spikes.md) +- [`ADR-046-validation-and-delivery`](ADR-046-validation-and-delivery.md) +- [`ADR-046-streamline`](ADR-046-streamline.md) + +**Provider dossiers (27)** — one dossier per installed `Provider/` +resource, indexed with owned/exported ResourceTypes and component placement in +[`providers/README.md`](providers/README.md). + +### Generated manifests + +`ADR-046-spec-set.json` and `ADR-046-work-items.json` are deterministic indexes, +regenerated from the member Markdown and not themselves members of the set. +This Proposed documentation set does not yet contain the future checked-in +generator. The current candidate was produced with a disposable script that was +removed after regeneration; `ADR046-delivery-004` and +`ADR046-delivery-009` own the future generator and fail-closed policy tests. + +- `ADR-046-spec-set.json` (`artifactKind: d2b-adr-spec-set`, `schemaVersion` 3) + binds the exact 55 member files: for each member, its `specId`, `path`, + `status`, `version`, resolved `dependsOn` edges (the `ADR-046-provider-*` + dependency glob is expanded to every Provider dossier), `supersedes`, + sorted `workItemPrefixes` registry (an empty array for a member with no + items), and the + lowercase SHA-256 of the exact Markdown bytes. It records the parent path and + the `v3` baseline commit and carries no timestamp or host path. +- `ADR-046-work-items.json` (`artifactKind: d2b-adr-work-items`, `schemaVersion` + 1) enumerates every implementation work item extracted from the member specs, + sorted by `workItemId`, each bound to its `specId` and `specPath`. Every + canonical required field is nonempty; `reuseSource` is `null` when a spec + declares no reuse source. Work-item IDs satisfy the canonical ID contract + below and are unique across the whole set. Generation fails closed on an + incomplete, malformed, duplicate, or extra Markdown or manifest item. +- `ADR-046-implementation-graph.json` (`artifactKind: + d2b-adr-implementation-graph`, `schemaVersion` 1) and its rendered human view + `ADR-046-implementation-graph.md` are the D095 machine-readable + implementation DAG. They map every one of the 55 member specs and every work + item exactly once to a `W0`–`W7` launch wave and a file-disjoint parallel + group, with typed edges, owner/destinations, entry contracts, prerequisites, + blockers, exit gate, and topological rank. They are generated from the two + manifests above plus the 8-wave topology in + [`ADR-046-validation-and-delivery.md` §3](ADR-046-validation-and-delivery.md), + are deterministic (no timestamps/host paths), and are **not** members of the + set. They do not change the 55-member count. See + [`ADR-046-implementation-graph.md`](ADR-046-implementation-graph.md). + +## Required metadata + +Each spec starts with this table: + +| Field | Meaning | +| --- | --- | +| Spec ID | Stable `ADR-046-` identifier | +| Parent | `ADR 0046` | +| Status | `Proposed` or `Accepted` | +| Version | Monotonic integer | +| Baseline | Exact v3 commit analyzed | +| Normative | `Yes` | +| Owners | Contract and future implementation owners | +| Depends on | Exact ADR 0046 spec IDs | +| Supersedes | Prior spec/version, if any | + +## Evidence and current-code fit + +Current behavior is cited by exact v3 file, symbol, and baseline commit. Every +claim is classified as: + +- `implemented-and-reachable`; +- `implemented-but-unwired`; +- `generated-or-eval-contract`; +- `test-only-or-preview`; +- `ADR-only`; +- `unknown-requires-spike`. + +The protected pre-ADR45 v3 tree remains the sole current-state and ancestry +baseline. Main may be used freely as an implementation reuse source. A work +item that borrows from main must separately record: + +- exact main commit, file, symbol, and tests; +- why that code is selected; +- which behavior is copied unchanged versus adapted; +- exact v3 destination and integration path; +- which surrounding ADR 0045 assumptions are deliberately excluded. + +Borrowed main code never changes a v3 claim from ADR-only/unwired to +implemented-and-reachable. + +Every design section ends with a current-code fit table: + +| Item | Required content | +| --- | --- | +| Current anchor | Exact current source/artifact and evidence class | +| Behavior retained | Tested semantics preserved | +| Required delta | Behavior absent from v3 | +| Reuse path | Exact code/symbol copied, extracted, adapted, or wrapped | +| Replacement/deletion | Old owner removed only after a live successor | +| Feasibility proof | Existing proof or pre-acceptance spike | +| Future owner | Exact work item/crate/component | + +Every ResourceType/Provider spec also contains a **Nix authoring and +configuration cleanup** section: + +- user-facing direct schema mirror: + `d2b.zones..resources. = { type = "..."; spec = { ... }; };`; +- exact canonical rendered ResourceSpec JSON; +- generated/committed ResourceTypeSchema and Provider settings schema; +- Nix eval/build validation of fields, bounds, ResourceRefs, Provider presence, + Host/Guest/domain policy, ownership, conflicts, and schema fingerprints; +- canonical sorted integrity-pinned per-Zone resource bundle/generation; +- activation/publication path; +- removed configured-resource cleanup, status, audit, and tests. + +Nix does not define a second resource vocabulary. `spec` field names, nesting, +types, defaults, bounds, and Provider extensions match the canonical +ResourceTypeSchema directly. metadata.name, metadata.zone, and apiVersion are +derived/defaulted. Users may author only metadata.ownerRef and bounded +presentation labels/annotations; status, UID, generation, revision, timestamps, +finalizers, managedBy, and configurationGeneration are core/controller-managed. +managedBy is `configuration`, `controller`, or `api`; API-managed resources +persist until explicitly deleted. + +Nix derivations are the one value class that cannot appear in JSON +ResourceSpecs. They live in a separate named `d2b.artifacts.` catalog. +ResourceSpecs use plain `artifactId`/`systemArtifactId` fields. Nix builds and +hashes each derivation and emits a private integrity-pinned ID-to-digest/closure +catalog; store paths never enter public spec/status/audit. + +After a new generation activates, a previously Nix-owned resource absent from +the new configured set enters normal asynchronous finalizer-safe deletion. The +generation reports Degraded/pending-cleanup until deletion completes. Activation +does not block. Controller-created resources are not deleted merely because +they are absent from Nix; their owner controller governs them. + +Every Provider dossier requires its crate's `src/`, `tests/`, `integration/`, +and `README.md` layout and assigns exact work items/files to each path. + +## Resource terminology + +A **resource** belongs to exactly one Zone. + +A canonical resource reference is: + +```text +/ +``` + +Examples: + +```text +Zone/dev +Provider/system-core +Provider/system-systemd +Host/host-system +Guest/dev-vm +Process/wayland-proxy +User/alice +``` + +Rules: + +- every field ending in `Ref` contains one canonical same-Zone resource + reference; +- a plain enum or inline value never uses a `Ref` suffix; +- standard ResourceTypes use Zone-unique short names; +- vendor ResourceTypes use a qualified name such as `acme.io.Widget`; +- API binding rejects ResourceType collisions; +- cross-Zone resource references do not exist unless a later reviewed special + case explicitly adds one. + +Every resource has zero or one `ownerRef`. Any committed child mutation +triggers reconciliation of that owner through the reverse owner index. + +ADR 0046 uses `ResourceType`, `ResourceTypeSchema`, and `ResourceSpec` +terminology. It does not expose Kubernetes-style ResourceKind/kind vocabulary. + +Every resource has all three top-level objects: + +- `metadata`, containing common name, Zone, UID, generation, revision, + ownerRef/finalizer/deletion, and creation/update timestamps; +- `spec`, present as `{}` even when the ResourceType has no desired fields; +- `status`, present from creation and writable only through the authorized + status subresource. + +Common status contains numeric `observedGeneration`, phase +`Pending|Ready|Succeeded|Degraded|Failed|Deleted|Unknown`, bounded +conditions, RFC 3339 transition/reconcile/start/completion datetimes, and the +latest outcome with stable code, optional exitCode, detailed bounded/redacted +message, and retryability. ResourceType-specific status extends that shape. +Prior status is available from the Zone revision log until compaction rather +than retained in an unbounded per-resource history. + +After finalizers complete, deletion emits one `phase=Deleted` revision event +and removes the resource immediately. The revision log is the only deletion +history. + +## Decision-required protocol + +Evidence may determine one answer. If it does not, the author stops the affected +spec and records `decision-required` rather than selecting an implicit default. +The decision entry contains: + +- one focused question; +- the viable options and consequences; +- current-code evidence; +- a recommendation; +- blocked specs and work items. + +The resolved choice is recorded in +[`ADR-046-decision-register.md`](ADR-046-decision-register.md) and propagated to +all affected specs. The set cannot enter pre-panel review with unresolved +decisions, placeholders, or implementation-defined behavior. + +## Implementation work items + +Each spec contains an **Implementation work items** section. Every item has: + +| Field | Requirement | +| --- | --- | +| Work item ID | Declared by an exact level-three heading `### ADR046--`; an optional table row must match it exactly | +| Dependency/owner | Prerequisites, future wave, crate/component, shared owner | +| Current source | Exact v3 paths, symbols, call sites, artifacts, and tests | +| Reuse source | Optional exact main commit/paths/symbols/tests used for copy/adaptation; explicit `None` serializes as `null` | +| Reuse action | Exactly one canonical `reuseAction` value defined below | +| Destination | Exact future crate/module/file and binary targets | +| Detailed design | Types, APIs, algorithms, state, limits, errors, security | +| Integration | Complete producer-to-consumer call/resource/process chain | +| Data migration | State/config/artifact/reset behavior | +| Validation | Exact test files/selectors and measurable acceptance | +| Removal proof | Live successor path and tests required before deletion | + +The exact work-item ID regex is +`^ADR046-[a-z0-9]+(?:-[a-z0-9]+)*-(?:00[1-9]|0[1-9][0-9]|[1-9][0-9]{2})$`. +`` is one entry in the owning member's +`workItemPrefixes` registry below; it is not required to equal the full Spec ID +suffix. `` is a three-digit value from `001` through `999`. A member +that owns work items registers a nonempty bytewise-sorted prefix list. Every +prefix is globally unique to exactly one member and is never inferred by +splitting an ID, Spec ID, or filename. + +| Normative member | Registered `workItemPrefixes` | +| --- | --- | +| `ADR-046-cli-and-operations` | `cli` | +| `ADR-046-components-processes-and-sandbox` | `process` | +| `ADR-046-componentsession-and-bus` | `bus`, `session` | +| `ADR-046-core-controllers` | `core` | +| `ADR-046-current-code-migration-map` | `[]` | +| `ADR-046-decision-register` | `decisions` | +| `ADR-046-feasibility-and-spikes` | `feasibility` | +| `ADR-046-nix-configuration` | `nix` | +| `ADR-046-primitive-resource-composition` | `primitives` | +| `ADR-046-provider-activation-nixos` | `activation` | +| `ADR-046-provider-audio-pipewire` | `audio` | +| `ADR-046-provider-clipboard-wayland` | `clipboard` | +| `ADR-046-provider-credential-entra` | `cred-entra` | +| `ADR-046-provider-credential-managed-identity` | `cred-mi`, `mi-topology` | +| `ADR-046-provider-credential-secret-service` | `cred-ss` | +| `ADR-046-provider-device-gpu` | `gpu` | +| `ADR-046-provider-device-security-key` | `security-key` | +| `ADR-046-provider-device-tpm` | `device-tpm` | +| `ADR-046-provider-device-usbip` | `usbip` | +| `ADR-046-provider-display-wayland` | `display` | +| `ADR-046-provider-model-and-packaging` | `provider` | +| `ADR-046-provider-network-local` | `nl` | +| `ADR-046-provider-notification-desktop` | `notify` | +| `ADR-046-provider-observability-otel` | `otel` | +| `ADR-046-provider-runtime-azure-container-apps` | `aca` | +| `ADR-046-provider-runtime-azure-virtual-machine` | `azure-vm` | +| `ADR-046-provider-runtime-cloud-hypervisor` | `ch` | +| `ADR-046-provider-runtime-qemu-media` | `qemu-media` | +| `ADR-046-provider-shell-terminal` | `sterm` | +| `ADR-046-provider-state` | `pstate` | +| `ADR-046-provider-system-core` | `system-core` | +| `ADR-046-provider-system-minijail` | `minijail` | +| `ADR-046-provider-system-systemd` | `systemd` | +| `ADR-046-provider-transport-azure-relay` | `transport-relay` | +| `ADR-046-provider-transport-unix` | `transport-unix` | +| `ADR-046-provider-transport-vsock` | `vsock` | +| `ADR-046-provider-volume-local` | `vl` | +| `ADR-046-provider-volume-virtiofs` | `vvfs`, `vvfs-export` | +| `ADR-046-reset-and-cutover` | `reset` | +| `ADR-046-resource-api-and-authorization` | `api` | +| `ADR-046-resource-object-model` | `object` | +| `ADR-046-resource-reconciliation` | `reconcile` | +| `ADR-046-resource-store-redb` | `store` | +| `ADR-046-resources-credential` | `credential` | +| `ADR-046-resources-device` | `device` | +| `ADR-046-resources-host-guest-process-user` | `exec`, `user-session` | +| `ADR-046-resources-network` | `network` | +| `ADR-046-resources-volume` | `volume` | +| `ADR-046-resources-zone-control` | `client`, `pkg`, `provider-agent`, `wire`, `zone-control` | +| `ADR-046-security-and-threat-model` | `security` | +| `ADR-046-streamline` | `streamline` | +| `ADR-046-telemetry-audit-and-support` | `audit`, `doctor`, `host-posture`, `reuse`, `telem` | +| `ADR-046-terminology-and-identities` | `identities` | +| `ADR-046-validation-and-delivery` | `delivery` | +| `ADR-046-zone-routing` | `routing` | + +The registry also resolves the two formerly shared prefixes: `bus` belongs +only to `ADR-046-componentsession-and-bus`, so Nix integration items use +`nix`; `network` belongs only to `ADR-046-resources-network`, so +`ADR-046-provider-network-local` items use `nl`. + +`reuseAction` is a closed scalar vocabulary: + +| Value | Disposition | +| --- | --- | +| `create` | Implement a net-new destination without copying an implementation source; `reuseSource` is `null`. | +| `copy-unchanged` | Copy the named source while preserving its behavior and tests. | +| `extract` | Separate the named reusable subset into the destination without changing its behavior. | +| `adapt` | Reuse the named source with the contract or behavior changes stated in Detailed design. | +| `wrap` | Keep the named implementation intact behind a new adapter, port, or facade. | +| `replace` | Introduce and cut callers over to a named successor for an existing implementation or owner. | +| `delete-after-cutover` | Remove the named old implementation only after the referenced successor and cutover tests are live. | + +Aliases, capitalization variants, free-form text, and compound values such as +`copy + adapt` are invalid; split work with different primary dispositions into +separate items. + +Generation and validation fail closed unless every normative member's +Implementation work items section is complete: + +- every work-item heading is exactly level three (`###`), matches the ID regex, + uses one prefix registered by its owning member, and is unique across the + set; `##` and `####` item declarations fail closed; +- every item has exactly one nonempty `Dependency/owner`, `Current source`, + `Reuse action`, `Destination`, `Detailed design`, `Integration`, + `Data migration`, `Validation`, and `Removal proof` field, with no duplicate + fields; an optional `Work item ID` row exactly matches its heading and an + optional `Reuse source` is nonempty; +- every heading prefix appears in the owning member's bytewise-sorted + `workItemPrefixes`; every registered prefix belongs globally to exactly one + member, and a member with no work items has an empty array in the generated + spec set; +- an absent or explicit-none Reuse source serializes as `null`; `create` + requires that null value; +- every Markdown item appears exactly once in `ADR-046-work-items.json`, every + manifest item resolves to exactly one Markdown item, and the bound `specId` + and `specPath` match the owning member; and +- manifest items are sorted bytewise by `workItemId`; any parse ambiguity, + malformed `ADR046-` heading, count mismatch, or unconsumed item is an error. + +Broad items such as “update d2bd” are invalid. A work item that copies current +behavior names the exact source symbols, the tests that move with them, the +adaptations required, and the exact point where callers switch. + +## Parallel authoring + +After the shared foundation is stable, file-disjoint specs are authored in +parallel. An agent owns only its assigned spec files and session evidence. +Shared parent/index/manifest/changelog/instruction files remain integrator-owned. +Agents return completed specs or `decision-required`; they do not make +cross-cutting choices or write implementation code. diff --git a/docs/specs/providers/ADR-046-provider-activation-nixos.md b/docs/specs/providers/ADR-046-provider-activation-nixos.md new file mode 100644 index 000000000..d947bc1b9 --- /dev/null +++ b/docs/specs/providers/ADR-046-provider-activation-nixos.md @@ -0,0 +1,1301 @@ +# ADR 0046 Provider dossier: activation-nixos + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-activation-nixos` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 3 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `packages/d2b-provider-activation-nixos/`, `d2b activation` CLI namespace, `activation-nixos.d2bus.org.NixosGeneration` ResourceType | +| Depends on | `ADR-046-provider-model-and-packaging`, `ADR-046-nix-configuration`, `ADR-046-core-controllers`, `ADR-046-resource-object-model`, `ADR-046-resource-api-and-authorization`, `ADR-046-componentsession-and-bus`, `ADR-046-resources-zone-control`, `ADR-046-resources-host-guest-process-user`, `ADR-046-provider-state`, `ADR-046-telemetry-audit-and-support`, `ADR-046-cli-and-operations` | +| Supersedes | Current `d2b switch`/`boot`/`test`/`rollback`/`build`/`generations`/`gc`/`migrate` top-level verbs in `packages/d2b/src/lib.rs` and hardlink-farm activation in `packages/d2b-host/src/hardlink_farm.rs` | + +--- + +## 1. Purpose and ownership boundary + +`Provider/activation-nixos` owns the NixOS system generation plan/apply/status/ +adopt/rollback lifecycle for every Guest or Host whose Nix configuration +declares it. It reconciles exactly one vendor-qualified ResourceType, +`activation-nixos.d2bus.org.NixosGeneration`, and owns the `d2b activation` CLI +namespace. + +### 1.1 What activation-nixos owns + +| Surface | Owned behavior | +| --- | --- | +| `activation-nixos.d2bus.org.NixosGeneration` | Plan, apply, status, adopt, and rollback of a NixOS system activation on a Host or Guest | +| `d2b activation` CLI projection | `build`, `switch`, `boot`, `test`, `rollback`, `adopt`, `generations`, `gc`, `migrate` subcommands | +| Per-generation activation-runner | One `EphemeralProcess` per reconcile cycle; runs the integrity-bound activation operation on the target execution context | +| Generation sequence and retention | Tracks active/superseded generation ordering per target; prunes surplus `NixosGeneration` resources to stay within `retainedGenerations` | + +### 1.2 What activation-nixos does NOT own + +| Surface | Owner | +| --- | --- | +| Zone bundle compilation, `bundle.json`, `artifact-catalog.json` | Nix compiler (`nixos-modules/bundle-zones.nix`, `nixos-modules/artifact-catalog.nix`); spec `ADR-046-nix-configuration` | +| Internal store paths for artifact IDs | Private `artifact-catalog.json`; read by the activation-runner only, never surfaced | +| Zone configuration generation pointer swap, `managedBy=configuration` classification | Core-controller configuration-publication handler; spec `ADR-046-core-controllers` | +| Name conflict resolution and retained-bundle pruning at Zone level | Core-controller; spec `ADR-046-core-controllers` | +| Provider state Volume declaration | None. Under D087, the ProviderStateSet is empty because controller operational state is bounded, non-secret, and derivable from resource status, the core Operation ledger, and external observation. | +| Provider state reconciliation | No Provider state Volume is reconciled. `Provider/volume-local` remains the owner for unrelated store/Volume authority surfaces only. | +| Hardlink farm layout, content operations, store-view GC | `Provider/volume-local`; spec `ADR-046-provider-state` | +| SSH key lifecycle | Out of scope; belongs to a separate identity Provider | +| Guest-editable config editing | Out of scope; belongs to a separate config-management Provider | +| `d2b-activation-helper` binary implementation | Reused from `packages/d2b-host/src/bin/d2b-activation-helper.rs`; adapted, not re-implemented | +| Guest runtime lifecycle (start/stop) | `Provider/runtime-cloud-hypervisor` and peer runtime Providers | +| Credential lifecycle | `Provider/credential-*` family | + +### 1.3 Core-controller boundary (do not duplicate) + +The core-controller configuration-publication handler is the sole authority for: + +1. Advancing the Zone's active configuration generation pointer. +2. Classifying bundle resources as `managedBy=configuration` with `configurationGeneration`. +3. Enqueuing absent-resource cleanup (resources absent from the new bundle). +4. Resolving `(type, name)` name conflicts. +5. Pruning retained bundle slots beyond `retainedGenerations`. + +`d2b-activation-helper` verifies artifact integrity and stages closures only. +It does **not** write `managedBy`, `configurationGeneration`, or any resource +field. + +--- + +## 2. Crate and package identity + +| Field | Value | +| --- | --- | +| Crate name | `d2b-provider-activation-nixos` | +| Artifact ID | `activation-nixos` | +| Provider resource name | `activation-nixos` | +| Install domain | `system` | +| Controller execution target | `Host/` (from `spec.config.controllerExecutionRef`) | +| Package workspace | `packages/d2b-provider-activation-nixos/` | +| Nix emit location | `nixos-modules/providers/activation-nixos.nix` | + +The crate must contain `src/`, `tests/`, `integration/`, and `README.md`. +Absence of any of these paths fails `make test-policy` via +`packages/d2b-contract-tests/tests/policy_provider_crate_layout.rs` +(`ADR-046-resources-zone-control` §4.8.2). + +--- + +## 3. Provider resource spec + +### 3.1 Canonical spec shape + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Provider +metadata: + name: activation-nixos + zone: dev + managedBy: configuration + configurationGeneration: 7 +spec: + artifactId: activation-nixos + config: + controllerExecutionRef: Host/host-system # required; where the controller Process runs + retainedGenerations: 3 # default 3; range [1, 16]; no TTL +``` + +`spec.config` is validated at build time and re-validated at admission against +the JSON Schema whose digest is stored in the private artifact catalog under +`configSchemaDigest`. The schema definition is part of the signed Provider +package. + +### 3.2 spec.config fields + +| Field | Type | Default | Bound | Description | +| --- | --- | --- | --- | --- | +| `controllerExecutionRef` | ResourceRef | required | `Host/` | Execution target for the controller Process. Must resolve to an existing Host in the same Zone. | +| `retainedGenerations` | integer | 3 | [1, 16] | Maximum number of `activation-nixos.d2bus.org.NixosGeneration` resources retained per target execution context, spanning all terminal phases. Surplus records (oldest `Succeeded` or `Failed`) are deleted when this bound is exceeded. No TTL fields. | + +### 3.3 Fields that are NOT in spec.config + +| Field | Location | Reason | +| --- | --- | --- | +| `packageDigest` | Private artifact catalog | Integrity pin; never in public spec | +| `executableDigests` | Private artifact catalog | Integrity pin; never in public spec | +| `manifestDigest` | Private artifact catalog | Integrity pin; never in public spec | +| `configSchemaDigest` | Private artifact catalog | Schema pin; never in public spec | +| Store paths (`systemStorePath`) | Controller process memory only | Private; resolved at activation time from `artifact-catalog.json` | + +--- + +## 4. ResourceType: activation-nixos.d2bus.org.NixosGeneration + +### 4.1 Type identity and qualification + +Provider-specific semantic ResourceTypes extend the standard catalog through +signed schemas and API bindings (`ADR-046-resource-object-model` §"Minimal +standard ResourceType catalog"). The activation-nixos Provider declares one +vendor-qualified ResourceType: + +``` +activation-nixos.d2bus.org.NixosGeneration +``` + +The vendor qualifier (`activation-nixos.d2bus.org`) distinguishes it from any +unqualified standard kind. All resource metadata, spec, and status fields +follow the universal resource envelope contract (`ADR-046-resource-object-model`). + +`Volume` is **not** an exported ResourceType for this Provider. Under D087 the +activation-nixos Provider declares no Provider state Volume, so it neither +declares, creates, mounts, nor manages Volumes for controller state. + +### 4.2 Spec schema + +```yaml +apiVersion: resources.d2bus.org/v3 +type: activation-nixos.d2bus.org.NixosGeneration +metadata: + name: dev-vm--gen-7 # bounded name; unique per Zone + zone: dev + managedBy: configuration # or controller for CLI/rollback-created records + configurationGeneration: 7 # present when managedBy=configuration + ownerRef: null # Nix-authored: no ownerRef; controller-created: ownerRef=Provider/activation-nixos +spec: + providerRef: Provider/activation-nixos # required; immutable + executionRef: Guest/dev-vm # required; Host/ or Guest/; immutable + systemArtifactId: dev-vm-system # required; bounded ID from d2b.artifacts catalog; immutable + activationMode: switch # required; switch|boot|test|adopt; immutable + priorGenerationRef: null # optional; activation-nixos.d2bus.org.NixosGeneration/; points to the generation being superseded or rolled back from; immutable +``` + +#### 4.2.1 spec fields + +| Field | Type | Required | Bound | Description | +| --- | --- | --- | --- | --- | +| `providerRef` | ResourceRef | yes | `Provider/` | Must resolve to `Provider/activation-nixos` in the same Zone. Immutable after creation. | +| `executionRef` | ResourceRef | yes | `Host/\|Guest/` | Target execution context for the NixOS system. Immutable. | +| `systemArtifactId` | bounded string | yes | `^[a-z][a-z0-9-]{0,127}$` | Global artifact catalog ID from `d2b.artifacts`. The controller resolves the internal store path from the private `artifact-catalog.json` at activation time. No store path appears in any spec or status field. Immutable. | +| `activationMode` | enum | yes | `switch\|boot\|test\|adopt` | `switch`: activate immediately and switch running services. `boot`: set as next-boot default without switching running services. `test`: activate temporarily; reverts on next boot. `adopt`: record an existing active generation without dispatching a new activation. Immutable. | +| `priorGenerationRef` | ResourceRef? | no | `activation-nixos.d2bus.org.NixosGeneration/` | The generation being superseded or rolled back from. Informational; the controller validates that the referenced resource exists and is in a terminal phase. Immutable. | + +### 4.3 Status schema + +```yaml +status: + phase: Pending # common framework phase; see §4.4 + conditions: [] # common Condition list + outcome: + code: null # bounded outcome code on terminal phase + message: null # bounded operator-visible message; no paths/digests + lastReconciledAt: "2026-07-22T12:00:00Z" + observedGeneration: 1 + resource: + activationDetail: planning # typed NixosGeneration detail enum; see §4.3.1 +``` + +Per D088, `NixosGeneration` status uses the universal `ResourceStatus` base at +top-level `status.*`; activation-specific typed fields are the +ResourceType-common `status.resource` object for +`activation-nixos.d2bus.org.NixosGeneration`. Optional `status.provider` carries +only implementation-only observation (`providerRef`, qualified immutable +`schemaId`, semver `schemaVersion`, numeric `observedProviderGeneration`, +strict unknown-field-denied redacted `details` ≤32 KiB registered/signed in the +Provider manifest); shared fields are never duplicated there. The controller +writes all present layers atomically in one status mutation. + +D091 currency and upgrade: the activation-nixos controller implements +`assess_update`, `plan_upgrade`, and `execute_upgrade` for +`activation-nixos.d2bus.org.NixosGeneration`. A new NixOS/system generation is +the prototypical `ImageOrSystemGenerationChanged` and `ArtifactChanged` trigger: +the `NixosGeneration` resource populates universal `status.update` with +`UpdateAvailable` or `UpgradeRequired`, and disruptive changes MUST return +`UpgradeRequired` rather than being applied in place, while non-disruptive +changes reconcile normally. These currency fields are universal/ResourceType +base fields, never `status.provider`. Operators apply the planned change +through `d2b upgrade` or activation apply; durable state is preserved across +activation, and `Replace` is used only with ownership/state transfer. + +D090 expedited reconcile: Create, UpdateSpec, and Delete requests that set +`waitForReconcile` perform no external effect, finalizer mutation, or status +mutation until Core supplies a typed `CommittedRevisionProof` +`{resourceUid,generation,revision,operationId}`. Abort produces no effect; a +durable commit is never rolled back on later reconcile timeout. The response is +the committed object plus one-pass projected layered status, a disposition +(`Converged`, `Progressing`, `Blocked`, `UpgradeRequired`, or `Failed`), and +`statusPersistence` (`pending` or `committed`); effect idempotency keys derive +from `(UID,generation,revision,operationId)` in the same per-resource +single-flight priority lane. + +#### 4.3.1 activationDetail enum + +`activationDetail` is a typed, bounded resource detail field. It +carries provider-internal progress that the common `phase` does not encode. +Operators use it to distinguish intermediate states within a phase. + +| Value | Meaning | +| --- | --- | +| `planning` | Controller has received the resource; preparing to dispatch activation runner | +| `staged` | Activation runner EphemeralProcess has been created; waiting for the process provider to start it | +| `applying` | Activation runner is executing the activation operation inside the target | +| `applied` | Activation runner completed; for `switch` mode: generation is active. For `boot` mode: generation is set as boot default | +| `boot-default` | `boot` mode only: generation is committed as the next-boot default; not yet confirmed active | +| `adopted` | `adopt` mode: existing active generation recorded without dispatching a new activation | +| `rolled-back` | `switch` mode with `priorGenerationRef`: rollback activation completed; prior generation superseded | +| `superseded` | A newer generation has become active for the same `executionRef`; this generation is now historical | + +`activationDetail` is a `status.resource` field only. It never appears in spec. + +### 4.4 Phase transitions + +The common framework `phase` follows `ADR-046-resource-object-model`. For +`activation-nixos.d2bus.org.NixosGeneration`, the expected transitions are: + +| From | To | Trigger | +| --- | --- | --- | +| (none) | `Pending` | Resource created (Nix bundle activate or CLI create) | +| `Pending` | `Pending` | Controller reconciling; `activationDetail` advances through `planning` → `staged` → `applying` | +| `Pending` | `Ready` | Activation runner completes successfully; generation is active (`switch`/`adopt`) or confirmed boot-default (`boot`) | +| `Pending` | `Succeeded` | `test` mode runner completes; generation applied temporarily | +| `Pending` | `Failed` | Activation runner reached `Failed`; or `startDeadline` exceeded | +| `Ready` | `Degraded` | Post-activation health probe failed; running services not healthy | +| `Ready` or `Succeeded` | `Ready` | Superseded by a newer `NixosGeneration`; `activationDetail` set to `superseded` (phase stays `Ready` to indicate last-known-good) | +| `Ready` or `Failed` or `Succeeded` | `Deleted` | Retention window exceeded and controller requests deletion (see §4.5) | +| any | `Unknown` | Controller cannot determine activation state; boot probe inconclusive | + +`Deleted` is an event-only terminal phase: the Zone runtime atomically emits +a `phase=Deleted` revision event and removes the resource row and all index +entries. No tombstone row is retained (`ADR-046-resource-object-model` +§"Deletion protocol"). + +### 4.5 Finalizer protocol + +The activation-nixos controller holds a finalizer on every +`activation-nixos.d2bus.org.NixosGeneration` resource it owns. When +`deletionRequestedAt` is set, the controller executes this ordered sequence +before releasing the finalizer: + +1. **Drain active activation operation.** If the owned activation-runner + `EphemeralProcess` is in a non-terminal phase (`Pending` or running), + update its `runtimeDeadline` to now and wait for it to reach `Succeeded`, + `Failed`, or `Unknown`. Do not issue `SIGKILL` directly; the EphemeralProcess + provider owns process termination. + +2. **Delete owned EphemeralProcess.** Once the runner reaches a terminal phase, + set `deletionRequestedAt` on it. Wait for the EphemeralProcess resource to + be removed (it has no further finalizers of its own). + +3. **Release typed generation/profile ownership.** Remove any GC-root + finalizers or lease records the controller holds that pin the generation's + store closure. This signals to the store/Volume authority + (`Provider/volume-local`) that it may collect the associated closure when + no remaining ownership reference exists. The activation-nixos controller + does not directly invoke `nix-collect-garbage` or any store-path operation; + ownership release is the only mechanism. + +4. **Clear activation finalizer.** Remove the controller's finalizer from the + `activation-nixos.d2bus.org.NixosGeneration` resource. + +5. **Core commits event-only Deleted/removal.** The Zone runtime atomically + emits `phase=Deleted` in the revision log and removes the resource row. + +6. **Post-commit audit.** The Zone audit chain fires + `NixosGenerationDeleted` after the commit is durable. No store path, artifact + digest, or private material appears in the audit record. + +### 4.6 Generation retention + +The controller maintains at most `spec.config.retainedGenerations` resources per +`executionRef` target across all non-`Deleted` phases. When a new generation +reaches `Ready` or `Succeeded`: + +1. Count all existing `activation-nixos.d2bus.org.NixosGeneration` resources for the + same `executionRef` that are in `Ready`, `Succeeded`, `Failed`, or + `Degraded` phase. +2. If the count exceeds `retainedGenerations`, identify the oldest surplus + records (by `metadata.createdAt`, ascending) that are not the current + `Ready` generation. +3. Set `deletionRequestedAt` on each surplus record, triggering the finalizer + protocol above. + +No TTL fields participate in retention. The `retainedGenerations` count is the +sole pruning criterion. + +--- + +## 5. Components and framework-created resources + +### 5.1 Component declarations + +The Provider package descriptor declares two components: + +| Component ID | Kind | processClass | Role | +| --- | --- | --- | --- | +| `controller` | `Process` (long-lived) | `controller` | Owns the `activation-nixos.d2bus.org.NixosGeneration` reconcile loop via d2b-bus | +| `activation-runner` | `EphemeralProcess` template | `worker` | One-shot per generation; dispatches the integrity-bound activation operation on the target execution context | + +### 5.2 Controller Process + +The core ProviderDeployment handler creates the controller `Process` resource +from the signed component descriptor. The activation-nixos Provider does **not** +author the Process resource itself, and declares no Provider state Volume for +the controller. + +Canonical `Process` resource shape (as created by ProviderDeployment): + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: activation-nixos--controller + zone: dev + ownerRef: Provider/activation-nixos +spec: + providerRef: Provider/system-minijail + executionRef: + domain: system + userRef: null + processClass: controller + template: activation-nixos-controller + configRef: null + credentialRefs: [] + mounts: [] + sandbox: + namespaceClasses: [pid, mount, ipc] + capabilityClasses: [] + seccompClass: activation-nixos-controller + noNewPrivileges: true + startRoot: false + environmentClass: provider-defined + readOnlyRoot: true + budget: + cpu: + request: "100m" + limit: "1000m" + memory: + request: "32Mi" + limit: "64Mi" + pids: + limit: 64 + fds: + limit: 256 + networkUsage: null + deviceUsage: [] + telemetry: {} + desiredLifecycle: running + restartPolicy: + class: on-failure + backoffBase: "1s" + backoffMax: "60s" + backoffMultiplier: 2.0 + maxRestarts: null + resetAfter: "300s" + readiness: + initialDelay: "0s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + class: ready-condition + healthCheck: + enabled: false + interval: "30s" + timeout: "5s" + failureThreshold: 3 + class: provider-defined + adoptionPolicy: adopt-on-restart + drainTimeout: "30s" +``` + +### 5.3 ProviderStateSet and status-first state + +The **ProviderStateSet** for `Provider/activation-nixos` is the set of all +`Volume` resources in the Zone whose `metadata.ownerRef` resolves to +`Provider/activation-nixos`. It is a logical, query-time grouping — not a +ResourceType or stored artifact: + +```text +ProviderStateSet(zone, "activation-nixos") = + { v : Volume | v.metadata.zone == zone + && v.metadata.ownerRef == "Provider/activation-nixos" } +``` + +The activation-nixos Provider declares **no Provider state Volume**. Its +ProviderStateSet is therefore empty: + +```text +ProviderStateSet(zone, "activation-nixos") = {} +``` + +This passes the D087 storage-need test without a durable Volume: active +generation references, reconcile checkpoints, retention decisions, and adoption +observations are bounded, non-secret operational state derivable from +`NixosGeneration.status`, the core Operation ledger, and re-verification against +the target execution context after restart. + +Status is observation only. The optimistic status writer records only bounded, +redacted, revisioned status fields and material changes; it never stores +secrets, authority-conferring handles, host paths, argv/env, PIDs, unit names, +store paths, raw command output, terminal/clipboard/notification bytes, large +blobs, or unbounded collections. Oversize status is rejected with +`status-oversize` and restart recovery re-verifies the status against external +reality before treating it as current. + +There is no controller state mount, identity-only state layout principal, +migration worker, Provider state reset/destroy path, or bootstrap state-Volume +mechanism. The previous bootstrap exception (D086, superseded by D087) does not +apply. + +### 5.4 Activation-runner EphemeralProcess + +For each `activation-nixos.d2bus.org.NixosGeneration` that requires reconciliation, the +controller creates one owned `EphemeralProcess` resource. This is the canonical +shape the controller emits: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: EphemeralProcess +metadata: + name: activation-nixos--runner--dev-vm--gen-7-- + zone: dev + ownerRef: activation-nixos.d2bus.org.NixosGeneration/dev-vm--gen-7 +spec: + providerRef: Provider/system-minijail + executionRef: # same Host or Guest as the generation; runner executes locally on that target + domain: system + userRef: null + processClass: worker + template: activation-nixos-runner + configRef: null + credentialRefs: [] + mounts: [] + sandbox: + namespaceClasses: [pid, mount, ipc] + capabilityClasses: [] + seccompClass: activation-nixos-runner + noNewPrivileges: true # see §7.3 + startRoot: true # see §7.3 + environmentClass: provider-defined + readOnlyRoot: true # rootfs read-only; all target mutation via LaunchTicket effect resources + budget: + cpu: + request: "100m" + limit: "2000m" + memory: + request: "32Mi" + limit: "128Mi" + pids: + limit: 128 + fds: + limit: 512 + networkUsage: null + deviceUsage: [] + telemetry: {} + startDeadline: "120s" + runtimeDeadline: "600s" + successfulTtl: "1h" + failedTtl: "24h" + incidentHold: false +``` + +The runner's `template` identifier resolves to the `activation-nixos-runner` +component in the Provider's signed descriptor. The descriptor specifies the +exact binary path, allowed arguments, and environment whitelist. No raw +command string, `argv`, environment variable, or store path is composed by +the controller or written into the EphemeralProcess spec. + +#### 5.4.1 What the runner does + +The runner executes locally on the target (`spec.executionRef`), whether that +is a Host or a Guest. `Provider/system-minijail` on the target itself starts +the runner binary. Host and Guest targets use the identical contract with no +bypass or fallback path (OQ-1 closed). + +The runner receives only fixed inherited operation attachments from its +`LaunchTicket`: a pre-opened activation portal, dirfd(s) for any required +integrity-bound inputs, and the manifest-declared system-manager effect +resources. It does not call d2b-bus (processClass worker carries no bus +authority), and it does not resolve, construct, or traverse any host paths. + +Steps the runner performs using its inherited attachments: + +1. Invokes the integrity-bound `d2b-activation-helper` through the pre-opened + activation portal FD, passing the activation mode. The helper operates + entirely within the inherited FD set; no path construction. +2. All target profile and system mutation occurs only through the + manifest-declared, pre-opened system-manager effect resources routed in + the LaunchTicket. No ambient host paths are accessed. +3. Exits with a structured outcome (bounded outcome code) returned through the + activation portal. No store path, no d2b-bus call, no path in output. + +The controller never directly reads store paths, mutates profiles, or calls +`nix-collect-garbage`. + +### 5.5 Endpoint resources (D092) + +`Provider/activation-nixos` declares standard `Endpoint` base-schema +conformance but does not expose a stable cross-boundary service endpoint in this +dossier. Controller health and activation-control channels are +controller-internal ComponentSession details, and activation runners are +one-shot workers with no inbound service. Therefore no activation-nixos +`Endpoint` child resource is created for the Process examples above. If a +future stable managed activation service is introduced, it must be an owned +`Endpoint` resource with `producerRef`; consumers must use `Endpoint/`, +and raw locators must stay out of spec, status, CLI, audit, and telemetry. +Resolution must go through an authorized EffectPort/LaunchTicket, unauthorized +resolution must return `endpoint-resolve-denied`, and producer restarts must +bump `Endpoint.status.endpointGeneration` to trigger `dependency-changed`. + +### 5.6 Retained opaque handles + +- pidfds: Process supervision handles, not stable service identities. +- Per-connection/session handles: reconcile operation IDs and ComponentSession + handles are scoped to one activation or controller session. +- Named streams: none are stable service identities in this dossier; any future + operation stream carries payload only. +- `OwnedTransport`: authenticated bus transport ownership remains an in-memory + capability. +- fd indexes: LaunchTicket effect descriptors are per-run slots and stay + opaque under D092. + +--- + +## 6. Nix authoring + +### 6.1 Provider install resource + +`nixos-modules/providers/activation-nixos.nix` emits the Provider install +resource using the canonical `spec.artifactId` + `spec.config` shape: + +```nix +# nixos-modules/providers/activation-nixos.nix +{ config, lib, ... }: +let + zone = config.d2b.zone.name; + controllerHost = config.d2b.zones.${zone}.controllerHost; + retained = config.d2b.zones.${zone}.retainedGenerations; +in { + d2b.zones.${zone}.resources.Provider.activation-nixos = { + spec = { + artifactId = "activation-nixos"; + config = { + controllerExecutionRef = "Host/${controllerHost}"; + retainedGenerations = retained; + }; + }; + }; +} +``` + +Rendered resource: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Provider +metadata: + name: activation-nixos + zone: dev + managedBy: configuration + configurationGeneration: 7 +spec: + artifactId: activation-nixos + config: + controllerExecutionRef: Host/host-system + retainedGenerations: 3 +``` + +The artifact catalog entry for `activation-nixos` is emitted separately into +the private `artifact-catalog.json`. It contains `packageDigest`, +`executableDigests`, `manifestDigest`, and `configSchemaDigest`. None of these +appear in the Provider spec. + +### 6.2 NixosGeneration resource authoring + +`activation-nixos.d2bus.org.NixosGeneration` resources are authored by the Nix module +for each Guest or Host that enables activation-nixos. They reference artifact +IDs from the global `d2b.artifacts` catalog: + +```nix +# nixos-modules/providers/activation-nixos.nix (continued) +let + # d2b.artifacts.${vmName}-system is declared by the Zone's VM Nix config + genName = "${vmName}--gen-${toString configGen}"; +in { + d2b.zones.${zone}.resources."activation-nixos.d2bus.org.NixosGeneration".${genName} = { + spec = { + providerRef = "Provider/activation-nixos"; + executionRef = "Guest/${vmName}"; + systemArtifactId = "${vmName}-system"; # references d2b.artifacts.${vmName}-system + activationMode = cfg.vms.${vmName}.activationMode or "switch"; + priorGenerationRef = null; + }; + }; +} +``` + +Rendered: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: activation-nixos.d2bus.org.NixosGeneration +metadata: + name: dev-vm--gen-7 + zone: dev + managedBy: configuration + configurationGeneration: 7 +spec: + providerRef: Provider/activation-nixos + executionRef: Guest/dev-vm + systemArtifactId: dev-vm-system + activationMode: switch + priorGenerationRef: null +``` + +### 6.3 Artifact catalog binding + +The `systemArtifactId` field (`dev-vm-system` above) names an entry in +`d2b.artifacts` declared elsewhere in the Zone's Nix configuration: + +```nix +d2b.artifacts."dev-vm-system" = { + system = config.d2b.zones.dev.vms.dev-vm.config.system.build.toplevel; +}; +``` + +The Nix build emits the store path and its SHA-256 digest into the private +`artifact-catalog.json`. Consumers of `systemArtifactId` (the controller and +runner) look up the artifact there. The store path never reaches any public +resource field. + +### 6.4 retainedGenerations Nix option + +`d2b.zones..retainedGenerations` is a Zone-level Nix option (default 3, +range 1–16). It is the **sole** location where the retention bound is declared. +It flows into `Provider.spec.config.retainedGenerations` only. It is not +duplicated in any `NixosGeneration` spec, nor in any other resource field. + +--- + +## 7. Generation lifecycle + +### 7.1 Overview + +``` +Nix build / CLI create + │ + ▼ +activation-nixos.d2bus.org.NixosGeneration + phase: Pending + activationDetail: planning + │ + ▼ controller reconciles + creates EphemeralProcess activation-runner + activationDetail: staged + │ + ▼ runner starts + activationDetail: applying + │ + ├─ runner Succeeded ──► phase: Ready (switch/adopt/boot) or Succeeded (test) + │ activationDetail: applied | boot-default | adopted | rolled-back + │ + └─ runner Failed ─────► phase: Failed + outcome.code: +``` + +### 7.2 Controller reconcile loop + +The controller watches all `activation-nixos.d2bus.org.NixosGeneration` resources in +the Zone whose `spec.providerRef` resolves to `Provider/activation-nixos`. For +each resource: + +1. **Admission check.** Validate `spec.executionRef` resolves to an existing + Host or Guest; validate `systemArtifactId` appears in the private + `artifact-catalog.json`; validate `priorGenerationRef`, if set, exists. + Any failure sets `phase: Failed` with an appropriate bounded `outcome.code`. + +2. **Idempotent runner dispatch.** If no owned `EphemeralProcess` exists for + this generation (or the previous one reached a terminal phase and a retry + is warranted), create the activation-runner EphemeralProcess (§ 5.4). + Set `activationDetail: staged`. + +3. **Runner observation.** Watch the EphemeralProcess status. On + `EphemeralProcess.phase=Pending` → running: set `activationDetail: + applying`. On `EphemeralProcess.phase=Succeeded`: transition + `NixosGeneration` phase as described in §4.4 and set the appropriate + `activationDetail`. + +4. **Supersession.** When a new `NixosGeneration` for the same `executionRef` + reaches `Ready`, set `activationDetail: superseded` on all prior `Ready` + records for the same target. Prior `Failed` and `Succeeded` records are + not superseded; they retain their phase for operator visibility. + +5. **Retention pruning.** After each phase transition, apply the retention + window (§ 4.6). Initiate deletion of surplus records via the finalizer + protocol (§ 4.5). + +### 7.3 startRoot justification + +The activation-runner EphemeralProcess requires `startRoot: true`. The +explicit Provider descriptor justification is: + +> The `activation-nixos-runner` template requires in-namespace root +> (`startRoot: true`) to operate the integrity-bound activation helper, which +> requires the fixed root identity to apply the system profile through the +> pre-opened system-manager effect resources. `noNewPrivileges: true` and +> `readOnlyRoot: true` are set; no writable ambient root paths or ambient host +> paths are accessible. All target profile and system mutation flows only +> through the manifest-declared, pre-opened activation portal and system-manager +> effect resources routed in the LaunchTicket. No persistent root process +> results; the runner is a one-shot EphemeralProcess that exits after the +> activation operation completes. + +### 7.4 Rollback + +A rollback is a new `activation-nixos.d2bus.org.NixosGeneration` resource created by +the CLI or controller with: + +- `spec.activationMode: switch` +- `spec.systemArtifactId`: the artifact ID of the generation to roll back to +- `spec.priorGenerationRef`: the `NixosGeneration` name of the failed or + unwanted current generation + +The controller reconciles a rollback generation identically to a normal switch. +On success, `activationDetail` is set to `rolled-back`. The prior generation +(pointed to by `priorGenerationRef`) is not automatically deleted; it remains +available for inspection until pruned by the retention window. + +### 7.5 Adopt + +`activationMode: adopt` records an existing active generation without +applying a new activation. The activation-runner executes target-locally, +reads the active system profile generation, verifies it matches +`systemArtifactId`, and exits with a structured outcome. On success, +`phase: Ready`, `activationDetail: adopted`. Useful for migrations from +pre-ADR-0046 activation paths. + +### 7.6 Retention and store GC + +When a surplus `NixosGeneration` is deleted per §4.6, step 3 of the finalizer +protocol releases the controller's typed generation/profile ownership. The +actual store closure GC is performed exclusively by the store/Volume authority +(`Provider/volume-local`) through its own resource ownership tracking. The +activation-nixos controller: + +- Does **not** invoke `nix-collect-garbage` or any store-path deletion command. +- Does **not** submit explicit `VolumeGcRequest` messages. +- Releases ownership references only (finalizer and lease removal). + +The store/Volume authority observes the released ownership and performs +store-view GC through its own reconcile loop, which tracks which closures +still have live ownership references. + +--- + +## 8. d2b activation CLI namespace + +`d2b activation` is the sole operator surface for activation operations. +It is the projection of `ADR-046-cli-and-operations` work item `ADR046-cli-007`. + +### 8.1 Subcommand summary + +| Subcommand | Primary effect | Auth | +| --- | --- | --- | +| `d2b activation build [--zone Z] [--target T]` | Evaluate Zone flake; produce bundle and artifact catalog | admin | +| `d2b activation switch --target T` | Create/update `NixosGeneration(switch)` for target T | admin | +| `d2b activation boot --target T` | Create/update `NixosGeneration(boot)` for target T | admin | +| `d2b activation test --target T` | Create `NixosGeneration(test)` for target T | admin | +| `d2b activation rollback --target T [--to G]` | Create `NixosGeneration(switch)` with `priorGenerationRef` pointing to G (or current active) | admin | +| `d2b activation adopt --target T` | Create `NixosGeneration(adopt)` for target T | admin | +| `d2b activation generations --target T` | List `activation-nixos.d2bus.org.NixosGeneration` resources for target T | read | +| `d2b activation gc --target T` | Release ownership references for surplus generations (triggers retention pruning immediately) | admin | +| `d2b activation migrate --target T --destination H` | Request execution target relocation (future; not in v1 scope) | admin | + +`read` auth requires `d2b` group membership with read role on the Zone. +`admin` auth requires admin role on the Zone. + +`--target T` accepts `Host/` or `Guest/`. Short form `--target +dev-vm` resolves to `Guest/dev-vm` if unambiguous within the Zone. + +### 8.2 Output + +All subcommands support `--output json` for machine-readable output. No store +path, private key material, artifact digest, or internal file path appears in +any CLI output in either format. + +### 8.3 Current-verb migration + +Top-level `d2b switch`, `d2b boot`, `d2b test`, `d2b rollback`, `d2b build`, +`d2b generations`, `d2b gc`, `d2b migrate` are superseded by the +`d2b activation *` group. Removal gated on work item `ADR046-activation-007`. + +--- + +## 9. Bus integration + +### 9.1 ComponentSession usage + +| Session purpose | Who | Operation | +| --- | --- | --- | +| Zone resource API | Controller | Create, update, and watch `activation-nixos.d2bus.org.NixosGeneration` and `EphemeralProcess` resources | +| Zone resource API | Activation runner (target-local) | Report structured activation outcome; update generation status | + +The runner executes locally on the target and uses the Zone resource API to +report its outcome. It does not open a ComponentSession to a remote Host or +Guest. The controller holds no long-lived session to any execution target; all +coordination is through Zone resource state. + +### 9.2 Bus endpoint declarations + +Component descriptor endpoint declarations: + +```yaml +busEndpoints: + - name: activation-control + transport: unix + purpose: activation-nixos-control +``` + +The runner does not declare endpoints; it is a worker with no inbound +ComponentSession. + +--- + +## 10. RBAC and security + +### 10.1 Resource-level access + +| ResourceType | Create | Read | Update | Delete | +| --- | --- | --- | --- | --- | +| `activation-nixos.d2bus.org.NixosGeneration` | configuration (bundle) / admin (CLI) | read | admin | admin (retention via controller) | +| `Volume` | — | — | — | — | + +The activation-nixos controller has **no** `Volume` resource rights. Volume +create/delete and reconciliation are not part of this Provider's state model: +under D087 it declares no Provider state Volume and mounts none. Bounded +non-secret operational observations are written only to resource status and the +core Operation ledger. + +Direct creation of `NixosGeneration` by operators (CLI `switch`/`rollback`/etc.) +results in `managedBy: controller` resources owned by the controller. +Bundle-created resources are `managedBy: configuration`. + +The controller creates and manages `EphemeralProcess` resources under its own +ownerRef. Operators do not directly create or delete EphemeralProcess activation +runners. + +### 10.2 Controller Process sandbox + +The controller runs under: + +- `User/d2b-activation-nixos`; system domain; `noNewPrivileges: true` +- `seccompClass: activation-nixos-controller` — allowlist defined in the + signed Provider package; no syscalls that operate outside the controller's + state `dirfd` or the d2b-bus socket +- No host network; no devices; no store-path access + +### 10.3 Runner sandbox + +The activation-runner runs with `startRoot: true` and the provider-declared +justification (§ 7.3). After the activation operation completes, the runner +process exits entirely. No persistent root process results. + +The runner's `seccompClass: activation-nixos-runner` allowlist is defined in +the signed Provider package. It permits only the syscalls required to: +- Read the private artifact catalog via the framework-provided integrity channel. +- Invoke the `d2b-activation-helper` subprocess. +- Return the activation outcome through the pre-opened activation portal from + the LaunchTicket. The runner is `processClass: worker` and has no d2b-bus + authority; it does not open any ComponentSession. + +### 10.4 Store path confidentiality + +Internal store paths (`systemStorePath`) are resolved from `artifact-catalog.json` +into the runner's process memory at activation time. They do not appear in: + +- Any `activation-nixos.d2bus.org.NixosGeneration` spec or status field +- Any EphemeralProcess spec, status, or output field +- Any CLI output +- Any audit record or OTEL span attribute +- Any log message at any severity level + +--- + +## 11. Conditions and error codes + +### 11.1 Conditions + +Standard conditions on `activation-nixos.d2bus.org.NixosGeneration`: + +| Condition type | Meaning | +| --- | --- | +| `RunnerReady` | Activation-runner EphemeralProcess is in a non-terminal phase | +| `ActivationComplete` | Activation runner reached `Succeeded`; generation active | +| `ArtifactResolvable` | `systemArtifactId` found in private artifact catalog | + +### 11.2 Bounded outcome codes + +| Code | Terminal phase | Meaning | +| --- | --- | --- | +| `activation-succeeded` | Ready / Succeeded | Activation completed normally | +| `artifact-not-found` | Failed | `systemArtifactId` absent from private artifact catalog | +| `start-deadline-exceeded` | Failed | Runner did not start within `startDeadline` | +| `runtime-deadline-exceeded` | Failed | Runner exceeded `runtimeDeadline` | +| `activation-helper-nonzero` | Failed | `d2b-activation-helper` returned non-zero; artifact integrity check or staging failed | +| `switch-to-configuration-nonzero` | Failed | `switch-to-configuration` on target returned non-zero | +| `prior-generation-invalid` | Failed | `priorGenerationRef` does not resolve to a valid terminal-phase generation | +| `adoption-mismatch` | Failed | `adopt` mode: active profile does not match `systemArtifactId` | + +Outcome codes appear only in `status.outcome.code`. They are not used as log +tokens, OTEL span attributes, or audit record payload keys. + +--- + +## 12. Telemetry, audit, and OTEL + +### 12.1 OTEL spans + +The controller emits one span per generation reconcile cycle. Attributes: + +| Attribute | Type | Notes | +| --- | --- | --- | +| `d2b.activation.mode` | string | `switch\|boot\|test\|adopt` | +| `d2b.activation.detail` | string | Current `activationDetail` value | +| `d2b.activation.outcome` | string | `succeeded\|failed\|timeout` on terminal transition | + +Zone and target identity are available only in bounded OTEL resource attributes +and permitted audit fields, never as span attributes. +No `systemArtifactId` value, store path, digest, or artifact catalog field +appears as a span attribute. + +### 12.2 Audit records + +Audit events are emitted by the Zone's post-commit audit chain after each +store transaction is durable. The controller declares the event shape; the +runtime fires it post-commit. No raw path, store path, digest, or private +material appears in any audit record. + +| Event | Trigger | Bounded fields | +| --- | --- | --- | +| `NixosGenerationCreated` | Resource created | zone, target, activationMode | +| `NixosGenerationActivated` | Phase → Ready/Succeeded | zone, target, activationMode, activationDetail | +| `NixosGenerationFailed` | Phase → Failed | zone, target, activationMode, outcomeCode | +| `NixosGenerationDeleted` | Deleted revision committed | zone, target, activationMode | +| `ProviderActivationNixosInstalled` | Provider phase → Ready | zone, version | +| `ProviderActivationNixosUninstalled` | Provider Deleted | zone | + +### 12.3 Metrics + +| Metric | Type | Labels (closed enumerations only) | +| --- | --- | --- | +| `d2b_activation_nixos_generations_total` | Counter | mode, outcome | +| `d2b_activation_nixos_generation_duration_seconds` | Histogram | mode | +| `d2b_activation_nixos_runner_total` | Counter | mode, outcome | + +No Zone/guest/resource name, artifact ID, store path, or file path appears as +a metric label. Zone identity remains in the bounded `d2b.zone` OTEL resource +attribute. + +--- + +## 13. Provider lifecycle + +### 13.1 Install + +1. Core-controller commits the Provider resource with `managedBy=configuration`. +2. ProviderDeployment handler verifies package trust and conformance. +3. ProviderDeployment creates the controller `Process` with no Provider state + Volume mount (see §5.2 and §5.3). +4. Controller starts; opens health socket. +5. Provider transitions to `Ready`. + +### 13.2 Update + +A Provider update changes `spec.artifactId` to a new version. ProviderDeployment +re-stages the artifact and restarts the controller Process under the new binary. + +### 13.3 Uninstall + +1. Core-controller sets `deletionRequestedAt` on the Provider. +2. Controller runs the finalizer protocol for every owned `NixosGeneration` + (§ 4.5), in parallel across all targets. +3. Controller gracefully stops; ProviderDeployment stops the controller Process + and waits for the Process finalizer to drain. +4. No Provider state Volume finalizer runs because none is declared. +5. Zone runtime emits event-only `phase=Deleted` for the Provider; no tombstone. +6. Post-commit: `ProviderActivationNixosUninstalled` audit event fires. + +--- + +## 14. Reuse of existing binaries + +### 14.1 d2b-activation-helper + +`packages/d2b-host/src/bin/d2b-activation-helper.rs` is reused unchanged in +ownership. In the ADR 0046 model, the runner binary invokes it via subprocess. +Required adaptations (work item `ADR046-activation-001`): + +- Accept a structured JSON input descriptor (`systemArtifactId`, `activationMode`) + instead of CLI flags. +- Emit structured JSON result (outcome, bounded error code) to stdout. +- Write **no** resource metadata; no call to any resource API. + +### 14.2 chgrp-by-numeric-gid helper + +`packages/d2b-host-activation-helper/` is reused without ownership change. + +--- + +## 15. Work items + +### ADR046-activation-001: Adapt d2b-activation-helper for structured invocation +| Field | Value | +| --- | --- | +| Dependency/owner | Provider/activation-nixos runner owner; reused helper owner in d2b-host | +| Current source | packages/d2b-host/src/bin/d2b-activation-helper.rs | +| Reuse action | adapt | +| Destination | packages/d2b-host/src/bin/d2b-activation-helper.rs | +| Detailed design | Replace the helper CLI flag interface with structured JSON input and JSON output, accept bounded systemArtifactId and activationMode, resolve store path internally, emit bounded outcome code, write no resource metadata, and preserve the no-bash-fallback invariant. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt in place. | +| Integration | activation-runner invokes the helper through the pre-opened activation portal and system-manager effect resources, then reports structured outcome to activation-nixos status. | +| Data migration | Full d2b 3.0 reset; no v2 activation-helper invocation compatibility | +| Validation | Unit tests for JSON protocol, bounded outcomes, no resource metadata writes, and no Command::new bash fallback. | +| Removal proof | Legacy flag-based helper invocation is removed from activation-nixos paths once runner JSON protocol tests pass. | + +**Scope:** `packages/d2b-host/src/bin/d2b-activation-helper.rs` + +- Replace CLI-flag interface with JSON-in / JSON-out protocol. +- Accept `systemArtifactId` (bounded string); resolve store path internally. +- Emit bounded outcome code; no resource metadata writes. +- Preserve no-bash-fallback invariant. + +### ADR046-activation-002: Implement activation-nixos.d2bus.org.NixosGeneration ResourceType schema +| Field | Value | +| --- | --- | +| Dependency/owner | ADR-046-resource-object-model and ADR-046-core-controllers; d2b-contracts activation-nixos owner | +| Current source | None — net-new v3 ResourceType; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | docs/reference/schemas/v3/activation-nixos.d2bus.org.NixosGeneration.json and packages/d2b-contracts/src/activation_nixos.rs | +| Detailed design | Define JSON schema and Rust DTOs for activation-nixos.d2bus.org.NixosGeneration, with systemArtifactId as the only artifact reference, no store path fields, activationDetail as a closed status.resource enum absent from spec, and phase constrained to the common framework enum values. Primary reuse disposition: `create`. Preserved source-plan detail: net-new schema and DTOs. | +| Integration | Resource API, resource store, Nix compiler, activation-nixos controller, and CLI projections consume the schema and DTOs. | +| Data migration | Full d2b 3.0 reset; no v2 generation resource import | +| Validation | Schema golden vectors, serde unknown-field rejection, phase enum tests, activationDetail-not-in-spec test, and no-store-path-in-spec-or-status test. | +| Removal proof | None — net-new; no prior owner to remove | + +**Scope:** `docs/reference/schemas/v3/activation-nixos.d2bus.org.NixosGeneration.json`, +`packages/d2b-contracts/src/activation_nixos.rs` + +Define JSON schema and Rust DTOs. Enforce: +- `systemArtifactId` is the only artifact reference (no store path fields). +- `activationDetail` is a closed enum; not present in spec. +- `phase` uses only the common framework enum values. + +### ADR046-activation-003: Implement controller crate +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-activation-002; activation-nixos controller owner | +| Current source | Current top-level activation behavior in packages/d2b/src/lib.rs and hardlink-farm/store ownership split described in this dossier | +| Reuse action | replace | +| Destination | packages/d2b-provider-activation-nixos/src/controller/ | +| Detailed design | Implement the reconcile loop for activation-nixos.d2bus.org.NixosGeneration: validate executionRef, systemArtifactId, and priorGenerationRef; dispatch one activation-runner EphemeralProcess with canonical startRoot=true shape; observe runner status; mark superseded generations; prune by retainedGenerations through the finalizer protocol; emit §12.3 metrics with fixed `mode`/`outcome` semantics and no Zone or resource-name-derived labels; never perform direct store-path operations, nix-collect-garbage, explicit VolumeGcRequest, raw argv composition, or store path writes to resources. Primary reuse disposition: `replace`. Preserved source-plan detail: replace top-level imperative activation flow with resource controller logic. | +| Integration | Controller watches NixosGeneration resources through Zone resource API, creates activation-runner EphemeralProcesses, releases ownership references for Provider/volume-local, and writes bounded status. | +| Data migration | Full d2b 3.0 reset; adopt mode records an existing active generation but does not import v2 controller state | +| Validation | Controller tests for retention, finalizer sequence, no TTL retention, no direct store ops, no store path in status, deleted event-only removal, runner shape, and a structural metric descriptor assertion that `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys are absent and a generation/Zone-name canary never enters labels. | +| Removal proof | Direct hardlink-farm and garbage-collection calls from activation-nixos reachable paths are absent after controller and runner tests pass. | + +**Scope:** `packages/d2b-provider-activation-nixos/src/controller/` + +Reconcile loop for `activation-nixos.d2bus.org.NixosGeneration`. Key invariants: + +- No direct store-path operations; no `nix-collect-garbage` invocation. +- EphemeralProcess dispatch creates runner with startRoot=true and the exact + spec shape from §5.4; no raw command/argv composition. +- Retention pruning via finalizer protocol; no explicit VolumeGcRequest or + store-path deletion. +- Store path never written to any resource field. + +### ADR046-activation-004: Implement activation-runner binary +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-activation-003; activation-runner owner | +| Current source | packages/d2b-host/src/bin/d2b-activation-helper.rs for helper invocation; runner process is net-new | +| Reuse action | adapt | +| Destination | packages/d2b-provider-activation-nixos/src/runner/ | +| Detailed design | Implement target-local activation-runner worker that executes on NixosGeneration.spec.executionRef for Host and Guest targets using the same contract, reads private artifact-catalog.json through the integrity channel, resolves systemArtifactId to a store path in memory only, invokes d2b-activation-helper through structured JSON, executes target-local switch-to-configuration through typed helper dispatch with no raw exec or SSH, emits structured outcome JSON, and never outputs store paths. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new runner invoking adapted helper. | +| Integration | Controller-created EphemeralProcess runs under Provider/system-minijail on the target execution context and returns outcome through the activation portal for status update. | +| Data migration | Full d2b 3.0 reset; no v2 runner state import | +| Validation | Runner tests for artifact lookup, JSON helper invocation, Host and Guest target parity, no raw argv, no SSH, no store path in output, and terminal nonzero handling. | +| Removal proof | No old SSH or raw command fallback path exists in activation-runner after tests assert typed helper dispatch only. | + +**Scope:** `packages/d2b-provider-activation-nixos/src/runner/` + +Target-local binary invoked by the EphemeralProcess template. Executes on the +target (`spec.executionRef`), not on the controller host. Host and Guest targets +use the same contract with no bypass. + +- Read private `artifact-catalog.json` via framework-provided integrity channel; + resolve `systemArtifactId` to store path in memory only. +- Invoke `d2b-activation-helper` via structured JSON protocol. +- Execute target-local `switch-to-configuration` through the helper's typed + dispatch; no raw exec, no SSH. +- Emit structured outcome JSON; exit 0 on success, non-zero on failure. +- No raw command composition; no store path in any output. + +### ADR046-activation-005: Implement d2b activation CLI projection +| Field | Value | +| --- | --- | +| Dependency/owner | ADR-046-cli-and-operations; activation CLI owner | +| Current source | packages/d2b/src/lib.rs top-level cmd_switch, cmd_boot, cmd_test, cmd_rollback, cmd_build, cmd_generations, cmd_gc, and cmd_migrate | +| Reuse action | replace | +| Destination | packages/d2b/src/activation.rs | +| Detailed design | Implement d2b activation build, switch, boot, test, rollback, adopt, generations, gc, and migrate subcommands, projecting ADR-046 CLI and Operations behavior and ensuring no store path, digest, or artifact catalog field appears in JSON or human output. Primary reuse disposition: `replace`. Preserved source-plan detail: replace with grouped CLI projection. | +| Integration | d2b CLI dispatcher calls resource API and activation-nixos controller by creating or listing NixosGeneration resources; legacy top-level verbs are removed by ADR046-activation-007 after integration tests pass. | +| Data migration | Full d2b 3.0 reset; CLI command surface changes with no runtime state import | +| Validation | CLI integration tests for subcommand parsing, authorization, resource creation/listing, rollback priorGenerationRef, gc ownership release, and output redaction. | +| Removal proof | Legacy top-level verb removal is gated on ADR046-activation-007 after the d2b activation integration matrix passes. | + +**Scope:** `packages/d2b/src/activation.rs` (new subcommand module) + +Implement all `d2b activation *` subcommands (§ 8.1). No store path, digest, or +artifact catalog field in any CLI output. Gated: remove top-level legacy verbs +only after this lands (work item ADR046-activation-007). + +### ADR046-activation-006: Nix module for activation-nixos Provider +| Field | Value | +| --- | --- | +| Dependency/owner | ADR-046-nix-configuration; activation-nixos Nix owner | +| Current source | Current VM Nix configuration emits activation inputs implicitly; this item creates the explicit Provider and NixosGeneration resource emitter | +| Reuse action | adapt | +| Destination | nixos-modules/providers/activation-nixos.nix | +| Detailed design | Emit Provider spec and activation-nixos.d2bus.org.NixosGeneration resources per target, flow retainedGenerations only through Provider.spec.config.retainedGenerations, reference systems by systemArtifactId only, omit store paths from all emitted resources, and avoid dedicated state-layout User or ComponentPrincipal because ProviderStateSet is empty. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new resource emitter adapted from existing Nix activation inputs. | +| Integration | Nix compiler emits Provider and NixosGeneration resources plus private artifact catalog entries consumed by core configuration publication and the activation-nixos controller. | +| Data migration | Full d2b 3.0 reset; existing d2b.vms activation settings are reauthored as Zone resources rather than imported | +| Validation | Nix eval tests for Provider config, NixosGeneration shape, retainedGenerations source, no systemStorePath in bundle, no state Volume or state-layout principal, and artifact ID resolution. | +| Removal proof | Old implicit activation Nix paths are unused by activation-nixos once resource emitter parity tests pass. | + +**Scope:** `nixos-modules/providers/activation-nixos.nix` + +Emit Provider spec (§ 6.1) and `activation-nixos.d2bus.org.NixosGeneration` resources +(§ 6.2) per declared target. `retainedGenerations` flows only through +`spec.config.retainedGenerations`. No store path in any emitted resource. +Does not declare a dedicated state-layout `User/` principal: the Provider +has no Provider state Volume, and process identity remains owned by the Process +Provider's normal principal model. + +### ADR046-activation-007: Remove legacy top-level activation verbs +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-activation-005; d2b CLI dispatcher owner | +| Current source | packages/d2b/src/lib.rs cmd_switch, cmd_boot, cmd_test, cmd_rollback, cmd_build, cmd_generations, cmd_gc, cmd_migrate and dispatcher registrations | +| Reuse action | delete-after-cutover | +| Destination | packages/d2b/src/lib.rs | +| Detailed design | Remove the legacy top-level activation command functions and their dispatcher registrations after the grouped d2b activation namespace passes integration tests. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete legacy top-level commands. | +| Integration | CLI dispatcher routes only d2b activation subcommands for activation operations; documentation and tests use the new namespace. | +| Data migration | Full d2b 3.0 reset; no command alias compatibility window | +| Validation | CLI integration matrix for d2b activation passes; grep or contract test confirms old cmd_* symbols and dispatcher registrations are absent. | +| Removal proof | cmd_switch, cmd_boot, cmd_test, cmd_rollback, cmd_build, cmd_generations, cmd_gc, cmd_migrate and their registrations are deleted from packages/d2b/src/lib.rs. | + +**Scope:** `packages/d2b/src/lib.rs` + +Remove `cmd_switch`, `cmd_boot`, `cmd_test`, `cmd_rollback`, `cmd_build`, +`cmd_generations`, `cmd_gc`, `cmd_migrate` and their dispatcher registrations. +Gated on ADR046-activation-005 passing integration tests. + +--- + +## 16. Tests + +### 16.1 Unit tests (`tests/`) + +| Test | Verifies | +| --- | --- | +| `generation::test_spec_no_store_path` | `systemArtifactId` resolves to store path in memory only; no store path written to spec/status | +| `generation::test_phase_common_enum` | phase only uses Pending/Ready/Succeeded/Degraded/Failed/Deleted/Unknown | +| `generation::test_activation_detail_not_in_spec` | `activationDetail` absent from spec schema | +| `generation::test_retention_from_config` | Retention reads `retainedGenerations` from Provider `spec.config`; ignores any other source | +| `generation::test_no_ttl_in_retention` | Retention pruning uses count only; no TTL field applied | +| `generation::test_finalizer_sequence` | Finalizer steps execute in order; runner deleted before ownership released | +| `generation::test_deleted_event_only` | Deletion emits event-only `phase=Deleted`; no tombstone | +| `runner::test_no_raw_argv` | Runner binary creates no raw command string; invokes helper via JSON protocol | +| `runner::test_storepath_not_in_output` | Runner output contains no store path in any field | +| `runner::test_nix_bundle_no_generation` | Compiled bundle contains no `activation-nixos.d2bus.org.NixosGeneration` resources with `systemStorePath` | +| `state::test_provider_state_set_empty` | Provider declares no Provider state Volume; ProviderStateSet query returns empty for `Provider/activation-nixos` | +| `state::test_no_state_layout_principal` | No dedicated state-layout `User/` or ComponentPrincipal reference is emitted for controller state | +| `state::test_status_first_operational_state` | Bounded non-secret controller operational observations are stored in revisioned status/core Operation ledger and re-verified after restart | +| `metrics::test_identity_labels_absent` | Metric descriptors use only closed `mode`/`outcome` semantics; exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys; Zone/generation-name canaries absent from values; `d2b.zone` resource attribute retained | + +### 16.2 Integration tests (`integration/`) + +| Test | Verifies | +| --- | --- | +| `test_switch_creates_generation` | `d2b activation switch` creates `NixosGeneration` with correct activationMode | +| `test_rollback_prior_generation_ref` | `d2b activation rollback` sets `priorGenerationRef`; activationDetail → rolled-back | +| `test_runner_ephemeral_process_shape` | EphemeralProcess resource matches canonical spec shape including startRoot and successfulTtl/failedTtl | +| `test_no_nixossshkey_in_bundle` | Compiled bundle contains no NixosSshKey or NixosConfigEdit resources | +| `test_provider_config_retained_generations` | `spec.config.retainedGenerations` is present; no other activation config fields | +| `test_storepath_absent_all_surfaces` | Store path absent from all resource spec/status, CLI output, audit records, OTEL spans | +| `test_adopt_records_existing_generation` | `activationMode: adopt` transitions to Ready/adopted without dispatching switch-to-configuration | +| `test_retention_prunes_surplus_generations` | After `retainedGenerations+1` generations, oldest is deleted | +| `test_activation_helper_no_resource_writes` | `d2b-activation-helper` completes without writing any resource metadata | +| `test_gc_no_direct_store_ops` | Retention deletion does not invoke `nix-collect-garbage`; ownership reference released only | +| `test_no_state_volume_provisioned_on_install` | Provider install creates no Provider state Volume and no `/state` Process mount | +| `test_provider_state_set_logical_query` | ProviderStateSet query returns an empty set for `Provider/activation-nixos` | +| `test_status_bounds_and_redaction` | Status rejects oversize/provider-detail overflow and never includes store paths, argv/env, PIDs, unit names, raw output, or authority handles | + +### 16.3 Conformance + +`packages/d2b-contract-tests/tests/policy_provider_crate_layout.rs` enforces +`src/`, `tests/`, `integration/`, `README.md` presence for this crate. That +test is the sole enforcement point; this dossier does not duplicate the +requirement. + +### Fast hermetic execution and test placement (D094) + +Per D094 and `ADR-046-validation-and-delivery` §10.16, this Provider's `src/` +unit tests and `tests/*.rs` hermetic suite are fast, in-process, deterministic, +and parallel-safe: an individual normal test has p95 ≤50 ms with no wall-clock +sleep, and `cargo test -p d2b-provider-activation-nixos --lib --tests` +completes in ≤2 s warm-cache execution time (compilation excluded). They use a +deterministic fake clock/RNG and the toolkit fakes/FakeEffectPort only — no +process spawn, container, network, DBus, systemd, broker daemon, Nix eval/build, +KVM, USB/GPU/TPM hardware, or live cloud, and no filesystem tree beyond tiny +temp fixtures. Any scenario needing those lives only in `integration/`, which +keeps a lane timeout/budget, parallel isolation, and fake external services by +default; such a need is re-placed into `integration/`, never given a sleep, +larger timeout, or `#[ignore]`. Bounded crypto/property tests are the only +classified exception, each named with a capped case count and a declared higher +per-test budget. + +--- + +## 17. Migration and removal + +### 17.1 Current-code mapping + +| Current symbol | ADR 0046 replacement | +| --- | --- | +| `cmd_switch`/`cmd_boot`/`cmd_test`/`cmd_rollback` (`packages/d2b/src/lib.rs`) | `d2b activation switch/boot/test/rollback` (ADR046-activation-005/008) | +| `cmd_build` | `d2b activation build` (ADR046-activation-005) | +| `cmd_generations` | `d2b activation generations` (ADR046-activation-005) | +| `cmd_gc` | `d2b activation gc` (ADR046-activation-005); ownership release only, no store-path ops | +| `cmd_migrate` | `d2b activation migrate` (future; out of v1 scope) | +| `hardlink_farm::build_farm`, `swap_current_symlink` (`packages/d2b-host/src/hardlink_farm.rs`) | Owned by `Provider/volume-local`; activation-nixos never references these primitives | +| `packages/d2b-host/src/bin/d2b-activation-helper.rs` | Adapted in place (ADR046-activation-001); JSON protocol interface added | + +Full mapping table is in `ADR-046-current-code-migration-map`. + +### 17.2 Removal conditions + +Legacy top-level verbs are removed after ADR046-activation-005 and +ADR046-activation-007 land and the `d2b activation` integration test matrix +passes. + +Direct hardlink-farm calls from any code path reachable from this Provider are +removed after ADR046-activation-003 and ADR046-activation-004 land. + +No new `Command::new("bash")` sites may be introduced. The activation-helper +adaptation preserves the no-bash-fallback invariant +(`ADR-046-current-code-migration-map` §"No bash fallbacks"). + +Per D094, each replaced current-code test is retired with an explicit +keep/adapt/move/delete disposition and a removal gate: the minimum reusable +semantic assertions migrate into this crate's hermetic `tests/`, and the old +duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest +entries are deleted once successor coverage and the removal proof pass — +updating `tests/layer1-jobs.json`, the closed gate manifests, the +flake/matrix/Nix-unit pins, the generated ledgers, and the CI workflow shards. +Old and new suites never run in parallel indefinitely. diff --git a/docs/specs/providers/ADR-046-provider-audio-pipewire.md b/docs/specs/providers/ADR-046-provider-audio-pipewire.md new file mode 100644 index 000000000..5b0a85e2f --- /dev/null +++ b/docs/specs/providers/ADR-046-provider-audio-pipewire.md @@ -0,0 +1,3160 @@ +# ADR 0046 Provider dossier: audio-pipewire + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-audio-pipewire` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 10 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-provider-audio-pipewire` crate, `AudioService` and `AudioBinding` controllers, `AudioMediator` service component | +| Depends on | `ADR-046-resource-object-model`, `ADR-046-primitive-resource-composition`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-provider-model-and-packaging`, `ADR-046-components-processes-and-sandbox`, `ADR-046-resources-volume`, `ADR-046-provider-state`, `ADR-046-componentsession-and-bus`, `ADR-046-nix-configuration`, `ADR-046-telemetry-audit-and-support`, `ADR-046-resources-host-guest-process-user` | +| Supersedes | `nixos-modules/components/audio/host.nix`, `nixos-modules/components/audio/guest.nix`, `packages/d2b-core/src/audio_policy.rs`, `packages/d2bd/src/audio_dispatch.rs`, `packages/d2bd/src/audio_host_controller.rs`, `packages/d2b-host/src/audio_argv.rs` | + +## Purpose + +This spec exhaustively defines the `audio-pipewire` Provider for d2b 3.0. It +covers: + +- Provider identity, crate layout, and package boundary; +- the two provider-neutral qualified ResourceTypes: + `audio.d2bus.org.AudioService` for owner authority and imported local + service projections, and `audio.d2bus.org.AudioBinding` for + per-Guest consumer bindings; +- both ResourceType schemas, three-layer status, lifecycle, and validation; +- the `runtime-audio` manifest dependency alias: runtime capability discovery + without implementation-ID branches in the spec; +- the host worker Process (vhost-user-sound) and its exact execution schema; +- the `AudioMediator` same-UID user-session service: receives a declared + pre-opened PipeWire portal FD from the user supervisor; exposes + `SetGrant`/`SetLevel` ComponentSession service; +- FD routing: controller requests an operation-scoped typed attachment transfer; + d2b-bus/ProviderSupervisor routes the FD directly mediator→worker LaunchTicket + without the Process Provider knowing audio and without the controller handling + FDs; +- the guest frontend provisioning model (virtio-snd + in-guest PipeWire stack); +- per-guestUser `GuestAudioAgent` Processes (libpipewire `AudioSet` service); +- process principal model: dedicated worker principals are core Process + principals from the bounded pool — not runtime-created User resources; +- static components (controller, AudioMediator) created by core + ProviderDeployment; the Provider controller reconciles AudioService and + AudioBinding, while the AudioBinding handler creates only its owned Process and + private Endpoint children; +- D096 export/import composition in which only an owner AudioService is + exported and core creates exactly one projection AudioService per + ResourceImport; ResourceExport and ResourceImport use the canonical signed + projection-factory type/fingerprint fields, the Service retains every + implementation Endpoint, and AudioBinding is never exported or generated as + a projection; +- speaker mixing plus initial-v3 exclusive microphone arbitration with a + bounded fair queue across authenticated consumer Zones; concurrent microphone + capture is deferred until a future spec defines a concrete consent + authorization resource and verb; +- RBAC, security invariants, and zero-broker-op controller boundary; +- async reconciliation, restart adoption, and status transitions; +- error codes and Degraded-state model; +- authoritative audit events and OTEL telemetry shape; +- Nix authoring, configuration, and resource compilation; +- implementation work items with exact source, destination, and tests; +- required crate layout (`src/`, `tests/`, `integration/`, `README.md`). + +Audio is an **interaction Provider**, not a device Provider. An owner Zone has +one real `audio.d2bus.org.AudioService` authority for its PipeWire +backing. A consumer Zone has one core-owned projection AudioService per +`ResourceImport`. Every opted-in Guest has its own +`audio.d2bus.org.AudioBinding` and vhost-user-sound worker; +that AudioBinding references a same-Zone AudioService through `spec.serviceRef`. + +The two ResourceType identities are audio-domain contracts, not PipeWire +implementation identities. `Provider/audio-pipewire` is their initial +implementation. PipeWire-specific desired fields are permitted only in a +strict signed `spec.provider` envelope or the Provider's own `spec.config`; +PipeWire-specific observations are permitted only in strict signed +`status.provider`. They never become fields in the provider-neutral +`spec.*`/`status.resource` schemas. + +**Controller boundary**: the `audio-binding-controller` creates, updates, and +deletes owned `Process` and private `Endpoint` resources exclusively through +the resource API. It never +calls `SpawnRunner`, `OpenPidfd`, adopts pidfds, creates `Volume` resources, +or creates `User` resources. `Provider/system-minijail` or +`Provider/system-systemd` owns process launch, wait, reap, and Process status +updates. ProcessEffect audit belongs to the Process Provider. + +**Execution schema boundary**: the live Process resource spec contains no +`executableRef`, `argv`, `env`, or inherited-FD endpoint records. Those are +signed component-template/LaunchTicket projections owned by the Process +Provider and never appear in the Zone store's Process ResourceSpec. + +**Enforcement model**: the `AudioMediator` owns the compositor-user PipeWire +connection (received as a declared portal FD from the user supervisor/display +portal) and exposes a typed `SetGrant`/`SetLevel` ComponentSession service. +Grant and level changes are applied directly via libpipewire API calls inside +the AudioMediator. No `wpctl` EphemeralProcess, node ID, argv, binary path, +or sealed command handle is required. + +**Static components**: the controller binary and the AudioMediator service +binary are created as Process resources by core ProviderDeployment when +`Provider/audio-pipewire` is activated. The controller binary registers +deterministic AudioService and AudioBinding handlers. The AudioBinding handler +creates only AudioBinding-owned child Process resources (worker, +GuestAudioAgent instances) and private Endpoints; it does not bootstrap its +own companion processes. + +## Terminology mapping (baseline → v3) + +All evidence citations use baseline symbol names. The v3 target name is +explicitly stated at each design boundary. + +| Baseline name / location | v3 ADR 0046 target | Evidence class | +| --- | --- | --- | +| `AudioPolicyState` (`d2b-core/src/audio_policy.rs:130`) | Per-Guest `AudioBinding` spec (the `mic`/`speaker`/`speakerLevel`/`micGain` fields migrate to `AudioBinding.spec.grants`; service ownership is separate in `AudioService`) | `implemented-and-reachable` | +| `AudioGrant::On/Off` (`audio_policy.rs:98`) | `AudioBinding.spec.grants.mic`/`speaker: "on"\|"off"` | `implemented-and-reachable` | +| `LevelPercent` (`audio_policy.rs:26`) | `AudioBinding.spec.grants.speakerLevel`/`micGain: 0..=100` | `implemented-and-reachable` | +| `parse_audio_state` / `to_v2_bytes` (`audio_policy.rs:282,215`) | Used only for baseline v1/v2 state-file migration on first activation; `AudioBinding.spec.grants` is durable per-Guest intent in v3 and `serviceRef` selects the backing Service; no state file maintained after migration completes | `implemented-and-reachable` | +| `AudioArgvInput` / `generate_audio_argv` (`d2b-host/src/audio_argv.rs:47,101`) | signed component template for `vhost-user-sound-worker`; argv shape is a template projection, not a live Process spec field | `implemented-and-reachable` | +| per-VM binary copy path `/run/d2b/vms//d2b-` (`audio_argv.rs:97`) | LaunchTicket verifier enforces the path shape against the component template; not exposed in the Process resource spec | `implemented-and-reachable` | +| `RunnerRole::Audio` (`d2b-contracts/src/broker_wire.rs:1524`) | `Process` resource with `spec.template: "vhost-user-sound-worker"`; launch owned by system Process Provider | `implemented-and-reachable` | +| `PipeWireHostController` (`d2bd/src/audio_host_controller.rs:85`) | `AudioMediator` user-session service; exposes `SetGrant`/`SetLevel` ComponentSession service; applies changes via libpipewire API | `implemented-and-reachable` | +| `QemuAudioController` (`audio_host_controller.rs:227`) | removed; audio discovers enforcement capability via the `runtime-audio` dependency alias; no implementation-ID branch in `audio.d2bus.org.AudioBinding.spec` | `implemented-and-reachable` | +| `WPCTL_PATH` / `PW_DUMP_PATH` env keys (`audio_host_controller.rs:103`) | superseded; AudioMediator uses libpipewire registry introspection and direct API calls; no wpctl binary or pw-dump subprocess | `implemented-and-reachable` | +| `PIPEWIRE_RUNTIME_DIR` env key (`audio_host_controller.rs:105`) | not a Process spec field; AudioMediator receives a declared pre-opened PipeWire portal FD from the user supervisor/display portal — it does not open the socket from the ambient runtime environment | `implemented-and-reachable` | +| `access(2)` credential posture check (`audio_host_controller.rs:134`) | replaced by AudioMediator readiness check; AudioMediator reports `ProviderSessionUnavailable` when the portal FD cannot be acquired from the user supervisor | `implemented-and-reachable` | +| `ofd_lock` / `acquire_audio_state_lock` (`audio_dispatch.rs:73,125`) | superseded; `AudioBinding.spec` is durable per-Guest intent and AudioService owns backing authority; no state file is maintained; OFD lock is removed | `implemented-and-reachable` | +| `write_audio_state_unlocked` atomic rename (`audio_dispatch.rs:221`) | superseded; no state file; grants are authoritative in `AudioBinding.spec` | `implemented-and-reachable` | +| `AudioHostEnforcementKind` / `AudioGuestEnforcementKind` (`provider_capabilities.rs:21,39`) | superseded; enforcement capability is discovered at runtime via the `runtime-audio` manifest dependency alias; no implementation-ID branch in `AudioBinding.spec` | `implemented-and-reachable` | +| `AudioProviderCapability` capability row (`provider_capabilities.rs:54`) | inline component descriptor field behind the `AudioService` implementation; not a third ResourceType | `implemented-and-reachable` | +| `AudioOp` / `AudioOpResponse` (`public_wire.rs:1934,2025`) | v3: `AudioBinding` spec mutations (`UpdateSpec`) via the resource API; no separate op wire | `implemented-and-reachable` | +| `AudioVmState` / `AudioChannelState` (`public_wire.rs:1955,1943`) | `AudioBinding.status.channels` inline status | `implemented-and-reachable` | +| `AudioEnforcementPosture` (`public_wire.rs:1848`) | `AudioBinding.status.enforcementPosture` | `implemented-and-reachable` | +| `AudioSetApplied` (`public_wire.rs:1997`) | `AudioBinding.status.lastSetApplied` | `implemented-and-reachable` | +| `AudioErrorKind` (`public_wire.rs:1870`) | `AudioBinding.status.outcome.code` closed enum | `implemented-and-reachable` | +| `AudioProviderKind` (`public_wire.rs:1889`) | removed from provider-neutral `AudioBinding.status.resource`; implementation identity is `spec.providerRef`, and bounded PipeWire-only observation belongs in `status.provider.details` | `implemented-and-reachable` | +| WirePlumber `client.conf.d/90-d2b` stream rules (`nixos-modules/components/audio/host.nix:252`) | retained as host Nix config; not a resource spec field | `implemented-and-reachable` | +| WirePlumber `monitor.alsa.rules` (`nixos-modules/components/audio/guest.nix:197`) | retained as guest Nix config; not a resource spec field | `implemented-and-reachable` | +| `services.pipewire.extraConfig.client."90-d2b"` (`host.nix:252`) | compiled host configuration owned by the `Provider/audio-pipewire` Nix module; not an AudioService/AudioBinding base field | `implemented-and-reachable` | +| `d2b.site.audio.inputTargetNode` (`host.nix:253`) | `Provider/audio-pipewire.spec.config.captureAlias` — bounded named alias (`^[a-z][a-z0-9-]*$`) resolved privately by AudioMediator via libpipewire registry; not a PipeWire node ID or socket path | `generated-or-eval-contract` | +| `vhost-device-sound v0.3.0` (`pkgs/vhost-device-sound/default.nix`) | `spec.artifactId` in `Provider/audio-pipewire` pointing to Nix artifact catalog entry | `implemented-and-reachable` | +| `microvm.extraArgsScript` CH audio injection (`guest.nix:112`) | `Guest.spec.audioExtension` arguments derived from the runtime-audio capability reported by the Guest's Runtime Provider; values such as `virtio_id` and `queue_sizes` are not spec fields | `implemented-and-reachable` | +| `/var/lib/d2b/vms//state/audio-state.json` (`host.nix:341`) | superseded legacy file; per-Guest grants live in `audio.d2bus.org.AudioBinding.spec`, while physical authority lives in `AudioService`; no per-Guest state file in v3 | `implemented-and-reachable` | +| `/run/d2b/locks/audio-.lock` (`host.nix:366`) | superseded; OFD lock and state file are removed in v3 | `implemented-and-reachable` | +| `d2b--snd` system user (`audio_argv.rs:140`) | superseded; dedicated worker principals are core Process principals from the bounded pool allocated by the Process Provider; not runtime-created `User` resources managed by the audio controller | `implemented-and-reachable` | +| `d2b.guestControl.wpctlPath` (`guest.nix:139`) | superseded; AudioMediator uses libpipewire API directly; no operator-visible wpctlPath option in v3 | `generated-or-eval-contract` | +| `d2b.audio.users` guest option (`guest.nix:92`) | `AudioBinding.spec.guestUsers` list of `User/` ResourceRefs; Nix/compiler sets `spec.groups: ["audio"]` on each referenced guest `User` resource at compile time; runtime API-created AudioBinding verifies `User.status.groupMembershipVerified` before sidecar start; no runtime `extraGroups` mutation | `generated-or-eval-contract` | +| `minijail-profiles.nix` audio role block / `seccompPolicyRef = "w1-audio"` | `Process.spec.sandbox.seccompClass: audio-pipewire-worker` | `implemented-and-reachable` | +| `minijail_audio_usbip.rs` Layer-1 contract tests | retained and extended in `d2b-provider-audio-pipewire/tests/` | `implemented-and-reachable` | +| `d2b audio status/set-volume/mute` CLI ops (`packages/d2b/src/`) | v3: `d2b resource update audio.d2bus.org.AudioBinding/` or a provider-specific `d2b audio` view | `implemented-and-reachable` | + +## Resolved design decisions + +All design decisions are resolved in this revision. + +| ID | Question | Resolution | +| --- | --- | --- | +| DRAUDIO-001 | Separate `AudioBinding` ResourceType or extend `Device`? | `audio.d2bus.org.AudioBinding` is an independent ResourceType. Audio is an interaction Provider; it does not model a Device inventory/arbitration/claim lifecycle. | +| DRAUDIO-002 | Per-Guest `AudioBinding` or Zone-global? | Per-Guest. Each Guest has independent grants, levels, and enforcement posture. | +| DRAUDIO-003 | Where does the vhost-user-sound socket path live? | Controller-generated private implementation detail. Never appears in `AudioBinding.spec`, `AudioBinding.status`, API responses, audit records, OTEL attributes, or any broker configuration. | +| DRAUDIO-004 | How does the controller enforce PipeWire stream routing after a grant change? | The AudioBinding controller calls the same-Zone AudioService selected by `serviceRef`. An owner Service dispatches to its local AudioMediator; a projection Service routes over its ResourceImport encrypted stream to the remote owner. Only the owner AudioMediator applies libpipewire changes. `AudioBinding.spec` is durable per-Guest intent; no state file is required. | +| DRAUDIO-005 | How is the `application.name = "d2b-"` PipeWire stream identity established? | The component template for `vhost-user-sound-worker` is a signed LaunchTicket projection that sets the per-Guest binary copy path as argv[0]. `libpipewire`'s `init_prgname()` reads `/proc/self/exe`. This is a template projection; it does not appear in the live Process resource spec. | +| DRAUDIO-006 | WirePlumber stream rules: resource spec or host Nix config? | Host Nix config. The operator capture target is stored only as `Provider/audio-pipewire.spec.config.captureAlias` — a bounded named alias (`^[a-z][a-z0-9-]*$`, ≤64 chars). The AudioMediator resolves it to the actual PipeWire node object via libpipewire registry introspection at runtime, privately. The alias never appears in AudioService/AudioBinding base spec/status, stream rules, audit, or telemetry. | +| DRAUDIO-007 | Guest PipeWire stack: resource spec or Nix guest config? | Guest Nix config. The in-guest virtio-snd module, PipeWire stack, WirePlumber virtio-snd profile, and diagnostic packages are Nix guest module concerns. | +| DRAUDIO-009 | Mic direction: null-target sentinel vs explicit routing? | WirePlumber stream rules in `client.conf.d/90-d2b` set initial stream-creation properties. Live changes reach the owner AudioMediator through the referenced AudioService (local or projection route) and use libpipewire. No worker restart or state file is required. | +| DRAUDIO-011 | How does the vhost-user-sound worker access PipeWire without ambient socket exposure? | A same-UID user-session `AudioMediator` receives a declared pre-opened PipeWire portal FD from the user supervisor/display portal (not from the ambient runtime environment). The controller requests an operation-scoped typed attachment transfer; d2b-bus/ProviderSupervisor routes the FD directly mediator→worker LaunchTicket without the Process Provider knowing audio and without the controller handling FDs. No socket path, SetSocketAcl, or `PIPEWIRE_RUNTIME_DIR` env entry appears in any resource spec, status, broker config, or public surface. | +| DRAUDIO-012 | Audio user group membership: resource spec or Nix guest config? | `AudioBinding.spec.guestUsers` is a list of `User/` ResourceRefs. For Nix/compiler-declared resources, the Nix module sets `spec.groups: ["audio"]` on each referenced guest `User` resource at compile time. For API-created `AudioBinding`, the operator sets `spec.groups` on the User resources; the controller verifies `User.status.groupMembershipVerified` before starting the sidecar and fails closed if not confirmed. The controller never mutates `User.spec.groups` at runtime. | +| DRAUDIO-013 | Who owns process launch, wait, reap, and pidfd? | `Provider/system-minijail` or `Provider/system-systemd` exclusively. The AudioBinding controller creates Process and private Endpoint resource specs only. It never calls `SpawnRunner`, `OpenPidfd`, or adopts pidfds. ProcessEffect audit belongs to the Process Provider. | +| DRAUDIO-014 | Provider root configuration key? | `Provider.spec.config` (not `rootConfig`). The canonical Provider spec shape is `{artifactId; config}`. | +| DRAUDIO-015 | wpctl EphemeralProcess vs AudioMediator service? | wpctl EphemeralProcess is removed. AudioBinding calls its AudioService. Owner Service dispatches to the local AudioMediator; projection Service routes to the remote owner. Only the owner mediator uses libpipewire. No EphemeralProcess, wpctl, or node ID enters any external surface. | +| DRAUDIO-016 | Guest-side enforcement: guestd wpctl path vs typed guest service? | guestd's wpctl dispatch path is superseded. A `GuestAudioAgent` Process running in the Guest under the guest workload user's UID exposes a typed `AudioSet` ComponentSession service over vsock. The `audio-binding-controller` calls this service via libpipewire API. No wpctl binary or command path. The `d2b.guestControl.wpctlPath` Nix option is removed from v3. | +| DRAUDIO-017 | How does audio discover the Guest's audio frontend without an `audioFrontend.kind` spec field? | The Provider manifest declares a `runtime-audio` dependency alias bound to the Guest's Runtime Provider. At activation the Runtime Provider advertises typed `AudioCapability` records (e.g., `VhostUserSound { virtio_id, queue_sizes, enforcement_posture }`) via the capability protocol. The controller reads these records via the dependency alias. No implementation-ID branch appears in `AudioBinding.spec`; `Guest.spec.audioExtension` arguments are derived from the capability. If the runtime advertises no audio capability, sidecar is not deployed. | +| DRAUDIO-018 | Dedicated worker principals: controller-created User resources or core Process principals? | Core Process principals from the bounded pool allocated per provider by the Process Provider. The audio controller does not create `User` resources for worker execution identity. Human guest `User/` references in `guestUsers` are observed from system-core, never created or modified by the audio controller. | +| DRAUDIO-019 | Who creates the static AudioMediator and controller Process resources? | Core ProviderDeployment creates them when `Provider/audio-pipewire` is activated. The AudioBinding handler creates only Binding-owned workers, GuestAudioAgents, and private Endpoints. The controller has no `Volume` or `User` verbs. | +| DRAUDIO-020 | How is the PipeWire FD routed from AudioMediator to worker without controller or Process Provider involvement? | The controller declares an operation-scoped typed attachment transfer when creating the worker Process resource. d2b-bus/ProviderSupervisor resolves the AudioMediator's active portal FD and delivers it in the worker's LaunchTicket. The Process Provider (system-minijail) receives and inherits the FD without knowing it is audio-specific. The controller never holds or transfers FDs directly. | +| DRAUDIO-021 | Which resource owns the physical audio authority and which resource is imported? | `audio.d2bus.org.AudioService`. The owner-Zone Service holds the D097 AuthorityDescriptor and references only same-Zone implementation Endpoints. `ResourceExport.resourceRef` and `serviceType` identify that Service; `projectionSchemaFingerprint` and `factoryFingerprint` bind its signed projection factory. The Endpoint remains a Service-owned implementation detail and is never an Export field. Core creates exactly one local projection AudioService per ResourceImport with `metadata.ownerRef: ResourceImport/`. AudioBinding never holds the AuthorityDescriptor, is never exported, and is never an import projection. | +| DRAUDIO-022 | How does a Guest select local or imported audio backing? | Every per-Guest AudioBinding has a required same-Zone `spec.serviceRef` to an AudioService plus its existing Guest ownership, grants, levels, and users. Owner Services use the local AudioMediator/PipeWire backing. Projection Services route encrypted named streams to the remote owner Service and are forbidden from opening PipeWire. | +| DRAUDIO-023 | What is the per-consumer ResourceType name? | `audio.d2bus.org.AudioBinding`. `State` is reserved for resource `status`; no former ResourceType name, serde rename, API alias, schema alias, or Nix compatibility alias is accepted. | +| DRAUDIO-024 | Are the audio ResourceTypes tied to PipeWire? | No. `audio.d2bus.org.AudioService` and `audio.d2bus.org.AudioBinding` are provider-neutral audio contracts initially implemented by `Provider/audio-pipewire`. The former `audio-pipewire.d2bus.org.*` ResourceType namespace and every AudioState spelling are unknown, with no alias. PipeWire-specific desired configuration is confined to strict `spec.provider` or `Provider/audio-pipewire.spec.config`; implementation observation is confined to strict `status.provider`. | +| DRAUDIO-025 | Is concurrent microphone capture supported in initial v3? | No. The owner AudioService exposes exactly one microphone capture slot across local and imported consumers and schedules pending requests with the fixed bounded per-Zone round-robin/FIFO policy in this dossier. Speaker playback remains multiplexed and mixed. `microphone: multiplexed`, consent/approval fields, priority overrides, and concurrent-capture verbs are rejected. Concurrent microphone capture is deferred until a future normative spec defines a concrete consent authorization ResourceType and resource-API verb, including revocation and audit semantics. | + +## Provider identity + +```text +Provider/audio-pipewire +``` + +- **Crate**: `d2b-provider-audio-pipewire` +- **Package name**: `d2b-provider-audio-pipewire` +- **Declares**: one Provider identity, one controller binary (`audio-pipewire-controller`), + one user-session service binary (`audio-pipewire-mediator`), one worker + template (`vhost-user-sound-worker`), one guest agent template + (`guest-audio-agent`), one manifest dependency alias (`runtime-audio`), and + initial implementation support for two provider-neutral qualified + ResourceTypes (`audio.d2bus.org.AudioService` and + `audio.d2bus.org.AudioBinding`) +- **Depends on**: public neutral contracts/toolkit crates only; no `d2bd`, + `d2b-priv-broker`, Zone-store, or other Provider internals + +### Provider resource catalog + +| Qualified ResourceType | Cardinality/scope | Authority/export/import semantics | Controller | +| --- | --- | --- | --- | +| `audio.d2bus.org.AudioService` | One real owner authority Service per owner Zone/physical backing; one projection Service per ResourceImport | Owner alone carries D097 AuthorityDescriptor and is the ResourceExport target. Core alone creates/deletes the projection with `ownerRef: ResourceImport/`. Projection routes to the remote owner and never opens PipeWire. | AudioService handler reconciles owner/projection semantics and Service-owned local Endpoints | +| `audio.d2bus.org.AudioBinding` | Exactly one per opted-in Guest | Per-Guest grants/levels/users with required same-Zone `serviceRef`; never carries authority, is never exported, and is never an import projection | AudioBinding handler creates the worker, GuestAudioAgents, and private Endpoints | + +These catalog entries register provider-neutral base schema identities. +`Provider/audio-pipewire` advertises matching schema fingerprints and is the +initial implementation selected by `spec.providerRef`. Registering the former +provider-qualified names as additional ResourceTypes is forbidden. + +Standard `Process`, `Endpoint`, `ResourceExport`, and `ResourceImport` +resources are composed as children/dependencies; they are not additional +audio ResourceTypes. + +### Controller components + +| Component | Binary | Class | Domain | Scope | +| --- | --- | --- | --- | --- | +| `audio-service-controller` | `audio-pipewire-controller` | controller handler | system | Watches `audio.d2bus.org.AudioService`; claims/observes an owner Service's D097 authority and local implementation Endpoints, or binds a projection Service to its ResourceImport encrypted-stream route; a projection never opens PipeWire | +| `audio-binding-controller` | `audio-pipewire-controller` | controller handler | system | Watches `audio.d2bus.org.AudioBinding`; resolves its same-Zone `serviceRef`; creates/updates/deletes AudioBinding-owned worker, GuestAudioAgent, and private Endpoint resources; calls the selected AudioService; never touches pidfds, broker spawn, Volume, or User resources; `Provider/audio-pipewire` declares no Provider state Volume under D087 | +| `audio-mediator` | `audio-pipewire-mediator` | service | user | Same-UID user-session component; receives declared pre-opened PipeWire portal FD from user supervisor; ProviderSupervisor routes FD to worker LaunchTicket; exposes `SetGrant`/`SetLevel` service; applies enforcement via libpipewire API | + +The controller runs as a Process in the system domain under the Host. The +mediator runs as a Process in the user domain under the compositor user's UID. +**The controller Process and mediator are static components created by core ProviderDeployment** when +`Provider/audio-pipewire` is activated; the controller does not bootstrap +them. Neither component receives a Zone store handle or a broker socket. + +**Process lifecycle boundary**: the AudioBinding handler creates `Process` +resources and private `Endpoint` resources through the resource API and watches +their status. It never calls `SpawnRunner`, `OpenPidfd`, `SIGTERM`, or any pidfd +operation. Those effects belong exclusively to `Provider/system-minijail` or +`Provider/system-systemd`. + +**Execution schema boundary**: the live Process resource spec contains no +`executableRef`, `argv`, `env`, or inherited-FD endpoint records. Those are +signed component-template/LaunchTicket projections. + +### `runtime-audio` dependency alias + +The Provider manifest declares: + +```text +dependencyAlias: runtime-audio +boundTo: Guest.runtimeProvider +purpose: audio-capability-query +``` + +At activation the Runtime Provider for the owning Guest is queried via the +`runtime-audio` alias for its typed `AudioCapability` set. The capability +record (if present) contains implementation details such as `virtio_id`, +`queue_sizes`, and `enforcement_posture`. The controller derives +`Guest.spec.audioExtension` arguments from these values. If the runtime +advertises no audio capability, the sidecar is not deployed. + +No audio capability field is stored in `AudioBinding.spec`. No +implementation-ID branch (cloud-hypervisor vs. qemu) appears in the spec or +the controller's reconcile logic — capability presence/absence and the typed +capability fields are the only dispatch surface. + +### Worker process template + +| Template name | Class | Domain | Role | +| --- | --- | --- | --- | +| `vhost-user-sound-worker` | worker | system | Per-Guest vhost-device-sound sidecar; long-lived; system-domain under `Provider/system-minijail`; uses its AudioBinding's resolved AudioService backend: an owner Service supplies the local AudioMediator attachment, while a projection Service supplies a local encrypted-stream route and never a PipeWire FD | + +## `AudioService` ResourceType + +`audio.d2bus.org.AudioService` is the provider-neutral audio +service/authority boundary, initially implemented by +`Provider/audio-pipewire`. It has exactly two roles: + +- **owner**: one real Service in the owner Zone holds the D097 + `AuthorityDescriptor`, arbitrates the physical microphone/speaker backing, + and references only same-Zone implementation Endpoints; +- **projection**: core creates exactly one local Service per `ResourceImport` + in a consumer Zone with `metadata.ownerRef: ResourceImport/`. It routes + through that import's lease and bounded encrypted named streams to the remote + owner Service. It never opens PipeWire and contains no remote ResourceRef, + FD, socket, or path. + +An AudioService is not per Guest. Per-Guest policy is always an AudioBinding that +references a same-Zone AudioService. + +### Owner Service example + +```yaml +apiVersion: resources.d2bus.org/v3 +type: audio.d2bus.org.AudioService +metadata: + name: host-audio + zone: host + ownerRef: Provider/audio-pipewire + uid: + generation: 1 + revision: + finalizers: + - audio-pipewire.d2bus.org/service-released +spec: + providerRef: Provider/audio-pipewire + serviceRole: owner + implementationEndpointRefs: + - Endpoint/audio-pipewire-authority + operations: [playback, capture] + authority: + authorityScope: physical-device + authorityClass: audio + authorityKey: host-default-audio + cardinality: zero-or-one + arbitration: + speaker: multiplexed + microphone: exclusive + exportability: explicit-export +status: + observedGeneration: 1 + phase: Ready + conditions: + - type: AuthorityClaimed + status: "True" + reason: owner-proof-verified + - type: ServiceReady + status: "True" + reason: local-endpoint-ready + - type: MicArbiterReady + status: "True" + reason: exclusive-slot-ready + resource: + serviceRole: owner + availability: ready + routeState: local + implementationEndpointRefs: + - Endpoint/audio-pipewire-authority + activeConsumerCount: 1 + activeMicCaptureCount: 0 + pendingMicRequestCount: 0 + pendingMicZoneCount: 0 + provider: + providerRef: Provider/audio-pipewire + schemaId: audio-pipewire.d2bus.org/status/audio-service + schemaVersion: 1.0.0 + observedProviderGeneration: 1 + details: + pipeWireSession: ready + portalAttachment: ready + outcome: + code: ok + message: null + retryable: false +``` + +`authority.authorityKey` is a bounded opaque key, not a PipeWire node name, +path, serial, address, or credential. Every +`implementationEndpointRefs` entry must resolve in the Service's Zone and be +owned by this Service or by its static Provider component. The owner Service +cannot reference a ResourceImport, projection Service, or cross-Zone resource. + +### Core-generated projection Service example + +```yaml +apiVersion: resources.d2bus.org/v3 +type: audio.d2bus.org.AudioService +metadata: + name: host-audio-projection + zone: work + ownerRef: ResourceImport/host-audio + uid: + generation: 1 + revision: + finalizers: + - audio-pipewire.d2bus.org/service-released +spec: + providerRef: Provider/audio-pipewire + serviceRole: projection + implementationEndpointRefs: + - Endpoint/host-audio-import-route + operations: [playback, capture] +status: + observedGeneration: 1 + phase: Ready + conditions: + - type: ImportBound + status: "True" + reason: encrypted-stream-session-ready + - type: ServiceReady + status: "True" + reason: projection-route-ready + resource: + serviceRole: projection + availability: ready + routeState: bound + implementationEndpointRefs: + - Endpoint/host-audio-import-route + activeConsumerCount: 1 + outcome: + code: ok + message: null + retryable: false +``` + +The projection carries no `authority` field. Its routing authority is the +local ownerRef chain +`AudioService -> ResourceImport -> ZoneLink/exportKey/lease`; no field names a +remote Service. Core is the only creator/deleter of projection AudioServices. +The audio Provider reconciles their semantic status and local route Endpoint. + +### `AudioService.spec` fields + +Per D089, the typed desired spec is Layer 2 and any implementation extension +uses only `spec.provider = { schemaId, schemaVersion, settings }`. The strict +base schema is: + +| Field | Type | Required | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | --- | +| `providerRef` | ResourceRef | yes | — | same-Zone Provider advertising the AudioService base fingerprint | Immutable; this dossier's initial implementation is `Provider/audio-pipewire` | +| `serviceRole` | enum | yes | — | `owner\|projection` | Immutable; must agree with ownerRef rules | +| `implementationEndpointRefs` | list[ResourceRef] | yes | — | 1..=4 local `Endpoint/` refs | Owner: only local backing implementation Endpoints; projection: only local encrypted-stream route Endpoints | +| `operations` | list[enum] | yes | — | unique subset of `playback\|capture` | Closed service capability set | +| `authority` | AuthorityDescriptor | role-dependent | — | D097 schema | Required only for `owner`; forbidden for `projection` | + +The initial PipeWire implementation accepts an absent `spec.provider` or the +exact signed envelope +`{schemaId:"audio-pipewire.d2bus.org/spec/audio-service", +schemaVersion:"1.0.0",settings:{}}`; the v1 `settings` object has no fields. +Provider-wide PipeWire desired configuration such as `captureAlias` belongs +under `Provider/audio-pipewire.spec.config`, not in this base spec. A future +per-resource PipeWire setting requires a versioned, deny-unknown +`spec.provider.settings` schema and may not shadow any base field. + +Admission requires an owner Service to have `metadata.ownerRef` equal to its +selected `spec.providerRef` (initially `Provider/audio-pipewire`) and a valid +D097 descriptor with +`authorityScope: physical-device`, `authorityClass: audio`, +`cardinality: zero-or-one`, `arbitration.speaker: multiplexed`, +`arbitration.microphone: exclusive`, and +`exportability: explicit-export|forbidden`. The initial schema rejects every +other microphone arbitration value and rejects consent, approval, priority, or +concurrent-capture fields as unknown. +Admission requires a projection Service to have +`metadata.ownerRef: ResourceImport/` and forbids `authority`. Core +supplies the projection spec from the matched import; operator/API creation or +mutation of a projection is rejected with `ProjectionCoreOwned`. + +Every Endpoint ref is same-Zone. Unknown base fields, a cross-Zone or non-Endpoint +ref, an owner descriptor on a projection, a missing owner descriptor, or a +role/ownerRef mismatch fail admission. No schema layer permits a PipeWire +locator, FD, remote Ref, audio byte, lease handle, or named-stream key. + +### `AudioService.status` fields and conditions + +Per D088, universal fields remain at `status.*`; the following typed fields +are `status.resource`. Optional `status.provider` follows the same strict, +signed, bounded Layer 3 envelope as AudioBinding. All layers are written +atomically. + +| Field | Type | Notes | +| --- | --- | --- | +| `serviceRole` | enum | Observed `owner\|projection` | +| `availability` | enum | `pending\|ready\|degraded\|revoked` | +| `routeState` | enum | Owner: `local`; projection: `pending\|bound\|degraded\|revoked` | +| `implementationEndpointRefs` | list[ResourceRef] | Ready same-Zone Endpoints, bounded to the spec set | +| `activeConsumerCount` | uint | Bounded aggregate count; no consumer names | +| `activeMicCaptureCount` | uint | Owner only; always `0` or `1` | +| `pendingMicRequestCount` | uint | Owner only; aggregate `0..=64`; no consumer identity or queue position | +| `pendingMicZoneCount` | uint | Owner only; aggregate `0..=64`; no Zone names | + +Owner Services report all three microphone arbitration aggregates on each +material change. Projection Services omit them because the remote owner alone +arbitrates capture; a projection never mirrors queue membership or consumer +identity into local status. + +For `Provider/audio-pipewire`, `status.provider.schemaId` is +`audio-pipewire.d2bus.org/status/audio-service` at `1.0.0`. Its strict +role-sensitive `details` object permits only `pipeWireSession` and +`portalAttachment` (`ready|unavailable`) for an owner Service; a projection +must omit those fields because it never opens PipeWire or receives a portal +attachment. No PipeWire field is admitted in `status.resource`. + +Universal `status.outcome.code` is a closed enum: `ok`, +`AuthorityConflict`, `EndpointNotReady`, `ImportNotBound`, `ImportRevoked`, +`ProviderSessionUnavailable`, or `ProviderMisconfigured`. Message and +retryability remain universal, bounded, and redacted. + +Closed conditions: + +| Type | Applies to | Meaning | +| --- | --- | --- | +| `AuthorityClaimed` | owner | D097 authority index accepted and ownerProof verified | +| `BackingReady` | owner | The selected Provider reports the local audio backing Ready | +| `ImportBound` | projection | ResourceImport lease and encrypted named streams are current | +| `ProjectionRouteReady` | projection | Same-Zone route Endpoint is Ready; no PipeWire open occurred | +| `MicArbiterReady` | owner | Exclusive microphone slot and bounded fair queue are available | +| `MicQueueSaturated` | owner | Fixed total or per-Zone pending-request bound was reached | +| `ServiceReady` | both | Required local implementation Endpoints and role semantics are Ready | +| `ServiceDegraded` | both | Authority, endpoint, import, or route observation is degraded | +| `Revoked` | projection | Import/export lease was revoked; consumers must degrade | + +An owner is `Ready` only when authority and local Endpoint observations are +ready. A projection is `Ready` only when its import is bound and its local +encrypted-stream route is ready. Link loss or export revocation marks the +projection `Degraded`/`revoked`; it never falls back to opening local PipeWire. +D091 currency propagates remote Service -> ResourceImport -> projection +AudioService -> referencing AudioBindings. Disruptive Service upgrades drain +AudioBindings and named streams before recycle; non-disruptive updates preserve +the service role and ownerRef chain. + +## `AudioBinding` ResourceType + +`audio.d2bus.org.AudioBinding` is the provider-neutral per-Guest consumer +binding, initially implemented by `Provider/audio-pipewire`. Its +`metadata.ownerRef` is the Guest, and its required `spec.serviceRef` selects +one same-Zone AudioService. It carries grants, levels, guest users, and the +observed realization for that Guest. It never holds a D097 +AuthorityDescriptor, is never the `resourceRef`/`serviceType` of a +ResourceExport, and is never generated as a ResourceImport projection. + +This is a clean-break ResourceType name. Every former provider-qualified +Binding spelling, every qualified or unqualified `AudioState` spelling, and +every other former per-consumer identifier are unknown. None is registered, +admitted, decoded, or served as an alias, and no schema or serde rename accepts +one. `State` is reserved for resource `status`. References below to baseline symbols such as +`AudioPolicyState`, `parse_audio_state`, or the legacy `audio-state.json` file +are migration evidence only; none defines a v3 ResourceType or compatibility +alias. + +### Envelope example + +```yaml +apiVersion: resources.d2bus.org/v3 +type: audio.d2bus.org.AudioBinding +metadata: + name: corp-vm-audio + zone: dev + uid: + generation: 3 + revision: + ownerRef: Guest/corp-vm + finalizers: + - audio-pipewire.d2bus.org/sidecar-stopped + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:01:00Z +spec: + providerRef: Provider/audio-pipewire + serviceRef: audio.d2bus.org.AudioService/host-audio + grants: + mic: "off" + speaker: "on" + speakerLevel: 75 + micGain: null + guestUsers: + - User/alice + suspendOnGuestAbsent: true +status: + observedGeneration: 3 + phase: Ready + conditions: + - type: RealizationReady + status: "True" + reason: provider-realization-ready + - type: ConsumerAttached + status: "True" + reason: runtime-audio-attached + - type: GrantsEnforced + status: "True" + reason: service-applied + - type: ServiceReady + status: "True" + reason: referenced-audio-service-ready + lastReconciledAt: 2026-07-22T00:01:01Z + resource: + channels: + speaker: + grant: "on" + level: 75 + liveEnforced: true + mic: + grant: "off" + gain: null + liveEnforced: true + arbitrationState: inactive + enforcementPosture: HostAndGuest + lastSetApplied: HostAndGuest + observedServiceRef: audio.d2bus.org.AudioService/host-audio + realizationRefs: + - Process/corp-vm-audio-sidecar + provider: + providerRef: Provider/audio-pipewire + schemaId: audio-pipewire.d2bus.org/status/audio-binding + schemaVersion: 1.0.0 + observedProviderGeneration: 1 + details: + pipeWireHostSession: ready + pipeWireGuestSessions: ready + outcome: + code: ok + exitCode: null + message: null + retryable: false +``` + +### `AudioBinding.spec` fields + +Per D089, `AudioBinding`'s typed desired spec is the ResourceType base spec +(Layer 2): top-level `spec.*`, including `spec.providerRef` where applicable. +Any implementation-variant desired settings use only the canonical Layer 3 +`spec.provider = { schemaId, schemaVersion, settings }` envelope, whose +`settings` are manifest-registered/signed, deny-unknown, bounded, +versioned/digested, validated against `spec.providerRef`, and forbidden to +shadow base fields; shared fields are promoted into the base spec. +`Provider/audio-pipewire` implements the exact base spec/status schema +version/fingerprint, accepts the canonical minimal base Spec, and rejects an +unsupported optional base capability only through its signed capability matrix +plus typed provider-neutral `unsupported-capability`. `spec.provider` aligns +with `status.provider`. + +| Field | Type | Required | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | --- | +| `providerRef` | ResourceRef | yes | — | same-Zone Provider advertising the AudioBinding base fingerprint | Immutable after creation; this dossier's initial implementation is `Provider/audio-pipewire` | +| `serviceRef` | ResourceRef | yes | — | same-Zone `audio.d2bus.org.AudioService/` | Immutable; owner or projection Service; must be Ready before realization | +| `grants.mic` | enum | yes | — | `"on"` \| `"off"` | Microphone grant | +| `grants.speaker` | enum | yes | — | `"on"` \| `"off"` | Speaker grant | +| `grants.speakerLevel` | uint \| null | no | `null` | `0..=100` | Speaker volume percent; null = system default | +| `grants.micGain` | uint \| null | no | `null` | `0..=100` | Microphone input gain percent; null = system default | +| `guestUsers` | list[ResourceRef] | no | `[]` | ≤16 entries; each `User/` where name matches `[a-z][a-z0-9_-]*` ≤32 chars | Guest User ResourceRefs; corresponding User resources must exist; group membership verified by controller before sidecar start | +| `suspendOnGuestAbsent` | bool | no | `true` | — | When `true` the sidecar Process is not started/is stopped when the owning Guest is not Running | + +The initial PipeWire implementation accepts an absent `spec.provider` or the +exact signed envelope +`{schemaId:"audio-pipewire.d2bus.org/spec/audio-binding", +schemaVersion:"1.0.0",settings:{}}`; the v1 `settings` object has no fields. +PipeWire aliases, node selectors, portal settings, frontend parameters, and +other implementation details are rejected as top-level AudioBinding fields. +Provider-wide `captureAlias` remains solely in +`Provider/audio-pipewire.spec.config`. + +Audio frontend parameters (virtio device ID, queue sizes) are not `AudioBinding` +spec fields. They are derived at runtime from the `runtime-audio` capability +record advertised by the Guest's Runtime Provider (see "runtime-audio +dependency alias" above). + +Schema validation is strict: unknown fields are rejected at resource API +admission time. Level and gain values are validated in `[0,100]`; null +sentinels are preserved and serialized as JSON `null`, never as absent fields. +`providerRef` and `serviceRef` are immutable after creation; a mutation attempt +returns `FieldImmutable`. `serviceRef` must be a syntactically valid qualified +AudioService Ref in the same Zone and may not point to ResourceImport directly. +Each `guestUsers` entry is validated as a syntactically correct +`User/` ResourceRef; referential existence is validated at runtime. + +### `AudioBinding.status` fields + +Per D088, `AudioBinding.status` is layered: universal `ResourceStatus` fields +(`observedGeneration`, `phase`, `conditions`, timestamps, and `outcome`) remain +at top-level `status.*`, while the typed audio fields below are the +ResourceType-common `status.resource` object for +`audio.d2bus.org.AudioBinding`. Optional `status.provider` carries only +implementation-only observation (`providerRef`, qualified immutable `schemaId`, +semver `schemaVersion`, numeric `observedProviderGeneration`, strict +unknown-field-denied redacted `details` ≤32 KiB registered/signed in the +Provider manifest); shared fields are never duplicated there. The controller +writes all present layers atomically in one status mutation. + +For `Provider/audio-pipewire`, the strict provider-status schema is +`audio-pipewire.d2bus.org/status/audio-binding` at `1.0.0`; `details` permits +only bounded `pipeWireHostSession` and `pipeWireGuestSessions` values +(`ready|unavailable|not-applicable`). These implementation observations are +optional and redacted. They are forbidden in `status.resource`, and the +provider envelope may not repeat grants, levels, service refs, child refs, +currency, or universal outcome data. + +D091 currency and upgrade: the audio-pipewire controller implements +`assess_update`, `plan_upgrade`, and `execute_upgrade` for the provider-neutral +ResourceTypes it implements and their semantic audio sessions. AudioService +currency propagates to every AudioBinding that references it. A +`ProviderGenerationChanged`, +`ArtifactChanged`, `DependencyChanged`, or `SpecChanged` reason populates +universal `status.update` with +`UpdateAvailable` or `UpgradeRequired`; disruptive changes MUST return +`UpgradeRequired` rather than being applied in place, while non-disruptive +changes reconcile normally. These currency fields are universal/ResourceType +base fields, never `status.provider`. Upgrades recycle only the audio +realization (owned `Process` resources, endpoints, and sessions) with +`disruption` set to `Reload`, `Restart`, or `Recycle`; durable config is +preserved, dependent sessions and attachments are drained and restarted by the +dependency-aware planner, and owned ephemeral session state remains process +memory. No audio samples, clipboard bytes, terminal bytes, notification content +bytes, secrets, paths, session bytes, or handles may appear in `status.update`. + +D090 expedited reconcile: Create, UpdateSpec, and Delete requests that set +`waitForReconcile` perform no external effect, finalizer mutation, or status +mutation until Core supplies a typed `CommittedRevisionProof` +`{resourceUid,generation,revision,operationId}`. Abort produces no effect; a +durable commit is never rolled back on later reconcile timeout. The response is +the committed object plus one-pass projected layered status, a disposition +(`Converged`, `Progressing`, `Blocked`, `UpgradeRequired`, or `Failed`), and +`statusPersistence` (`pending` or `committed`); effect idempotency keys derive +from `(UID,generation,revision,operationId)` in the same per-resource +single-flight priority lane. + +| Field | Type | Notes | +| --- | --- | --- | +| `phase` | enum | Common framework phase: `Pending\|Ready\|Degraded\|Failed\|Unknown`. `Deleted` exists only as a revision-log event, not a live resource phase. Audio-specific detail is in `conditions` and `outcome.code`. | +| `conditions` | list | See condition types below | +| `resource.channels.speaker.grant` | `"on"\|"off"` | Last observed speaker grant | +| `resource.channels.speaker.level` | uint \| null | Last observed speaker level | +| `resource.channels.speaker.liveEnforced` | bool | True when confirmed by successful call through the referenced AudioService this reconcile | +| `resource.channels.mic.grant` | `"on"\|"off"` | Last observed mic grant | +| `resource.channels.mic.gain` | uint \| null | Last observed mic gain | +| `resource.channels.mic.liveEnforced` | bool | True when confirmed through the referenced AudioService this reconcile | +| `resource.channels.mic.arbitrationState` | enum | `inactive\|queued\|active\|blocked`; `active` is possible for at most one consumer of an owner Service | +| `resource.enforcementPosture` | enum | `HostAndGuest\|HostOnly\|GuestOnly\|None` | +| `resource.lastSetApplied` | enum | `HostAndGuest\|HostOnly\|GuestOnly\|OfflineOnly` | +| `resource.observedServiceRef` | ResourceRef | Last resolved same-Zone AudioService; must equal `spec.serviceRef` | +| `resource.realizationRefs` | list[ResourceRef] | At most 32 same-Zone owned `Process`/`Endpoint` refs; no implementation locator or identity | +| `outcome.code` | string | Closed enum; see error codes | +| `outcome.exitCode` | int \| null | Worker exit code when phase is Failed | +| `outcome.message` | string \| null | Bounded ≤256 chars; redacted: no paths, credentials, or VM-identifying details | +| `outcome.retryable` | bool | Whether the controller will retry | + +**`Deleted` phase**: after all finalizers complete, the resource is removed +from the store immediately. A single `phase=Deleted` revision event is emitted +to the revision log before removal. The audit record for the deletion is emitted +post-commit, after the revision event is durable. + +### Condition types + +| Type | Meaning | +| --- | --- | +| `RealizationReady` | The selected Provider's owned realization resources are Ready | +| `ConsumerAttached` | The Guest runtime reports its selected audio capability attached | +| `GrantsEnforced` | Last `SetGrant`/`SetLevel` service calls completed with `liveEnforced: true` on every requested channel; false while requested microphone capture is queued or blocked | +| `GrantEnforcementFailed` | AudioMediator `SetGrant`/`SetLevel` service returned an error | +| `ServiceReady` | Referenced owner/projection AudioService is Ready and current | +| `ServiceUnavailable` | Referenced AudioService is absent, degraded, revoked, cross-Zone, or stale | +| `MicBlocked` | The selected Provider confirms that microphone capture is blocked | +| `MicQueued` | A requested microphone grant is waiting in the owner Service's bounded fair queue | +| `MicCaptureActive` | The owner Service granted this Binding the single microphone capture slot | +| `MicQueueFull` | The owner Service rejected this Binding's request because the fixed total or authenticated per-Zone bound is full | +| `SpeakerBlocked` | The selected Provider confirms that speaker playback is blocked | +| `ProviderSessionUnavailable` | The selected Provider cannot currently access its local audio session; implementation detail is confined to `status.provider` | +| `GuestAbsent` | Guest is not Running and `suspendOnGuestAbsent: true`; owned realization is intentionally stopped | +| `ConsumerEnforcementReady` | The selected Provider reports all required consumer-side enforcement ready | +| `GuestUserAudioGroupMissing` | One or more `guestUsers` User refs do not have `User.status.groupMembershipVerified: true` for the `audio` group | +| `RuntimeCapabilityUnavailable` | The `runtime-audio` dependency alias returned no audio capability; sidecar not deployed | + +### Phase semantics + +| Phase | Meaning | +| --- | --- | +| `Pending` | `audio.d2bus.org.AudioBinding` committed; referenced AudioService or owned realization not yet Ready, Guest not yet Running, runtime-audio capability not yet advertised, or a requested microphone grant is fairly queued | +| `Ready` | Referenced AudioService Ready, realization ready, consumer attached, speaker grant enforced, and microphone either off or holding the exclusive capture slot | +| `Degraded` | Referenced AudioService degraded/revoked, realization present but enforcement failed, microphone queue admission is temporarily full, Guest temporarily absent, or required provider session/route unavailable | +| `Failed` | Owned realization failed unrecoverably, or runtime audio capability is permanently absent | +| `Unknown` | Controller cannot currently observe Process or Guest status | + +## Host worker process + +### Architecture + +The host worker is a long-lived `vhost-user-sound` sidecar +(`vhost-device-sound` with the backend selected by the referenced +AudioService). For an owner AudioService it connects to the owner Zone's +compositor PipeWire session using a **pre-opened connected PipeWire FD** +received via the operation-scoped typed attachment transfer routed by +d2b-bus/ProviderSupervisor from the AudioMediator. For a projection +AudioService it connects only to the projection's same-Zone route Endpoint; +audio frames then use the ResourceImport's bounded encrypted named streams. +The projection worker never opens PipeWire and never receives a PipeWire FD. +It exposes a `vhost-user` server as an owned `Endpoint` resource. The backing +Unix socket locator is a controller-private sealed LaunchTicket value. The owning Guest attaches it via the +`Guest.spec.audioExtension` arguments derived from the `runtime-audio` +capability (e.g., `--generic-vhost-user socket=,virtio_id=25,...` +for Cloud Hypervisor). No `audioFrontend` spec field is involved. + +One worker Process per AudioBinding/Guest. The `audio-binding-controller` creates +and owns the Process and its private Endpoint resources after resolving +`spec.serviceRef`. The system Process Provider (`Provider/system-minijail`) +launches, supervises, and reaps the worker and owns all ProcessEffect audit +records. The worker's execution principal is a core Process principal from the +bounded pool — not a controller-created `User` resource. + +### Process resource + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: corp-vm-audio-sidecar + zone: dev + ownerRef: audio.d2bus.org.AudioBinding/corp-vm-audio + finalizers: [audio-pipewire.d2bus.org/sidecar-stopped] +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + processClass: worker + template: vhost-user-sound-worker + sandbox: + namespaceClasses: [mount, pid, ipc, uts] + capabilityClasses: [] + seccompClass: audio-pipewire-worker + noNewPrivileges: true + startRoot: false + readOnlyRoot: true + environmentClass: minimal + budget: + cpu: + limit: "500m" + request: null + memory: + limit: "64Mi" + request: null + pids: + limit: 32 + fds: + limit: 64 + restartPolicy: + class: on-failure + backoffBase: "1s" + backoffMax: "30s" + backoffMultiplier: 2.0 + maxRestarts: 5 + resetAfter: "300s" + readiness: + initialDelay: "0s" + timeout: "10s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined +``` + +### Execution schema notes + +- **No `executableRef`, `argv`, `env`** — these are signed component-template + projections in the LaunchTicket. The Process Provider resolves them from the + compiled `vhost-user-sound-worker` template. The live Process resource spec + stored in the Zone store contains no executable path, argument list, or + environment. +- **No `inherited-fd` endpoint in the Process spec** — for an owner Service, + the PipeWire FD attachment from the AudioMediator is declared in the + component descriptor (the private signed template), not the live resource + spec. For a projection Service, the template instead resolves the local + projection route Endpoint and no PipeWire FD exists. d2b-bus/ + ProviderSupervisor performs either authorized local attachment without the + Process Provider knowing audio and without the controller handling FDs. +- **No `mounts` block in the live Process spec** — the worker receives its + configuration via the sealed component descriptor and the operation-scoped FD + transfer. `AudioBinding.spec` is the durable desired intent for grants/levels, + and its AudioService is the backing authority/route; no application state + file is written. The worker declares no Provider state + Volume; bounded non-secret observations are stored in `AudioBinding.status`, the + Provider status subresource where applicable, and the core Operation ledger. +- `domain: system` — the worker runs in the system domain. The execution + principal is a core Process principal from the bounded pool allocated by the + Process Provider; it is not a controller-created `User` resource and does not + appear in the live Process spec. No `userRef` field is set. +- `sandbox.namespaceClasses: [mount, pid, ipc, uts]` — network namespace is + **not** in this list. An owner worker receives the PipeWire FD as an inherited + descriptor; a projection worker receives only an authorized same-Zone route + attachment and the Provider's stream component owns encrypted carriage. +- `sandbox.capabilityClasses: []` — zero host capabilities; load-bearing + invariant; see security section. +- `sandbox.seccompClass: audio-pipewire-worker` — the Process Provider + resolves the exact seccomp profile from the compiled Process template. + Maps to the `w1-audio` seccomp policy in the baseline minijail profile table. +- `sandbox.startRoot: false` — the Process Provider must not elevate to root + before exec. +- `Endpoint/corp-vm-audio-vhost-user` is the AudioBinding-owned private service + identity for the vhost-user server. The AudioBinding controller creates the + Endpoint resource; the Process Provider creates the backing Unix socket + before exec and seals its locator into the LaunchTicket. The locator never + appears in resource spec or status. +- `budget` uses the canonical nested `cpu`/`memory`/`pids`/`fds` shape. + `pids` and `fds` use the `{limit: N}` object form (not a bare scalar). +- `restartPolicy.class: on-failure` — canonical class name. + `backoffBase`/`backoffMax` are duration strings; `backoffMultiplier` is the + exponential factor; `maxRestarts` is the per-launch-cycle ceiling; `resetAfter` + resets the counter if the process stays Running for this duration. +- `readiness.class: provider-defined` — the `vhost-user-sound-worker` template + declares a provider-defined readiness mechanism (vhost-user socket ready). + Fields: `initialDelay`, `timeout`, `failureThreshold`, `successThreshold`. + +**ProcessEffect audit**: all process-launch, signal, and exit audit records +for this Process are owned by `Provider/system-minijail`. The +`audio-binding-controller` emits only resource-level `AudioBinding` audit events. + +### Guest command-line extension + +The `audio-binding-controller` mutates `Guest.spec.audioExtension` to add the +runtime's audio arguments. The exact arguments (socket path, device ID, queue +sizes) are derived from two sealed sources: + +- The vhost-user socket path is derived from `AudioBinding.metadata.uid` by the + Process Provider and embedded in the LaunchTicket; it is never stored in any + public field. +- Device parameters (`virtio_id`, `queue_sizes`, runtime-specific flags) are + read from the `runtime-audio` capability record at reconcile time and applied + as the extension; they are not `AudioBinding.spec` fields. + +The mutation is conditional: when the runtime advertises no audio capability, +or when both grants are `"off"`, the controller removes `audioExtension` from +the Guest spec. + +## AudioMediator user-session service + +### Purpose and identity + +The `AudioMediator` is the owner AudioService's same-UID user-session +implementation `Process`; it runs under the compositor user's UID. A +projection AudioService never starts or calls a local AudioMediator. The +mediator's responsibilities are: + +1. **Receive a declared pre-opened PipeWire portal FD** from the user + supervisor/display portal. The AudioMediator does not open the PipeWire + socket from the ambient runtime environment; it receives the FD as a + declared pre-opened attachment from the user supervisor, which owns the + compositor session. +2. **Participate in the operation-scoped typed attachment transfer**: + d2b-bus/ProviderSupervisor routes the portal FD directly from the + AudioMediator to the worker LaunchTicket at launch time (see "FD routing + via ProviderSupervisor" below). The Process Provider (system-minijail) and + the controller are both audio-agnostic in this path. +3. Resolve a bounded `captureAlias` label to the actual PipeWire node object + via libpipewire registry introspection at runtime (privately, never + exported). +4. Expose the owner AudioService's `SetGrant`/`SetLevel` typed ComponentSession + service over d2b-bus. When a same-Zone AudioBinding or an authorized remote + projection route calls `SetGrant(channel, value)` or + `SetLevel(channel, value)`, the AudioMediator applies the change directly via + libpipewire API (`pw_node_set_param`, `pw_stream_set_control`) on the + worker's live PipeWire node. + +### AudioMediator Process resource + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: audio-pipewire-mediator + zone: dev + ownerRef: Provider/audio-pipewire +spec: + providerRef: Provider/system-systemd + executionRef: Host/host-system + domain: user + userRef: User/compositor-user + processClass: service + template: audio-mediator-service + sandbox: + namespaceClasses: [] + capabilityClasses: [] + seccompClass: audio-pipewire-mediator + noNewPrivileges: true + startRoot: false + readOnlyRoot: true + environmentClass: provider-defined + budget: + cpu: + limit: "250m" + request: null + memory: + limit: "32Mi" + request: null + pids: + limit: 16 + fds: + limit: 128 + restartPolicy: + class: on-failure + backoffBase: "2s" + backoffMax: "30s" + backoffMultiplier: 2.0 + maxRestarts: null + resetAfter: "300s" + readiness: + initialDelay: "0s" + timeout: "10s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined +``` + +**No `executableRef`, `argv`, `env`** — these are component-template +projections. The live Process resource spec stored in the Zone store contains +only the fields shown above. `budget`, `restartPolicy`, and `readiness` use the +same canonical field names as the worker Process spec. `readiness.class: +provider-defined` — the `audio-mediator-service` template declares its own +readiness mechanism (ComponentSession endpoint accepting connections). + +### FD routing via ProviderSupervisor + +For an AudioBinding that references an owner Service, the controller declares an +**operation-scoped typed attachment transfer** on the worker Process resource +at creation time. The transfer descriptor names the AudioMediator as the FD +source and the worker as the FD destination, without specifying audio details: + +```text +attachmentTransfer: + source: Process/audio-pipewire-mediator + sourceHandle: pipewire-portal-fd + destinationRole: inherited-fd +``` + +When the Process Provider receives the new worker Process resource, it does +**not** resolve this transfer itself. Instead, d2b-bus/ProviderSupervisor +orchestrates: + +1. ProviderSupervisor resolves the AudioMediator's active `pipewire-portal-fd` + handle from its component-descriptor declaration. +2. Validates the requesting subject (worker Process generation/UID) against the + AudioMediator's ACL. +3. Delivers the FD via an atomic seqpacket SCM_RIGHTS transfer bound to the + transfer descriptor. +4. Embeds the received FD into the worker's LaunchTicket as an inherited + descriptor (CLOEXEC cleared). + +The Process Provider (system-minijail) launches the worker with the FD already +present in the LaunchTicket. It does not know the FD is a PipeWire connection. +The controller does not hold or transfer FDs at any point. + +This transfer is a component-descriptor declaration, not a Process resource +spec field. No socket path, runtime directory path, or user identifier appears +in the Process resource spec or any API response. + +For an AudioBinding that references a projection Service, this transfer is +absent. The worker resolves only the projection's local route Endpoint, and the +AudioService controller binds that Endpoint to the ResourceImport's encrypted +named streams. No FD is forwarded across a Zone. + +### `SetGrant` and `SetLevel` service + +The AudioMediator exposes a `SetGrant`/`SetLevel` typed ComponentSession service +on the owner Service's `Endpoint/audio-pipewire-authority`. A same-Zone +AudioBinding controller reaches it through its owner `serviceRef`; a consumer +Zone reaches it through the projection Service's encrypted route. No +AudioBinding references this Endpoint directly. + +Service interface (conceptual): + +```text +SetGrant(consumer: OpaqueConsumerHandle, channel: "mic"|"speaker", value: "on"|"off") + → Applied | Queued + | Error(code: GrantEnforcementFailed | MicQueueFull + | ProviderSessionUnavailable | ...) + +SetLevel(consumer: OpaqueConsumerHandle, channel: "mic"|"speaker", valuePercent: u8) + → Ok | Error(code: GrantEnforcementFailed | ...) +``` + +The authenticated ComponentSession or import route supplies the consumer Zone; +it is never accepted from request data. For a local caller, the AudioService +maps the opaque handle to the same-Zone AudioBinding. A projection maps its +local Binding to a route-scoped opaque handle before forwarding, so no +ResourceRef crosses a Zone. + +The AudioMediator applies the change via: +- `pw_node_set_param` with `SPA_PARAM_Props` to update `mute` or routing on + the worker's virtual device node; +- `pw_stream_set_control` on any active stream attached to the node; +- WirePlumber session policy enforcement through the node properties already + set by the component template's initial stream configuration. + +No node ID, PipeWire object path, wpctl binary, or any external process is +involved. The service call is synchronous within the AudioMediator; the caller +receives the result before updating `AudioBinding.status`. + +No node ID, node path, or PipeWire runtime directory path appears in any +service request, response, d2b-bus message, audit record, or log entry. + +### Initial-v3 microphone arbitration + +Speaker `SetGrant` calls are applied immediately and all admitted speaker +streams remain multiplexed through the owner Service's mixer. Microphone +capture has one global slot per owner AudioService across the owner Zone and +all importing Zones. The owner admits or queues `mic: "on"` as follows: + +1. A request is keyed by the authenticated consumer Zone and its + route-scoped opaque consumer handle. Duplicate requests are idempotent, and + at most one pending entry exists per handle. +2. Pending requests are bounded to 16 per Zone and 64 total. A request that + would exceed either bound returns `MicQueueFull` and creates no entry. +3. Each Zone has an owner-sequenced FIFO subqueue. Non-empty Zones form a + round-robin ring. On release or lease expiry, the arbiter selects the head + request from the Zone after the last-served Zone; a Zone that still has + pending requests moves to the ring tail. Client timestamps and requested + priority are ignored. +4. The active capture lease is 30 seconds. It may renew while no other Zone is + waiting. Once another Zone waits, renewal is denied; at the deadline the + mediator mutes and disconnects the old capture stream, dequeues the next + Zone, and, if the old consumer still requests `mic: "on"`, atomically places + it at its Zone FIFO tail. Thus no two microphone streams overlap and a + continuously requesting holder cannot bypass waiting Zones. +5. `mic: "off"`, Binding deletion, import revocation, ZoneLink loss, session + cancellation, or ResourceImport lease expiry removes that consumer's + active/pending entry. Release is idempotent. Queue membership and ordering + contain opaque handles only and are never persisted, logged, audited, or + exported; only the bounded aggregate counts defined below may leave the + arbiter. + +`AudioService.status.resource` exposes only the bounded aggregate active, +pending-request, and pending-Zone counts. `AudioBinding.status.resource` +exposes only that Binding's `inactive|queued|active|blocked` state; it never +exposes queue position, another consumer, or a Zone name. + +Initial v3 has no concurrent microphone capture. `microphone: multiplexed`, +consent or approval fields, priority overrides, and concurrent-capture verbs +are unknown and rejected by schema/admission. A future version may add +multiplexed capture only after a separate normative spec defines a concrete +consent authorization ResourceType and resource-API verb, including grant, +revocation, expiry, status, and audit semantics. This dossier reserves no +placeholder consent field or verb. + +### captureAlias resolution + +When `Provider/audio-pipewire.spec.config.captureAlias` is non-null, the +AudioMediator resolves it at grant-change time via libpipewire registry +introspection. It iterates the PipeWire global object list and finds the node +whose `node.nick` or `node.name` matches the alias. The resolution is private +to the AudioMediator process. The resolved node object never leaves the mediator +— it is used in place directly for `pw_node_set_param` routing calls. No node +ID appears in any bus message, resource spec, status, audit record, or OTEL +attribute. + +## Guest frontend + +### Architecture + +The guest frontend is a kernel virtio device driver (`snd_virtio`) and an +in-guest PipeWire stack. These are provisioned by the guest NixOS module +compiled into the Guest's NixOS configuration at Provider activation time. + +Activated for any Guest that owns an `AudioBinding` resource, it installs: + +- `boot.kernelModules: ["snd_virtio"]` — in-tree since 5.16; +- `services.pipewire.enable = true` with `alsa.enable`, `alsa.support32Bit`, + and `pulse.enable` (PulseAudio compat layer); +- `security.rtkit.enable = true` — realtime priority for audio threads; +- WirePlumber `monitor.alsa.rules` override: `device.profile = "pro-audio"` + and `api.alsa.use-acp = false` for the virtio-snd card; this is required + because the virtio-snd ALSA driver has no ACP entry and WirePlumber defaults + to `"Off"`, leaving no Sink or Source; +- `services.pulseaudio.enable = lib.mkForce false` — prevents PulseAudio + collision; +- diagnostic packages: `pipewire`, `wireplumber`, `alsa-utils`. + +### Guest `audio` group membership + +**Nix/compiler path** (static configuration): when `AudioBinding` resources are +declared in the Nix configuration, the compiler sets `spec.groups: ["audio"]` +on the corresponding guest `User` resources for each name in +`AudioBinding.spec.guestUsers`. system-core verifies group membership +(`User.status.groupMembershipVerified`) and sets `GroupsVerified: True` when +confirmed. + +**API-created AudioBinding** (runtime path): the controller checks +`User.status.conditions.GroupsVerified == True` for every name in +`spec.guestUsers` before starting the sidecar. If any User's audio group is +not confirmed, the controller sets `GuestUserAudioGroupMissing` condition and +phase becomes `Degraded`. The operator must update the User resource's +`spec.groups` to include `"audio"` and rebuild the guest to resolve this. The +controller does **not** mutate `User.spec.groups` at runtime. + +### GuestAudioAgent and AudioSet service + +Guest-side enforcement is performed by the **`GuestAudioAgent`** — a Process +resource running inside the Guest under the guest workload user's UID. It is +part of the audio-pipewire Provider's guest component set. + +The `GuestAudioAgent`: +1. Opens a PipeWire connection in the Guest's compositor session (same-UID + user domain, natural session access — no socket path or ambient ACL needed). +2. Exposes a typed `AudioSet` ComponentSession service over vsock (Guest→Zone + d2b-bus transport). +3. When the `audio-binding-controller` calls `AudioSet(mic, speaker, speakerLevel, + micGain)`, the GuestAudioAgent applies changes directly via libpipewire API + (`pw_node_set_param` with `SPA_PARAM_Props`, `pw_stream_set_control`) on the + guest virtio-snd PipeWire node. + +**No wpctl binary, command path, or external process** is involved on either +the host or the guest side. The baseline `d2b.guestControl.wpctlPath` option is +superseded and removed from v3. The baseline guestd `AudioSet` RPC is superseded +by this typed ComponentSession service. + +#### GuestAudioAgent Process resources (one per guestUsers entry) + +The controller creates one `GuestAudioAgent` Process per entry in +`AudioBinding.spec.guestUsers`. Each Process: +- Is named using an opaque UID digest derived from `AudioBinding.metadata.uid` + and `User/.metadata.uid`, e.g. `ag-4a7f2c1b` — never by username. +- Has `ownerRef: audio.d2bus.org.AudioBinding/corp-vm-audio` and is located in the controller's + component identity index for that `AudioBinding`; never selected by mutable + label selector. +- Has `userRef` pointing to the corresponding `User/` resource in the + Zone (the guest workload user's User resource). +- Opens a PipeWire connection in that user's guest session (same-UID). +- Serves the `AudioSet` ComponentSession service for that user's audio context. + +The controller calls all active GuestAudioAgent instances for each grant change +and aggregates failures: if all agents succeed, `liveEnforced: true`; if any +fail, `GuestEnforcementFailed` condition is set per-agent; if all fail, +`enforcementPosture: HostOnly`. + +Representative Process resource (for one guestUser, `User/alice`): + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: ag-4a7f2c1b + zone: dev + ownerRef: audio.d2bus.org.AudioBinding/corp-vm-audio +spec: + providerRef: Provider/system-systemd + executionRef: Guest/corp-vm + domain: user + userRef: User/alice + processClass: service + template: guest-audio-agent + sandbox: + namespaceClasses: [] + capabilityClasses: [] + seccompClass: guest-audio-agent + noNewPrivileges: true + startRoot: false + readOnlyRoot: true + environmentClass: provider-defined + budget: + cpu: + limit: "250m" + request: null + memory: + limit: "32Mi" + request: null + pids: + limit: 16 + fds: + limit: 64 + restartPolicy: + class: on-failure + backoffBase: "2s" + backoffMax: "30s" + backoffMultiplier: 2.0 + maxRestarts: null + resetAfter: "300s" + readiness: + initialDelay: "0s" + timeout: "10s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined +``` + +Like all Process resources, this spec contains no `executableRef`, `argv`, or +`env` — these are signed component-template projections in the LaunchTicket. + +## Endpoint resources (D092) + +`Provider/audio-pipewire` declares standard `Endpoint` base-schema conformance. +Owner/projection AudioService implementation routes and every AudioBinding's +vhost-user and GuestAudioAgent services are owned `Endpoint` resources with +`producerRef`; they are not inline `Process.spec` fields. The AudioService +controller creates Service-owned implementation/route Endpoints; the +AudioBinding controller creates Binding-owned private vhost-user and guest-agent +Endpoints. Consumers use `Endpoint/` references. No raw socket path, +PipeWire node path, CID, port, fd number, credential, level value, or content +byte appears in Endpoint spec, Endpoint status, CLI output, audit, or telemetry. +Resolution occurs only through an authorized EffectPort/LaunchTicket; +unauthorized resolution returns `endpoint-resolve-denied`. Producer restarts +bump `Endpoint.status.endpointGeneration`, causing consumers to receive a +`dependency-changed` trigger. + +Representative owned Endpoint resources: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: corp-vm-audio-vhost-user + zone: dev + ownerRef: audio.d2bus.org.AudioBinding/corp-vm-audio +spec: + providerRef: Provider/audio-pipewire + producerRef: Process/corp-vm-audio-sidecar + endpointClass: data + transport: unix + purpose: audio-pipewire.d2bus.org/vhost-user-sound + serviceFingerprint: audio-pipewire.d2bus.org/vhost-user-sound.v3 + locality: host-local + visibility: zone + attachmentPolicy: launch-ticket + consumerPolicy: + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: audio-pipewire-authority + zone: host + ownerRef: audio.d2bus.org.AudioService/host-audio +spec: + providerRef: Provider/audio-pipewire + producerRef: Process/audio-pipewire-mediator + endpointClass: service + transport: unix + purpose: audio-pipewire.d2bus.org/audio-control + serviceFingerprint: audio-pipewire.d2bus.org/AudioMediator.v3 + locality: host-local + visibility: zone + attachmentPolicy: component-session + consumerPolicy: + allowedSubjects: [User/alice] + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: host-audio-import-route + zone: work + ownerRef: audio.d2bus.org.AudioService/host-audio-projection +spec: + providerRef: Provider/audio-pipewire + producerRef: Process/audio-pipewire-controller + endpointClass: service + transport: unix + purpose: audio-pipewire.d2bus.org/import-route + serviceFingerprint: audio-pipewire.d2bus.org/AudioServiceRoute.v3 + locality: zone-local + visibility: zone + attachmentPolicy: component-session + consumerPolicy: + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +This projection-Service-owned local Endpoint is only the front door to the +import adapter; it is an ordinary Endpoint child, never an import projection. +Its producer binds a ComponentSession named stream internally; it contains no +remote Ref or locator and never opens PipeWire. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: ag-4a7f2c1b-audio-set + zone: dev + ownerRef: audio.d2bus.org.AudioBinding/corp-vm-audio +spec: + providerRef: Provider/audio-pipewire + producerRef: Process/ag-4a7f2c1b + endpointClass: service + transport: vsock + purpose: audio-pipewire.d2bus.org/guest-audio-set + serviceFingerprint: audio-pipewire.d2bus.org/AudioSet.v3 + locality: guest-local + visibility: zone + attachmentPolicy: component-session + consumerPolicy: + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +## Retained opaque handles + +- pidfds: kernel Process supervision handles; they are authority-bearing and not + durable resource identities. +- Per-connection/session handles: PipeWire core/node handles and + ComponentSession IDs are high-churn per session. +- Named streams: audio control streams, when present, carry operation payloads + and do not identify a stable endpoint. +- `OwnedTransport`: authenticated transport ownership remains an in-memory + ComponentSession capability. +- fd indexes: the PipeWire portal FD and inherited vhost-user descriptors are + LaunchTicket-local slots and stay opaque under D092. + +### Enforcement sequence + +For Guests with an active audio capability (advertised via `runtime-audio`), +the enforcement sequence for a grant/level change is: + +1. Controller resolves the AudioBinding's same-Zone `serviceRef` and requires + `ServiceReady`. +2. For an owner Service, it calls the Service's AudioMediator + `SetGrant`/`SetLevel` Endpoint via local d2b-bus. For a projection Service, + it calls the local projection route, which carries the operation over the + ResourceImport's bounded encrypted named stream to the remote owner + Service. Only the remote owner AudioMediator applies libpipewire changes; + the projection never opens PipeWire. The call returns `Ok` or a typed error. + (`AudioBinding.spec` is durable per-Guest intent; no state-file write is + required.) +3. Controller calls `AudioSet` service on every active `GuestAudioAgent` + Process (one per `guestUsers` entry, identified by ownerRef component + identity index) in parallel, via d2b-bus, vsock transport. Collects all + results. +4. Service and guest results are aggregated; `AudioBinding.status.channels`, + `enforcementPosture`, and `lastSetApplied` are updated in a single + `UpdateStatus` batch committed post-reconcile. +5. Audit event `audio-binding.grant-changed` is emitted after the status commit + is durable (post-commit audit). +6. If the Service call fails: `GrantEnforcementFailed` + condition is set; phase becomes `Degraded`. +7. If any `GuestAudioAgent.AudioSet` call fails: `GuestEnforcementFailed` + condition is set for that agent (keyed by opaque digest); if all fail, + `enforcementPosture` is set to `HostOnly`; phase becomes `Degraded`. + +## PipeWire stream mediation + +### No ambient socket exposure + +The vhost-user-sound worker **never** opens, connects to, or receives a +PipeWire socket path. No socket path, runtime directory path, or +`PIPEWIRE_RUNTIME_DIR` value appears in: + +- `AudioBinding.spec` or `AudioBinding.status`; +- `AudioService.spec` or `AudioService.status`; +- any resource API response; +- any broker configuration or operation; +- any OTEL attribute or audit record; +- any log message or error message. + +PipeWire session access is exclusively through the pre-opened connected FD +declared in the component descriptor and passed at launch time by the +AudioMediator (see AudioMediator section). No `SetSocketAcl`, `ChownSocket`, +or any broker socket-ACL operation is issued by the audio controller or by the +Process Provider on behalf of the audio worker. + +### WirePlumber stream rules (host Nix config) + +The following configuration is compiled into the host NixOS system by the +audio-pipewire Provider Nix module. It is not a resource spec field. + +**`services.pipewire.extraConfig.client."90-d2b"` (`client.conf.d/`):** + +The component template for `vhost-user-sound-worker` sets initial stream +properties (including `d2b.mic` and `d2b.speaker`) at connection time. The +WirePlumber stream rules in `client.conf.d/90-d2b` match on these initial +properties and apply `target.object = "-1"`, `node.dont-reconnect = true`, +`node.dont-fallback = true`, and `node.linger = true` when a direction is +`"off"` at stream creation. Live grant changes after connection are applied +by the AudioMediator via libpipewire API calls (not stream rule re-evaluation). + +When `grants.mic == "on"` and `captureAlias` is non-null, the AudioMediator +resolves the alias privately via libpipewire registry and routes the capture +stream via `pw_node_set_param`. The alias label remains only in +`Provider/audio-pipewire.spec.config`; it never enters stream rule text, node +property values, AudioService/AudioBinding base spec/status, audit, or +telemetry. + +**Placement invariant**: stream rules belong in `client.conf.d/` (PipeWire +client configuration), not in `wireplumber.conf.d/`. WirePlumber's +`stream.rules` section governs state restoration only and does not update +live node properties at creation time. Placing the rule in the wrong file +causes WirePlumber to incorrectly match ALSA hardware devices and removes host +audio sinks. This invariant is load-bearing; see baseline `host.nix` comments +for the full failure analysis. + +**`services.pipewire.wireplumber.extraConfig."91-d2b-virtio-snd"` (guest Nix config):** + +In-guest WirePlumber forces `pro-audio` profile and disables ACP for the +virtio-snd ALSA card. Guest module concern; not a host rule. + +### Stream rule update on grant change + +When `AudioBinding.spec.grants` changes, the controller: +1. Calls the referenced AudioService for the changed channels. An owner + Service dispatches to its local AudioMediator; a projection Service routes + the request to the remote owner over its encrypted named stream. Only the + owner AudioMediator applies libpipewire changes. No Process restart or + UpdateSpec is needed for grant changes. `AudioBinding.spec` is durable + per-Guest intent; no state file is written. Speaker changes return + `Applied`; `mic: "on"` returns `Applied`, `Queued`, or `MicQueueFull` under + the exclusive arbitration contract, and `mic: "off"` idempotently releases + any active or pending request. +2. Calls `GuestAudioAgent.AudioSet` service (vsock transport) for the guest side. + For microphone, `Queued` or `MicQueueFull` keeps the guest input muted; + only `Applied` permits the guest-side `mic: "on"` call. +3. Updates `AudioBinding.status` in a single post-reconcile commit. + +## ProviderStateSet + +A **ProviderStateSet** is the set of all `Volume` resources in a Zone whose +`metadata.ownerRef` resolves to `Provider/audio-pipewire`. It is a query-time +grouping of ordinary Volume resources, not a ResourceType or stored artifact: + +```text +ProviderStateSet(zone, "audio-pipewire") = + { v : Volume | v.metadata.zone == zone + && v.metadata.ownerRef == "Provider/audio-pipewire" } +``` + +Under D087, `Provider/audio-pipewire` declares **no Provider state Volume**. Its +ProviderStateSet is therefore empty: + +```text +ProviderStateSet(zone, "audio-pipewire") = {} +``` + +The audio components fail the storage-need test for a durable Provider state +Volume: their operational state is bounded, non-secret, and derivable from +`AudioService`/`AudioBinding` spec and status, component `Process.status`, the +core Operation ledger, ResourceImport lease state, and external +PipeWire/guest observation after restart. The two provider-neutral qualified +ResourceTypes are the audio-domain resource model implemented by this +Provider; neither is a Provider state Volume. + +No component declares a state namespace, state-layout `User/` principal, +identity marker, migration worker, or Provider state mount. The +`audio-binding-controller` reconcile loop does not create, update, or delete +Volumes of any kind. The mediator's PipeWire portal FD, worker LaunchTicket +configuration, and GuestAudioAgent interactions are runtime operational +carriage, not Provider state Volumes. + +Status is observation only. It is revisioned, optimistic-status-writer +controlled, RBAC-readable, redacted, bounded to the global/provider-detail +limits, written only on material change, and re-verified against external +PipeWire and guest-agent reality after restart. It never contains secrets, +tokens, socket paths, argv/env, paths, PIDs, unit names, private PipeWire object +dumps, terminal/clipboard/notification/audio bytes, authority-conferring +handles, large blobs, or unbounded collections; oversize status is rejected +with `status-oversize`. + +There is no bootstrap state-Volume mechanism; the previous bootstrap exception +(D086, superseded by D087) does not apply. + +**Baseline migration note**: the one-time v1/v2 legacy `audio-state.json` migration (if +a legacy file is found on the host during Provider installation) reads it with +`parse_audio_state`, requires the explicitly configured same-Zone owner +AudioService, writes the parsed grants plus that `serviceRef` to AudioBinding via +the resource API, and removes the legacy file. Missing or ambiguous Service +selection fails closed. This runs before any component Process enters Ready; it +is not a Volume lifecycle contract and uses no ProviderStateSet Volume. + +## Identities, principals, and OS accounts + +### Process principals + +| Principal kind | Source | Used for | +| --- | --- | --- | +| Worker core Process principal | Bounded pool allocated by `Provider/system-minijail` per Provider | vhost-user-sound worker Process; sealed in LaunchTicket; not a controller-created `User` resource | +| AudioMediator compositor user | `User/` pre-existing host user | AudioMediator user-session Process; observed from system-core | +| GuestAudioAgent guest user | `User/` per declared guestUser ResourceRef | GuestAudioAgent user-session Process inside the Guest; observed from system-core | + +**Worker principal model**: the audio controller creates no `User` resources +for worker execution identity. The dedicated worker principal is a core Process +principal from the bounded pool allocated by `Provider/system-minijail` for the +`audio-pipewire` Provider. This principal is sealed in the LaunchTicket and +never appears in the live Process resource spec or any public spec field. + +**Guest `User` refs are system-core observed**: the `guestUsers` list contains +`User/` ResourceRefs pointing to guest workload users that already exist +as system-core resources. The audio controller never creates, updates, or +deletes guest `User` resources. + +**AudioMediator compositor user**: the AudioMediator's `userRef` points to the +pre-existing `User/` resource managed by system-core. The +audio controller observes its status but does not own it. + +### Guest `audio` group membership + +**Nix/compiler path** (static configuration): when `AudioBinding` resources are +declared in the Nix configuration, the compiler sets `spec.groups: ["audio"]` +on the corresponding guest `User` resources for each ResourceRef in +`AudioBinding.spec.guestUsers`. system-core verifies group membership +(`User.status.groupMembershipVerified`) and sets `GroupsVerified: True` when +confirmed. + +**API-created AudioBinding** (runtime path): the controller checks +`User.status.conditions.GroupsVerified == True` for every ref in +`spec.guestUsers` before starting the sidecar. If any User's audio group is +not confirmed, the controller sets `GuestUserAudioGroupMissing` condition and +phase becomes `Degraded`. The operator must update the User resource's +`spec.groups` to include `"audio"` and rebuild the guest to resolve this. The +controller does **not** mutate `User.spec.groups` at runtime. + +The worker core Process principal: +- has no login shell, no home directory, and no supplementary groups; +- is never a member of the `kvm`, `render`, `video`, or `audio` host groups; +- has no ACL entry on any PipeWire socket, runtime directory, or compositor + socket path; PipeWire access is exclusively through the FD from the + ProviderSupervisor-routed attachment transfer. + +## RBAC and authorization + +### Roles + +| Role name | Verbs | ResourceTypes | Notes | +| --- | --- | --- | --- | +| `audio-pipewire:view-status` | `get`, `list`, `watch` | `audio.d2bus.org.AudioService`, `audio.d2bus.org.AudioBinding` selected by `Provider/audio-pipewire` | Read-only Service and Binding status viewer | +| `audio-pipewire:manage-grants` | `get`, `list`, `watch`, `updateSpec` | `audio.d2bus.org.AudioBinding` selected by `Provider/audio-pipewire` | May update `spec.grants` and `spec.guestUsers` only; intended CLI role | +| `audio-pipewire:admin` | `get`, `list`, `watch`, `create`, `updateSpec`, `delete` | owner `audio.d2bus.org.AudioService`; `audio.d2bus.org.AudioBinding`; both selected by `Provider/audio-pipewire` | Full owner-Service and Binding lifecycle; cannot create/mutate projection Services | +| `audio-pipewire:controller` | `get`, `list`, `watch`, `updateStatus`, `updateFinalizers`; child lifecycle verbs | `audio.d2bus.org.AudioService`, `audio.d2bus.org.AudioBinding` with `spec.providerRef: Provider/audio-pipewire`, plus owned `Process` and `Endpoint` children | Provider controller identity only; cannot create/delete projection AudioService or mutate a resource selected for another Provider | +| `system-core:resource-import-controller` | `get`, `create`, `delete` | projection `audio.d2bus.org.AudioService` only | Creates/deletes the exact local projection named by ResourceImport and preserves its selected Provider; cannot create AudioBinding or owner Service, update grants, or claim backing authority | + +### Spec field authorization + +| Field | Required role | +| --- | --- | +| `AudioService.serviceRole=owner`, `implementationEndpointRefs`, `operations`, `authority` | `audio-pipewire:admin`; immutable role/authority identity after creation | +| `AudioService.serviceRole=projection` and projection fields | core ResourceImport controller only; operator and Provider controller mutation denied | +| `AudioBinding.serviceRef` | `audio-pipewire:admin`; immutable after creation and same-Zone only | +| `grants.*` | `audio-pipewire:manage-grants` or higher | +| `guestUsers` | `audio-pipewire:admin` | +| `suspendOnGuestAbsent` | `audio-pipewire:admin` | +| `providerRef` | immutable after creation; must name a Provider advertising the neutral type's signed base fingerprint; resource API admission rejects any mutation | + +### Broker operations requested + +The `audio-binding-controller` requests **zero** broker operations. It +communicates exclusively through the resource API (create/update/delete +owned `Process`/private `Endpoint` resources and read their status). The +AudioService handler likewise uses resource/ComponentSession APIs for its +local Endpoints and authority/route observations. The Process Provider handles all +broker-mediated process lifecycle effects. No `SetSocketAcl`, `ChownSocket`, +`SpawnRunner`, `OpenPidfd`, `StoreViewFarm`, `SwtpmDir`, or +`UsbipBindFirewallRule` operation is requested by the audio-pipewire Provider. + +## Security invariants + +The following invariants are load-bearing. Any change requires corresponding +test coverage and is subject to panel review. + +1. **Zero host capabilities.** The vhost-user-sound worker Process + `spec.sandbox.capabilityClasses` is always `[]` and `startRoot` is always + `false`. The worker runs in the system domain (`domain: system`) under a + core Process principal sealed in the LaunchTicket; no `userRef` is set in + the live Process resource spec and no controller-created `User` resource is + needed. The Process Provider rejects any LaunchTicket derived from this + template that carries a non-empty capability class or `startRoot: true`. + Tests: `minijail_contract.rs::audio_rendered_capabilities_empty`, + `audio_source_startRoot_false`, `audio_worker_domain_system_no_userref`, + `audio_worker_no_controller_created_user`. + +2. **No ambient PipeWire socket exposure.** No PipeWire socket path, runtime + directory path, or `PIPEWIRE_RUNTIME_DIR` value appears in + `AudioService.spec`, `AudioService.status`, `AudioBinding.spec`, + `AudioBinding.status`, any Process/Endpoint resource spec, any API response, + OTEL attribute, audit record, log message, or broker configuration. Tests: + Service, Binding, Process, and Endpoint schema round-trip tests assert the + absence of any socket-path-shaped string in every serialized form. + +3. **No SetSocketAcl or ambient ACL grant.** No broker `SetSocketAcl` or + equivalent socket-ACL operation is issued. The AudioMediator's PipeWire + access is through a declared pre-opened portal FD from the user supervisor, + not an ambient socket ACL. Tests: broker-op policy test asserts the + audio-pipewire Provider's allowed broker op set is empty. + +4. **No executableRef/argv/env in live Process spec.** The live Process + resource spec (stored in the Zone store) contains no `executableRef`, `argv`, + or `env`. These are signed component-template projections. Tests: + `minijail_contract.rs::audio_process_spec_no_argv_env_executableref`. + +5. **Per-Guest binary copy argv[0] enforcement in template.** The component + template for `vhost-user-sound-worker` enforces the per-Guest binary copy + path as argv[0]. The LaunchTicket verifier enforces the path shape; no Nix + store path, symlink, or cross-guest copy is accepted. Tests: `argv.rs` + rejection matrix (Nix store path, current-system symlink, wrong-guest copy, + empty binary, empty VM name). + +6. **No direct process lifecycle.** The AudioBinding controller creates and + updates owned Process and private Endpoint resource specs only. It never calls `SpawnRunner`, + `OpenPidfd`, issues a LaunchTicket, or adopts a pidfd. It creates no + `Volume` or `User` resources. Tests: controller conformance test asserts the + audio-binding-controller's allowed resource API verb set. + +7. **No per-workload systemd unit.** There is no `d2b--snd.service` + systemd unit, no `systemctl start` call in the controller, and no direct + `spawn()`. Tests: workspace policy gate `no-systemd-unit-for-audio.sh`. + +8. **AudioMediator does not export PipeWire node IDs.** The AudioMediator's + internal node registry never appears in any resource spec, status, d2b-bus + message, audit record, or OTEL attribute. Tests: + `mediator.rs::node_id_not_in_any_bus_message_or_spec`. + +9. **No wpctl EphemeralProcess.** No `EphemeralProcess` with `purpose` + containing `wpctl` or any external command binary is created by the audio + Provider. Grant enforcement is exclusively through the AudioMediator's + `SetGrant`/`SetLevel` service using libpipewire. Tests: + `audio_binding_controller.rs::no_ephemeral_process_created`. + +10. **No path leakage in audit or OTEL.** The audio controller's audit emitter + never includes socket paths, PipeWire paths, or compositor runtime directory + paths. Tests: `audio_telemetry.rs` redaction conformance. + +11. **Controller creates no Volume or User resources.** The + `audio-binding-controller` is not permitted to issue any verb against `User` + or `Volume` ResourceTypes; both are absent from `allowedResourceVerbs`. + Under D087, `Provider/audio-pipewire` declares no Provider state Volume and + its ProviderStateSet is empty. The semantic controller is not a Volume owner, + does not export `Volume` as a ResourceType, and does not create prerequisite + Volumes. + Tests: controller conformance test verifies the absence of User and Volume + verbs; ProviderDeployment integration validates that no state Volume or + state mount is created and that bounded operational state is status-first. + +12. **Service/Binding separation and canonical core-owned projections.** Only + `audio.d2bus.org.AudioService` may carry the audio D097 authority or be + named by ResourceExport `resourceRef`/`serviceType` and ResourceImport + `expectedServiceType`. Export and Import projection-schema/factory + fingerprints must exactly match the signed factory. Every Endpoint remains + Service-owned and no Export field names one. + AudioBinding always has `ownerRef: Guest/` plus a same-Zone + `serviceRef`; it is never exported or generated by the import controller. + Core creates/deletes only projection AudioService resources, and the + Provider rejects operator-created projections. Tests: + `service_binding_separation`, `core_projection_only_audio_service`, and + `audio_binding_never_exportable`, plus + `canonical_export_import_fields`. + +13. **Projection ownerRef chain and no local PipeWire open.** Every projection + AudioService has `ownerRef: ResourceImport/`, no AuthorityDescriptor, + and only same-Zone route Endpoint refs. Its implementation can bind only + the import's encrypted named streams and is denied the AudioMediator portal + attachment/PipeWire-open capability. Tests: + `projection_ownerref_chain`, `projection_forbids_authority`, and + `projection_never_opens_pipewire`. + +14. **Provider-neutral type identity and field isolation.** Audio resources use + only `audio.d2bus.org.AudioService` and + `audio.d2bus.org.AudioBinding`. Provider-qualified Service/Binding names and + every AudioState name are unknown with no API/schema/serde/Nix alias. + `Provider/audio-pipewire` reconciles only resources whose immutable + `spec.providerRef` selects it. PipeWire-specific desired fields occur only + in its strict `spec.provider` or Provider config, and PipeWire-specific + observations occur only in strict `status.provider`; neutral base fields + reject them. Tests: `provider_neutral_type_registration`, + `resource_type_name_clean_break`, `foreign_provider_ignored`, and + `provider_field_isolation`. + +15. **Exclusive microphone capture and defined fair queue.** Initial v3 permits + exactly one active microphone capture stream per owner AudioService across + local and imported consumers. The owner-authenticated Zone is the fairness + principal; requests use owner-sequenced per-Zone FIFO subqueues and + round-robin Zone selection, bounded to 16 pending requests per Zone and 64 + total, with one entry per opaque consumer handle. A contended 30-second + lease cannot renew; the old stream is muted/disconnected and re-enters its + Zone's FIFO tail if intent remains on before the next Zone activates. + Speaker streams remain multiplexed/mixed. + `microphone: multiplexed`, consent/approval fields, priority overrides, and + concurrent-capture verbs fail closed. No concurrent capture may be added + until a future normative spec defines its concrete consent authorization + ResourceType and resource-API verb. Tests: + `mic_exclusive_across_zones`, `mic_zone_round_robin_fifo`, + `mic_queue_bounds`, `mic_contended_lease_handoff`, + `mic_no_overlap`, and `mic_consent_surface_rejected`. + +## Lifecycle, restart, and adoption + +### Install sequence + +1. Operator creates `Provider/audio-pipewire` resource with `spec.artifactId`. +2. Core ProviderDeployment creates the `audio-binding-controller` Process (system + domain) and the `audio-mediator` Process (user domain) as static components; + no Provider state Volume or state mount is created. +3. The controller Process registers the provider-neutral AudioService and + AudioBinding watch plans constrained to + `spec.providerRef: Provider/audio-pipewire`. An owner Zone creates its one + real owner AudioService; consumer Zones receive projection AudioServices + only from the core ResourceImport controller. +4. `audio.d2bus.org.AudioBinding` resources created by Nix or the API + resolve their required same-Zone AudioService and become Ready through the + reconcile loop. + +### Service lifecycle and import projection + +1. An operator/Nix creates the owner AudioService with its D097 descriptor and + local implementation Endpoint refs. The AudioService controller claims the + authority index entry, verifies ownerProof, and reconciles only local + AudioMediator/PipeWire semantics. +2. A ResourceExport in that owner Zone references only the owner AudioService + and carries the canonical Service type plus signed projection-schema/factory + fingerprints. Its local authority Endpoint remains owned by the Service and + is never an Export field. AudioBinding is not exported. +3. In a consumer Zone, a ResourceImport matches that exported AudioService. + The **core import controller** creates exactly one projection AudioService + named by `projectionName`, with + `ownerRef: ResourceImport/`. It does not create AudioBinding, Process, + Endpoint, worker, GuestAudioAgent, or PipeWire state. +4. The AudioService controller observes the ResourceImport and reconciles the + projection's local route Endpoint and encrypted named-stream binding. It + never opens PipeWire in the consumer Zone. +5. On import deletion/revocation, core first drives referencing AudioBindings + degraded, cancels every active or queued microphone request from that + consumer Zone, and waits for child cleanup; it then releases the remote + lease, deletes only the projection AudioService, and clears the import + finalizer. + +### Per-Guest enable sequence + +1. Operator (or Nix compilation) creates + `audio.d2bus.org.AudioBinding/corp-vm-audio` with + `ownerRef: Guest/corp-vm` and a same-Zone `spec.serviceRef`. +2. `audio-binding-controller` resolves that AudioService and requires + `ServiceReady`. An absent/degraded/revoked Service sets + `ServiceUnavailable`; no worker opens PipeWire as fallback. +3. `audio-binding-controller` queries the `runtime-audio` dependency alias for + the Guest's runtime capability. If no audio capability is advertised, sets + `RuntimeCapabilityUnavailable`; Pending. +4. For an owner Service, controller checks its AudioMediator Endpoint; for a + projection Service, it checks the local import-route Endpoint. The + projection path has no PipeWire portal FD. +5. Controller checks each `spec.guestUsers` User ref status for + `GroupsVerified: True` (audio group membership). If any fails, sets + `GuestUserAudioGroupMissing`; Degraded. +6. Determine sidecar desired state: if both grants are `"off"` or no runtime + capability → desired: stopped. Else → desired: running. +7. Controller creates `Process/corp-vm-audio-sidecar`, its private vhost-user + Endpoint, each GuestAudioAgent Process, and each private guest-agent + Endpoint. For an owner Service the template includes the operation-scoped + AudioMediator attachment; for a projection it includes only the same-Zone + route attachment. +8. On an owner path, d2b-bus/ProviderSupervisor resolves the AudioMediator + portal FD and routes it to the worker's LaunchTicket. On a projection path, + no PipeWire FD exists or is transferred. +9. System Process Provider (system-minijail) launches the worker. + The vhost-user endpoint becomes ready; Process Provider sets + `Process.status.phase = Ready`. +10. Controller watches `Process.status`; on `Ready`, sets + `RealizationReady: True`. +11. Controller mutates `Guest.spec.audioExtension` with the runtime-capability- + derived arguments. +12. On next Guest start, the runtime attaches the selected audio capability; + `ConsumerAttached` becomes `True`. +13. Controller applies speaker intent immediately and submits microphone + intent to the owner Service. `mic: "off"` is inactive; an admitted + `mic: "on"` becomes `MicCaptureActive`; a fair-queue admission becomes + `MicQueued`/`Pending`; a saturated queue becomes + `MicQueueFull`/`Degraded`. +14. `AudioBinding.status.phase` transitions to `Ready` only when speaker intent + is enforced and microphone intent is either off or owns the exclusive slot. + +### Restart and adoption + +A Zone runtime restart is a continuation event (ADR 0034). The audio +controller does not hold pidfds; it observes Process/Endpoint status through +the resource API. The Process Provider re-adopts worker identity. The +AudioService handler revalidates the D097 ownerProof or the projection's import +lease/generation without opening a second backing; the AudioBinding handler then +reconverges status from its serviceRef and children. + +The owner mediator fails closed across restart: every reconstructed capture +path starts muted, no prior active slot is assumed, and it never adopts two +active captures. Local and projection controllers resubmit committed +`mic: "on"` intent; the owner assigns fresh owner-observed enqueue sequence +numbers and resumes the same bounded per-Zone round-robin/FIFO policy. Queue +state is not a durable authority record, and no requester bypasses the queue +because of restart. + +If the worker exited between restarts (Process.status.phase Failed or +Unknown), the controller detects this on its first reconcile post-restart +and sets `AudioBinding.status.phase = Degraded`. The audio controller does +not issue any restart signal. + +`suspendOnGuestAbsent: true`: when the Guest transitions out of `Running`, +the controller issues a `Process UpdateSpec` setting the desired phase to +`Stopped`. The system Process Provider performs the graceful stop. The +controller sets `GuestAbsent` condition; phase becomes `Degraded`. When +the Guest becomes `Running` again, the full enable sequence from step 6 +repeats. + +### Deletion sequence + +1. `deletionRequestedAt` is set on `AudioBinding`. +2. `audio-binding-controller` finalizer handler: + a. Idempotently releases any active or pending microphone request through + the referenced AudioService. + b. Issues `Process.spec` mutation setting desired phase to `Stopped`. + c. Waits for `Process.status.phase` to reach a terminal phase (system + Process Provider performs graceful stop: SIGTERM → 10s → SIGKILL). + d. Issues owned private `Endpoint` and `Process` Deletes via resource API. + e. Removes `--generic-vhost-user` (or equivalent) from `Guest.spec.audioExtension`. + f. Removes `audio-pipewire.d2bus.org/sidecar-stopped` finalizer. +3. After all finalizers are removed: resource is deleted from the store; + a single `phase=Deleted` revision event is committed to the revision log. +4. Audit event `audio-binding.deleted` is emitted **post-commit** after the + revision event is durable. No audit event is emitted inline with the + finalizer steps. + +## Errors + +### Outcome code enum + +| Code | Phase | Retryable | Meaning | +| --- | --- | --- | --- | +| `ok` | `Ready` | false | All conditions satisfied | +| `ServiceNotReady` | `Pending` | true | Referenced owner/projection AudioService not yet Ready | +| `ServiceUnavailable` | `Degraded` | true | Referenced AudioService absent, degraded, revoked, stale, or cross-Zone | +| `AuthorityConflict` | `Failed` | false | Owner Service D097 authority claim conflicts with an incumbent | +| `EndpointNotReady` | `Pending` | true | Required same-Zone Service or Binding-owned private Endpoint not Ready | +| `ImportNotBound` | `Pending` | true | Projection Service's owning ResourceImport has no current lease | +| `ImportRevoked` | `Degraded` | true | Projection Service's import/export lease was revoked | +| `ProjectionLocalBackingDenied` | `Failed` | false | Projection attempted to open a forbidden local backing; PipeWire detail, if any, is provider status only | +| `RealizationNotReady` | `Pending` | true | Selected Provider's owned realization is not yet Ready | +| `GuestAbsent` | `Degraded` | true | Guest not Running; owned realization suspended | +| `GrantEnforcementFailed` | `Degraded` | true | Selected AudioService implementation returned an enforcement error | +| `MicCaptureQueued` | `Pending` | true | Microphone intent is admitted to the owner Service's bounded fair queue | +| `MicQueueFull` | `Degraded` | true | The fixed total or authenticated per-Zone pending-request bound is full; no queue entry was created | +| `ProviderSessionUnavailable` | `Degraded` | true | Selected Provider cannot access its local audio session; implementation detail is in `status.provider`; retries | +| `ConsumerEnforcementUnavailable` | `Degraded` | true | Selected Provider's consumer-side enforcement is not Ready; retries | +| `GuestEnforcementFailed` | `Degraded` | false | Selected Provider's consumer-side grant enforcement returned an error | +| `GuestUserAudioGroupMissing` | `Degraded` | false | One or more `guestUsers` lack confirmed `audio` group; operator must set `User.spec.groups` and rebuild guest | +| `RuntimeCapabilityUnavailable` | `Pending` | true | `runtime-audio` alias returned no audio capability; retries when runtime updates | +| `RealizationCrashLoop` | `Failed` | false | An owned realization Process exceeded its restart ceiling | +| `ProviderMisconfigured` | `Failed` | false | Missing artifact or malformed capability record | +| `AudioNotEnabled` | `Failed` | false | `AudioBinding` exists but owning Guest has no runtime audio capability | + +Error messages are bounded to 256 characters. They must not contain socket +paths, lock paths, state-file paths, PipeWire paths, compositor runtime +directory paths, PipeWire node IDs, credential digests, or volume/gain level +values. + +## Audit events + +All audio controller audit events use the Zone authoritative audit path. +Audit records are JSONL with V3 payload/checksum. Audit events are emitted +**post-commit** after the relevant revision is durable in the Zone store. + +No audio-specific audit event includes socket paths, volume levels, gain +values, PipeWire node IDs, PipeWire runtime directory paths, microphone queue +positions, opaque consumer handles, or consumer Zone identities. + +| Event kind | Trigger | Redacted | +| --- | --- | --- | +| `audio-service.created` | Owner or core-generated projection AudioService committed | role and operation set only; no authority key, endpoint locator, import key, or remote identity | +| `audio-service.authority-claimed` | Owner Service D097 claim committed | authority class and outcome only; no opaque key value or PipeWire identity | +| `audio-service.projection-bound` | Projection Service observes a current ResourceImport lease | outcome only; no ZoneLink/export key, session generation, or stream identifier | +| `audio-service.degraded` | Owner authority or projection route becomes unavailable/revoked | closed reason only | +| `audio-service.mic-arbitration` | Microphone request queued, activated, released, expired, or rejected at a bound | closed action/outcome and aggregate queue counts only; no Binding, Guest, Zone, handle, position, or client timestamp | +| `audio-binding.created` | `AudioBinding` resource committed to store | `spec.grants.*` direction values included; no paths; no levels | +| `audio-binding.grant-changed` | `spec.grants` `UpdateSpec` committed and durable | direction (`"on"`/`"off"`) changes included; `speakerLevel`/`micGain` values **omitted**; no node IDs | +| `audio-binding.enforcement-applied` | Referenced AudioService call returned `Applied` | result: `Applied\|Degraded` per channel; no route, node ID, or level value | +| `audio-binding.realization-ready` | All required owned realization resources become Ready | no socket path or provider-only status detail | +| `audio-binding.realization-stopped` | Owned realization resources reach terminal phases | includes a bounded aggregate outcome only; no socket path or implementation identity | +| `audio-binding.deleted` | post-commit after `phase=Deleted` revision event is durable | — | + +**Suppressed from all audit records:** +- vhost-user socket path; +- PipeWire runtime directory path; +- AudioMediator FD attachment details; +- initial stream property values; +- volume/gain levels (`speakerLevel`, `micGain`); +- PipeWire node IDs or object paths; +- GuestAudioAgent AudioSet service payload bytes; +- compositor user session details; +- microphone queue entries, positions, client timestamps, opaque handles, and + consumer Zone identities. + +`subject_digest` is always the authenticated Zone subject digest (`sha256:`). +ProcessEffect audit records (process launch, signal, exit) are emitted by +the Process Provider, not by this Provider. + +## OTEL telemetry + +### Metric labels + +All audio-pipewire metrics use only the following closed label set. + +| Label key | Allowed values | Notes | +| --- | --- | --- | +| `provider` | `audio-pipewire` | Always present | +| `channel` | `speaker\|mic` | For per-channel metrics | +| `outcome` | `ok\|degraded\|failed\|unknown` | Terminal state | +| `enforcement_posture` | `host-and-guest\|host-only\|guest-only\|none` | | +| `arbitration_state` | `inactive\|queued\|active\|blocked` | Microphone arbitration metrics only | + +Guest name, Zone name, socket path, level values, PipeWire node IDs, and +runtime capability implementation IDs, opaque consumer handles, and queue +positions are not metric labels. + +### Metrics + +| Metric name | Type | Description | +| --- | --- | --- | +| `d2b_audio_pipewire_services_total` | gauge | Current count of owner/projection AudioServices by `outcome` | +| `d2b_audio_pipewire_bindings_total` | gauge | Current count of per-Guest AudioBinding resources by `outcome` | +| `d2b_audio_pipewire_sidecars_running` | gauge | Count of worker Processes in Ready phase | +| `d2b_audio_pipewire_enforcement_attempts_total` | counter | AudioService enforcement attempts by `channel`, `outcome` | +| `d2b_audio_pipewire_enforcement_latency_seconds` | histogram | AudioService call completion latency by `channel` | +| `d2b_audio_pipewire_sidecar_restarts_total` | counter | Worker Process restart count (observed from Process.status) | +| `d2b_audio_pipewire_grant_changes_total` | counter | `spec.grants` mutations by `channel` | +| `d2b_audio_pipewire_mediator_fd_handoffs_total` | counter | PipeWire FD handoffs completed by AudioMediator by `outcome` | +| `d2b_audio_pipewire_mic_active` | gauge | Owner-Service active microphone capture count; always `0` or `1` | +| `d2b_audio_pipewire_mic_queue_depth` | gauge | Aggregate bounded pending microphone request count; no Zone or consumer label | +| `d2b_audio_pipewire_mic_arbitration_total` | counter | Microphone arbitration transitions by `arbitration_state` and `outcome` | +| `d2b_audio_pipewire_telemetry_drop_total` | counter | Dropped telemetry frames when emitter ring is full | + +No level or gain value appears in any metric. `grant_changes_total` counts +direction transitions; it does not record the level value. + +### Span attributes + +Controller spans carry: +- `d2b.provider`: `audio-pipewire` +- `d2b.component`: `audio-binding-controller` or `audio-mediator` +- `d2b.resource.type`: `AudioService`, `AudioBinding`, `Process`, or `Endpoint` +- `d2b.resource.generation`: current `metadata.generation` +- `d2b.outcome`: outcome code + +Zone/resource identity is available only in bounded OTEL resource attributes +and permitted audit fields, never as a span attribute. +Spans must not carry socket paths, PipeWire paths, PipeWire node IDs, level +values, gain values, or guest workload usernames. + +## Async reconciliation + +### Controller descriptor + +```yaml +watchSelectors: + - resourceType: audio.d2bus.org.AudioService + specProviderRef: Provider/audio-pipewire + verbs: [spec, status, deletion] + - resourceType: ResourceImport + ownerOfType: audio.d2bus.org.AudioService + specProviderRef: Provider/audio-pipewire + verbs: [spec, status, deletion] + - resourceType: Endpoint + ownerRefType: audio.d2bus.org.AudioService + verbs: [status] + - resourceType: audio.d2bus.org.AudioBinding + specProviderRef: Provider/audio-pipewire + verbs: [spec, status, deletion] + - resourceType: Process + ownerRefType: audio.d2bus.org.AudioBinding + verbs: [status] + - resourceType: Endpoint + ownerRefType: audio.d2bus.org.AudioBinding + verbs: [status] + - resourceType: audio.d2bus.org.AudioService + specProviderRef: Provider/audio-pipewire + dependencyRefIn: AudioBinding.spec.serviceRef + verbs: [spec, status, deletion] + - resourceType: Guest + verbs: [status] + ownerTrigger: true + - resourceType: Process + componentIdentity: audio-mediator + verbs: [status] + - resourceType: User + dependencyRefIn: AudioBinding.spec.guestUsers + verbs: [status] +dependencySelectors: + - resourceType: ResourceImport + resolveFrom: AudioService.metadata.ownerRef + - resourceType: Endpoint + resolveFrom: AudioService.spec.implementationEndpointRefs + - resourceType: audio.d2bus.org.AudioService + specProviderRef: Provider/audio-pipewire + resolveFrom: AudioBinding.spec.serviceRef + - resourceType: Guest + resolveFrom: AudioBinding.metadata.ownerRef + - resourceType: User + resolveFrom: AudioBinding.spec.guestUsers +allowedResourceVerbs: + - { type: audio.d2bus.org.AudioService, specProviderRef: Provider/audio-pipewire, verbs: [get, list, watch, update-status, update-finalizers] } + - { type: audio.d2bus.org.AudioBinding, specProviderRef: Provider/audio-pipewire, verbs: [get, list, watch, update-status, update-finalizers] } + - { type: ResourceImport, verbs: [get, watch] } + - { type: Process, verbs: [get, list, watch, create, update-spec, delete] } + - { type: Endpoint, verbs: [get, list, watch, create, update-spec, delete] } + - { type: Guest, verbs: [get, watch, update-spec] } +reconcileConcurrency: 8 +maxPendingResources: 512 +observePolicy: on-status-change +resyncPeriod: "5m" +finalizers: + - audio-pipewire.d2bus.org/service-released + - audio-pipewire.d2bus.org/sidecar-stopped +deadlines: + reconcile: "30s" + finalize: "120s" + observe: "10s" +retryClasses: + - code: ServiceNotReady + backoff: exponential-bounded-30s + - code: ImportNotBound + backoff: exponential-bounded-30s + - code: EndpointNotReady + backoff: exponential-bounded-30s + - code: AuthorityConflict + policy: no-retry + - code: ProjectionLocalBackingDenied + policy: no-retry + - code: RealizationNotReady + backoff: exponential-bounded-30s + - code: ProviderSessionUnavailable + backoff: exponential-bounded-30s + - code: ConsumerEnforcementUnavailable + backoff: exponential-bounded-30s + - code: GrantEnforcementFailed + backoff: exponential-bounded-30s + - code: RealizationCrashLoop + policy: no-retry + - code: GuestUserAudioGroupMissing + policy: no-retry +``` + +`EphemeralProcess` is absent from `allowedResourceVerbs`. The controller +cannot create EphemeralProcess resources. `SpawnRunner`, `OpenPidfd`, and all +broker operations are also absent. `Volume` and `User` are absent. `Endpoint` +verbs are limited to Service-owned local implementation/route Endpoints and +AudioBinding-owned private endpoints. The Provider has no `create`/`delete` verb +for AudioService: operators/Nix own owner-Service lifecycle and the **core +ResourceImport controller** owns projection-Service creation/deletion. Core's +projection authority is constrained to +`serviceRole: projection`, `ownerRef: ResourceImport/` and cannot create +AudioBinding. The ProviderStateSet is empty under D087, so core +ProviderDeployment creates no Provider state Volume and the component consumes +no state view `dirfd`. + +The ResourceType names in this descriptor are neutral. The +`specProviderRef: Provider/audio-pipewire` selector and matching authorization +constraint are mandatory: this controller ignores resources selected for a +different conforming audio Provider and cannot update their status or +finalizers. + +### Reconcile flow (per `AudioService`) + +```text +reconcile(AudioService): + 1. Load Service spec/status; require spec.providerRef Provider/audio-pipewire + and classify immutable serviceRole. Ignore a resource selected for any + other conforming audio Provider. + 2. If owner: + a. Require ownerRef Provider/audio-pipewire and a valid D097 + AuthorityDescriptor. + b. Resolve only same-Zone implementationEndpointRefs. + c. Claim/revalidate the authority index by ownerProof; on duplicate, + set AuthorityConflict and perform no PipeWire open. + d. Observe the local AudioMediator/PipeWire backing and Endpoint status. + e. Reconcile the exclusive microphone slot and bounded aggregate + per-Zone fair-queue status; never persist queue entries. + 3. If projection: + a. Require ownerRef ResourceImport/; reject any authority field. + b. Resolve that same-Zone import and require bound/current generation, + expected AudioService type, and both signed projection-schema/factory + fingerprints. + c. Create/update only the Service-owned local route Endpoint and bind it + to the import's bounded encrypted named streams. + d. Deny any AudioMediator portal attachment or PipeWire open. + 4. Atomically write layered Service status and D091 currency. + 5. On delete, drain referencing AudioBindings and Service Endpoints, release + owner authority or projection route, then clear service-released. +``` + +### Reconcile flow (per `AudioBinding`) + +```text +reconcile(AudioBinding): + 1. Load AudioBinding spec/status from store (MVCC snapshot); require + spec.providerRef Provider/audio-pipewire and ignore another Provider's + conforming Binding. + 2. Resolve ownerRef Guest and same-Zone spec.serviceRef AudioService. + Reject ResourceImport refs and cross-Zone Services. If Service is not + Ready/current, set ServiceUnavailable and create no worker fallback. + 3. Check Guest.status.phase. + 4. If suspendOnGuestAbsent && Guest not Running: + → idempotently release any active/pending microphone request; + → issue Process.spec update (desired: stopped) if Process exists; + set GuestAbsent condition; Degraded. + 5. Query runtime-audio dependency alias for the Guest's AudioCapability. + If no audio capability advertised: set RuntimeCapabilityUnavailable; Pending; retry. + 6. For each ref in spec.guestUsers: verify User.status.groupMembershipVerified == true + for the "audio" group. If any not verified: set GuestUserAudioGroupMissing; + Degraded; no-retry (operator action required). + 7. Resolve Service backend: + owner → require local AudioMediator Endpoint; + projection → require local import-route Endpoint and no PipeWire FD. + 8. Determine sidecar desired state. + If mic grant is "off", idempotently release any active/pending microphone + request before evaluating Process state. + If both grants "off" → desired: stopped; remove Guest.spec.audioExtension. + Else → desired: running. + 9. If desired: running: + a. Create Process/-audio-sidecar if absent (CREATE resource API). + b. Create its private vhost-user Endpoint if absent. + c. For each ref in spec.guestUsers: + - Compute opaque digest of AudioBinding.metadata.uid + User..metadata.uid. + - Create Process/ag- if absent (CREATE resource API), with + ownerRef: audio.d2bus.org.AudioBinding/ and userRef: . + - Create its private AudioSet Endpoint if absent. + - Locate each existing GuestAudioAgent Process via ownerRef component + identity index, not label selector. + d. If sidecar Process/status or private Endpoint is not Ready: retry. + e. For each GuestAudioAgent Process/Endpoint: if not Ready, + set ConsumerEnforcementUnavailable (keyed by digest); Degraded; retry. + 10. If desired: stopped: + a. If sidecar Process exists and not terminal: issue Process UpdateSpec + (desired phase: stopped). Process Provider performs graceful stop. + b. Delete/stop private Endpoints and, for each GuestAudioAgent Process + (ownerRef identity index), if not terminal, + issue UpdateSpec (desired phase: stopped). + c. Set GuestAbsent/SidecarStopped; wait for terminal phase. + 11. Enforce grants (if Service, sidecar, and all agents/Endpoints are Ready): + a. Call the resolved AudioService for each changed channel. Owner + dispatches locally to AudioMediator; projection routes over the + import's encrypted named stream. Speaker receives Applied or a typed + error. Microphone receives Applied, Queued, MicQueueFull, or another + typed error. + b. Call AudioSet service on ALL GuestAudioAgent Processes in parallel + (d2b-bus, vsock transport). Collect all results. A microphone + Applied result permits guest `mic: "on"`; Queued or MicQueueFull + requires guest `mic: "off"` so a waiting consumer stays muted. + c. Aggregate results: if any agent fails, set GuestEnforcementFailed + (keyed by agent digest); if all fail, enforcementPosture = HostOnly. + For microphone, map Applied to MicCaptureActive/active, Queued to + MicQueued/queued/Pending, and MicQueueFull to + MicQueueFull/blocked/Degraded. Update status.channels and + enforcementPosture. + 12. Update Guest.spec.audioExtension if needed (derived from AudioCapability record). + 13. Commit UpdateStatus batch (post-reconcile; single commit). + 14. Emit post-commit audit event if grants changed. +``` + +All steps are asynchronous. The controller's reconcile work queues use stable +UID ordering and are distinct from the owner Service's microphone arbitration +queue. Steps for independent resources run concurrently, and an AudioService event +enqueues every AudioBinding indexed by `spec.serviceRef`. +Each task holds its own optimistic revision precondition; a conflict causes a +retry. + +## Audio authority and cross-Zone sharing (D096/D097) + +### Resource graphs and ownership + +The two provider-neutral qualified ResourceTypes have non-overlapping jobs. +The owner-Zone graph is: + +```text +Provider/audio-pipewire +├── Process/audio-pipewire-mediator (static implementation) +└── audio.d2bus.org.AudioService/host-audio (real authority) + ├── AuthorityDescriptor (D097 physical backing) + └── Endpoint/audio-pipewire-authority (same-Zone only) + +ResourceExport/host-audio +├── resourceRef -> audio.d2bus.org.AudioService/host-audio +├── serviceType -> audio.d2bus.org.AudioService +├── projectionSchemaFingerprint -> signed projection schema +└── factoryFingerprint -> signed semantic factory + +Guest/corp-vm +└── AudioBinding/corp-vm-audio + ├── serviceRef -> AudioService/host-audio + ├── Process/corp-vm-audio-sidecar + ├── Endpoint/corp-vm-audio-vhost-user (private) + ├── Process/ag- + └── Endpoint/ag--audio-set (private) +``` + +The consumer-Zone graph is: + +```text +ResourceImport/host-audio +└── AudioService/host-audio-projection (core-created projection) + ├── ownerRef -> ResourceImport/host-audio + └── Endpoint/host-audio-import-route (same-Zone route) + +Guest/work-vm +└── AudioBinding/work-vm-audio + ├── serviceRef -> AudioService/host-audio-projection + ├── Process/work-vm-audio-sidecar + └── private vhost-user/GuestAudioAgent Endpoints + +projection route == bounded encrypted named streams == owner Service +``` + +AudioBinding remains per Guest in both graphs. It is not exported, imported, or +core-generated. The only core-generated semantic projection is AudioService. + +### Owner authority Service (D097) + +Exactly one owner `audio.d2bus.org.AudioService` per owner Zone holds +the real PipeWire connection to the physical microphone and speakers and is the +single arbiter of every local or imported consumer. It is grounded in the +current host controller (`packages/d2bd/src/audio_host_controller.rs`: the +dyn-safe `HostAudioController` trait, +`PipeWireHostController::{from_audio_node,find_audio_node}`, with +`QemuAudioController`/`FakeHostController` as test doubles) and argv generator +(`packages/d2b-host/src/audio_argv.rs`: `AudioBackend`, `AudioArgvInput`, and +`generate_audio_argv`). + +The owner Service alone declares the D097 `AuthorityDescriptor` with +`authorityScope: physical-device`, opaque `authorityKey`, `cardinality: +zero-or-one`, and split arbitration: + +- **Speaker: `multiplexed`.** The authority runs one mixer. Every consumer + AudioBinding receives a mixed stream with its admitted per-Zone quota and + per-Guest level. No consumer opens the physical sink. +- **Microphone: `exclusive` only.** Exactly one local or imported consumer may + capture. Pending requests enter the fixed bounded queue: owner-sequenced FIFO + within each authenticated Zone, round-robin across non-empty Zones, at most + 16 requests per Zone and 64 total, one request per opaque consumer handle. + A contended 30-second lease cannot renew, and the old stream is muted and + disconnected before the next activates; if its intent remains on, it + re-enters its Zone FIFO tail. No priority or client timestamp affects order. + Concurrent/multiplexed capture and every consent/approval placeholder are + rejected until a future normative spec defines a concrete consent + authorization ResourceType and resource-API verb. + +Core's authority index rejects a second owner AudioService for the same +`(Zone, physical-device, opaqueKeyDigest)` with `duplicateConflict` before any +open. Restart adopts the exact Service by ownerProof; ambiguity quarantines. +AudioBinding carries no AuthorityDescriptor and cannot claim this index. + +### ResourceExport and ResourceImport (D096) + +The owner Zone exports the **owner AudioService**, not AudioBinding: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: ResourceExport +metadata: + name: host-audio + zone: host +spec: + providerRef: Provider/audio-pipewire + resourceRef: audio.d2bus.org.AudioService/host-audio + serviceType: audio.d2bus.org.AudioService + projectionSchemaFingerprint: sha256: + factoryFingerprint: sha256: + operations: [playback, capture] + arbitration: multiplexed + quota: + maxConsumers: 16 + fairness: fifo + leaseDeadlineMs: 30000 + consumerZonePolicy: + zones: [Zone/work] + capabilityCeiling: [playback, capture] + visibility: named-zones +``` + +The consumer Zone imports a local projection AudioService: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: ResourceImport +metadata: + name: host-audio + zone: work +spec: + providerRef: Provider/audio-pipewire + zoneLinkRef: ZoneLink/work-uplink + exportKey: host/host-audio + expectedServiceType: audio.d2bus.org.AudioService + expectedProjectionSchemaFingerprint: sha256: + expectedFactoryFingerprint: sha256: + projectionName: host-audio-projection + requestedCapabilities: [playback, capture] + requestedQuota: + leaseDeadlineMs: 30000 + disconnectPolicy: + mode: degrade +``` + +The Export's `arbitration: multiplexed` describes concurrent Service sessions +and speaker mixing; it does not relax the AudioService schema's +`authority.arbitration.microphone: exclusive`. Capture operations from all +imports still enter the single owner-side fair queue defined above. The audio +adapter admits exactly `quota.fairness: fifo` and +`quota.leaseDeadlineMs: 30000`; these generic Export fields cannot introduce a +priority or extend a contended microphone lease. + +The signed audio projection factory and the live Export/Import fields agree +exactly: + +| Factory/authoring field | Audio value | +| --- | --- | +| `serviceType` / `expectedServiceType` | `audio.d2bus.org.AudioService` | +| `bindingType` | `audio.d2bus.org.AudioBinding` | +| `allowedBackingRefTypes` | `Endpoint` | +| `allowedBindingTargetRefTypes` | `Guest` | +| `projectionSchema` | Strict projection AudioService schema: `providerRef`, role/import semantic fields, and no `spec.provider`, authority, Endpoint locator, FD, path, credential, payload, or consent field | +| `projectionSchemaFingerprint` / `expectedProjectionSchemaFingerprint` | SHA-256 of that canonical committed projection schema | +| `factoryFingerprint` / `expectedFactoryFingerprint` | SHA-256 binding the semantic factory fields and projection-protocol version; never Provider/adapter identity | + +Provider install, Nix build, API admission, export, import, and reconnect fail +closed unless the signed factory, Service type, projection-schema fingerprint, +and factory fingerprint match exactly. The Export has no Endpoint field: its +`resourceRef` selects the owner Service, and that Service alone owns its local +authority Endpoint. + +ResourceExport status reports the verified projection-schema/factory +fingerprints, owner Service generation/readiness, active/pending consumer +counts, and no Endpoint locator. Its closed conditions are +`ExportAdvertised`, `AuthorityReady`, `ConsumersAdmitted`, and `Revoking`. +ResourceImport status reports the same verified fingerprints, local projection +Service Ref, lease state, and bounded session-generation digest. Its closed +conditions include `ExportReachable`, `FactoryMatched`, `SchemaMatched`, +`Bound`, `ProjectionReady`, `BindingReferencesRemain`, and `Degraded`. + +Core owns ResourceExport/ResourceImport routing and base lifecycle. On a bound +import, core creates only +`audio.d2bus.org.AudioService/host-audio-projection` with +`metadata.ownerRef: ResourceImport/host-audio`; it never creates AudioBinding, +workers, guest agents, or private Endpoints. The Provider import adapter +reconciles the projection's local route semantics. An operator then declares a +normal per-Guest AudioBinding whose `serviceRef` names that projection. + +No PipeWire FD, socket, path, authority handle, or ResourceRef crosses a Zone. +Audio frames and service operations flow only over per-import bounded encrypted +named streams, grounded in `packages/d2b-realm-core/src/stream.rs` +(`StreamKind::{AudioPlayback,AudioCapture}` and split-direction +`StreamChannel`) and `packages/d2b-realm-core/src/mux.rs` credit-based flow. +Each import has its own session generation, authorization, credits/backpressure, +cancel, deadline, and idempotency. Intermediaries see ciphertext. + +### Deterministic controller ownership + +| Controller | Creates/deletes | Reconciles | Must never do | +| --- | --- | --- | --- | +| Core ResourceImport controller | Exactly one projection AudioService per ResourceImport | Import lease, projection ownerRef/lifecycle, D091 propagation | Create AudioBinding, Process, Endpoint, GuestAudioAgent, or open PipeWire | +| AudioService controller | Service-owned local implementation/route Endpoints only | Owner AuthorityDescriptor, speaker mixer, exclusive bounded fair mic arbiter, owner endpoint; or projection import/stream route | Create/delete projection Service, put authority on a projection, open PipeWire for a projection | +| AudioBinding controller | Per-Binding vhost-user worker, GuestAudioAgent Processes, and private Endpoints | Guest ownership, same-Zone serviceRef, grants/levels, guest frontend and status | Export/project AudioBinding, claim physical authority, follow a cross-Zone Ref, handle FDs directly | + +Export removal or ZoneLink loss revokes leases and marks the projection Service +`Degraded`/`revoked`; its referencing AudioBindings degrade through the serviceRef +index. Reconnect revalidates generation, Service type, and both fingerprints +before binding. D091 +currency is ordered remote owner Service -> ResourceImport -> projection +Service -> AudioBinding -> owned realization. A disruptive owner upgrade drains +consumers and named streams before recycling authority; no stale projection +continues. + +Guest AudioSet calls remain concurrent across all active GuestAudioAgent +Processes. The authority aggregates per-guest results and never serializes one +slow Guest behind another. + +### Selected invariants + +- **vhost-device-sound v0.3.0** is required; nixpkgs v0.2.0 has the known + PipeWire-backend format-negotiation bug and MUST NOT be used. +- **No `monitor.rules`.** WirePlumber split-direction enforcement uses + `client.conf.d` stream rules or scripts, never the section that broke host + audio output. +- **Volume/gain redaction.** Speaker level, mic gain, authority key, raw node + identity, import keys, and stream/session identifiers are redacted from audit, + OTEL labels, and logs; status remains bounded and provider-neutral. +- **Encrypted credit streams.** All cross-Zone audio bytes use the bounded + encrypted credit streams above; no plaintext, FD, socket, path, or authority + grant crosses a Zone. +- **Exclusive microphone, mixed speaker.** Speaker streams remain + multiplexed/mixed. Initial-v3 capture has one owner-side slot and the fixed + bounded per-Zone fair queue; no schema, Provider extension, Export policy, or + route admits concurrent capture or an undefined consent override. +- **No legacy shortcuts.** The two-ResourceType model adds no new ProcessRole, + direct broker path, per-VM state file, or ambient PipeWire access. Effects use + D077 EffectPort/LaunchTicket, observations are D087 status-first, schemas and + status retain D088/D089 three-layer shape, and cross-Zone sharing is only + D096 ResourceExport/ResourceImport plus Service-owned Endpoints. + +## Nix authoring and configuration + +### Operator-facing provider-neutral resource schema + +```nix +d2b.zones.dev.resources = { + host-audio = { + type = "audio.d2bus.org.AudioService"; + metadata.ownerRef = "Provider/audio-pipewire"; + spec = { + providerRef = "Provider/audio-pipewire"; + serviceRole = "owner"; + implementationEndpointRefs = [ "Endpoint/audio-pipewire-authority" ]; + operations = [ "playback" "capture" ]; + authority = { + authorityScope = "physical-device"; + authorityClass = "audio"; + authorityKey = "host-default-audio"; + cardinality = "zero-or-one"; + arbitration = { + speaker = "multiplexed"; + microphone = "exclusive"; + }; + exportability = "explicit-export"; + }; + }; + }; + + host-audio-export = { + type = "ResourceExport"; + spec = { + providerRef = "Provider/audio-pipewire"; + resourceRef = "audio.d2bus.org.AudioService/host-audio"; + serviceType = "audio.d2bus.org.AudioService"; + projectionSchemaFingerprint = + "sha256:"; + factoryFingerprint = "sha256:"; + operations = [ "playback" "capture" ]; + arbitration = "multiplexed"; # speaker sessions; mic remains exclusive + quota = { + maxConsumers = 16; + fairness = "fifo"; + leaseDeadlineMs = 30000; + }; + consumerZonePolicy = { + zones = [ "Zone/work" ]; + capabilityCeiling = [ "playback" "capture" ]; + }; + visibility = "named-zones"; + }; + }; + + corp-vm-audio = { + type = "audio.d2bus.org.AudioBinding"; + metadata.ownerRef = "Guest/corp-vm"; + spec = { + providerRef = "Provider/audio-pipewire"; + serviceRef = "audio.d2bus.org.AudioService/host-audio"; + grants = { + mic = "off"; + speaker = "on"; + speakerLevel = 75; # null = system default + micGain = null; + }; + guestUsers = [ "User/alice" ]; # Nix/compiler sets spec.groups=["audio"] on User/alice + suspendOnGuestAbsent = true; + }; + }; +}; +``` + +A consumer Zone declares ResourceImport plus a normal per-Guest AudioBinding. It +does not author the projection Service; core generates it: + +```nix +d2b.zones.work.resources = { + host-audio-import = { + type = "ResourceImport"; + spec = { + providerRef = "Provider/audio-pipewire"; + zoneLinkRef = "ZoneLink/work-uplink"; + exportKey = "dev/host-audio-export"; + expectedServiceType = "audio.d2bus.org.AudioService"; + expectedProjectionSchemaFingerprint = + "sha256:"; + expectedFactoryFingerprint = "sha256:"; + projectionName = "host-audio-projection"; + requestedCapabilities = [ "playback" "capture" ]; + requestedQuota.leaseDeadlineMs = 30000; + }; + }; + + work-vm-audio = { + type = "audio.d2bus.org.AudioBinding"; + metadata.ownerRef = "Guest/work-vm"; + spec = { + providerRef = "Provider/audio-pipewire"; + serviceRef = + "audio.d2bus.org.AudioService/host-audio-projection"; + grants = { + mic = "off"; + speaker = "on"; + speakerLevel = 75; + micGain = null; + }; + guestUsers = [ "User/alice" ]; + suspendOnGuestAbsent = true; + }; + }; +}; +``` + +### Provider root configuration (`spec.config`) + +```nix +d2b.zones.dev.resources.audio-pipewire = { + type = "Provider"; + spec = { + artifactId = "audio-pipewire"; + config = { + captureAlias = null; + # When non-null: a bounded named alias matching ^[a-z][a-z0-9-]*$, + # max 64 chars. The AudioMediator resolves it to the actual PipeWire + # node via libpipewire registry introspection at runtime, privately. + # This is NOT a PipeWire node ID, object path, or socket path. + # Example: captureAlias = "desk-headset"; + }; + }; +}; +``` + +This Provider config is strict and deny-unknown. In its initial schema, +`captureAlias` is its only operator-set PipeWire field; compiled +`client.conf.d`/WirePlumber policy remains Provider package configuration, not +an AudioService or AudioBinding field. Provider config cannot shadow grants, +levels, service roles, authority, or references from the neutral base schemas. +Microphone queue bounds, ordering, and lease behavior are fixed initial-v3 +contract values, not authorable config. Consent, approval, priority, and +concurrent-capture settings are unknown. + +### Nix validation + +Nix eval-time validation checks the two provider-neutral qualified +ResourceTypes: + +- owner `AudioService` has `metadata.ownerRef: Provider/audio-pipewire`, a + D097 AuthorityDescriptor, and only same-Zone Endpoint refs; +- projection `AudioService` has + `metadata.ownerRef: ResourceImport/`, no AuthorityDescriptor, and only + same-Zone route Endpoint refs; a projection may only be compiler materialized + by core from a matching ResourceImport; +- ResourceExport `serviceType`, `projectionSchemaFingerprint`, and + `factoryFingerprint` exactly match the installed signed AudioService + projection factory; ResourceImport uses matching `expectedServiceType`, + `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`; + only `audio.d2bus.org.AudioService`, never AudioBinding, is selected; +- the obsolete ResourceExport fields `endpointRef`, `exportedType`, and + `baseSchemaFingerprint`, and obsolete ResourceImport fields `expectedType`, + `expectedBaseSchemaFingerprint`, and `projectionType`, each fail as unknown + in explicit rejection tests; no compatibility alias is emitted; +- a ResourceExport never contains an Endpoint field; the owner AudioService + retains all `implementationEndpointRefs`; +- audio ResourceExport requires `arbitration = "multiplexed"` for Service + sessions/speaker mixing, `quota.fairness = "fifo"`, and + `quota.leaseDeadlineMs = 30000`, and an audio ResourceImport requests that + same deadline; none overrides exclusive microphone arbitration or introduces + priority; +- `spec.providerRef` resolves to an installed `Provider/audio-pipewire` in the + same Zone; evaluation fails with a descriptive error if absent; +- `AudioBinding.spec.serviceRef` resolves to an owner or projection AudioService + in the same Zone and never to ResourceImport directly; +- `spec.grants.speakerLevel` is `null` or an integer in `[0,100]`; +- `spec.grants.micGain` is `null` or an integer in `[0,100]`; +- `spec.guestUsers` contains ≤16 entries, each a valid `User/` ResourceRef + where `` matches `[a-z][a-z0-9_-]*` ≤32 chars; at compile time, the + Nix module sets `spec.groups: ["audio"]` on each referenced guest `User` + resource; for API-created resources, the controller verifies + `User.status.groupMembershipVerified` and fails closed; +- `metadata.ownerRef` resolves to an existing `Guest/` in the same Zone; +- no two `AudioBinding` resources share the same Guest `metadata.ownerRef`; +- no AudioBinding contains `authority`, has `ownerRef: ResourceImport/`, or + is selected as an exported/projection type; +- owner AudioService requires speaker `multiplexed` and microphone `exclusive`; + every other microphone arbitration value and every consent, approval, + priority, or concurrent-capture field fails as unknown; +- every provider-qualified Service/Binding type and every former AudioState + spelling fails as unknown; the Nix compiler emits no alias or deprecation + shim; +- PipeWire-specific fields at AudioService/AudioBinding base `spec.*` fail as + unknown. The initial provider accepts only absent or strict versioned + `spec.provider` with empty `settings`; `captureAlias` is accepted only at + `Provider/audio-pipewire.spec.config`. + +The Nix module also validates that: +- `Provider/audio-pipewire.spec.config.captureAlias`, when non-null, matches + `^[a-z][a-z0-9-]*$` and is ≤64 characters; no path separator, whitespace, + PipeWire syntax, numeric start, or uppercase is permitted; +- `d2b.site.audio.inputTargetNode` (the current legacy option) is absent when + the v3 Provider is installed; a clear migration error is emitted if both + are set. + +The generated base schemas are owned by the `audio.d2bus.org` contract +namespace. The selected Provider supplies signed implementation capability and +provider-envelope schemas; it does not mint a second PipeWire-qualified copy of +either ResourceType. + +### Generated canonical ResourceSpec + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "audio.d2bus.org.AudioBinding", + "metadata": { + "name": "corp-vm-audio", + "zone": "dev", + "ownerRef": "Guest/corp-vm" + }, + "spec": { + "providerRef": "Provider/audio-pipewire", + "serviceRef": "audio.d2bus.org.AudioService/host-audio", + "grants": { + "mic": "off", + "speaker": "on", + "speakerLevel": 75, + "micGain": null + }, + "guestUsers": ["User/alice"], + "suspendOnGuestAbsent": true + } +} +``` + +### Resource cleanup contract + +When an `AudioBinding` resource is removed from the Nix configuration: +1. The new generation activates immediately; the resource enters + `deletionRequestedAt` asynchronous deletion. +2. The generation reports `Degraded/pending-cleanup` until deletion completes. +3. Activation does not block; the rest of the Zone configuration activates + normally. +4. The owned `Process` resources are deleted finalizer-safely per the deletion + sequence above. +5. The `GuestAudioAgent` Process for the deleted Guest is stopped and deleted + by the finalizer handler. On next guest NixOS config generation rebuild, + the guest user's `spec.groups` audio entry is removed (Nix/compiler removes + the declaration; system-core reconciles). + +When an owner AudioService is removed, its finalizer first drains referencing +AudioBindings and exports, releases the authority and local implementation +Endpoints, then removes the Service. A projection AudioService is never removed +directly from Nix: deleting/revoking its ResourceImport drives the core +child-first sequence and core deletes that projection after consumers and the +remote lease are released. + +## Current-code fit + +| Item | Value | +| --- | --- | +| Current anchor | `nixos-modules/components/audio/host.nix`, `guest.nix`, `packages/d2b-core/src/audio_policy.rs`, `packages/d2bd/src/audio_dispatch.rs`, `packages/d2bd/src/audio_host_controller.rs`, `packages/d2b-host/src/audio_argv.rs` | +| Evidence class | Mixed; `audio_policy.rs` / `audio_argv.rs` are `implemented-and-reachable`; v3 Provider/two-ResourceType AudioService+AudioBinding/AudioMediator wiring is `ADR-only` | +| Behavior retained | Per-VM mic/speaker grants, `LevelPercent` 0..=100, component-template argv shape, PipeWire `client.conf.d/` stream rule placement, WirePlumber virtio-snd profile, zero host capabilities | +| Required delta | Provider-neutral qualified `audio.d2bus.org.AudioService` owner/projection ResourceType plus per-Guest `audio.d2bus.org.AudioBinding` ResourceType with required same-Zone `serviceRef`, initially implemented by `Provider/audio-pipewire`; strict provider-field isolation; deterministic Service and Binding handlers; core-created projection-Service contract; owner `AudioMediator` with `SetGrant`/`SetLevel`; GuestAudioAgent and private Endpoints; runtime-audio alias; encrypted import streams; D097 authority/mixer/mic arbitration; three-layer status and D091 propagation; RBAC/watch plans; Nix authoring and tests | +| Reuse path | `d2b-core/src/audio_policy.rs` → adapt into `src/audio_policy.rs` (one-time activation migration from v1/v2 on-disk format only; no ongoing state file); `audio_argv.rs` → adapt into component template (not live Process spec); WirePlumber stream rule Nix logic → port to Provider Nix module | +| Replacement/deletion | `audio_dispatch.rs`, `audio_host_controller.rs` retired after `audio-binding-controller` passes e2e parity; `host.nix`, `guest.nix` retired after v3 Nix module deployed; `d2b-core/src/audio_policy.rs` may remain as re-export shim | +| Feasibility proof | Minijail contract tests in `d2b-contract-tests/tests/minijail_audio_usbip.rs`; ComponentSession FD-attachment protocol from ADR-046-componentsession-and-bus; libpipewire `pw_node_set_param` API confirmed in upstream PipeWire 1.x | +| Future owner | `d2b-provider-audio-pipewire` crate; exact work items listed below | + +## Implementation work items + +### ADR046-audio-001: Extract `AudioPolicyState` into Provider crate + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-001` | +| Dependency/owner | No prerequisites; `d2b-provider-audio-pipewire` crate | +| Current source | `packages/d2b-core/src/audio_policy.rs` (all symbols); `packages/d2b-core/tests/audio_policy.rs` | +| Reuse source | Same baseline paths | +| Reuse action | copy-unchanged | +| Destination | `packages/d2b-provider-audio-pipewire/src/audio_policy.rs`; re-exported from crate root | +| Detailed design | `LevelPercent`, `AudioGrant`, `AudioPolicyState`, `parse_audio_state`, `to_v2_bytes`, `AudioPolicyError` copy unchanged. `AudioPolicyState` is the canonical in-memory representation of `AudioBinding.spec.grants`. `parse_audio_state`/`to_v2_bytes` are used only once during first-activation migration from a prior v1/v2 on-disk file; there is no ongoing state file in v3. Primary reuse disposition: `copy-unchanged`. Preserved source-plan detail: `copy-unchanged` (no daemon imports; pure DTO library). | +| Integration | First-activation migration uses `parse_audio_state` and writes grants only into a per-Guest AudioBinding that also names the explicitly configured same-Zone owner AudioService. | +| Data migration | Parse v1/v2 once; require exactly one configured owner Service; write grants plus `serviceRef`; fail closed on missing/ambiguous Service; remove prior file only after successful commit. | +| Validation | `tests/audio_policy.rs`: all existing tests from `d2b-core/tests/audio_policy.rs` plus AudioBinding spec serialization tests | +| Removal proof | `d2b-core/src/audio_policy.rs` deleted when no `d2bd` caller references it; confirmed by `cargo check --no-default-features`. | + +### ADR046-audio-002: Adapt `AudioArgvInput` into vhost-user-sound component template + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-002` | +| Dependency/owner | Depends on `ADR046-audio-001`; Process Provider template schema | +| Current source | `packages/d2b-host/src/audio_argv.rs` (all symbols + tests); `tests/golden/runner-shape/audio-argv-minimal.txt` | +| Reuse source | Same baseline paths | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-audio-pipewire/src/argv.rs` (component template renderer) | +| Detailed design | `generate_audio_argv` remains the canonical argv builder for the `vhost-user-sound-worker` component template. The resulting argv/env/executableRef are sealed into the LaunchTicket. The per-Guest binary copy path enforcement remains via the LaunchTicket verifier. The live Process resource spec contains no argv or executableRef. The `--socket` argument is removed; the vhost-user service identity is `Endpoint/corp-vm-audio-vhost-user`, while the backing locator is resolved into the LaunchTicket under authorization. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — argv builder retained; becomes a signed component-template projection, not a live Process spec field. | +| Integration | The component template for `vhost-user-sound-worker` embeds the output of `generate_audio_argv`; the Process Provider resolves arg0 from the artifact catalog. | +| Data migration | No runtime state migration; argv template output is regenerated from the v3 component template, and live Process specs never store argv. | +| Validation | `tests/argv.rs`: rejection matrix (Nix store path, symlink, cross-guest copy, empty name); no-socket-in-argv assertion; no-argv-in-process-spec assertion | +| Removal proof | `d2b-host/src/audio_argv.rs` deleted after `d2bd` has no callers; confirmed by `cargo check -p d2b-host`. | + +### ADR046-audio-004: Implement AudioMediator SetGrant/SetLevel service and libpipewire enforcement + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-004` | +| Dependency/owner | Depends on `ADR046-audio-001`; ComponentSession service contract; libpipewire 1.x API | +| Current source | `packages/d2bd/src/audio_host_controller.rs` `PipeWireHostController` enforcement logic; `QemuAudioController` | +| Reuse source | Same baseline paths | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-audio-pipewire/src/mediator/enforcement.rs` | +| Detailed design | Owner AudioService only: speaker `SetGrant` maps `"off"` to `pw_node_set_param(SPA_PARAM_Props, mute=true, target.object=-1)` on the worker's node and `"on"` to `mute=false`; `SetLevel` maps to a bounded volume. Microphone `"on"` is applied only after the owner authority grants its single capture slot; queued/blocked consumers remain muted, and release mutes/disconnects before handoff. `captureAlias` resolves privately through the registry. A projection AudioService routes the operation to the remote owner over its import stream and is denied any local mediator/PipeWire open. `FakeAudioMediator` is the hermetic test double. No state file, wpctl, EphemeralProcess, or node ID in any external surface. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — enforcement logic becomes a libpipewire API implementation behind the `SetGrant`/`SetLevel` ComponentSession service. | +| Integration | AudioBinding controller calls its resolved AudioService. Owner Service dispatches locally to AudioMediator; projection Service dispatches over the encrypted import route to the remote owner. | +| Data migration | No state migration; mediator applies current AudioBinding grants and levels from resource state during reconcile, replacing host-controller direct writes. | +| Validation | `tests/mediator.rs` and `tests/enforcement.rs`: owner-Service SetGrant/SetLevel round-trip; speaker mixing; microphone queued consumers remain muted; release/lease-expiry mute-before-handoff and no-overlap proof; projection routing with fake streams; projection-PipeWire-open denial; no-node-id-in-bus-message; ProviderSessionUnavailable; captureAlias registry resolution | +| Removal proof | `d2bd/src/audio_host_controller.rs` retired after `d2bd` audio dispatch path is replaced. | + +### ADR046-audio-005: Implement `AudioService` and `AudioBinding` schemas and admission + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-005` | +| Dependency/owner | ADR046-provider-004 common audio Service/Binding base; Core resource-api foundation; `d2b-provider-audio-pipewire` crate | +| Current source | None (ADR-only); structured after `d2b-contracts/src/public_wire.rs` audio types | +| Reuse source | `public_wire.rs` `AudioChannel`, `AudioEnforcementPosture`, `AudioErrorKind`, `AudioProviderKind`, `AudioSetApplied`; `AudioProviderKind` is removal evidence, not a base-status field | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-audio-pipewire/src/{resource_type,admission,provider_extension}.rs` (strict implementation extensions and binding only; common base lives under ADR046-provider-004) | +| Detailed design | Bind the shared D098 `audio.d2bus.org.AudioService` and `audio.d2bus.org.AudioBinding` base schema versions/fingerprints from ADR046-provider-004 and define only strict audio-pipewire Provider extensions/admission. AudioService validates immutable `serviceRole`, same-Zone local Endpoint refs, owner-only D097 AuthorityDescriptor, projection-only `ownerRef: ResourceImport/`, Core-only projection creation, and projection `spec.provider` rejection. Initial-v3 owner admission requires speaker `multiplexed` and microphone `exclusive` and rejects multiplexed capture plus every consent/approval/priority placeholder. Owner Service status carries only bounded aggregate microphone active/request/Zone counts; Binding status carries only its own arbitration state. AudioBinding validates Guest ownership, required immutable same-Zone `serviceRef`, grants/levels/users, and forbids authority/export/projection semantics. Semantic authority/import/attachment observations stay under `status.resource`; implementation observations stay under `status.provider`. PipeWire fields are rejected from base spec/status. Register no provider-qualified or AudioState identifier and no serde/schema alias. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` provider-neutral closed enums; remove `AudioProviderKind` from base status; `ADR-only` for schema/admission. | +| Integration | `Provider/audio-pipewire` signs and publishes implementation support for both neutral qualified ResourceTypeSchemas and strict provider-envelope schemas. Core import controller may create/delete only projection AudioService; ordinary resource API admission handles owner Services and AudioBindings. | +| Data migration | Full d2b 3.0 reset; owner Services and per-Guest AudioBindings are authored as new v3 resources; projection Services are core-generated from ResourceImport. | +| Validation | `tests/resource_type.rs`: consume the ADR046-provider-004 common fixtures/fingerprints; canonical minimal base without `spec.provider`; neutral qualified-name registration; both schema/status round-trips including bounded aggregate owner queue status and per-Binding arbitration state; clean-break rejection of provider-qualified names, every AudioState spelling, and all aliases; fake alternate-provider base conformance; strict base/provider unknown-field matrices; projection `spec.provider` rejection; D088 `status.resource`/`status.provider` placement; PipeWire fields only in strict provider envelopes/config; Service role/AuthorityDescriptor/ownerRef/Endpoint-locality rules; initial-v3 exclusive-mic/mixed-speaker schema and consent/approval/priority/concurrent-capture rejection; Core-only projection admission; AudioBinding required same-Zone serviceRef and Guest owner; immutable refs; out-of-range levels/users; explicit tests that AudioBinding cannot be exported or projected | +| Removal proof | None — both ResourceTypes are net-new | + +### ADR046-audio-006: Implement deterministic AudioService and AudioBinding handlers + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-006` | +| Dependency/owner | Depends on `ADR046-audio-001` through `ADR046-audio-005`; core ResourceImport controller; system Process Provider; AudioMediator (`ADR046-audio-007`); GuestAudioAgent (`ADR046-audio-011`); no Provider state Volume under D087 | +| Current source | `packages/d2bd/src/audio_dispatch.rs` lines 250–end (dispatch ordering reference) | +| Reuse source | None directly; reconcile flow is new async controller | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-audio-pipewire/src/controller/audio_service.rs`; `src/controller/audio_binding.rs` | +| Detailed design | One controller binary registers deterministic handlers for the two neutral ResourceTypes, constrained to immutable `spec.providerRef: Provider/audio-pipewire`. Service handler watches AudioService, its ResourceImport owner and local Endpoints; owner semantics claim/revalidate D097, local mediator, and aggregate exclusive-mic queue state, while projection semantics bind only encrypted import streams and deny PipeWire. It cannot create/delete projection Service. Binding handler watches AudioBinding, same-Zone serviceRef, Guest/User, owned Process, and private Endpoints; creates the vhost-user worker, GuestAudioAgents, and private Endpoints, then calls the resolved Service and guest agents. It maps `Applied|Queued|MicQueueFull` into the closed conditions/status/phase, and release/delete/revocation cancels queue state before child teardown. A Service event enqueues serviceRef-indexed Bindings. A resource selecting another conforming Provider is ignored and cannot be status/finalizer-mutated. Neither handler uses broker/pidfd/EphemeralProcess/Volume/User operations or direct filesystem access. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — dispatch logic is the reference for step ordering only. | +| Integration | Registered with Zone core as a controller under `Provider/audio-pipewire`. | +| Data migration | v1/v2 audio policy file migration is handled by ADR046-audio-001 before reconcile; the controller keeps no Provider state Volume and imports no additional runtime state. | +| Validation | Fast hermetic `tests/audio_service_controller.rs`: neutral type/provider selection, foreign-provider ignore/deny, owner authority, bounded aggregate microphone status, projection ownerRef/import chain, core-only create/delete, projection no-PipeWire-open, revocation queue cancellation, and D091 propagation. `tests/audio_binding_controller.rs`: neutral type/provider selection, required same-Zone serviceRef, owner/projection dispatch, child Process/private Endpoint state machine, `Applied|Queued|MicQueueFull` status mapping, off/delete/revocation cancellation, grant changes, absence/failures/deletion. Conformance asserts no AudioBinding export/projection, no broker/pidfd/EphemeralProcess/Volume/User ops. ProviderDeployment integration remains fake-only and validates empty ProviderStateSet. | +| Removal proof | Supersedes `audio_dispatch.rs`; `d2bd` audio dispatch deleted after e2e parity test confirms | + +### ADR046-audio-007: Implement `AudioMediator` user-session service + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-007` | +| Dependency/owner | Depends on `ADR046-audio-004`; ComponentSession service (ADR-046-componentsession-and-bus); libpipewire 1.x | +| Current source | `packages/d2bd/src/audio_host_controller.rs` PipeWire session access patterns (reference only) | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-provider-audio-pipewire/src/mediator/mod.rs`; `src/bin/audio_pipewire_mediator.rs` | +| Detailed design | Owner AudioService implementation only. Long-lived user-session Process maintains per-AudioBinding nodes under the single owner backing, receives the pre-opened local PipeWire portal FD, and exposes `SetGrant`/`SetLevel` through `Endpoint/audio-pipewire-authority`. It enforces the authority arbiter's single microphone slot and mute-before-handoff result while speaker nodes remain mixed. Projection Services never start/call a local mediator and cannot receive its FD. No EphemeralProcess, wpctl, remote Ref, or node identity in external surfaces. | +| Integration | Second binary in the `d2b-provider-audio-pipewire` package. Registered as a user-session service under `Provider/audio-pipewire`. | +| Data migration | No persisted mediator state migration; the service rebuilds its PipeWire node map from the registry on start and consumes current AudioBinding through controller calls. | +| Validation | `tests/mediator.rs`: owner-Service FD handoff and calls; captureAlias; node-id sealing; session-unavailable; concurrent speaker Guest isolation; exclusive microphone no-overlap and mute-before-handoff; teardown; projection Service cannot resolve mediator Endpoint or portal attachment | +| Removal proof | Supersedes `d2bd`'s `PipeWireHostController` direct session access; `d2bd` audio host controller deleted after e2e parity | + +### ADR046-audio-008: Nix module for v3 AudioService/AudioBinding authoring + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-008` | +| Dependency/owner | Depends on `ADR046-audio-005`; Nix resource compilation framework; `ADR-046-nix-configuration` | +| Current source | `nixos-modules/components/audio/host.nix` and `guest.nix` | +| Reuse source | Same | +| Reuse action | replace | +| Destination | `nixos-modules/components/audio/v3-resource.nix`; `nixos-modules/components/audio/host-config.nix`; `nixos-modules/components/audio/guest-config.nix` | +| Detailed design | `v3-resource.nix` emits only `audio.d2bus.org.AudioService` and `audio.d2bus.org.AudioBinding`, selected by `Provider/audio-pipewire`, with required same-Zone serviceRef. For projection identity, ResourceExport emits `resourceRef` plus canonical `serviceType`, `projectionSchemaFingerprint`, and `factoryFingerprint`; its Service retains the Endpoint. ResourceImport emits the matching three `expected*` fields. Projection Services are never authored: core materializes them from ResourceImport. Eval rejects every obsolete Export/Import field in the explicit rejection matrix, provider-qualified ResourceTypes, every AudioState spelling, aliases, PipeWire fields in neutral base spec/status, AudioBinding export/projection, non-exclusive microphone or consent/approval/priority/concurrent-capture fields, Service role/ownerRef/authority mismatches, cross-Zone Endpoint/service refs, and duplicate owner authority. Existing captureAlias stays in Provider config; guestUsers/group injection, runtime-audio derivation, host stream rules, and guest stack remain. | +| Integration | `nixos-modules/default.nix` imports all three modules. | +| Data migration | Full d2b 3.0 reset; legacy Nix audio options emit/deprecate to one owner AudioService plus per-Guest AudioBindings that reference it; no projection Service is authored. | +| Validation | `tests/unit/nix/cases/audio-v3-resource.nix`: exact neutral type names; provider-qualified/AudioState/alias rejection; strict provider-field placement; owner Service and Binding round-trip; same-Zone serviceRef; exact canonical Export/Import Service type and both fingerprints; obsolete Export/Import field rejection; Export Endpoint-field rejection; projection core-only ownerRef chain; AudioBinding export/projection rejection; exclusive-mic/mixed-speaker and consent/approval/priority/concurrent-capture rejection; authority uniqueness; Endpoint locality; plus existing grants/users/captureAlias/deprecation/no-wpctl/no-audioFrontend assertions | +| Removal proof | `host.nix` and `guest.nix` kept as compat shims until v3 module deployed on all Zones | + +### ADR046-audio-009: Minijail contract test migration + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-009` | +| Dependency/owner | Depends on `ADR046-audio-005`; `d2b-contract-tests` crate | +| Current source | `packages/d2b-contract-tests/tests/minijail_audio_usbip.rs` audio section | +| Reuse source | Same | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-audio-pipewire/tests/minijail_contract.rs` (provider-local); retain cross-bundle source greps in `d2b-contract-tests` | +| Detailed design | Retain worker sandbox assertions and add role-sensitive attachment checks: an owner-Service worker may receive only the local AudioMediator attachment; a projection-Service worker receives only its same-Zone route Endpoint and can never receive a PipeWire FD. Binding-owned private Endpoint ownership/producerRef is explicit. All Service/Binding/Process/Endpoint serialized forms remain locator-free. | +| Integration | Provider-local contract tests run in `d2b-provider-audio-pipewire`; retained cross-bundle greps in `d2b-contract-tests` ensure bundle-wide invariants still hold. | +| Data migration | None — test migration only; no runtime state. | +| Validation | `cargo test -p d2b-provider-audio-pipewire -- minijail` must pass; existing cross-bundle tests must continue to pass | +| Removal proof | The superseded duplicate shell validator `tests/minijail-validator-audio.sh` is deleted only after the successor Rust gate (`minijail_audio_usbip.rs` cross-bundle + provider-local `minijail_contract.rs`) is green and its removal-proof check passes; the `seccomp_policy_ref == "w1-audio"` assertion migrates to `spec.sandbox.seccompClass == "audio-pipewire-worker"` before removal. Cross-bundle Rust tests are retained. | + +### ADR046-audio-010: OTEL telemetry and audit emitters + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-010` | +| Dependency/owner | Depends on `ADR046-audio-006`, `ADR046-audio-007`, `ADR046-audio-011`; `d2b-telemetry` lightweight emitter | +| Current source | `packages/d2bd/src/audio_dispatch.rs` audit call sites (redaction pattern reference) | +| Reuse source | Same; adapt redaction pattern | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-audio-pipewire/src/telemetry.rs` | +| Detailed design | Emit closed-label Service and Binding metrics plus post-commit audit. Service events distinguish only `owner\|projection` and closed outcomes; microphone arbitration emits closed transition/outcome plus bounded aggregate counts only. Metrics expose active count `0|1`, aggregate queue depth, and closed arbitration state without Zone/Binding/handle/position labels. Events omit authority keys, import/export keys, remote identity, stream/session ids, endpoints, client timestamps, and queue entries. Enforcement metrics cover owner-local and projection-routed calls without exposing route identity. ProcessEffect audit remains Process Provider-owned. | +| Integration | Audio controller and mediator call telemetry/audit emitters after commit or enforcement; d2b-telemetry exporter and policy_observability consume the resulting records. | +| Data migration | No telemetry/audit data migration; v3 emits new closed-label OTEL/audit records after cutover and old audio_dispatch audit sites are removed. | +| Validation | `tests/audio_telemetry.rs`: Service/Binding event separation, microphone aggregate metrics and transition audit, redaction of Zone/Binding/handle/position/timestamp, post-commit ordering, label cardinality, forbidden authority/import/stream/path fields, no ProcessEffect duplication | +| Removal proof | `audio_dispatch.rs` audit call sites deleted after cutover | + +### ADR046-audio-011: Implement `GuestAudioAgent` in-guest service component + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-011` | +| Dependency/owner | Depends on `ADR046-audio-004`; ComponentSession service contract; libpipewire 1.x; system-systemd Process Provider for guest domain | +| Current source | `packages/d2b-guestd/src/audio_set.rs` (guestd wpctl dispatch — reference only) | +| Reuse source | `packages/d2bd/src/audio_host_controller.rs` libpipewire enforcement patterns (reference only) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-audio-pipewire/src/guest_agent/mod.rs`; `src/guest_agent/enforcement.rs`; `src/bin/audio_pipewire_guest_agent.rs` | +| Detailed design | Long-lived user-domain Process running in the Guest under the guest workload user's UID. One Process resource per entry in `AudioBinding.spec.guestUsers`; each named by opaque UID digest (`ag-`) and carrying label `audio-pipewire.d2bus.org/role: guest-audio-agent`. `userRef` is the corresponding `User/` Zone resource. Opens a PipeWire connection in the Guest's compositor session (same-UID, natural access). Exposes a typed `AudioSet` ComponentSession service through an owned `Endpoint/ag--audio-set` (vsock transport, Guest→Zone d2b-bus). `AudioSet(mic, speaker, speakerLevel, micGain)` applies changes via libpipewire API (`pw_node_set_param` with `SPA_PARAM_Props`, `pw_stream_set_control`) on the guest virtio-snd PipeWire node. No wpctl binary, no command path, no EphemeralProcess. Controller calls ALL active GuestAudioAgent instances in parallel for each grant change and aggregates failures. `FakeGuestAudioAgent` is a test double behind `#[cfg(test)]`. Primary reuse disposition: `adapt`. Preserved source-plan detail: `ADR-only` (new component; supersedes guestd wpctl dispatch path). | +| Integration | Third binary in the `d2b-provider-audio-pipewire` package. Declared as GuestAudioAgent Process resources by the audio-binding-controller (one per guestUser; template: `guest-audio-agent`). System Process Provider (`Provider/system-systemd`) launches each inside the Guest under the respective guest workload user's UID. | +| Data migration | No guest runtime state migration; GuestAudioAgent reconnects to guest PipeWire and applies current AudioBinding grants and levels on reconcile, replacing guestd wpctl dispatch. | +| Validation | `tests/guest_agent.rs`: AudioSet service call → libpipewire apply; mute/route/level; session-unavailable path; reconnect state restore; no wpctl binary; no command path; N-agent creation (one per guestUser); parallel call and aggregated failure | +| Removal proof | `d2b-guestd` wpctl audio dispatch path deleted after all Guests have GuestAudioAgent deployed and e2e parity test passes | + +### ADR046-audio-012: Cross-Zone audio export/import adapter (D096) + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-012` | +| Dependency/owner | ADR046-zone-control-019, ADR046-zone-control-020; audio Provider owner | +| Current source | None — net-new ADR 0046 cross-Zone sharing (D096) | +| Reuse source | audio authority/mediator service (this dossier); `packages/d2b-provider/src/share_adapter.rs` `ExportAdapter`/`ImportAdapter` traits | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-audio-pipewire/src/share_adapter.rs` | +| Detailed design | Implement signed `Provider/audio-pipewire` adapters only for canonical `serviceType: audio.d2bus.org.AudioService` when `spec.providerRef` selects this Provider. ResourceExport carries that `serviceType` plus the signed `projectionSchemaFingerprint` and semantic `factoryFingerprint`; ResourceImport carries the matching `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`. Export adapter admits only the owner Service. Its local authority Endpoint remains Service-owned and is never an Export field. Core creates/deletes the projection AudioService with `ownerRef: ResourceImport/`, `providerRef`, and semantic base/import fields but no `spec.provider`; routing derives from the signed local descriptor and ResourceImport record. The semantic factory fingerprint binds factory metadata plus projection-protocol version, never Provider/adapter identity, which the signed descriptor authenticates separately. The import adapter reconciles its semantic route and never creates AudioBinding or opens PipeWire. Per-Guest AudioBindings are ordinary consumer resources with same-Zone serviceRef. No provider-qualified type alias, FD/path/socket/remote Ref crosses a Zone. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (implement the signed audio export/import adapter). | +| Integration | Core export/import controller (ADR046-zone-control-019); local projection lifecycle (ADR046-zone-control-020); ComponentSession bounded encrypted named streams | +| Data migration | None — full d2b 3.0 reset | +| Validation | Fast hermetic `tests/share_adapter.rs`: exact neutral AudioService `serviceType`; exact projection-schema/factory fingerprint match; explicit rejection of obsolete `endpointRef`, `exportedType`, `baseSchemaFingerprint`, `expectedType`, `expectedBaseSchemaFingerprint`, and `projectionType`; reject every Export Endpoint field, provider-qualified alias, and AudioBinding export/projection; accept owner AudioService export; Core-only projection creation/deletion; exact ResourceImport -> projection AudioService ownerRef chain with no `spec.provider`; semantic factory fingerprint unchanged by Provider/adapter identity mutation while signed identity authentication remains exact; projection never opens PipeWire; reconnect/revocation/D091 propagation with fake streams. Only `integration/real_stream.rs` exercises a real encrypted named stream. | +| Removal proof | Not applicable (new surface) | + +### ADR046-audio-013: Audio authority service — speaker mixer and mic arbiter (D096/D097) + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-013` | +| Dependency/owner | Depends on `ADR046-audio-001`, `ADR046-audio-004`, `ADR046-zone-control-019`; audio Provider owner | +| Current source | `packages/d2bd/src/audio_host_controller.rs` (`HostAudioController` trait, `PipeWireHostController::{from_audio_node,find_audio_node}`, `QemuAudioController`, `FakeHostController`); `packages/d2b-core/src/audio_policy.rs` (`LevelPercent`, `AudioGrant`, `AudioPolicyState`) | +| Reuse source | Same baseline controller/policy symbols | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-audio-pipewire/src/authority.rs` (speaker mixer + mic arbiter); `AuthorityDescriptor` on owner `AudioService` | +| Detailed design | Exactly one owner AudioService holds the real PipeWire connection and D097 AuthorityDescriptor. Projection Services and AudioBindings cannot carry it. Speaker streams remain multiplexed/mixed. Microphone capture has exactly one slot across owner and importing Zones. The arbiter keys requests by authenticated Zone plus route-scoped opaque consumer handle, permits one pending entry per handle, bounds pending entries to 16 per Zone and 64 total, uses owner-sequenced FIFO per Zone and round-robin across non-empty Zones, and ignores client timestamps/priority. Its 30-second active lease renews only while no other Zone waits; contended expiry mutes/disconnects, dequeues the next Zone, and atomically requeues a still-requesting old holder at its Zone FIFO tail. Off/delete/revoke/disconnect cancel idempotently. Queue entries are memory-only and restart rebuild fails closed with capture muted. Multiplexed capture and consent/approval/priority/concurrent-capture surfaces are rejected; a future spec must define a concrete consent authorization ResourceType and resource-API verb before concurrent capture exists. Core rejects duplicate owner Services before open and adopts by ownerProof. No new ProcessRole, broker path, or state file. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — the host controller becomes the single authority service; no daemon `Mutex`/state-file wrapping. | +| Integration | Owner AudioService references `Endpoint/audio-pipewire-authority`; same-Zone AudioBindings and remote projection Services call it. Core authority index admits exactly one owner Service. | +| Data migration | None — full d2b 3.0 reset; grants are authoritative in `AudioBinding.spec` (no state file). | +| Validation | Fast hermetic `tests/authority.rs`: AuthorityDescriptor accepted only on owner AudioService; Binding/projection rejection; duplicate conflict; multiplexed speaker mix/quota; one active mic across local/imported Zones; per-Zone FIFO and cross-Zone round-robin; one-entry-per-handle, per-Zone/total bounds and `MicQueueFull`; idempotent cancellation; contended 30-second lease; mute-before-handoff/no overlap; restart-muted rebuild; multiplexed-capture and consent/approval/priority/concurrent-verb rejection; ownerProof adoption; D091 drain/recycle with fake clock/FakeHostController | +| Removal proof | `audio_host_controller.rs` daemon-side controller deleted after the authority service reaches parity; confirmed by `cargo check`. | + +### ADR046-audio-014: Per-import encrypted audio credit streams (D096) + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-audio-014` | +| Dependency/owner | Depends on `ADR046-audio-013`, `ADR046-zone-control-019`; audio Provider owner | +| Current source | `packages/d2b-realm-core/src/stream.rs` (`StreamKind::{AudioPlayback,AudioCapture}` → `Capability::{AudioPlayback,AudioCapture}`, `StreamAuthz`, `StreamChannel` split-direction); `packages/d2b-realm-core/src/mux.rs` (credit-based flow); `packages/d2bd/src/guest_control_bridge.rs` (`audio_set_authenticated`/`audio_status_authenticated`, `GuestAudioSetRequest`/`GuestAudioStatus`) | +| Reuse source | Same baseline stream/mux/bridge symbols | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-audio-pipewire/src/streams.rs` | +| Detailed design | Per-import audio frames flow only over bounded encrypted named streams: one stream with two `StreamChannel`s for playback/capture split direction and a single `StreamAuthz` (a consumer never opens two authz contexts to split direction), credit-based backpressure (a sender spends only receiver-granted credit), per-import session generation, cancel, and deadline. `StreamKind::AudioPlayback`/`AudioCapture` require `Capability::AudioPlayback`/`AudioCapture`. Playback streams may run concurrently; the owner activates capture frames for only its single granted opaque consumer handle, and revocation/disconnect cancels that Zone's active/pending requests before route teardown. No PipeWire FD/socket crosses a Zone; intermediaries see ciphertext. Guest audio calls (`audio_set`/`audio_status`) are issued to all active guests concurrently and results aggregated. Volume/gain (`LevelPercent`), queue identity/position, and node identity are redacted from audit/OTEL/logs. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — audio frames ride the existing ComponentSession named-stream credit machinery. | +| Integration | Owner and projection AudioService adapters allocate per-import streams over ComponentSession; AudioBinding only consumes the same-Zone Service Ref; core routes encrypted records only. | +| Data migration | None — full d2b 3.0 reset | +| Validation | Fast hermetic `tests/streams.rs`: projection-Service ownerRef/import binding, split-direction single-authz stream, credits, generation isolation, cancel/deadline, concurrent playback, one active capture across imports, route loss cancels that Zone's active/pending capture, ciphertext-only intermediary, redaction. Only `integration/real_stream.rs` runs the slower real encrypted stream. | +| Removal proof | Not applicable (new surface) | + +## Required crate layout + +```text +d2b-provider-audio-pipewire/ + README.md + src/ + lib.rs + audio_policy.rs # AudioPolicyState DTOs (ADR046-audio-001) + argv.rs # Component template argv builder (ADR046-audio-002) + resource_type/ + mod.rs + audio_service.rs # AudioService owner/projection schema + status + audio_binding.rs # per-Guest AudioBinding schema + status/serviceRef + admission.rs # Resource API admission validation (ADR046-audio-005) + authority.rs # owner D097 speaker mixer + exclusive fair mic arbiter + share_adapter.rs # AudioService export/import semantics + streams.rs # projection encrypted named-stream route + runtime_capability.rs # runtime-audio capability query client + telemetry.rs # Metrics, post-commit audit, span attributes (ADR046-audio-010) + controller/ + mod.rs + audio_service.rs # owner/projection Service handler + audio_binding.rs # audio-binding-controller handler (ADR046-audio-006) + mediator/ + mod.rs # AudioMediator service + SetGrant/SetLevel (ADR046-audio-007) + enforcement.rs # host libpipewire enforcement API (ADR046-audio-004) + guest_agent/ + mod.rs # GuestAudioAgent service + AudioSet (ADR046-audio-011) + enforcement.rs # guest libpipewire enforcement API (ADR046-audio-011) + bin/ + audio_pipewire_controller.rs + audio_pipewire_mediator.rs + audio_pipewire_guest_agent.rs + tests/ + audio_policy.rs # AudioPolicyState / policy migration (ADR046-audio-001) + argv.rs # Component template argv rejection matrix (ADR046-audio-002) + resource_type.rs # Both schemas/status + Service/Binding separation + audio_service_controller.rs # owner/projection state machine + audio_binding_controller.rs # AudioBinding handler state machine (ADR046-audio-006) + share_adapter.rs # ownerRef chain/core-only projection/fake streams + authority.rs # D097 mixed speaker/exclusive bounded mic arbitration + streams.rs # fake encrypted-stream credit semantics + mediator.rs # FD handoff + SetGrant/SetLevel + captureAlias (ADR046-audio-007) + enforcement.rs # host libpipewire enforcement + offline path (ADR046-audio-004) + guest_agent.rs # AudioSet service + guest libpipewire (ADR046-audio-011) + audio_telemetry.rs # Redaction / label cardinality / post-commit (ADR046-audio-010) + minijail_contract.rs # Zero-caps / seccompClass / no-argv/env/executableRef (ADR046-audio-009) + integration/ + README.md + audio_e2e.rs # End-to-end: enable guest, sidecar start, grant change, delete + grant_enforcement.rs # SetGrant/SetLevel + exclusive mic handoff round-trip + guest_enforcement.rs # GuestAudioAgent AudioSet service + libpipewire round-trip + real_stream.rs # only real encrypted named-stream cross-Zone test + README.md +``` + +`src/`, `tests/`, `integration/`, and `README.md` are all required. Workspace +policy rejects a provider crate missing any of these paths. + +### Fast hermetic execution and test placement (D094) + +Per D094 and `ADR-046-validation-and-delivery` §10.16, this Provider's `src/` +unit tests and `tests/*.rs` hermetic suite are fast, in-process, deterministic, +and parallel-safe: an individual normal test has p95 ≤50 ms with no wall-clock +sleep, and `cargo test -p d2b-provider-audio-pipewire --lib --tests` completes +in ≤2 s warm-cache execution time (compilation excluded). They use a +deterministic fake clock/RNG and the toolkit fakes/FakeEffectPort only — no +process spawn, container, network, DBus, systemd, broker daemon, Nix eval/build, +KVM, USB/GPU/TPM hardware, or live cloud, and no filesystem tree beyond tiny +temp fixtures. Any scenario needing those lives only in `integration/`, which +keeps a lane timeout/budget, parallel isolation, and fake external services by +default; such a need is re-placed into `integration/`, never given a sleep, +larger timeout, or `#[ignore]`. Bounded crypto/property tests are the only +classified exception, each named with a capped case count and a declared higher +per-test budget. + +The Service/Binding split is a mandatory fast hermetic matrix: + +- `provider_neutral_type_registration`: only + `audio.d2bus.org.AudioService` and `audio.d2bus.org.AudioBinding` register, + with `Provider/audio-pipewire` as an implementation selected by + `spec.providerRef`; +- `service_binding_separation`: only owner AudioService carries authority or is + exportable; AudioBinding always has Guest owner + same-Zone serviceRef; +- `resource_type_name_clean_break`: provider-qualified Service/Binding and + every AudioState ResourceType/serde/schema spelling are rejected and no alias + is emitted; +- `provider_field_isolation`: PipeWire fields are rejected in neutral + `spec.*` and `status.resource`, accepted only by the exact signed + `spec.provider`/`status.provider` schema or Provider config, and unknown + provider-envelope fields fail closed; +- `foreign_provider_ignored`: the PipeWire controller cannot reconcile or + status/finalizer-mutate a neutral audio resource selected for another + conforming Provider; +- `projection_ownerref_chain`: ResourceImport owns exactly one projection + AudioService, and AudioBinding references that Service rather than the import; +- `canonical_export_import_fields`: Export and Import carry the exact + Service/projection-schema/factory fields and matching fingerprints, an Export + never carries an Endpoint, and the obsolete field matrix is rejected; +- `core_projection_scope`: core creates/deletes only projection AudioService; +- `projection_no_pipewire`: fake EffectPort proves no portal/PipeWire-open + request can be emitted by a projection; +- `service_currency_propagation`: fake generations prove D091 ordering from + remote owner to import, projection Service, and Binding; +- `exclusive_mic_fair_queue`: fake clock and authenticated local/imported Zones + prove one active capture, fixed per-Zone/total bounds, per-Zone FIFO, + cross-Zone round-robin, contended lease expiry, mute-before-handoff, restart + fail-closed, and rejection of multiplexed/consent/priority surfaces while + speaker streams remain concurrent. + +These use fake stores, adapters, clocks, and streams in `tests/*.rs`. No +Service/Binding/ownerRef test is moved to integration. The only cross-Zone +transport case in `integration/` is `real_stream.rs`, which verifies actual +encrypted named-stream framing/credit behavior; all other stream semantics are +fast fake-stream tests. + +### `integration/README.md` content requirements + +The `integration/README.md` must document: + +1. Required host environment: Linux x86_64, PipeWire user session running + under the compositor user's UID, `vhost-device-sound` binary present, + libpipewire 1.x with `pw_node_set_param` support; +2. Required guest environment for guest-enforcement tests: running Guest with + virtio-snd device, PipeWire session, and GuestAudioAgent Process Ready; +3. How to run: `cargo test -p d2b-provider-audio-pipewire --test audio_e2e`; +4. How to skip PipeWire-dependent tests in CI without a live compositor + session: `D2B_SKIP_PIPEWIRE_LIVE=1 cargo test ...`; +5. Fixture setup for the fake-bus hermetic path (fake AudioMediator service, + fake GuestAudioAgent service, fake ComponentSession with fake + `SetGrant` `Applied|Queued|MicQueueFull`, `SetLevel`, and `AudioSet` + responses); +6. What `D2B_FIXTURES_FULL` provides for the minijail contract tests. +7. How to run `integration/real_stream.rs`, the only real cross-Zone stream + test; Service/Binding separation and ownerRef-chain coverage remain hermetic. + +### `README.md` content requirements + +The crate `README.md` must document: + +1. Provider identity: `Provider/audio-pipewire`; +2. Provider-neutral qualified ResourceTypes: + `audio.d2bus.org.AudioService` and `audio.d2bus.org.AudioBinding`, initially + implemented by `Provider/audio-pipewire`; no provider-qualified or + AudioState aliases; only owner AudioService is exportable and only + projection AudioService is core-generated for ResourceImport; +3. Controller and service components; one controller binary with deterministic + Service/Binding handlers, one owner host mediator binary, one guest agent binary; +4. Worker template: `vhost-user-sound-worker`; guest service template: `guest-audio-agent`; +5. Nix authoring schema (verbatim example from this spec); +6. `Provider/audio-pipewire.spec.config` options (`captureAlias`; regex + `^[a-z][a-z0-9-]*$`) and strict PipeWire field placement in + `spec.provider`/`status.provider`; +7. Dependency chain: vhost-device-sound v0.3.0, PipeWire/WirePlumber, + virtio-snd, libpipewire 1.x `pw_node_set_param`; +8. Security: zero capabilities, no PipeWire socket path in public surfaces, + AudioMediator receives declared pre-opened portal FD from user supervisor + (not ambient socket), FD routed via ProviderSupervisor to worker LaunchTicket, + projection Service never opens PipeWire, AudioBinding always references a + same-Zone Service and is never exported/projected, AudioBinding reconcile + creates no Volume or User resources (`Provider/audio-pipewire` + declares no Provider state Volume and its ProviderStateSet is empty), no broker + process lifecycle ops, no EphemeralProcess for enforcement, no wpctl binary + on host or guest, no runtime User.spec.groups mutation; ResourceExport uses + only canonical Service/projection-factory fields and never carries the + Service Endpoint; speakers remain multiplexed/mixed while microphone + capture is exclusive with the fixed bounded fair queue across authenticated + Zones; there is no consent/approval/priority override or concurrent capture + until a future spec defines a concrete consent authorization ResourceType + and verb; +9. Build: `cargo build -p d2b-provider-audio-pipewire`; +10. Test: `cargo test -p d2b-provider-audio-pipewire`; +11. Integration: see `integration/README.md`; +12. Standalone-repository consumption path. + +## Removal schedule + +| Artifact | Condition for removal | +| --- | --- | +| `nixos-modules/components/audio/host.nix` | After `v3-resource.nix` and `host-config.nix` deployed on all Zones and `make test-drift` passes | +| `nixos-modules/components/audio/guest.nix` | After `guest-config.nix` deployed and all Guests rebuilt with v3 module | +| `packages/d2bd/src/audio_dispatch.rs` | After `audio-binding-controller` passes e2e parity test and `d2bd` has no callers | +| `packages/d2bd/src/audio_host_controller.rs` | Same as `audio_dispatch.rs` | +| `packages/d2b-host/src/audio_argv.rs` | After `d2b-host` has no callers; confirmed by `cargo check -p d2b-host` | +| `packages/d2b-core/src/audio_policy.rs` | After no caller outside `d2b-provider-audio-pipewire` imports it; may remain as re-export shim | +| `d2b.site.audio.inputTargetNode` legacy option | After all Zones deployed v3 Provider; replaced by `Provider/audio-pipewire.spec.config.captureAlias` | +| `d2b.guestControl.wpctlPath` Nix option | Superseded by GuestAudioAgent libpipewire implementation; removed with guest.nix | +| guestd wpctl audio dispatch path | After all Guests have GuestAudioAgent deployed and e2e parity passes (ADR046-audio-011 removal proof) | +| `d2b.vms..audio.*` legacy Nix options | After all configured VMs use `d2b.zones..resources.` authoring | + +No removal is performed until the live successor is integrated, tested, and +confirmed by the removal proof listed in each work item. A current path is +never deleted speculatively. + +Per D094, each replaced current-code test is retired with an explicit +keep/adapt/move/delete disposition and a removal gate: the minimum reusable +semantic assertions migrate into this crate's hermetic `tests/`, and the old +duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest +entries are deleted once successor coverage and the removal proof pass — +updating `tests/layer1-jobs.json`, the closed gate manifests, the +flake/matrix/Nix-unit pins, the generated ledgers, and the CI workflow shards. +Old and new suites never run in parallel indefinitely. diff --git a/docs/specs/providers/ADR-046-provider-clipboard-wayland.md b/docs/specs/providers/ADR-046-provider-clipboard-wayland.md new file mode 100644 index 000000000..410df4b95 --- /dev/null +++ b/docs/specs/providers/ADR-046-provider-clipboard-wayland.md @@ -0,0 +1,1727 @@ +# ADR 0046 Provider dossier: clipboard-wayland + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-clipboard-wayland` | +| Version | 2 | +| Parent | ADR 0046 | +| Status | Proposed | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `Provider/clipboard-wayland` controller | +| Depends on | `ADR-046-provider-model-and-packaging`, `ADR-046-componentsession-and-bus`, `ADR-046-resources-host-guest-process-user`, `ADR-046-provider-state`, `ADR-046-nix-configuration`, `ADR-046-telemetry-audit-and-support`, `ADR-046-resource-reconciliation`, `ADR-046-core-controllers`, [ADR 0042](../../adr/0042-d2b-clipboard-authority-and-picker-split.md) | +| Supersedes | `nixos-modules/clipboard.nix` (v3 migration), ADR-046-provider-clipboard-wayland v1 | + +--- + +## Purpose + +`Provider/clipboard-wayland` delivers the split-trust clipboard bridge for +NixOS desktop microVM hosts running Wayland-only compositors. It exposes +controlled, policy-audited clipboard transfer between the host compositor +and one or more Guest VMs while upholding the invariants established in +ADR 0042: + +- clipboard bytes never appear in resources, status, audit, or logs; +- picker UI runs as an isolated EphemeralProcess without clipboard FDs; +- FD transfer uses SCM_RIGHTS over ComponentSession-authenticated transport only; +- all compositor integration flows through the mandatory `Provider/display-wayland` + typed dependency; +- no direct compositor socket (WAYLAND_DISPLAY, NIRI_SOCKET) connections from + clipboard-wayland components; +- no shared filesystem bridge, no per-Guest Unix socket groups or ACLs. + +This dossier is normative for the clipboard-wayland crate, Nix module, +ResourceType schema, ComponentSession services, RBAC, lifecycle, audit +format, and test requirements. + +--- + +## Scope + +- Host component: `clipd-host` user-domain service Process +- Host component: `clipboard-controller` system-domain controller Process +- Guest component: none (Guest clipboard access is mediated through + `Provider/display-wayland`'s wayland-proxy, which connects to clipd-host + via the `d2b.clipboard.bridge.v3` service) +- Typed dependency: `Provider/display-wayland` (alias: `display`) + +Out of scope: + +- DND drag-and-drop (no DataDeviceManager protocol) +- primary X11 selection (no primary selection protocol) +- cross-Zone clipboard (default-deny; requires explicit future ADR) +- clipboard history persistence (in-memory only) +- Wayland protocol client implementation (delegated to display-wayland) + +--- + +## Architecture overview + +``` + ┌─────────────────────────────────────────────┐ + │ Provider/clipboard-wayland │ + │ │ + │ Process/clipboard-controller │ + │ (system-minijail, system domain) │ + │ - owns ResourceType/clipboard-wayland-* │ + │ - serves d2b.clipboard.picker-coord.v3 │ + │ - creates EphemeralProcess/picker- │ + │ │ (bus: picker coord) │ + │ ▼ │ + │ Process/clipd-host │ + │ (system-systemd, user domain) │ + │ - serves d2b.clipboard.bridge.v3 │ + │ - serves d2b.clipboard.v3 (management) │ + │ - consumes d2b.display.host-clipboard.v3 │ + └────────────┬──────────────────────┬────────┘ + │ enrolled KK │ enrolled KK + d2b.clipboard.bridge.v3 d2b.display.host-clipboard.v3 + │ │ + ┌────────────▼──────┐ ┌───────────▼────────┐ + │ Provider/ │ │ Provider/ │ + │ display-wayland │ │ display-wayland │ + │ (wayland-proxy) │ │ (compositor svc) │ + └───────────────────┘ └────────────────────┘ +``` + +Key structural rules: + +1. **Core creates both Processes.** The `Provider lifecycle` handler in + `Provider/system-core` creates `Process/clipboard-controller` and + `Process/clipd-host` from the signed component templates in the + clipboard-wayland descriptor. The controller does not create itself or + the service Process. + +2. **Controller creates only operation-scoped EphemeralProcesses.** + `Process/clipboard-controller` creates `EphemeralProcess/picker-` per + paste request via the resource API. It owns no other child resources. + +3. **No shared filesystem IPC.** All inter-component communication uses + ComponentSession over private local transport with enrolled KK Noise + authentication. There are no bridge directories, per-Guest socket paths, + or SO_PEERCRED peer config in sealed configuration. + +4. **Core derives Provider status.** The core controller aggregates exact + child Process/EphemeralProcess statuses into `Provider/clipboard-wayland` + status. The clipboard-wayland controller does not write Provider status. + Per D088, that core-owned status uses universal top-level `status.*` plus + the Provider ResourceType-common `status.resource`; optional + `status.provider` is only for implementation observation (`providerRef`, + qualified immutable `schemaId`, semver `schemaVersion`, numeric + `observedProviderGeneration`, strict unknown-field-denied redacted `details` + ≤32 KiB registered/signed in the Provider manifest) and never duplicates + shared fields. Core writes all present layers atomically in one status + mutation. + +5. **Core delivers Guest lifecycle messages.** The orchestrator sends + authenticated `GuestStopped`, `GuestLocked`, `GuestDestroyed` messages + directly to the clipboard controller via ComponentSession. The controller + does not watch `Guest/*` resources broadly. + +--- + +## Provider resource spec + +Per D089, provider-owned clipboard ResourceTypes use their typed desired spec +as the ResourceType base spec (Layer 2): top-level `spec.*`, including +`spec.providerRef` where applicable. Any implementation-variant desired +settings use only the canonical Layer 3 `spec.provider = { schemaId, +schemaVersion, settings }` envelope, whose `settings` are +manifest-registered/signed, deny-unknown, bounded, versioned/digested, +validated against `spec.providerRef`, and forbidden to shadow base fields; +shared fields are promoted into the base spec. The owning Provider implements +the exact base spec/status schema version/fingerprint, accepts the canonical +minimal base Spec, and rejects an unsupported optional base capability only +through its signed capability matrix plus typed provider-neutral +`unsupported-capability`. `spec.provider` aligns with `status.provider`. The +`Provider` resource itself remains the D075 `{ artifactId, config }` +exception. + +### Operator-authored Nix form + +```nix +d2b.zones.dev.resources.clipboard-wayland = { + type = "Provider"; + spec = { + artifactId = "clipboard-wayland"; # registered in d2b.artifacts catalog + config = { + # Required: execution placement + hostExecutionRef = "Host/host-system"; + hostUserRef = "User/alice"; + + # Optional: typed dependency on display-wayland. + # null = host-only mode (no VM clipboard bridge; dependencyStatus.display = Absent) + displayWaylandRef = "Provider/display-wayland"; + + # Optional: signed picker EphemeralProcess template override. + # null = use the default picker bundled with clipboard-wayland. + pickerArtifactId = null; + + # Policy flags + policy = { + allowHostCapture = true; # host compositor → clipboard history + allowGuestCapture = true; # VM selection → clipboard history + requirePickerForPaste = true; # interactive picker before guest paste + suppressEcho = true; # loop suppression (default true) + crossZone.enable = false; # deny cross-Zone clipboard (default) + }; + + # Capability bounds + caps = { + maxHistoryEntries = 20; # LRU bound; [1, 200] + maxItemBytes = 8388608; # 8 MiB; [4096, 67108864] + maxTotalBytes = 67108864; # 64 MiB total; ≥ maxItemBytes + maxConcurrentFds = 32; # active SCM_RIGHTS FDs in flight; [1, 256] + maxGuestRatePerMin = 60; # materialization requests per Guest per minute + }; + + # Timing + ttl = { + pickerTimeoutSeconds = 20; # [5, 120] + fdWriteTimeoutSeconds = 30; # [5, 120] + hostEntrySeconds = 3600; # clipboard history TTL for host entries + guestEntrySeconds = 3600; # clipboard history TTL for guest entries + }; + }; + }; +}; +``` + +### YAML canonical form + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Provider +metadata: + name: clipboard-wayland + zone: dev + ownerRef: null # root config-owned; core configuration publication handler owns it +spec: + artifactId: clipboard-wayland + config: + hostExecutionRef: Host/host-system + hostUserRef: User/alice + displayWaylandRef: Provider/display-wayland # null for host-only mode + pickerArtifactId: null + policy: + allowHostCapture: true + allowGuestCapture: true + requirePickerForPaste: true + suppressEcho: true + crossZone: + enable: false + caps: + maxHistoryEntries: 20 + maxItemBytes: 8388608 + maxTotalBytes: 67108864 + maxConcurrentFds: 32 + maxGuestRatePerMin: 60 + ttl: + pickerTimeoutSeconds: 20 + fdWriteTimeoutSeconds: 30 + hostEntrySeconds: 3600 + guestEntrySeconds: 3600 +``` + +### Nix notes + +- `spec.config` mirrors the signed Provider component JSON Schema projection. + There are no `spec.settings`, `spec.componentPlacements`, or `spec.status` + fields in the Nix authoring form. +- `d2b.artifacts.clipboard-wayland` must be registered in the Zone artifact + catalog before this resource is declared. The `artifactId` is a plain + bounded ID, not a ResourceRef. +- `hostExecutionRef` and `hostUserRef` carry placement intent; framework + derives Process `executionRef`/`userRef` from these fields during + ProviderDeployment. +- `displayWaylandRef` must be a Ready `Provider/display-wayland` instance in + the same Zone when non-null. Framework resolves the typed `display` + dependency alias from this value. +- Ref validation rejects forward references to non-existent resources at + config-publication time. + +--- + +## Provider typed dependency + +The clipboard-wayland manifest declares one dependency alias: + +```text +alias: display +type: Provider +serviceContract: d2b.display.host-clipboard.v3 +optional: true +``` + +Zone config binds `display` to the `displayWaylandRef` value from +`spec.config`. When `displayWaylandRef` is null the dependency is absent; the +framework does not fail the Provider but sets `dependencyStatus.display = +Absent` in derived Provider status. + +When display-wayland is present and Ready, the framework sets up an enrolled +KK ComponentSession transport from `Process/clipd-host` (consumer) to the +display-wayland `Endpoint/`. The transport +uses Noise_KK_25519_ChaChaPoly_SHA256 with both static public keys registered +in the Zone identity registry before handshake. Neither component receives a +global route table. + +--- + +## Process resources + +### Process/clipboard-controller + +Core ProviderDeployment creates this Process from the signed +`clipboard-controller` component template. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: clipboard-controller + zone: dev + ownerRef: Provider/clipboard-wayland +spec: + providerRef: Provider/system-minijail + executionRef: + domain: system + userRef: null + processClass: controller + template: clipboard-controller + configRef: null + credentialRefs: [] + mounts: [] + sandbox: + namespaceClasses: [mount, ipc, uts, network] + capabilityClasses: [] + seccompClass: strict + noNewPrivileges: true + startRoot: false + readOnlyRoot: true + environmentClass: minimal + budget: + cpu: + request: "50m" + limit: "500m" + memory: + request: "32Mi" + limit: "64Mi" + pids: + limit: 64 + fds: + limit: 256 + networkUsage: null + deviceUsage: [] + telemetry: + metricsEnabled: true + tracingEnabled: true + logLevel: info + sensitiveLabels: false + desiredLifecycle: running + restartPolicy: + class: on-failure + backoffBase: "1s" + backoffMax: "60s" + backoffMultiplier: 2.0 + maxRestarts: null + resetAfter: "300s" + readiness: + initialDelay: "0s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined + healthCheck: + enabled: true + interval: "60s" + timeout: "5s" + failureThreshold: 3 + class: provider-defined + adoptionPolicy: adopt-on-restart + drainTimeout: "30s" +``` + +The clipboard-controller: + +- serves the internal `d2b.clipboard.picker-coord.v3` service on + `Endpoint/clipboard-picker-coord`; +- creates `EphemeralProcess/picker-` resources via the resource API per paste + request that requires interactive confirmation; +- receives `GuestStopped`, `GuestLocked`, `GuestDestroyed`, `GuestSuspended` + lifecycle messages from core orchestrator via ComponentSession and forwards + corresponding `PurgeZone`/`SuspendZone` instructions to `clipd-host` over + the `d2b.clipboard.picker-coord.v3` service; +- writes only bounded, redacted operational observations through the optimistic + status writer for `Provider/clipboard-wayland` and related Operations; +- does not own, export, reconcile, or mount any Provider state Volume. Under + D087 its ProviderStateSet is empty because clipboard operational state is + derivable from status, the core Operation ledger, and external observation. + +### Process/clipd-host + +Core ProviderDeployment creates this Process from the signed +`clipd-host` component template. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: clipd-host + zone: dev + ownerRef: Provider/clipboard-wayland +spec: + providerRef: Provider/system-systemd + executionRef: + domain: user + userRef: + processClass: service + template: clipd-host + configRef: null + credentialRefs: [] + mounts: [] + sandbox: + namespaceClasses: [] # user-domain; inherits user namespace + capabilityClasses: [] + seccompClass: strict + noNewPrivileges: true + startRoot: false + readOnlyRoot: true + environmentClass: provider-defined # system-systemd user scope provides XDG_RUNTIME_DIR + budget: + cpu: + request: "100m" + limit: "1000m" + memory: + request: "64Mi" + limit: "256Mi" + pids: + limit: 128 + fds: + limit: 1024 + networkUsage: null + deviceUsage: [] + telemetry: + metricsEnabled: true + tracingEnabled: true + logLevel: info + sensitiveLabels: false + desiredLifecycle: running + restartPolicy: + class: on-failure + backoffBase: "2s" + backoffMax: "120s" + backoffMultiplier: 2.0 + maxRestarts: null + resetAfter: "300s" + readiness: + initialDelay: "1s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined + healthCheck: + enabled: true + interval: "30s" + timeout: "5s" + failureThreshold: 3 + class: provider-defined + adoptionPolicy: adopt-on-restart + drainTimeout: "30s" +``` + +`clipd-host`: + +- serves `d2b.clipboard.bridge.v3` on `Endpoint/clipboard-bridge` (consumed by + display-wayland's wayland-proxy); +- serves `d2b.clipboard.v3` management API on `Endpoint/clipboard-management` + (consumed by authorized CLI/operator sessions); +- opens one enrolled KK ComponentSession to display-wayland's + `d2b.display.host-clipboard.v3` service to receive host selection events, + focus attribution data, and to publish selections back to the host compositor; +- opens one enrolled KK ComponentSession to the clipboard-controller's + `Endpoint/clipboard-picker-coord` for picker session dispatch and result + delivery; +- does not connect to WAYLAND_DISPLAY or NIRI_SOCKET directly; +- does not own ResourceTypes; +- holds clipboard history in bounded process memory only (never in any Volume). + +--- + +## EphemeralProcess/picker-session + +`Process/clipboard-controller` creates one `EphemeralProcess/picker-` +per paste request that requires interactive user confirmation. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: EphemeralProcess +metadata: + name: picker-3f7a9c12 + zone: dev + ownerRef: Process/clipboard-controller # controller-owned, not Provider-owned +spec: + providerRef: Provider/system-systemd + executionRef: + domain: user + userRef: + processClass: worker + template: picker-session + configRef: null # metadata arrives via named stream from controller + credentialRefs: [] + mounts: [] + sandbox: + namespaceClasses: [] + capabilityClasses: [] + seccompClass: strict + noNewPrivileges: true + startRoot: false + readOnlyRoot: true + environmentClass: minimal # WAYLAND_SOCKET FD pre-opened by ProviderSupervisor (FD number only) + budget: + cpu: + request: "50m" + limit: "500m" + memory: + request: "16Mi" + limit: "64Mi" + pids: + limit: 32 + fds: + limit: 64 + networkUsage: null + deviceUsage: [] + telemetry: + metricsEnabled: false # worker; no per-picker metrics stream + tracingEnabled: true + logLevel: warn + sensitiveLabels: false + startDeadline: "10s" + runtimeDeadline: "120s" # picker must complete within 2 min + successfulTtl: "1h" + failedTtl: "24h" + incidentHold: false +``` + +### Picker invariants + +- `processClass` is always `worker`; a controller or service EphemeralProcess + is rejected at spec admission. +- Picker receives **no clipboard FDs**, no clipboard bytes, no compositor + credentials, and no NIRI_SOCKET path in sealed config or as SCM_RIGHTS + attachments. +- Picker metadata (source attribution, MIME list hint, operation ID) arrives + via a bounded named stream from `Process/clipboard-controller` over an + inherited-socketpair ComponentSession established at spawn time. +- Picker Wayland access: `d2b.display.host-clipboard.v3` on display-wayland + exposes a fixed presentation-only compositor portal. At picker spawn, + ProviderSupervisor pre-opens a restricted Wayland connection FD backed by + this portal and passes it to the picker with `WAYLAND_SOCKET` set to the + FD number (not a socket path). GTK4 connects via this FD. The portal + exposes only the presentation subset of the compositor protocol; + `zwlr_data_control_manager_v1` and all clipboard-manager globals are absent. + No compositor credential, socket path, or WAYLAND_DISPLAY path reaches the + picker process. +- GTK4 and all its runtime dependencies are packaged in the `picker-session` + artifact's Nix closure. There is no ambient host GTK4 dependency. +- Picker sends exactly one `Select(item_digest)` or `Cancel` message back to + the controller via the same stream. No clipboard content transits this stream. +- `successfulTtl: "1h"` — completed/cancelled picker retained 1 hour for + debug correlation. +- `failedTtl: "24h"` — failed picker retained 24 hours for incident hold. +- Controller writes status to the EphemeralProcess resource; never to + `Provider/clipboard-wayland` directly. + +--- + +## Endpoint resources (D092) + +`Provider/clipboard-wayland` declares standard `Endpoint` base-schema +conformance. Stable clipboard service identities are owned `Endpoint` resources +with `producerRef`; they are not inline `Process.spec` fields. Consumers use +`Endpoint/` references. Endpoint spec/status/CLI/audit/telemetry never +include raw socket paths, compositor locators, clipboard bytes, MIME payloads, +fds, or credentials. Resolution occurs only through an authorized +EffectPort/LaunchTicket; unauthorized resolution returns +`endpoint-resolve-denied`. Producer restart bumps +`Endpoint.status.endpointGeneration`, which triggers `dependency-changed` for +consumers. + +Representative owned Endpoint resources: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: clipboard-picker-coord + zone: dev + ownerRef: Provider/clipboard-wayland +spec: + providerRef: Provider/clipboard-wayland + producerRef: Process/clipboard-controller + endpointClass: service + transport: unix + purpose: clipboard-wayland.d2bus.org/picker-coordination + serviceFingerprint: clipboard-wayland.d2bus.org/picker-coord.v3 + locality: host-local + visibility: zone + attachmentPolicy: component-session + consumerPolicy: + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: clipboard-bridge + zone: dev + ownerRef: Provider/clipboard-wayland +spec: + providerRef: Provider/clipboard-wayland + producerRef: Process/clipd-host + endpointClass: service + transport: unix + purpose: clipboard-wayland.d2bus.org/bridge + serviceFingerprint: clipboard-wayland.d2bus.org/bridge.v3 + locality: host-local + visibility: zone + attachmentPolicy: component-session + consumerPolicy: + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: clipboard-management + zone: dev + ownerRef: Provider/clipboard-wayland +spec: + providerRef: Provider/clipboard-wayland + producerRef: Process/clipd-host + endpointClass: service + transport: unix + purpose: clipboard-wayland.d2bus.org/management + serviceFingerprint: clipboard-wayland.d2bus.org/management.v3 + locality: host-local + visibility: zone + attachmentPolicy: component-session + consumerPolicy: + allowedSubjects: [User/alice] + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +## Retained opaque handles + +- pidfds: kernel supervision handles for Process resources, not stable service + identities. +- Per-connection/session handles: selection tokens, picker IDs, and + ComponentSession IDs are high-churn and scoped to one interaction. +- Named streams: clipboard data/control streams carry bounded records or FDs and + never represent a stable managed endpoint. +- `OwnedTransport`: authenticated session transport ownership remains an + in-memory capability. +- fd indexes: `WAYLAND_SOCKET` and transfer-FD slots are LaunchTicket-local + descriptor numbers and stay opaque. + +--- + +## ComponentSession services + +### d2b.display.host-clipboard.v3 + +**Served by:** `Provider/display-wayland` (compositor service component) +**Consumed by:** `Process/clipd-host` +**Profile:** Enrolled KK (`Noise_KK_25519_ChaChaPoly_SHA256`) +**Transport:** enrolled Zone transport established by framework on display-wayland +dependency readiness + +Methods and named streams: + +| Name | Direction | Class | Description | +| --- | --- | --- | --- | +| `SubscribeSelectionChanges` | clipd-host → display-wayland → clipd-host | named stream | Stream of `HostSelectionChangedEvent` (MIME type list, byte hint, source attribution; no payload bytes). | +| `SubscribeFocusEvents` | clipd-host → display-wayland → clipd-host | named stream | Stream of `HostFocusEvent` (app_id, title, output, workspace; no PII or raw window content). | +| `MaterializeData` | clipd-host → display-wayland | request+attachment | Request data for a given MIME type from current host selection. Response carries attachment class `host-selection-transfer-fd` (one `O_RDONLY` FD, read-once, validated by fstat/fstatfs). | +| `AnnounceSelection` | clipd-host → display-wayland | request | Declare clipd-host as host selection owner for given MIME type list. Returns opaque `SelectionToken`. | +| `ServeDataRequest` | display-wayland → clipd-host | named stream | Reverse stream: display-wayland sends `DataRequest(token, mime_type)` when host compositor needs data from clipd's announced selection. clipd sends `DataResponse` with attachment class `host-selection-supply-fd`. | +| `RevokeSelection` | clipd-host → display-wayland | request | Relinquish the announced selection (SelectionToken required). | + +Clipboard bytes appear only as SCM_RIGHTS attachment FDs. They never appear +in method arguments, named stream frames, status fields, audit payloads, or +traces. + +### d2b.clipboard.bridge.v3 + +**Served by:** `Process/clipd-host` +**Consumed by:** `Provider/display-wayland` (wayland-proxy component) +**Profile:** Enrolled KK +**Transport:** inherited private transport via framework enrollment on dependency + +This service carries the Guest ↔ Host clipboard bridge. wayland-proxy calls +this service when a Guest selection becomes available or when a paste into a +Guest is authorized. + +Methods: + +| Name | Direction | Class | Description | +| --- | --- | --- | --- | +| `NotifyGuestSelection` | wayland-proxy → clipd-host | request | Notify clipd-host that a Guest VM has a new selection. Carries: `zone_id`, `guest_name`, MIME type list, byte hint, source attribution. No clipboard payload. Returns `EntryToken`. | +| `FetchGuestData` | clipd-host → wayland-proxy | request+attachment | clipd-host requests clipboard data for a given `EntryToken` and `mime_type`. Response carries attachment class `clipboard-transfer-fd` (one `O_RDONLY` FD). Validated: fstat, fstatfs, MSG_CMSG_CLOEXEC, MSG_CTRUNC = fail-closed. | +| `AuthorizeGuestPaste` | clipd-host → wayland-proxy | request+attachment | clipd-host authorizes a paste to a given Guest. Carries `entry_token`, `mime_type`, attachment class `clipboard-transfer-fd` (one `O_WRONLY` FD, write-once, size-limited). | +| `CancelEntry` | clipd-host → wayland-proxy | request | Cancel a pending `EntryToken`; wayland-proxy releases associated selection offer. | + +### d2b.clipboard.picker-coord.v3 + +**Served by:** `Process/clipboard-controller` +**Consumed by:** `Process/clipd-host` +**Profile:** Local NN (same Host, inherited socketpair) +**Transport:** inherited socketpair at clipd-host startup + +Methods: + +| Name | Direction | Class | Description | +| --- | --- | --- | --- | +| `RequestPickerSession` | clipd-host → controller | request | Request picker confirmation for a pending paste. Carries: `source_zone_id`, `dest_guest_name`, MIME type list (no payload, no FDs). Returns `picker_session_id`. | +| `NotifyPickerResult` | controller → clipd-host | callback | Controller calls back when picker EphemeralProcess completes. Carries: `picker_session_id`, outcome (`Selected(item_digest)` or `Cancelled` or `TimedOut`). | +| `CancelPickerSession` | clipd-host → controller | request | Cancel a pending picker (Guest disconnected, entry expired, etc.). | +| `PurgeZoneClipboard` | controller → clipd-host | request | Instructs clipd-host to purge all history entries for a given zone/guest. Sent when core delivers `GuestStopped`/`GuestDestroyed`. | +| `SuspendZoneClipboard` | controller → clipd-host | request | Suspend paste authorization for a given zone/guest. Sent on `GuestLocked`/`GuestSuspended`. | + +### d2b.clipboard.v3 + +**Served by:** `Process/clipd-host` +**Consumed by:** authorized CLI sessions and operator tooling +**Profile:** enrolled KK or local NN (operator tool subject) +**Authorization:** `Role/clipboard-admin` (arm/disarm/delete), `Role/clipboard-viewer` +(list/status/events) + +Methods: + +| Name | Description | +| --- | --- | +| `ArmBridge` | Enable clipboard capture from host and/or guests. | +| `DisarmBridge` | Disable capture; pending FDs are closed. | +| `ListHistory` | Return bounded list of clipboard history metadata entries. No payload bytes. | +| `DeleteEntry` | Delete a specific history entry by opaque ID. | +| `GetStatus` | Return current operational status (Ready/Degraded/ArmedHost/ArmedGuest). | +| `SubscribeEvents` | Named stream of clipboard lifecycle events (no payload bytes). | + +--- + +## MIME and FD safety model + +### MIME allowlist + +Only the following MIME types are accepted into clipboard history. All other +MIME types produce audit event `reason: mime-rejected`. + +```text +text/plain;charset=utf-8 +text/plain +text/html +image/png +``` + +The allowlist is a closed enum defined in the clipboard-wayland descriptor's +signed policy projection. It cannot be extended at runtime. + +### Secret-hint MIME detection + +The following MIME types trigger automatic suppression (entry not added to +history; audit reason: `secret-hint-mime`): + +```text +x-kde-passwordManagerHint +application/x-password +x-secret-content +``` + +Detection is on the source MIME type list before any FD transfer. No clipboard +payload inspection is performed. + +### FD safety validation + +Every SCM_RIGHTS-received FD is validated before use: + +1. `fstat(2)` — confirm `st_nlink == 1`, `st_size ≤ maxItemBytes`, expected + `st_mode` (regular file or pipe as declared by attachment class). +2. `fstatfs(2)` — confirm filesystem type is not a network filesystem. +3. `MSG_CMSG_CLOEXEC` — set at `recvmsg` call; validated before use. +4. `MSG_CTRUNC` — if set, the ancillary message was truncated; fail closed, + do not proceed with partial FD set. +5. Size guard — read at most `maxItemBytes` bytes before treating as oversized + and closing the FD without adding an entry. + +FDs are never duplicated after validation, never sent downstream, and are +closed after exactly one read/write operation. + +### Backpressure and cancellation + +Named streams use credit-based flow control from ComponentSession. clipd-host +issues one credit per `maxConcurrentFds` slots. When all FD slots are in use, +clipd-host withholds credits on the `d2b.clipboard.bridge.v3` bridge stream; +wayland-proxy cannot enqueue further offers until credits are restored. This +prevents unbounded FD accumulation. + +Every pending FD transfer has a `fdWriteTimeoutSeconds` deadline enforced via +ComponentSession cancellation token. Expiry closes the FD and emits audit +event `reason: fd-write-timeout`. + +--- + +## Clipboard policy model + +### Direction and allowlist + +| Direction | Controlled by | Default | +| --- | --- | --- | +| Host compositor → clipboard history | `policy.allowHostCapture` | `true` | +| Guest VM → clipboard history | `policy.allowGuestCapture` | `true` | +| Clipboard history → Guest VM paste | `policy.requirePickerForPaste` | `true` | +| Clipboard history → Host compositor | automatic on selection announcement | always | +| Cross-Zone clipboard | `policy.crossZone.enable` | `false` (deny) | + +`requirePickerForPaste` is enforced by the clipboard-controller via the picker +session dispatch protocol. If `false`, paste is authorized directly by +clipd-host without picker confirmation (for automated/headless environments). + +### Loop suppression + +`policy.suppressEcho` (default `true`) activates per-entry source tracking. +When clipd-host publishes a selection to the host compositor and that same +selection arrives back as a new host selection event (same content digest and +source attribution within a 2-second window), it is silently dropped without +creating a new history entry. Audit event `reason: suppressed-echo`. + +### Same-Guest route + +When the source and destination of a paste are the same Guest, the same-MIME +path is preserved: rich MIME types (text/html, image/png) are preserved +without downgrade. Cross-Guest paste always uses the MIME allowlist +intersection. + +### Rate limiting + +clipd-host enforces a per-Guest materialization rate limit of +`caps.maxGuestRatePerMin` requests per minute using a per-Guest sliding +window. Requests that exceed the rate are rejected with audit reason +`rate-limit-exceeded` and d2b-bus error `resource-exhausted`. This prevents +a compromised Guest from exhausting FD slots. + +--- + +## ProviderStateSet + +A `ProviderStateSet` is the set of all Volume resources in the Zone whose +`metadata.ownerRef` resolves to `Provider/clipboard-wayland`. It is a +query-time logical grouping, not a ResourceType or stored artifact. + +```text +ProviderStateSet(zone, clipboard-wayland) = + { v : Volume | v.metadata.zone == zone + && v.metadata.ownerRef == "Provider/clipboard-wayland" } +``` + +Under D087, `Provider/clipboard-wayland` declares **no Provider state Volume**. +Its ProviderStateSet is therefore empty: + +```text +ProviderStateSet(zone, clipboard-wayland) = {} +``` + +The two formerly-declared component state entries fail the storage-need test: +their operational state is bounded, non-secret, and derivable from +`Provider/clipboard-wayland.status`, the core Operation ledger, component +readiness, and external compositor/guest observation after restart. Provider +configuration is delivered exclusively via the sealed LaunchTicket config FD at +Process start — not through any Volume. + +The clipboard-wayland controller (`Process/clipboard-controller`) does not +create, own, watch, update, delete, or mount Provider state Volumes, and does +not add `Volume` to any exported or reconciled ResourceType list. No dedicated +state-layout `User/` principals, identity markers, migration workers, +or reset/destroy hooks are declared for component state. + +Clipboard history remains bounded in-memory state in `clipd-host`'s process +heap and is never written to a Volume or status. Clipboard bytes, entry data, +terminal or notification bytes, secrets, paths, socket paths, FDs, PIDs, unit +names, and authority-conferring handles are excluded from every status layer, +audit, metrics, and Operations. Status is revisioned, optimistic status-writer +controlled, RBAC-readable, redacted, written only on material change, and +re-verified against external reality after restart; oversize status is rejected +with `status-oversize`. + +There is no bootstrap state-Volume mechanism; the previous bootstrap exception +(D086, superseded by D087) does not apply. This dossier declares no runtime +socket/config/tmpfs Volume either: Wayland access is carried by display-wayland +and ProviderSupervisor as pre-opened FDs, not filesystem mounts. +--- + +## RBAC + +The clipboard-controller creates and manages these RBAC resources as part of +its reconcile loop. Core does not pre-create them. The controller manages only +service-RBAC (ComponentSession roles and bindings); it does not create or manage +any Volume RBAC, does not hold a Volume reconciler role, and does not interact +with Volume resources in any way. + +### Roles + +| Role name | Verbs | Resource targets | +| --- | --- | --- | +| `clipboard-admin` | `connect`, `invoke`, `stream` | `d2b.clipboard.v3` (ArmBridge, DisarmBridge, DeleteEntry) | +| `clipboard-viewer` | `connect`, `invoke`, `stream` | `d2b.clipboard.v3` (ListHistory, GetStatus, SubscribeEvents) | +| `clipboard-bridge-peer` | `connect`, `stream` | `d2b.clipboard.bridge.v3` | +| `clipboard-picker-worker` | `connect`, `stream` | `d2b.clipboard.picker-coord.v3` (picker result stream only) | + +### RoleBindings + +| Binding name | Subject | Role | +| --- | --- | --- | +| `display-wayland-bridge` | `Provider/display-wayland` | `clipboard-bridge-peer` | +| `host-admin-clipboard` | `User/alice` | `clipboard-admin` | +| `picker-session-worker` | `Process/picker-*` (by template label) | `clipboard-picker-worker` | + +All RoleBindings are Zone-local and scoped to the clipboard-wayland Provider +instance. The `Process/picker-*` binding uses a label selector matching the +`clipboard-picker-worker` processClass from the `picker-session` template. + +--- + +## Lifecycle phases and conditions + +Provider lifecycle is derived by core from child Process statuses. The +clipboard-wayland controller does not write `Provider/clipboard-wayland.status`. + +### Provider phases (core-derived) + +| Phase | Condition | +| --- | --- | +| `Pending` | Either child Process is not yet Ready | +| `Ready` | Both `clipboard-controller` and `clipd-host` Processes are Ready; `display-wayland` dependency is Ready or Absent | +| `Degraded` | One Process is Degraded or display-wayland is down in non-host-only mode | +| `Failed` | Any required Process has failed beyond maxRestarts | +| `Disabled` | `desiredLifecycle: stopped` set on both Processes | + +### Controller reconcile states + +``` +Initial → AwaitingDependency → Ready → Degraded (transient) → Ready + ↘ Failed (terminal) +``` + +- `AwaitingDependency`: display-wayland not yet Ready (if non-null); waiting + for `Endpoint/clipboard-bridge` to report Ready. +- `Ready`: enrolled KK sessions established; `Endpoint/clipboard-bridge`, + `Endpoint/clipboard-management`, and `Endpoint/clipboard-picker-coord` are + accepting authorized connections. +- `Degraded`: display-wayland transiently unavailable; clipd-host is restarting; + new paste requests held in bounded queue (`maxRestarts` not yet exceeded). +- `Failed`: Process exhausted maxRestarts; EphemeralProcess cleanup handler + detects unrecoverable picker failures. + +D091 currency and upgrade: the clipboard-wayland controller implements +`assess_update`, `plan_upgrade`, and `execute_upgrade` for its qualified +ResourceTypes and semantic clipboard sessions. A `ProviderGenerationChanged`, +`ArtifactChanged`, `DependencyChanged`, or `SpecChanged` reason populates +universal `status.update` with +`UpdateAvailable` or `UpgradeRequired`; disruptive changes MUST return +`UpgradeRequired` rather than being applied in place, while non-disruptive +changes reconcile normally. These currency fields are universal/ResourceType +base fields, never `status.provider`. Upgrades recycle only the clipboard realization +(owned `Process` resources, endpoints, and sessions) with `disruption` set to +`Reload`, `Restart`, or `Recycle`; durable config is preserved, dependent +sessions and attachments are drained and restarted by the dependency-aware +planner, and owned ephemeral session state remains process memory. No clipboard +content bytes, terminal bytes, notification content bytes, session bytes, +secrets, paths, or handles may appear in `status.update`. + +D090 expedited reconcile: Create, UpdateSpec, and Delete requests that set +`waitForReconcile` perform no external effect, finalizer mutation, or status +mutation until Core supplies a typed `CommittedRevisionProof` +`{resourceUid,generation,revision,operationId}`. Abort produces no effect; a +durable commit is never rolled back on later reconcile timeout. The response is +the committed object plus one-pass projected layered status, a disposition +(`Converged`, `Progressing`, `Blocked`, `UpgradeRequired`, or `Failed`), and +`statusPersistence` (`pending` or `committed`); effect idempotency keys derive +from `(UID,generation,revision,operationId)` in the same per-resource +single-flight priority lane. + +### clipd-host startup sequence + +1. Open inherited-socketpair ComponentSession to `clipboard-controller` + (`d2b.clipboard.picker-coord.v3`); receive and validate sealed config. +2. Open enrolled KK ComponentSession to display-wayland + (`d2b.display.host-clipboard.v3`); subscribe `HostSelectionChangedEvent` + and `HostFocusEvent` streams. +3. Resolve the backing locator for `Endpoint/clipboard-bridge` through the + LaunchTicket; signal readiness via provider-defined mechanism to + system-systemd. +4. Resolve the backing locator for `Endpoint/clipboard-management`. +5. Enter main event loop: bridge requests, host selection events, picker coord + callbacks, management API calls. + +--- + +## Guest lifecycle handling + +Core delivers the following authenticated messages to `Process/clipboard-controller` +via ComponentSession when Guest lifecycle events occur: + +| Core message | Controller action | +| --- | --- | +| `GuestStarted(guest_name, zone_id)` | Arm clipboard tracking for that guest; notify clipd-host via `PurgeZoneClipboard` (idempotent) to initialize per-guest state. | +| `GuestStopped(guest_name, zone_id)` | Cancel all in-flight picker sessions for that guest; call `PurgeZoneClipboard` on clipd-host; release associated `EntryToken`s. | +| `GuestLocked(guest_name, zone_id)` | Call `SuspendZoneClipboard` on clipd-host; paste authorization denied while suspended. | +| `GuestUnlocked(guest_name, zone_id)` | Resume paste authorization for the guest. | +| `GuestDestroyed(guest_name, zone_id)` | `PurgeZoneClipboard` + revoke all associated history entries; release any pending picker sessions. | +| `GuestSuspended(guest_name, zone_id)` | Same as `GuestLocked`. | +| `GuestResumed(guest_name, zone_id)` | Same as `GuestUnlocked`. | + +The controller does not subscribe to `Guest/*` resource watch events. It +receives only the authenticated lifecycle messages forwarded by core. + +--- + +## Picker session full lifecycle + +``` +1. Guest initiates paste (wayland-proxy detects DataOffer) +2. wayland-proxy calls NotifyGuestSelection on d2b.clipboard.bridge.v3 + → clipd-host receives EntryToken +3. policy.requirePickerForPaste == true: + clipd-host calls RequestPickerSession on d2b.clipboard.picker-coord.v3 + → controller creates EphemeralProcess/picker- + spec.startDeadline = "10s", runtimeDeadline = "120s" +4. picker binary starts; receives metadata stream from controller: + { operation_id, source_zone, dest_guest, mime_list_hint } + (no FDs, no payload, no compositor credentials) +5. picker renders GTK4 UI via ProviderSupervisor pre-opened WAYLAND_SOCKET FD + (presentation-only portal; no clipboard-manager globals) +6. user selects item or cancels (or runtimeDeadline expires → TimedOut) +7. picker sends Select(item_digest)|Cancel back to controller stream + picker exits 0 (Select/Cancel) or non-zero (error) +8. controller calls NotifyPickerResult(session_id, outcome) on clipd-host +9. on Selected: + clipd-host calls AuthorizeGuestPaste(entry_token, mime_type, fd) + on d2b.clipboard.bridge.v3 + → wayland-proxy writes clipboard data to Guest selection +10. on Cancelled/TimedOut: + clipd-host calls CancelEntry(entry_token) on d2b.clipboard.bridge.v3 + → wayland-proxy releases selection offer +11. EphemeralProcess/picker- enters Succeeded|Failed phase; + cleanupEligibleAt set per successfulTtl/failedTtl +``` + +--- + +## Error taxonomy + +| Error code | Description | Retryable | +| --- | --- | --- | +| `mime-rejected` | MIME type not in MIME allowlist | No | +| `secret-hint-mime` | Clipboard content matches secret-hint MIME pattern | No | +| `item-too-large` | Clipboard item exceeds `maxItemBytes` | No | +| `total-quota-exceeded` | History total exceeds `maxTotalBytes`; LRU eviction ran but insufficient | No | +| `fd-count-exceeded` | `maxConcurrentFds` in-flight FDs reached | Yes (backoff) | +| `fd-write-timeout` | FD write not completed within `fdWriteTimeoutSeconds` | Yes (limited) | +| `msg-ctrunc` | `MSG_CTRUNC` detected on recvmsg; partial ancillary data | No | +| `fd-safety-violation` | fstat/fstatfs validation failed | No | +| `rate-limit-exceeded` | Per-Guest materialization rate exceeded | Yes (backoff) | +| `picker-timed-out` | Picker runtimeDeadline expired without user action | No | +| `picker-cancelled` | User cancelled picker | No | +| `picker-start-failed` | EphemeralProcess/picker failed startDeadline | Yes (limited) | +| `echo-suppressed` | Selection suppressed as host echo | No | +| `dependency-absent` | display-wayland dependency is Absent; bridge operations rejected | No | +| `dependency-degraded` | display-wayland transiently unavailable | Yes | +| `zone-suspended` | Paste rejected because zone is in Suspended state | No | +| `unauthorized` | RBAC check denied the operation | No | +| `cross-zone-denied` | `crossZone.enable == false` | No | + +--- + +## Audit format + +Audit events are emitted by `clipd-host` to the Zone audit sink via the +`d2b.audit.v3` service using the `fail-closed` per-Zone queue model from +ADR 0042. If the queue is full, new clipboard operations are rejected rather +than proceeding without an audit record. + +### AuditEvent schema + +```rust +pub struct ClipboardAuditEvent { + pub operation_id: Uuid, // unique per operation + pub event_type: ClipboardEventType, + pub source_zone_id: Option, // max 63 chars; no path/payload + pub dest_zone_id: Option, + pub mime_type: Option, // from MIME allowlist only; null if rejected before check + pub byte_hint: Option, // discretized: <1K, 1–64K, 64K–1M, >1M; never exact size + pub reason: ReasonCode, + pub attribution_quality: AttributionQuality, + pub occurred_at: OffsetDateTime, + pub operation_duration_ms: u32, +} + +pub enum ClipboardEventType { + HostCapture, + GuestCapture, + PasteAuthorized, + PasteRejected, + EchoSuppressed, + EntryExpired, + EntryPurged, + BridgeArmed, + BridgeDisarmed, + PickerSessionStarted, + PickerSessionCompleted, + PickerSessionFailed, +} + +// closed enum — no fallback/unknown variant that accepts arbitrary strings +pub enum ReasonCode { + Ok, + MimeRejected, + SecretHintMime, + ItemTooLarge, + TotalQuotaExceeded, + FdCountExceeded, + FdWriteTimeout, + MsgCtrunc, + FdSafetyViolation, + RateLimitExceeded, + PickerTimedOut, + PickerCancelled, + PickerStartFailed, + EchoSuppressed, + DependencyAbsent, + DependencyDegraded, + ZoneSuspended, + Unauthorized, + CrossZoneDenied, +} + +pub enum AttributionQuality { + Verified, // focus window attribution confirmed via display-wayland FocusEvent + Approximate, // focus changed between selection and capture + Unknown, // no attribution data available +} + +pub enum SizeBucket { + Lt1K, + K1To64K, + K64ToM1, + GtM1, +} +``` + +**Redaction rules:** + +- Clipboard bytes, MIME content, raw FD data: never in any field. +- Zone IDs are bounded opaque IDs; no host-visible user data. +- `byte_hint` is a discrete bucket, never an exact byte count. +- Guest names, window titles, app_id, and user-visible text: excluded. +- PID, cgroup path, socket path, pidfd FD numbers: never in audit. +- Attribution quality is a coarse enum; no raw window manager metadata. + +--- + +## Telemetry and OTEL + +All metrics use stable closed label sets. No clipboard bytes, content hashes, +window titles, user identifiers, or zone credentials appear in spans or metrics. + +### Metrics + +| Metric name | Type | Labels | Description | +| --- | --- | --- | --- | +| `clipboard_operations_total` | Counter | `operation`, `reason`, `direction` | Clipboard operations by type and outcome. | +| `clipboard_active_fds` | Gauge | `direction` | Currently in-flight SCM_RIGHTS FDs. | +| `clipboard_history_entries` | Gauge | `source` (`host`/`guest`) | Current history entry count. | +| `clipboard_history_bytes_total` | Gauge | — | Total in-memory history bytes. | +| `clipboard_picker_sessions_total` | Counter | `outcome` (`selected`/`cancelled`/`timed_out`/`failed`) | Picker session outcomes. | +| `clipboard_mime_rejections_total` | Counter | `reason` | MIME rejections by reason code. | + +`operation` labels: `host-capture`, `guest-capture`, `paste-authorized`, +`paste-rejected`, `purge`, `suspend`, `arm`, `disarm`. + +`direction` labels: `host-to-guest`, `guest-to-host`. + +No per-Zone, per-Guest, or per-user dimension labels on metrics (cardinality +control). + +### Spans + +- One span per clipboard operation (correlation with `operation_id`). +- Span attributes: `operation_type`, `reason_code`, `mime_type` (from + allowlist or `rejected`), `attribution_quality`, `direction`. +- Excluded span attributes: byte sizes, content hashes, zone IDs, guest names, + user names, process IDs, socket paths, FD numbers. + +--- + +## Nix catalog registration + +```nix +# In the host NixOS configuration (or d2b Zone config): +d2b.artifacts.clipboard-wayland = { + package = inputs.d2b.packages.${system}.clipboard-wayland; + type = "provider"; + # artifactId is derived from package metadata; no manual ID needed. +}; +``` + +The `artifactId = "clipboard-wayland"` in Provider spec resolves through the +offline catalog compiled from this artifact declaration. The Nix store path is +never exposed in resource spec, status, or audit. + +### Migration from nixos-modules/clipboard.nix + +| Old option | New mechanism | +| --- | --- | +| `d2b.clipboard.enable` | Declare `d2b.zones..resources.clipboard-wayland` with `type = "Provider"` | +| `d2b.clipboard.bridgeSocketDir` | Removed: no bridge socket directory; FDs flow via ComponentSession | +| `d2b.clipboard.allowedMimeTypes` | Closed MIME allowlist in Provider descriptor; not operator-configurable | +| `d2b.clipboard.pickerCommand` | Replaced by `spec.config.pickerArtifactId` (optional; null = bundled picker) | +| `d2b.clipboard.niriSocket` | Removed: compositor access via `d2b.display.host-clipboard.v3` only | +| `d2b.clipboard.historySize` | `spec.config.caps.maxHistoryEntries` | +| `d2b.clipboard.maxItemSize` | `spec.config.caps.maxItemBytes` | +| `d2b.clipboard.requirePicker` | `spec.config.policy.requirePickerForPaste` | +| `d2b.clipboard.guestGroups` | Removed: no per-Guest Unix groups; access via enrolled ComponentSession only | + +--- + +## Invariants (normative) + +The following invariants are checked by contract tests and must not be +violated by any implementation: + +1. **No bytes in resources.** No clipboard payload byte sequence may appear + in any resource spec, status, condition message, or audit payload field. + +2. **Closed MIME allowlist.** Only `text/plain;charset=utf-8`, `text/plain`, + `text/html`, `image/png` are accepted. No runtime extension. + +3. **FD safety.** Every SCM_RIGHTS-received FD is validated with fstat, + fstatfs, MSG_CMSG_CLOEXEC, and MSG_CTRUNC detection. MSG_CTRUNC = fail closed. + +4. **Picker authority.** Picker EphemeralProcess receives no clipboard FDs, + clipboard bytes, compositor credentials, or NIRI_SOCKET path. + +5. **No DND.** wl_data_device_manager drag-and-drop is never implemented. + +6. **No primary selection.** zwp_primary_selection_device_manager_v1 is + never implemented. + +7. **Fail-closed audit.** If the Zone audit queue is full, new clipboard + operations are rejected; they do not proceed unaudited. + +8. **ReasonCode closed enum.** No arbitrary string reason code is accepted. + Unknown proto fields fail closed. + +9. **No filesystem IPC.** No bridge directory, per-Guest Unix socket, or + shared path is used. All IPC is ComponentSession over private transport. + +10. **Loop suppression default-on.** `policy.suppressEcho` defaults to `true`. + Implementations may not change this default. + +11. **No cross-Zone default.** `policy.crossZone.enable` defaults to `false`. + Cross-Zone is denied unless explicitly enabled. + +12. **Controller does not create itself.** Core ProviderDeployment creates both + `Process/clipboard-controller` and `Process/clipd-host`. The controller + does not create or adopt itself. + +13. **Core derives Provider status.** `Provider/clipboard-wayland.status` is + written only by core. The clipboard-wayland controller does not write it. + D088 layers that status as universal `status.*` plus core-owned + `status.resource`; any optional `status.provider` extension is strict, + unknown-field-denied, manifest-registered/signed, bounded, redacted, and + shared-field-free. + +--- + +## Work items + +### ADR046-clipboard-001 — Crate skeleton +| Field | Value | +| --- | --- | +| Dependency/owner | ADR-046-provider-model-and-packaging; Provider/clipboard-wayland crate owner | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent for the provider crate skeleton | +| Reuse action | create | +| Destination | packages/d2b-provider-clipboard-wayland/ with src, tests, integration, README.md, and binaries clipboard-controller, clipd-host, picker-session | +| Detailed design | Create the provider crate skeleton, required source layout, three binaries, and README covering purpose, component map, local build instructions, test commands, and display-wayland fake dependency for integration tests. | +| Integration | Workspace package manifest and Provider artifact catalog consume the crate; provider packaging registers component templates for core ProviderDeployment. | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | Workspace provider layout policy plus README content review and follow-on make test-rust -p d2b-provider-clipboard-wayland once implementation exists. | +| Removal proof | None — net-new; no prior owner to remove | + +**Type:** implementation +**Inputs:** This dossier, `ADR-046-provider-model-and-packaging` + +Create `packages/d2b-provider-clipboard-wayland/` with root directories +`src/`, `tests/`, `integration/`, and `README.md` as required by the +source layout section of this dossier. Binaries: `clipboard-controller`, +`clipd-host`, `picker-session`. + +`README.md` must include: purpose, component map, local build instructions, +test commands, dependency on display-wayland fake in integration tests. + +### ADR046-clipboard-002 — Service process (clipd-host) +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-clipboard-001; clipd-host service owner | +| Current source | packages/d2b-clipd/ algorithms and types listed in the Reuse from baseline section | +| Reuse action | adapt | +| Destination | packages/d2b-provider-clipboard-wayland/src/clipd_host/ service binary modules such as service, display_client, bridge, picker_coord, policy, fd, audit, history | +| Detailed design | Adapt d2b-clipd into clipd-host: call RequestPickerSession over d2b.clipboard.picker-coord.v3, consume d2b.display.host-clipboard.v3 instead of WAYLAND_DISPLAY or NIRI_SOCKET, serve d2b.clipboard.bridge.v3 on Endpoint/clipboard-bridge, remove filesystem bridge and peer group ACL logic, and preserve MIME allowlist, FD safety, fail-closed audit, loop suppression, and LRU history algorithms. Primary reuse disposition: `adapt`. Preserved source-plan detail: port and adapt algorithms; replace direct compositor, picker subprocess, Unix bridge, SO_PEERCRED, bridge directory, and group ACL paths. | +| Integration | Core creates Process/clipd-host; display-wayland wayland-proxy consumes Endpoint/clipboard-bridge; clipd-host consumes display-wayland host-clipboard service and controller picker coordination service. | +| Data migration | Full d2b 3.0 reset; clipboard history is bounded process memory and no v2 clipboard runtime state is imported | +| Validation | make test-rust -p d2b-provider-clipboard-wayland plus unit coverage for MIME policy, FD safety, audit fail-closed queue, history bounds, lifecycle purge and suspension, no filesystem bridge, and no bytes in status or audit. | +| Removal proof | Baseline picker subprocess, direct compositor/Niri clients, Unix bridge socket server, bridge directories, SO_PEERCRED peer config, and per-Guest groups are absent from the provider crate and covered by invariant tests. | + +**Type:** implementation +**Inputs:** ADR046-clipboard-001; ported from `packages/d2b-clipd/` + +Adapt clipboard algorithms from `d2b-clipd` into the `clipd-host` service +binary. Key changes from baseline: + +- Replace `picker.rs` subprocess fork/exec with `RequestPickerSession` call + to controller via `d2b.clipboard.picker-coord.v3`. +- Replace direct WAYLAND_DISPLAY connection with `d2b.display.host-clipboard.v3` + client session (display_client.rs). +- Replace NIRI_SOCKET NiriJsonClient with focus events from the display client. +- Replace Unix bridge socket server with `d2b.clipboard.bridge.v3` ComponentSession + service on `Endpoint/clipboard-bridge`. +- Remove all `SO_PEERCRED` peer config, bridge directories, and group ACL logic. +- Port MIME allowlist, FD safety, audit, loop suppression, LRU history from + d2b-clipd verbatim (algorithm preservation invariant). +- `environmentClass: provider-defined` (system-systemd user scope provides + XDG_RUNTIME_DIR; clipd-host does not use WAYLAND_DISPLAY itself). + +Conformance gates: `make test-rust -p d2b-provider-clipboard-wayland`. + +### ADR046-clipboard-003 — Controller process (clipboard-controller) +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-clipboard-001; clipboard-controller owner | +| Current source | None — net-new v3 controller; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | packages/d2b-provider-clipboard-wayland/src/controller/ and clipboard-controller binary | +| Detailed design | Implement Process/clipboard-controller as a system-domain system-minijail Process serving d2b.clipboard.picker-coord.v3, creating picker EphemeralProcesses from the signed picker-session template, observing picker status, relaying Guest lifecycle messages to clipd-host, creating clipboard RBAC Roles and RoleBindings, writing only bounded redacted operational observations through the optimistic status writer, and never owning or mounting Provider state Volumes. Primary reuse disposition: `create`. Preserved source-plan detail: net-new controller using existing resource API and ComponentSession contracts. | +| Integration | Core ProviderDeployment creates the controller Process; controller uses Zone resource API for EphemeralProcess and RBAC resources and ComponentSession to clipd-host for picker and purge or suspend coordination. | +| Data migration | Full d2b 3.0 reset; no controller durable state import because ProviderStateSet is empty | +| Validation | Controller unit tests for picker request validation, EphemeralProcess spec shape, terminal status callback, GuestStopped and GuestLocked handling, RBAC resources, status bounds, and empty ProviderStateSet. | +| Removal proof | None — net-new; no prior controller owner to remove | + +**Type:** implementation +**Inputs:** ADR046-clipboard-001 + +Implement `Process/clipboard-controller` as a system-domain system-minijail +Process. Responsibilities: + +- Serve `d2b.clipboard.picker-coord.v3` on + `Endpoint/clipboard-picker-coord`. +- On `RequestPickerSession`: validate, create `EphemeralProcess/picker-` + via resource API with signed template `picker-session`, `processClass: worker`, + `successfulTtl: "1h"`, `failedTtl: "24h"`, `startDeadline: "10s"`, + `runtimeDeadline: "120s"`. Picker config is null; metadata arrives via + inherited-socketpair ComponentSession stream at spawn time. +- Watch `EphemeralProcess/picker-*` status for terminal transitions; call + `NotifyPickerResult` back to clipd-host. +- Receive `GuestStopped`/`GuestLocked`/etc. from core orchestrator; + call `PurgeZoneClipboard`/`SuspendZoneClipboard` on clipd-host. +- Create and manage RBAC Role/RoleBinding resources listed in this dossier. +- Writes only bounded, redacted operational observations to + `Provider/clipboard-wayland.status` through the optimistic status writer. +- Does not own, export, reconcile, or mount any Provider state Volume; the + ProviderStateSet is empty under D087. + +### ADR046-clipboard-004 — EphemeralProcess picker binary +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-clipboard-001 and ADR046-clipboard-003; picker worker owner | +| Current source | packages/d2b-clipd/src/picker.rs subprocess flow is replacement context only; picker-session worker is net-new | +| Reuse action | adapt | +| Destination | packages/d2b-provider-clipboard-wayland/src/picker_session/ and picker-session binary | +| Detailed design | Implement picker-session as a user-domain worker EphemeralProcess with minimal environment, metadata over inherited ComponentSession named stream, restricted WAYLAND_SOCKET FD from display-wayland presentation portal, GTK4 closure-contained runtime, one Select or Cancel response, no clipboard FDs, no clipboard bytes, no compositor credentials, no socket paths, and typed PickerStartFailed on install or start failure instead of bypass. Primary reuse disposition: `adapt`. Preserved source-plan detail: rewrite as EphemeralProcess worker. | +| Integration | clipboard-controller creates picker EphemeralProcess per paste request; ProviderSupervisor pre-opens restricted Wayland FD; picker returns result to controller which notifies clipd-host. | +| Data migration | Full d2b 3.0 reset; picker state is per-operation EphemeralProcess status only | +| Validation | Contract test that picker cannot bind zwlr_data_control_manager_v1 plus unit tests for processClass worker, no FDs or payload in picker config, TTL defaults, response framing, and requirePickerForPaste false bypass semantics in clipd-host. | +| Removal proof | Old d2b-clipd subprocess picker path is absent once RequestPickerSession and picker EphemeralProcess tests pass. | + +**Type:** implementation +**Inputs:** ADR046-clipboard-001, ADR046-clipboard-003 + +Implement `picker-session` worker binary. Invariants: + +- `processClass: worker`; `domain: user`; system-systemd; `environmentClass: minimal`. +- Receives metadata from clipboard-controller via inherited-socketpair + ComponentSession named stream (operation_id, source_zone, dest_guest, + mime_list_hint). +- Wayland access: ProviderSupervisor pre-opens a restricted compositor + connection FD backed by display-wayland's presentation-only portal and + passes it with `WAYLAND_SOCKET=` (FD number only, no path). + GTK4 connects via this FD. `zwlr_data_control_manager_v1` and all + clipboard-manager globals are absent from the portal; seccomp policy + prevents any attempt to open a compositor socket path. +- GTK4 and all runtime dependencies are in the picker artifact's Nix closure. + No ambient host GTK4 dependency. +- Sends exactly one `Select(item_digest)` or `Cancel` frame back to the + controller via the named stream. No clipboard content transits this stream. +- Receives no clipboard FDs, no compositor credentials, no socket path. +- Exits 0 on Select/Cancel; non-zero on startup failure. +- If `spec.config.policy.requirePickerForPaste` is `false`, this binary is + not invoked; clipd-host authorizes pastes directly without a picker session. + Install or start failure of the picker EphemeralProcess is a typed error + (`PickerStartFailed`), not a silent bypass. + +Contract test: picker binary must fail to bind `zwlr_data_control_manager_v1` +(absent from seccomp allowlist and restricted compositor portal). + +### ADR046-clipboard-005 — ComponentSession service definitions +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-clipboard-001; ADR-046-componentsession-and-bus; clipboard service contract owner | +| Current source | None — net-new clipboard-wayland ComponentSession service definitions; display-wayland client stubs are consumed from the display-wayland contract | +| Reuse action | create | +| Destination | packages/d2b-provider-clipboard-wayland service descriptors and generated Rust async ttrpc bindings, plus any shared contracts crate selected by ADR-046-componentsession-and-bus | +| Detailed design | Generate stubs for d2b.clipboard.bridge.v3, d2b.clipboard.picker-coord.v3, and d2b.clipboard.v3, consume display-wayland d2b.display.host-clipboard.v3 client stubs, reject service-name collisions, and declare attachment classes clipboard-transfer-fd, host-selection-transfer-fd, and host-selection-supply-fd in the signed descriptor for ComponentSession handshake validation. Primary reuse disposition: `create`. Preserved source-plan detail: net-new generation of service stubs and named-stream types. | +| Integration | Service registry, Zone ComponentSession enrollment, clipd-host, clipboard-controller, display-wayland wayland-proxy, and CLI/operator clients all consume the generated bindings. | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | Contract tests for wire format, service-name collision rejection, attachment class matching, and descriptor handshake validation. | +| Removal proof | Shared filesystem bridge path and SO_PEERCRED contract tests are removed in ADR046-clipboard-011 after ComponentSession contracts pass. | + +**Type:** implementation +**Inputs:** ADR046-clipboard-001; `ADR-046-componentsession-and-bus` + +Generate Rust async ttrpc stubs and named-stream types for: + +- `d2b.clipboard.bridge.v3` (server: clipd-host; client: wayland-proxy) +- `d2b.clipboard.picker-coord.v3` (server: controller; client: clipd-host) +- `d2b.clipboard.v3` (server: clipd-host; client: CLI/operator) + +Consume display-wayland's `d2b.display.host-clipboard.v3` generated client +stubs (imported from the display-wayland crate or a shared contracts crate). +Service names `d2b.display.host-clipboard.v3` and `d2b.clipboard.picker-coord.v3` +are normative; generation compilation rejects service-name collisions in the +Zone service registry. + +All attachment class definitions (`clipboard-transfer-fd`, +`host-selection-transfer-fd`, `host-selection-supply-fd`) must be declared in +the signed service descriptor and validated at ComponentSession handshake. + +### ADR046-clipboard-006 — Provider Nix configuration +| Field | Value | +| --- | --- | +| Dependency/owner | ADR-046-nix-configuration; Provider/clipboard-wayland Nix owner | +| Current source | nixos-modules/clipboard.nix and current d2b.clipboard.* options | +| Reuse action | replace | +| Destination | nixos-modules/providers/clipboard-wayland.nix and d2b.artifacts.clipboard-wayland catalog entry | +| Detailed design | Implement Nix module emitting d2b.zones..resources. Provider resources with spec.artifactId and spec.config, validate hostExecutionRef, hostUserRef, displayWaylandRef, and pickerArtifactId, forbid spec.componentPlacements, spec.settings, and spec.status, and remove nixos-modules/clipboard.nix in the same landing sequence as the new module. Primary reuse disposition: `replace`. Preserved source-plan detail: replace option surface with Provider resource Nix module. | +| Integration | Nix resource compiler emits Provider resource and artifact catalog data consumed by core configuration publication and ProviderDeployment. | +| Data migration | Full d2b 3.0 reset; operators translate old d2b.clipboard.* options using the dossier mapping table | +| Validation | Nix eval tests for resource shape, reference validation, null displayWaylandRef host-only mode, artifact catalog lookup, and absence of deprecated spec fields. | +| Removal proof | nixos-modules/clipboard.nix import is removed and examples/static checks no longer reference old d2b.clipboard.* options. | + +**Type:** implementation +**Inputs:** `ADR-046-nix-configuration`; this dossier Nix authoring section + +Implement the d2b Nix module for `Provider/clipboard-wayland`: + +- `d2b.zones..resources.` with `type = "Provider"` and + `spec.{artifactId, config}` shape as defined in this dossier. +- Validate: `hostExecutionRef`/`hostUserRef` resolve to declared resources; + `displayWaylandRef` resolves to a `Provider/display-wayland` if non-null; + `pickerArtifactId` null or registered in artifact catalog. +- No `spec.componentPlacements`, `spec.settings`, or `spec.status` in Nix. +- Attribute path: `d2b.artifacts.clipboard-wayland` for the package. +- Remove `nixos-modules/clipboard.nix` in the same commit as the new module + lands (see ADR046-clipboard-012). + +### ADR046-clipboard-007 — RBAC resources +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-clipboard-003; clipboard RBAC owner | +| Current source | None — net-new Zone RBAC resources for clipboard-wayland; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | packages/d2b-provider-clipboard-wayland/src/controller/rbac.rs or equivalent controller reconcile module | +| Detailed design | Controller creates Role/clipboard-admin, Role/clipboard-viewer, Role/clipboard-bridge-peer, Role/clipboard-picker-worker and RoleBindings display-wayland-bridge, host-admin-clipboard, picker-session-worker, all Zone-scoped, owned by Process/clipboard-controller, selector-bound for Process/picker-*, and cleaned up when Provider is deleted. | +| Integration | Resource API stores RBAC resources; ComponentSession authorization checks consume Roles and RoleBindings for management, bridge, and picker worker services. | +| Data migration | Full d2b 3.0 reset; no v2 RBAC state import | +| Validation | Controller RBAC reconcile tests for create, idempotent update, provider deletion cleanup, selector scoping, bridge peer authorization, and denied unauthorized management calls. | +| Removal proof | None — net-new; no prior owner to remove | + +**Type:** implementation +**Inputs:** ADR046-clipboard-003; RBAC section of this dossier + +The clipboard-controller reconcile loop creates: + +- `Role/clipboard-admin`, `Role/clipboard-viewer`, + `Role/clipboard-bridge-peer`, `Role/clipboard-picker-worker` +- `RoleBinding/display-wayland-bridge`, `RoleBinding/host-admin-clipboard`, + `RoleBinding/picker-session-worker` + +All Roles and RoleBindings are Zone-scoped, owned by +`Process/clipboard-controller`, and are cleaned up when the Provider is +deleted. + +### ADR046-clipboard-008 — Audit and telemetry +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-clipboard-002; ADR-046-telemetry-audit-and-support; clipboard observability owner | +| Current source | packages/d2b-clipd/src/audit.rs and policy types from packages/d2b-clipd/src/policy.rs | +| Reuse action | adapt | +| Destination | packages/d2b-provider-clipboard-wayland/src/service/audit.rs and packages/d2b-provider-clipboard-wayland/src/service/metrics.rs | +| Detailed design | Implement ClipboardAuditEvent and fail-closed Zone audit queue by porting baseline audit code, renaming realm fields to source_zone_id and dest_zone_id, making ReasonCode a closed enum with unknown protobuf fields rejected, replacing exact byte counts with SizeBucket, emitting to d2b.audit.v3, and adding closed-semantic-label OTEL metrics and spans from the dossier tables. Metric descriptors carry no Zone/resource-name-derived identity; `d2b.zone` remains a resource attribute. Primary reuse disposition: `adapt`. Preserved source-plan detail: port and adapt audit plus resource-name-free metrics and redaction changes. | +| Integration | clipd-host emits audit events to the Zone audit sink and OTEL metrics/spans to the observability Provider pipeline during clipboard operations. | +| Data migration | Full d2b 3.0 reset; audit stream is v3 Zone-local and no v2 audit records are imported | +| Validation | Audit tests for no bytes in events, closed ReasonCode deserialization, fail-closed queue rejection, SizeBucket discretization, excluded span attributes, and structural metric descriptor assertions for exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus clipboard/Zone-name canary absence while preserving `d2b.zone` resource attributes. | +| Removal proof | Old audit shape with realm field names and exact byte counts is absent after ported tests assert the v3 ClipboardAuditEvent schema. | + +**Type:** implementation +**Inputs:** ADR046-clipboard-002; `ADR-046-telemetry-audit-and-support` + +Implement `ClipboardAuditEvent` and fail-closed queue in `service/audit.rs`: + +- Port `audit.rs` from `packages/d2b-clipd/src/audit.rs`. +- Rename `source_realm`/`destination_realm` → `source_zone_id`/`dest_zone_id`. +- `ReasonCode` must be a closed Rust enum (`#[non_exhaustive]` is forbidden). + Unknown protobuf fields fail the deserialization. +- `SizeBucket` discretization replaces exact byte counts. +- Emit `ClipboardAuditEvent` to Zone audit sink via `d2b.audit.v3`. +- Implement OTEL metrics with label sets from the Metrics table above. +- Implement OTEL spans with allowed/excluded attribute lists from the Spans + section above. + +### ADR046-clipboard-009 — Hermetic unit tests +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-clipboard-001 through ADR046-clipboard-008; clipboard test owner | +| Current source | packages/d2b-clipd/ behavior and this dossier invariants; no single baseline test path is declared for every assertion | +| Reuse action | extract | +| Destination | packages/d2b-provider-clipboard-wayland/tests/ | +| Detailed design | Create hermetic unit and Cargo integration tests covering closed MIME policy, secret-hint suppression, FD validation and bounds, LRU and TTL history, fail-closed audit, lifecycle purge and suspend, picker EphemeralProcess invariants, no filesystem bridge, core-created Processes, empty ProviderStateSet, no state mounts or state-layout principals, status-first observation, and no clipboard bytes in status, audit, metrics, Operations, or Volumes. Primary reuse disposition: `extract`. Preserved source-plan detail: extract semantic assertions into hermetic provider tests. | +| Integration | cargo test -p d2b-provider-clipboard-wayland --lib --tests consumes the provider crate and fake clocks/effect ports without live Wayland, systemd, broker, or Nix eval. | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | All tests listed in the Required test coverage table must pass under make test-rust -p d2b-provider-clipboard-wayland. | +| Removal proof | Replaced current-code tests receive explicit keep/adapt/move/delete dispositions and old duplicate tests are deleted once successor coverage passes. | + +**Type:** test +**Inputs:** ADR046-clipboard-001 through ADR046-clipboard-008 + +Required test coverage (in `packages/d2b-provider-clipboard-wayland/tests/`): + +| Test | What it verifies | +| --- | --- | +| `policy::test_mime_allowlist_closed` | All listed MIME types accepted; all others rejected | +| `policy::test_secret_hint_detection` | All secret-hint MIME types trigger suppression | +| `fd::test_msg_ctrunc_fail_closed` | MSG_CTRUNC on recvmsg → operation rejected, no partial FD | +| `fd::test_fstat_nlink_guard` | FD with st_nlink > 1 → rejected | +| `fd::test_item_too_large` | FD content exceeds maxItemBytes → closed without entry | +| `history::test_lru_eviction` | History bounded at maxHistoryEntries; LRU entry evicted | +| `history::test_total_quota` | Total bytes quota enforced across entries | +| `history::test_ttl_expiry` | Entries expire after hostEntrySeconds/guestEntrySeconds | +| `audit::test_no_bytes_in_event` | ClipboardAuditEvent contains no clipboard bytes | +| `audit::test_reason_code_closed_enum` | Unknown reason code → deserialization error | +| `audit::test_fail_closed_queue` | Full queue → operation rejected, not bypassed | +| `lifecycle::test_guest_stopped_purge` | GuestStopped → all entries for that guest purged | +| `lifecycle::test_guest_locked_suspend` | GuestLocked → paste requests rejected with zone-suspended | +| `picker::test_ephemeral_no_fds` | Picker spec contains no SCM_RIGHTS attachments | +| `picker::test_ephemeral_process_class` | processClass = worker; controller/service rejected | +| `picker::test_ephemeral_ttl_defaults` | successfulTtl=1h, failedTtl=24h | +| `invariants::test_no_filesystem_bridge` | No socket path or dir appears in Process spec config | +| `invariants::test_core_creates_processes` | Controller does not create Process/clipd-host | +| `state::test_provider_state_set_empty` | Provider declares no Provider state Volume; ProviderStateSet query returns empty for `Provider/clipboard-wayland` | +| `state::test_no_state_mounts` | Component Process specs contain no `/state` mount and no Provider state Volume reference | +| `state::test_no_state_layout_principals` | No dedicated state-layout `User/` or ComponentPrincipal reference is emitted for component state | +| `state::test_status_first_operational_state` | Bounded non-secret operational observations live in revisioned status/core Operation ledger and are re-verified after restart | +| `state::test_no_clipboard_bytes_in_status` | No clipboard bytes, entry data, FD content, socket path, or authority handle appears in status, audit, metrics, Operations, or any Volume | + +### ADR046-clipboard-010 — Integration tests +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-clipboard-009; display-wayland fake and clipboard integration owner | +| Current source | None — net-new v3 provider integration scenarios; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | packages/d2b-provider-clipboard-wayland/integration/ | +| Detailed design | Implement e2e paste, host capture, bridge backpressure, rate limiting, echo suppression, dependency absent host-only mode, GuestDestroyed purge, audit fail-closed, picker start timeout, and cross-zone denied scenarios using fake d2b.display.host-clipboard.v3 server and fake wayland-proxy bridge client without requiring a live compositor. Primary reuse disposition: `create`. Preserved source-plan detail: net-new integration suite with fake display-wayland and fake wayland-proxy. | +| Integration | Provider integration lane exercises clipd-host, clipboard-controller, generated ComponentSession services, fake display-wayland service, and fake bridge client end-to-end. | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | All integration scenarios in the dossier table pass and assert no live Wayland compositor dependency. | +| Removal proof | None — net-new; no prior owner to remove | + +**Type:** test +**Inputs:** ADR046-clipboard-009; display-wayland fake + +Required integration test scenarios (in `packages/d2b-provider-clipboard-wayland/integration/`): + +| Test | Scenario | +| --- | --- | +| `e2e_paste.rs` | Full paste flow: fake wayland-proxy calls NotifyGuestSelection → RequestPickerSession → fake picker selects → AuthorizeGuestPaste → verify FD arrives | +| `e2e_host_capture.rs` | Host selection event from fake display client → entry stored → ListHistory returns metadata (no bytes) | +| `bridge_backpressure.rs` | maxConcurrentFds reached → NotifyGuestSelection returns backpressure error; after FD closed → proceeds | +| `rate_limit.rs` | maxGuestRatePerMin exceeded → rate-limit-exceeded audit event and rejection | +| `echo_suppression.rs` | Host selection echoed back from clipd publish → suppressed, not re-added | +| `dependency_absent.rs` | displayWaylandRef = null → bridge methods return dependency-absent; management API works | +| `guest_destroy_purge.rs` | GuestDestroyed → all history entries for zone purged; picker sessions cancelled | +| `audit_fail_closed.rs` | Audit queue filled → clipboard operation rejected; no unaudited operation proceeds | +| `picker_start_timeout.rs` | Picker EphemeralProcess startDeadline exceeded → PickerStartFailed audit event; operation cancelled | +| `cross_zone_denied.rs` | crossZone.enable = false → cross-Zone paste rejected | + +Integration tests use a fake `d2b.display.host-clipboard.v3` server and a +fake wayland-proxy bridge client. They do not require a live Wayland compositor. + +### ADR046-clipboard-011 — Contract tests +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-clipboard-005; packages/d2b-contract-tests owner | +| Current source | packages/d2b-contract-tests/tests/policy_clipboard.rs | +| Reuse action | adapt | +| Destination | packages/d2b-contract-tests/tests/policy_clipboard.rs | +| Detailed design | Add contract tests for d2b.clipboard.bridge.v3 and d2b.clipboard.picker-coord.v3 wire formats, ReasonCode numeric stability, and attachment class descriptor names while removing tests that assume shared filesystem bridge paths or SO_PEERCRED config. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt contract tests and delete obsolete filesystem bridge assumptions. | +| Integration | Contract test suite consumes generated service descriptors and guards downstream ComponentSession consumers. | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | packages/d2b-contract-tests policy_clipboard.rs passes with v3 wire format and attachment descriptor assertions. | +| Removal proof | Tests for shared filesystem bridge paths and SO_PEERCRED config are removed from policy_clipboard.rs after v3 ComponentSession contract coverage lands. | + +**Type:** test +**Inputs:** ADR046-clipboard-005; `packages/d2b-contract-tests/` + +Update `packages/d2b-contract-tests/tests/policy_clipboard.rs`: + +- Add contract tests for `d2b.clipboard.bridge.v3` wire format. +- Add contract tests for `d2b.clipboard.picker-coord.v3`. +- Add tests verifying ReasonCode proto numbers are stable across schema + versions (closed enum numeric stability). +- Verify attachment class names match descriptor declarations. +- Remove tests that assume shared filesystem bridge paths or SO_PEERCRED config. + +### ADR046-clipboard-012 — Remove nixos-modules/clipboard.nix +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-clipboard-006; Nix/module removal owner | +| Current source | nixos-modules/clipboard.nix and its import in nixos-modules/default.nix | +| Reuse action | delete-after-cutover | +| Destination | nixos-modules/default.nix, docs/how-to/ migration guide, tests/static.sh example iteration expectations, CHANGELOG.md | +| Detailed design | After the new Provider Nix module validates, delete nixos-modules/clipboard.nix, remove its default.nix import, update migration guide with the option mapping table, ensure tests/static.sh examples no longer rely on removed paths, and add an Unreleased changelog entry. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete superseded module and update migration docs. | +| Integration | Nix module aggregator, docs migration flow, static example iteration, and changelog all reflect Provider/clipboard-wayland as the only clipboard surface. | +| Data migration | Full d2b 3.0 reset; no v2 clipboard runtime state or option import | +| Validation | New module eval tests pass and tests/static.sh example iteration has no references to removed option paths. | +| Removal proof | nixos-modules/clipboard.nix is deleted, default.nix import removed, and grep/static checks show no old d2b.clipboard option references in examples. | + +**Type:** removal +**Inputs:** ADR046-clipboard-006 + +After `ADR046-clipboard-006` lands and is validated: + +- Delete `nixos-modules/clipboard.nix`. +- Remove the `clipboard.nix` import from `nixos-modules/default.nix`. +- Update `docs/how-to/` migration guide with option mapping table from this + dossier's Nix migration section. +- Ensure `tests/static.sh` example iterations do not fail on removed option + paths. +- Add a CHANGELOG entry under `## [Unreleased]` noting the removal. + +--- + +## Reuse from baseline + +The following algorithms and types are ported verbatim from the +`packages/d2b-clipd/` baseline (algorithm preservation invariant from +ADR 0042): + +| Source | Destination | Notes | +| --- | --- | --- | +| `src/policy.rs` — `ALLOWED_MIME_TYPES`, `SECRET_HINT_MIME_TYPES`, `ReasonCode`, `AttributionQuality` | `service/policy.rs` | Exact port; rename `source_realm`/`dest_realm` → `_zone_id` | +| `src/fd.rs` — `FdCapModel`, `FdSafetyError`, MSG_CTRUNC validation | `service/fd.rs` | Exact port | +| `src/audit.rs` — `AuditEvent`, fail-closed queue | `service/audit.rs` | Port; adapt zone field names; add `SizeBucket` | +| `src/framing.rs` — `PICKER_TO_DAEMON_MAX_FRAME_BYTES`, encode/decode | `service/picker_coord.rs` | Adapt to ComponentSession named-stream framing | +| `packages/d2b-wayland-proxy/src/clipboard.rs` — `ClipboardMimePolicy`, `ClipboardRoute`, `ClipboardObjectForwarding` | wayland-proxy crate (display-wayland Provider) | Not ported here; clipboard-wayland consumes via bridge service | + +The following are **not** ported and must be rewritten: + +| Baseline | Replacement | +| --- | --- | +| `src/picker.rs` subprocess spawn | `EphemeralProcess/picker-` via resource API | +| `src/niri.rs` NiriJsonClient | `d2b.display.host-clipboard.v3` focus event stream | +| Unix bridge socket server | `d2b.clipboard.bridge.v3` ComponentSession service | +| Bridge directory / `clipboard-bridge-root` Volume | None (no filesystem bridge) | +| NIRI_SOCKET / WAYLAND_DISPLAY injection | `d2b.display.host-clipboard.v3` + ProviderSupervisor pre-opened WAYLAND_SOCKET FD | +| `SO_PEERCRED` bridge peer validation | Enrolled KK ComponentSession authentication | +| Per-Guest Unix groups and ACLs | Zone RBAC RoleBinding | + +--- + +## Required source and test layout + +The following root directories must exist before ADR046-clipboard-001 closes: + +```text +packages/d2b-provider-clipboard-wayland/ + Cargo.toml + src/ + tests/ + integration/ + README.md +``` + +`README.md` must cover: purpose, component map, local build instructions, test +commands, and the display-wayland fake used by integration tests. + +`packages/d2b-contract-tests/tests/policy_clipboard.rs` must be updated +as part of ADR046-clipboard-011. `nixos-modules/clipboard.nix` is removed in +ADR046-clipboard-012. + +### Fast hermetic execution and test placement (D094) + +Per D094 and `ADR-046-validation-and-delivery` §10.16, this Provider's `src/` +unit tests and `tests/*.rs` hermetic suite are fast, in-process, deterministic, +and parallel-safe: an individual normal test has p95 ≤50 ms with no wall-clock +sleep, and `cargo test -p d2b-provider-clipboard-wayland --lib --tests` +completes in ≤2 s warm-cache execution time (compilation excluded). They use a +deterministic fake clock/RNG and the toolkit fakes/FakeEffectPort only — no +process spawn, container, network, DBus, systemd, broker daemon, Nix eval/build, +KVM, USB/GPU/TPM hardware, or live cloud, and no filesystem tree beyond tiny +temp fixtures. Any scenario needing those lives only in `integration/`, which +keeps a lane timeout/budget, parallel isolation, and fake external services by +default; such a need is re-placed into `integration/`, never given a sleep, +larger timeout, or `#[ignore]`. Bounded crypto/property tests are the only +classified exception, each named with a capped case count and a declared higher +per-test budget. + +Per D094, each replaced current-code test is retired with an explicit +keep/adapt/move/delete disposition and a removal gate: the minimum reusable +semantic assertions migrate into this crate's hermetic `tests/`, and the old +duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest +entries are deleted once successor coverage and the removal proof pass — +updating `tests/layer1-jobs.json`, the closed gate manifests, the +flake/matrix/Nix-unit pins, the generated ledgers, and the CI workflow shards. +Old and new suites never run in parallel indefinitely. diff --git a/docs/specs/providers/ADR-046-provider-credential-entra.md b/docs/specs/providers/ADR-046-provider-credential-entra.md new file mode 100644 index 000000000..7ce89c79f --- /dev/null +++ b/docs/specs/providers/ADR-046-provider-credential-entra.md @@ -0,0 +1,731 @@ +# Provider dossier: `credential-entra` + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-credential-entra` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 2 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `packages/d2b-provider-credential-entra/` | +| Depends on | `ADR-046-resources-credential`, `ADR-046-provider-model-and-packaging`, `ADR-046-componentsession-and-bus`, `ADR-046-resource-reconciliation`, `ADR-046-nix-configuration`, `ADR-046-telemetry-audit-and-support`, `ADR-046-resources-host-guest-process-user` | +| Decisions applied | D087 status-first state, D088/D089 status/spec layering, D090 expedited reconcile, D091 currency/upgrade, D092 Endpoint ResourceType, **D093 Entrablau identity Guest credential flow** | +| Supersedes | No Host login/token chains; no direct `EntraCredentialClient` production egress; no `DefaultAzureCredential`, environment, DBus, browser, or path discovery | + +--- + +## 1. Provider identity and D093 outcome + +| Field | Value | +| --- | --- | +| `providerRef` | `Provider/credential-entra` | +| Implements | `Credential` ResourceType | +| Provider schema ID | `credential-entra.d2bus.org/Credential/spec` | +| Status schema ID | `credential-entra.d2bus.org/Credential/status` | +| Login Endpoint purpose | `credential-entra.d2bus.org/entra-login-token` | +| Production token client | Entrablau identity Guest `Endpoint/` implementing `credential-entra.d2bus.org/EntrablauLoginTokenService/v1` | +| Secret custody | Entrablau-enabled identity `Guest/` only | +| Controller/agent custody | Secret-free; no token, refresh token, cookie, device-code authority, MSAL cache, browser state, or machine credential | + +D093 grounds every `Provider/credential-entra` login and token acquisition in an +Entrablau-enabled identity Guest. The credential-entra controller and any helper +agent are orchestration components only: they validate ResourceRefs, authorize +ComponentSession routes, project bounded status, and coordinate lifecycle. They +never perform a Host login, never hold refresh tokens, never run a browser flow, +and never call Microsoft Entra directly. + +**Non-exportable (D096).** Entra `Credential` resources are non-exportable: the +identity stays a same-Zone identity Guest and there is no `ResourceExport` for +Entra credentials/tokens unless a future, explicitly reviewed cross-Zone export +capability is added. Credential/token bytes never cross a Zone. + +Each Entra `Credential` binds: + +- an exact same-Zone `identityGuestRef: Guest/`; and +- a stable `loginEndpointRef: Endpoint/` whose producer is the Entrablau + login/token service running inside that identity Guest. + +The `scope.executionRef`/`consumerRef` pair names the exact allowed consumer. The +consumer may be the identity Guest itself or a different same-Zone Guest. If the +consumer is a different Guest, access-token bytes move only in end-to-end +`Noise_KK_25519_ChaChaPoly_SHA256` records from the Entrablau service to the +exact authenticated consumer process. The Host, d2b-bus, controllers, and all +intermediate transport see ciphertext only. Cross-Zone `identityGuestRef`, +`loginEndpointRef`, `scope.executionRef`, or consumer ResourceRefs are rejected. + +--- + +## 2. Secret-free controller and placement model + +`credential-entra` has a zero-secret control plane: + +| Component | Placement | Secret custody | Responsibility | +| --- | --- | --- | --- | +| `entra-controller` | Zone controller placement selected by ProviderDeployment | None | Watch `Credential` resources, validate Entrablau bindings, manage status/finalizers, request expedited reconciles, and register capability/status schemas. | +| `entra-agent` (optional helper) | Inside `identityGuestRef` or the exact allowed consumer Guest | None | Open authenticated ComponentSessions to `loginEndpointRef`, enforce `consumerRef`/scope/RBAC, and report bounded observations. It does not decrypt access-token records. | +| Entrablau login/token service | Inside `identityGuestRef` | Owns all Entra secrets and login state | Performs interactive login, refresh-token handling, token cache, TPM-bound machine state, Entra network/TLS, and end-to-end access-token delivery. | + +A `Credential` using `Provider/credential-entra` is never scoped to `Host/*`. +`scope.executionRef` and `identityGuestRef` must resolve to `Guest/` in the +same Zone. Host placement is rejected because it would recreate Host credential +custody. User-domain or system-domain consumer processes are allowed only inside +Guests that the credential spec and RBAC authorize. + +The controller/agent Process templates declare no Provider state Volume, no +secret mount, no browser path, no DBus path, no ambient network claim for Entra, +and no environment-derived credential input. Any helper agent uses the standard +bus/Endpoint launch-ticket path and receives only non-secret ResourceRefs, +generations, deadlines, and route digests. + +--- + +## 3. Provider and Credential spec schemas + +### 3.1 Provider `spec.config` (bounded, non-secret) + +```yaml +spec: + artifactId: credential-entra-provider + config: + maxLeases: 64 + interactionPolicy: interaction-required +``` + +| Field | Type | Required | Validation rule | Default | +| --- | --- | --- | --- | --- | +| `maxLeases` | u32 | No | 1..256; caps concurrent access-token lease requests brokered for this Provider | `64` | +| `interactionPolicy` | enum | No | `interaction-required` or `fail-closed`; controls whether `BeginLogin` may be started when no authenticated Entrablau session exists | `interaction-required` | + +Provider config contains no tenant secret, authority URL, token endpoint, client +secret, certificate path, store path, browser path, DBus address, or environment +variable name. Tenant and authority policy that is necessary for Entra login is +owned by the Entrablau Guest service configuration supplied by the sibling +package, not by d2b core or the credential-entra controller. + +### 3.2 Credential base fields used by D093 + +`identityGuestRef` and `loginEndpointRef` are Credential base fields supplied by +`ADR-046-resources-credential`. This Provider requires both. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Credential +metadata: + name: work-entra + zone: work +spec: + providerRef: Provider/credential-entra + identityGuestRef: Guest/work-identity + loginEndpointRef: Endpoint/work-identity-entra-login-token + scope: + executionRef: Guest/aca-gateway + domainFilter: system + userRef: null + consumerRef: Provider/runtime-azure-container-apps + audience: azure-resource-manager + allowedOperations: [acquire-token, refresh-token, revoke-token, inspect-metadata] + rotation: + policy: proactive + proactiveWindowMs: 300000 + maxLeaseLifetimeMs: 3600000 + revocation: + onOwnerDelete: immediate + onProviderGeneration: immediate + provider: + schemaId: credential-entra.d2bus.org/Credential/spec + schemaVersion: 1.0.0 + settings: + tokenPurpose: azure-resource-manager + loginMode: entrablau-guest +``` + +Rules: + +1. `identityGuestRef` resolves to a `Guest/` in the same Zone. +2. `loginEndpointRef` resolves to an `Endpoint/` in the same Zone whose + `purpose = credential-entra.d2bus.org/entra-login-token`, whose producer runs + inside `identityGuestRef`, and whose `endpointGeneration` is current. +3. `scope.executionRef` resolves to the same Guest as `identityGuestRef` or to a + different same-Zone Guest explicitly permitted by `consumerRef`, scope, and + RBAC. +4. `consumerRef` is required and must name the exact Provider/component allowed + to receive access-token plaintext. +5. `allowedOperations` is a non-empty subset of `acquire-token`, `refresh-token`, + `revoke-token`, and `inspect-metadata`; `sign-challenge` is not provided by + this Entrablau token service unless a future schema version adds it. +6. Provider-specific settings are bounded desired metadata only. They do not + carry access tokens, refresh tokens, cookies, login URLs, device codes, + store paths, service principal secrets, or tenant-private diagnostics. + +--- + +## 4. Entrablau identity Guest dependency and Endpoint contract + +### 4.1 External sibling classification + +The identity Guest's NixOS system is consumer-composed with the sibling +`vicondoa/entrablau.nix` module. d2b core never imports that flake and never +vendors the Entrablau implementation. + +```nix +inputs.entrablau.url = "github:vicondoa/entrablau.nix"; +inputs.entrablau.inputs.nixpkgs.follows = "nixpkgs"; + +# Inside the NixOS system closure for Guest/work-identity only. +d2b.zones.work.resources.work-identity.spec.config.imports = [ + inputs.entrablau.nixosModules.default +]; +``` + +This is an ADR-only / external-sibling integration target for ADR 0046. The +sibling/Guest package is expected to supply the manifest-declared Process and +Endpoint contract below. This dossier specifies the required contract; it does +not claim that current d2b core implements Entrablau. + +The sibling package owns all guest-local secret and private state: Himmelblau / +Entrablau enrollment, TPM binding, machine credential, refresh-token state, token +cache, interactive login implementation, browser/device/desktop integration, +network/TLS policy for Microsoft Entra, and any large/private diagnostics. That +state is inside the identity Guest and outside d2b resource status. It is not a +d2b Provider state Volume and is never mounted into the Host. + +### 4.2 Dependency alias + +`Provider/credential-entra` declares one production dependency alias: + +| Alias | Resolves to | Required | Notes | +| --- | --- | --- | --- | +| `entra-login-token` | `Credential.spec.loginEndpointRef` | Yes | `Endpoint/` with purpose `credential-entra.d2bus.org/entra-login-token`, producer inside `identityGuestRef`, same Zone, ComponentSession attachment. | + +The alias is resolved by ResourceRef and Endpoint generation, not by a Unix path, +store path, DBus name, process ID, environment variable, or hostname. + +### 4.3 Endpoint resource schema + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: work-identity-entra-login-token + zone: work + ownerRef: Guest/work-identity +spec: + providerRef: Provider/credential-entra + producerRef: Process/work-identity-entrablau-login-token + endpointClass: service + transport: unix + purpose: credential-entra.d2bus.org/entra-login-token + serviceFingerprint: credential-entra.d2bus.org/EntrablauLoginTokenService/v1 + locality: guest-local + visibility: provider + attachmentPolicy: component-session + consumerPolicy: + allowedSubjects: + - Provider/credential-entra + - Provider/runtime-azure-container-apps + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + endpointGeneration: 7 + observedProducerGeneration: 3 + connectionAvailability: available +``` + +Endpoint spec/status never contains raw Unix paths, fd numbers, browser URLs, +login URLs, device codes, cookies, tenant-private endpoints, token cache paths, +TPM paths, credential file paths, access tokens, refresh tokens, or user PII. +Endpoint resolution returns an authorized launch ticket or fails closed with +`endpoint-resolve-denied`. + +The example deliberately uses the canonical `provider` visibility value. +`owner` would exclude the exact external consumer Provider, while `zone` would +make every same-Zone subject a visibility candidate. `consumerPolicy` then +narrows resolution to the orchestration Provider and the Credential's exact +consumer Provider; ordinary Role/RoleBinding authorization must also allow +`resolve`. Admission accepts only `owner|provider|zone`, denies unknown +visibility aliases, and requires every present `consumerPolicy` dimension to +match. + +### 4.4 Typed service methods + +Service fingerprint: `credential-entra.d2bus.org/EntrablauLoginTokenService/v1`. +All methods are ComponentSession methods. The interactive byte/UI stream is a +named stream on the authenticated session, not status text. + +| Method | Required Role permission | Direction | Secret output? | Purpose | +| --- | --- | --- | --- | --- | +| `BeginLogin` | `use-credential/acquire-token` | request/reply + named stream | No status secret; UI bytes stay on stream | Start or resume an Entrablau interactive login session inside `identityGuestRef`. Opens optional named stream `credential-entra.d2bus.org/login-ui`. | +| `ObserveLogin` | `use-credential/inspect-metadata` | request/reply | No | Return bounded non-secret observation for `interactionState`, session generation, deadline, and challenge metadata. | +| `CancelLogin` | `use-credential/revoke-token` | request/reply | No | Cancel a pending login session by generation; idempotent. | +| `AcquireAccessTokenLease` | `use-credential/acquire-token` | request/reply + sensitive KK record | Yes, only in KK record to consumer | Issue an on-demand access-token lease for `audience`/operation; outer reply carries metadata only. | +| `RefreshAccessTokenLease` | `use-credential/refresh-token` | request/reply + sensitive KK record | Yes, only in KK record to consumer | Refresh an existing access-token lease; outer reply carries metadata only. | +| `RevokeAccessTokenLease` | `use-credential/revoke-token` | request/reply | No | Revoke a non-secret lease handle or mark it unusable. | +| `InspectAccessTokenLease` | `use-credential/inspect-metadata` | request/reply | No | Return bounded non-secret lease state. | + +`BeginLogin`, `ObserveLogin`, and `CancelLogin` are the Credential base +interactive-login operations. The service may conduct browser, device, desktop, +or brokered login UI inside the identity Guest, but no device code conferring +authority, login URL, cookie, token, account UPN, display name, tenant-private +identifier, or user PII is copied into status, audit, OTEL, or outer RPC DTOs. +`challengeMetadata` is bounded and non-secret: examples are a closed +`challengeKind`, an expiry timestamp, a user-action class, and optional +operator-facing text that has passed redaction. It is never sufficient to +complete login outside the Guest. + +--- + +## 5. Interactive login flow + +### 5.1 State model + +Credential base status owns the common interaction fields: + +| Field | Allowed values/content | +| --- | --- | +| `status.resource.interactionState` | `NotRequired`, `Required`, `Starting`, `AwaitingUser`, `Authenticated`, `Failed`, `Unknown` | +| `status.resource.loginSessionGeneration` | Monotonic u64 issued by the Entrablau service; non-secret | +| `status.resource.loginDeadline` | RFC3339 or null; controller never waits for a human past this deadline | +| `status.resource.challengeMetadata` | Bounded non-secret object; no authority-conferring bytes or PII | + +Entra-specific status under `credential-entra.d2bus.org/Credential/status` +contains only provider observations such as endpoint generation, last observed +login generation, lease state, bounded retry counters, and closed error codes. +It never duplicates base status fields. + +### 5.2 `BeginLogin` + +1. The caller must be authorized for the `Credential/` by `consumerRef`, + scope, the exact `use-credential/acquire-token` Role verb/subresource pair, + and same-Zone ResourceRefs. +2. The credential-entra helper opens an authenticated ComponentSession to + `loginEndpointRef` inside `identityGuestRef`. +3. `BeginLogin` binds the Credential UID/generation, `identityGuestRef`, + `loginEndpointRef`, Endpoint generation, `consumerRef`, operation ID, + requested deadline, and schema fingerprints into the session prologue. +4. Entrablau starts or resumes login inside the Guest and, if UI is required, + attaches named stream `credential-entra.d2bus.org/login-ui` directly between + the caller and the Entrablau service. The stream bytes are not status, + audit, logs, metrics, or d2b-bus plaintext inspection data. +5. The controller projects `interactionState: Starting` or `AwaitingUser` and the + bounded deadline/challenge metadata. It does not block until the user + completes login. + +### 5.3 `ObserveLogin` and `CancelLogin` + +`ObserveLogin` returns the latest non-secret observation for the current +`loginSessionGeneration`. It may transition status to `Authenticated`, `Failed`, +`Required`, or `Unknown`. `CancelLogin` is idempotent and generation-bound; a +stale cancel cannot affect a newer login session. + +### 5.4 D090 expedited reconcile behavior + +For `Create`, `UpdateSpec`, and `Delete` with `waitForReconcile`, the controller +performs no external effect, status mutation, finalizer change, or Endpoint +session until Core supplies `CommittedRevisionProof`. After the durable commit, +the expedited pass may return a projected status with +`interactionState: Required`, `Starting`, or `AwaitingUser`. It never waits for +human login past `loginDeadline`; if the deadline would be exceeded, the response +is `Blocked` or `Progressing` with the projected interaction state and +`statusPersistence: pending|committed`. + +--- + +## 6. On-demand access-token lease delivery + +The Credential service exposes access tokens only as short-lived leases requested +on demand. `credential-entra` itself never stores or decrypts token bytes. + +```text +Entrablau service (Guest/work-identity) + owns refresh token + token cache + Entra TLS + │ + │ Noise_KK_25519_ChaChaPoly_SHA256 sensitive records + │ prologue binds Credential UID/gen, Endpoint gen, + │ identityGuestRef, consumerRef, audience, operation, deadline, + │ schema fingerprint, RBAC revision, and maxTokenBytes + ▼ +Exact authorized consumer process (same-Zone Guest) + uses access token in memory for the requested operation only +``` + +Security requirements reuse the D055/D056 end-to-end KK delivery contract: + +1. Static keys are enrolled before delivery. NN/NX/unauthenticated profiles are + rejected. +2. The raw access token reaches only the exact authenticated consumer process + permitted by `Credential.spec.consumerRef`, `scope`, and RBAC. +3. The Host, bus, controller, helper agent, relay, and any intermediate Provider + see only encrypted records and bounded metadata. +4. Refresh tokens, token caches, machine credentials, cookies, account session + state, and private login state never leave `identityGuestRef`. +5. Outer RPC replies carry only non-secret lease metadata: lease handle digest, + expiry, issued-at time, generation, interaction state, and stable outcome. +6. Ambiguous delivery is not success and is not automatically replayed. The + consumer must retry with a new operation/session. +7. Token buffers are zeroizing in the Entrablau service and consumer. No token, + prefix/suffix, bearer string, JWT, cookie, refresh token, or device code is + logged, audited, traced, labeled, stored in status, or written to a Volume. + +`AcquireAccessTokenLease` may return `interactionState: Required` rather than a +token if no authenticated Entrablau session exists or if the identity Guest says +interactive login is required. That is a recoverable interaction condition, not a +policy denial. + +--- + +## 7. Production token API client + +The token API client remains injected and typed for testability, but the only +production implementation is the Entrablau identity Guest Endpoint described in +§4. + +```rust +trait EntraTokenLeaseClient: Send + Sync { + async fn begin_login(&self, request: BeginLoginRequest) -> Result; + async fn observe_login(&self, request: ObserveLoginRequest) -> Result; + async fn cancel_login(&self, request: CancelLoginRequest) -> Result; + async fn acquire_access_token_lease(&self, request: TokenLeaseRequest) -> Result; + async fn refresh_access_token_lease(&self, request: TokenLeaseRequest) -> Result; + async fn revoke_access_token_lease(&self, request: RevokeLeaseRequest) -> Result; + async fn inspect_access_token_lease(&self, request: InspectLeaseRequest) -> Result; +} +``` + +The fake implementation used by hermetic tests may simulate login states and +access-token delivery, but production construction resolves +`Credential.spec.loginEndpointRef` and opens a ComponentSession to that Endpoint. +The credential-entra controller and helper agent perform no direct Entra network +egress; Entrablau owns Microsoft Entra network/TLS flows inside the identity +Guest. + +There is no `DefaultAzureCredential`, no Azure SDK default chain, no environment +variable credential source, no DBus credential source, no filesystem token cache +path, no developer-tool credential path, no Host login fallback, no browser +fallback on the Host, and no env var fallback. Removing or disabling the +Entrablau Endpoint is a hard `interactionState: Unknown` or +`ProviderUnavailable` condition, not a fallback trigger. + +--- + +## 8. Status-first state and zero-secret invariant + +`Provider/credential-entra` declares no Provider state Volume. The ProviderStateSet +is empty. Entrablau's private state is guest-local state owned by the sibling +package inside the identity Guest, not a d2b Provider Volume. + +Credential status may contain only bounded non-secret observations: + +| Surface | Allowed | Forbidden | +| --- | --- | --- | +| `Credential.status.resource` | interaction state, login session generation, login deadline, bounded challenge metadata, lease state, expiry/issued timestamps | Credential name/ResourceRef/UID/identity digest; tokens, refresh tokens, device codes, login URLs, cookies, account names, tenant-private IDs, PII | +| `Credential.status.provider.details` | Endpoint generation, identity Guest generation digest, retry counters, closed error/outcome enums, non-authorizing lease handle digests | Credential name/ResourceRef/UID/identity digest; token bytes, Entra response bodies, MSAL cache entries, filesystem paths, DBus names, browser profile paths | +| Authorized bounded Zone audit | operation class, RBAC verb/subresource, result code, generation, and base `resource_name_digest` after authorization | raw Credential name/ResourceRef/UID; token/refresh/cookie/device-code bytes, login URL, user PII, cloud response bodies, host paths | +| OTEL Resource attributes | only applicable generic collector-allowlisted attributes: `d2b.zone`, `d2b.provider`, `d2b.component`, `service.name`, `service.namespace`, `service.version` | Credential name/ResourceRef/UID/identity digest and every Credential-derived attribute | +| Span attributes, metric labels, logs/Debug, status, and errors | closed provider/operation/outcome values that do not identify a Credential | Credential name/ResourceRef/UID/identity digest; route/session-binding digests derived from Credential identity; all secret/private values forbidden above | + +The zero-secret invariant applies across spec, status, resource store, audit, +OTEL, logs, debug output, process descriptors, launch tickets, Endpoint spec/status, +and outer RPC DTOs. Secret bytes are present only inside the Entrablau Guest and, +for access tokens, inside the end-to-end KK record and exact consumer process. +Credential identity is a separate observability restriction: it remains in the +resource envelope and authenticated internal routing/session bindings, but +among observable outputs only the authorized bounded audit record may contain +it, and that record uses `resource_name_digest` rather than raw identity. + +--- + +## 9. Nix composition + +A consumer composes the identity Guest and then declares Credentials against the +Endpoint exported by that Guest. d2b core does not import the sibling flake. + +```nix +{ + inputs.entrablau.url = "github:vicondoa/entrablau.nix"; + inputs.entrablau.inputs.nixpkgs.follows = "nixpkgs"; + + # Identity Guest NixOS system. The sibling module declares the Entrablau + # login/token Process and the Endpoint purpose + # credential-entra.d2bus.org/entra-login-token. + d2b.zones.work.resources.work-identity = { + type = "Guest"; + spec = { + providerRef = "Provider/runtime-cloud-hypervisor"; + defaultDomain = "system"; + allowedDomains = [ "system" "user" ]; + systemArtifactId = "work-identity-system"; + config.imports = [ inputs.entrablau.nixosModules.default ]; + }; + }; + + d2b.zones.work.resources.work-identity-entra-login-token = { + type = "Endpoint"; + spec = { + providerRef = "Provider/credential-entra"; + producerRef = "Process/work-identity-entrablau-login-token"; + endpointClass = "service"; + transport = "unix"; + purpose = "credential-entra.d2bus.org/entra-login-token"; + serviceFingerprint = "credential-entra.d2bus.org/EntrablauLoginTokenService/v1"; + locality = "guest-local"; + visibility = "provider"; + attachmentPolicy = "component-session"; + consumerPolicy = { + allowedSubjects = [ + "Provider/credential-entra" + "Provider/runtime-azure-container-apps" + ]; + allowedOperations = [ "resolve" ]; + }; + lifecyclePolicy = "recycle-with-producer"; + }; + }; + + d2b.zones.work.resources.work-entra = { + type = "Credential"; + spec = { + providerRef = "Provider/credential-entra"; + identityGuestRef = "Guest/work-identity"; + loginEndpointRef = "Endpoint/work-identity-entra-login-token"; + scope.executionRef = "Guest/aca-gateway"; + scope.domainFilter = "system"; + consumerRef = "Provider/runtime-azure-container-apps"; + audience = "azure-resource-manager"; + allowedOperations = [ + "acquire-token" + "refresh-token" + "revoke-token" + "inspect-metadata" + ]; + rotation.policy = "proactive"; + rotation.proactiveWindowMs = 300000; + rotation.maxLeaseLifetimeMs = 3600000; + revocation.onOwnerDelete = "immediate"; + revocation.onProviderGeneration = "immediate"; + provider = { + schemaId = "credential-entra.d2bus.org/Credential/spec"; + schemaVersion = "1.0.0"; + settings = { + tokenPurpose = "azure-resource-manager"; + loginMode = "entrablau-guest"; + }; + }; + }; + }; +} +``` + +Eval-time validation rejects missing `identityGuestRef`/`loginEndpointRef`, any +cross-Zone reference, any Endpoint whose producer is not inside the identity +Guest, Host-scoped credential placement, an absent `consumerRef`, mismatched +consumer execution scope, any Endpoint visibility outside +`owner|provider|zone`, a `provider`-visible login Endpoint whose +`consumerPolicy.allowedSubjects` omits either `Provider/credential-entra` or the +Credential's exact `consumerRef`, and any string that matches known +secret/token shapes. + +--- + +## 10. Reset, upgrade, and deletion + +Credential reset and deletion affect only d2b credential metadata and active +lease/session observations unless the operator explicitly requests identity Guest +state destruction through the Entrablau sibling's own interface. + +| Operation | Identity Guest TPM/login state | +| --- | --- | +| Credential spec update | Preserved unless the identity Guest or Endpoint binding changes; old sessions are generation-invalidated. | +| Provider generation update | Active access-token leases are revoked or allowed to drain per `revocation.onProviderGeneration`; Entrablau refresh state is preserved. | +| Credential delete | Active leases are revoked; status/finalizer cleanup occurs; Entrablau enrollment and TPM state are preserved. | +| Explicit identity reset | Must be a separate, auditable operation owned by the Entrablau Guest/sibling and must declare whether TPM/login state is preserved or destroyed. | + +A reset must never silently wipe or recreate the identity Guest TPM/login state. +Destroying that state can force Entra re-enrollment and therefore requires an +explicit operator action and audit record. Preserving state is the default for +credential-entra lifecycle changes. + +--- + +## 11. RBAC and errors + +### RBAC verbs + +| Verb | Who | Purpose | +| --- | --- | --- | +| `get`, `list`, `watch` | Authorized readers | Inspect non-secret spec/status. | +| `create` plus `admin-credential/create` | Authorized deployer/configuration controller | Create a Credential; neither permission implies the other. | +| `update-spec` plus `admin-credential/update-spec` | Authorized deployer/configuration controller | Update a Credential spec; neither permission implies the other. | +| `delete` plus `admin-credential/delete` | Authorized deployer/configuration controller | Delete a Credential; neither permission implies the other. | +| `update-status` | `Provider/credential-entra` controller only | Write projected interaction/lease observations. | +| `update-finalizers` | `Provider/credential-entra` controller | Revoke/finalize Credential resources. | +| `use-credential/acquire-token` | Exact `consumerRef` plus RBAC | Begin login or acquire an access-token lease when `spec.allowedOperations` includes `acquire-token`. | +| `use-credential/refresh-token` | Exact `consumerRef` plus RBAC | Refresh an access-token lease when `spec.allowedOperations` includes `refresh-token`. | +| `use-credential/revoke-token` | Exact `consumerRef` plus RBAC | Cancel login or revoke an access-token lease when `spec.allowedOperations` includes `revoke-token`. | +| `use-credential/inspect-metadata` | Exact `consumerRef` plus RBAC | Observe login or inspect bounded lease/status metadata when `spec.allowedOperations` includes `inspect-metadata`. | + +The value after `/` is an exact entry in the existing Role rule +`subresources` field. The dossier defines no alternate Credential-operation +Role field and no method-name verbs such as `begin-login`. A consumer Role rule +is: + +```yaml +rules: + - resourceTypes: [Credential] + verbs: [use-credential] + subresources: [acquire-token, refresh-token, revoke-token, inspect-metadata] + resourceNames: [work-entra] + zones: [work] + executionRefs: [Guest/aca-gateway] + sessionVerbs: [] +``` + +A separate deployer Role rule is: + +```yaml +rules: + - resourceTypes: [Credential] + verbs: [create, update-spec, delete] + subresources: [] + resourceNames: [work-entra] + zones: [work] + executionRefs: [] + sessionVerbs: [] + - resourceTypes: [Credential] + verbs: [admin-credential] + subresources: [create, update-spec, delete] + resourceNames: [work-entra] + zones: [work] + executionRefs: [] + sessionVerbs: [] +``` + +Service admission authenticates the exact consumer and requires the same exact +operation class in both `Credential.spec.allowedOperations` and the +`use-credential` Role `subresources` before Endpoint dispatch. Empty, wildcard, +unknown, noncanonical aliases, and mismatched classes fail closed. +Administrative create/update/delete additionally require the matching +`admin-credential` subresource (`create`, `update-spec`, or `delete`) and the +corresponding ordinary CRUD verb. + +### Stable error codes + +| Code | Condition | +| --- | --- | +| `credential-interaction-required` | Entrablau reports login is required before token acquisition. | +| `credential-login-starting` | `BeginLogin` accepted; user interaction stream starting. | +| `credential-login-cancelled` | Generation-bound login was cancelled. | +| `credential-login-timeout` | Login did not complete before `loginDeadline`. | +| `credential-endpoint-unavailable` | `loginEndpointRef` is absent, not Ready, or generation mismatched. | +| `credential-endpoint-generation-mismatch` | Caller observed stale Endpoint generation. | +| `credential-consumer-mismatch` | Authenticated caller does not match `consumerRef`/scope/RBAC. | +| `credential-placement-mismatch` | Host placement, cross-Zone reference, or non-Guest identity binding attempted. | +| `credential-provider-unavailable` | Entrablau service or identity Guest unavailable. | +| `credential-operation-denied` | RBAC or Entrablau policy denied the requested operation. | +| `credential-redaction-violation` | A candidate status/audit/log field contains secret-shaped content. | + +All error messages are bounded stable text. They do not include login URLs, +device codes, user identifiers, tenant-private identifiers, HTTP bodies, paths, +DBus names, tokens, refresh tokens, cookies, a Credential name/ResourceRef/UID, +or any digest derived from Credential identity. + +--- + +## 12. Work item + +### ADR046-cred-entra-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | `ADR046-credential-001` (Credential base fields and status), `ADR046-credential-002` (`d2b.credential.v3` service), D092 Endpoint, D093 Entrablau identity Guest decision, credential-entra owner | +| Current source | `d2b-realm-provider/src/credential.rs:OpaqueAzureRef` remains a bounded opaque identifier reuse source only; no Host login/token implementation is retained | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-credential-entra/src/{lib.rs,controller.rs,service.rs,controller_main.rs,agent_main.rs,audit.rs,telemetry.rs}` and corresponding tests/integration docs | +| Detailed design | Implement secret-free controller/helper; require Credential base `identityGuestRef` and `loginEndpointRef`; resolve dependency alias `entra-login-token`; validate same-Zone Guest placement and exact `consumerRef`; implement typed `EntraTokenLeaseClient` whose production implementation is the Entrablau Endpoint; implement `BeginLogin`/`ObserveLogin`/`CancelLogin` projection to Credential base interaction status; admit service calls only when the same exact allowed operation (`acquire-token`, `refresh-token`, `revoke-token`, or `inspect-metadata`) is present in both `Credential.spec.allowedOperations` and the canonical `use-credential` Role `subresources`; require matching `create`, `update-spec`, or `delete` under `admin-credential` in addition to ordinary CRUD, with no Credential-specific Role fields or coarse aliases; emit the login Endpoint with canonical `visibility = provider` and an exact `consumerPolicy` for the orchestration Provider and configured consumer; route access-token leases end-to-end from Entrablau service to exact consumer over Noise_KK; keep all refresh/login/TPM state inside the Entrablau Guest; declare no Provider state Volume; reject Host placement and all ambient fallback chains; enforce Credential name/ResourceRef/UID/digest as audit-only observable identity, with only `resource_name_digest` retained in authorized bounded audit and no Credential identity in status/errors/logs/OTEL Resource or span attributes/metric labels. Primary reuse disposition: `adapt`. Preserved source-plan detail: Adapt tests/types where non-secret; replace production token acquisition with Entrablau Endpoint client. | +| Integration | Consumer composes `inputs.entrablau.nixosModules.default` into the identity Guest; sibling package declares login/token Process and Endpoint; d2b Credential resource binds `identityGuestRef`, `loginEndpointRef`, and `consumerRef` | +| Data migration | Full v3 reset of d2b Credential metadata; Entrablau Guest state is preserved unless explicitly destroyed by the sibling-owned reset flow | +| Validation | See §13 | +| Removal proof | Old abstract Host credential paths and any direct Entra effect client are deleted only after the Entrablau Endpoint-backed provider passes the test matrix | + +--- + +## 13. Tests + +### Unit and hermetic integration tests + +| Test | Purpose | +| --- | --- | +| `test_fake_guest_login_success` | Fake Entrablau Guest reports `Required → Starting → AwaitingUser → Authenticated`; status carries only interaction fields and bounded challenge metadata. | +| `test_fake_guest_login_required` | Token acquisition without an authenticated Entrablau session returns `interactionState: Required`, no token, and no denial. | +| `test_fake_guest_login_cancel` | `CancelLogin` is generation-bound and idempotent; stale cancel cannot cancel a newer session. | +| `test_fake_guest_login_timeout` | `loginDeadline` expiration returns `credential-login-timeout`; expedited reconcile does not wait past deadline. | +| `test_fake_guest_login_restart_reobserve` | Controller/helper restart reconstructs status by observing Entrablau Endpoint; status is observation, not authority. | +| `test_endpoint_unavailable` | Missing/not Ready `loginEndpointRef` yields `credential-endpoint-unavailable` and no fallback. | +| `test_endpoint_generation_mismatch` | Stale Endpoint generation invalidates cached sessions and requires reacquire. | +| `test_endpoint_visibility_consumer_policy` | Login Endpoint accepts only `owner`, `provider`, or `zone`, uses `provider`, and requires exact `consumerPolicy.allowedSubjects` for `Provider/credential-entra` plus the configured `consumerRef`; `zone`, omitted consumer, aliases, and mismatches fail the dossier conformance case. | +| `test_host_placement_rejected` | Any `Host/*` `identityGuestRef`, `scope.executionRef`, or Host-system placement fails closed. | +| `test_same_zone_accepted_cross_zone_rejected` | Same-Zone identity/consumer Guest works; any cross-Zone ResourceRef is rejected at eval/admission. | +| `test_exact_consumer_rbac` | Only the authenticated `consumerRef` process can receive token delivery; other callers fail before Endpoint dispatch. | +| `test_credential_role_subresource_matrix` | Each service action requires the same exact `acquire-token`, `refresh-token`, `revoke-token`, or `inspect-metadata` value in `spec.allowedOperations` and `use-credential` subresources; each lifecycle action requires matching ordinary CRUD plus `admin-credential/create`, `admin-credential/update-spec`, or `admin-credential/delete`; empty/wildcard/unknown/mismatched/coarse aliases fail before Endpoint dispatch. | +| `test_token_refresh_redaction` | Access token, refresh token, cookies, device codes, login URLs, and MSAL cache canaries are absent from spec/status/audit/OTEL/logs/Debug. | +| `test_credential_identity_audit_only` | Credential name, canonical ResourceRef, UID, and derived-digest canaries are absent from status, errors, logs/Debug, every OTEL Resource/span attribute, and every metric label; the authorized bounded audit record retains only `resource_name_digest`, and an unauthorized request cannot elicit identity-bearing audit. Generic allowlisted OTEL Resource attributes remain present. | +| `test_e2e_record_only_delivery` | Raw access token appears only in the Entrablau→consumer Noise_KK record; bus/controller/helper see ciphertext only. | +| `test_no_ambient_fallbacks` | Production code path never constructs SDK default chains, environment credential sources, DBus/browser/path fallbacks, Host login flows, or direct Entra egress. | +| `test_nix_composition_identity_guest` | Consumer-composed `inputs.entrablau.nixosModules.default` in the identity Guest declares the expected Process and Endpoint schema without d2b core importing the sibling flake. | +| `test_tpm_preserve_on_credential_reset` | Credential reset/delete preserves Entrablau TPM/login state by default. | +| `test_tpm_destroy_requires_explicit_reset` | Destroying identity Guest TPM/login state requires explicit sibling-owned reset intent and emits a distinct audit record. | +| `test_status_size_and_challenge_bounds` | `challengeMetadata` and provider details obey D087/D088 size bounds and reject secret-shaped strings. | +| `test_d090_interaction_required_projection` | `waitForReconcile` after create/update returns projected `interactionState: Required` or `AwaitingUser` without waiting for human login. | + +### Nix/eval assertions + +- `identityGuestRef` and `loginEndpointRef` are required for every + `Provider/credential-entra` Credential. +- `loginEndpointRef` must have purpose + `credential-entra.d2bus.org/entra-login-token` and producer inside + `identityGuestRef`. +- `consumerRef` is required and must match the authorized consumer Provider. +- The login Endpoint uses canonical `visibility = "provider"` and an exact + `consumerPolicy.allowedSubjects` containing `Provider/credential-entra` and + the configured `consumerRef`. +- Host placement and cross-Zone ResourceRefs are rejected. +- No Provider state Volume is emitted for `credential-entra`; Entrablau private + state remains guest-local under the sibling package. + +### Fast hermetic execution and test placement (D094) + +Per D094 and `ADR-046-validation-and-delivery` §10.16, this Provider's `src/` +unit tests and `tests/*.rs` hermetic suite are fast, in-process, deterministic, +and parallel-safe: an individual normal test has p95 ≤50 ms with no wall-clock +sleep, and `cargo test -p d2b-provider-credential-entra --lib --tests` +completes in ≤2 s warm-cache execution time (compilation excluded). They use a +deterministic fake clock/RNG and the toolkit fakes/FakeEffectPort only — no +process spawn, container, network, DBus, systemd, broker daemon, Nix eval/build, +KVM, USB/GPU/TPM hardware, or live cloud, and no filesystem tree beyond tiny +temp fixtures. Any scenario needing those lives only in `integration/`, which +keeps a lane timeout/budget, parallel isolation, and fake external services by +default; such a need is re-placed into `integration/`, never given a sleep, +larger timeout, or `#[ignore]`. Bounded crypto/property tests are the only +classified exception, each named with a capped case count and a declared higher +per-test budget. + +Per D094, each replaced current-code test is retired with an explicit +keep/adapt/move/delete disposition and a removal gate: the minimum reusable +semantic assertions migrate into this crate's hermetic `tests/`, and the old +duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest +entries are deleted once successor coverage and the removal proof pass — +updating `tests/layer1-jobs.json`, the closed gate manifests, the +flake/matrix/Nix-unit pins, the generated ledgers, and the CI workflow shards. +Old and new suites never run in parallel indefinitely. + +--- + +## 14. Current-code fit + +| Item | Value | +| --- | --- | +| Evidence class | Full Entrablau-backed login/token acquisition is ADR-only in this branch; sibling integration is external. | +| Retained concepts | Bounded opaque identifiers, `ExactConsumer` authorization, redacted Debug/canary discipline, D055/D056 Noise_KK token delivery. | +| Replaced concepts | No abstract Host-login path, no direct `EntraCredentialClient` production Entra egress, no effect-port proxy through consumer runtime, no Host/user-agent token custody, no ambient fallback chains, and no Provider state Volume for Entra credentials. | +| Zero-secret invariant | Preserved and strengthened: controller/helper are secret-free; refresh/private login state lives only in the Entrablau identity Guest; access tokens go only to exact consumers over end-to-end KK records; Credential identity is observable only as the authorized bounded audit digest and is absent from status/errors/logs and every OTEL/metric identity surface. | diff --git a/docs/specs/providers/ADR-046-provider-credential-managed-identity.md b/docs/specs/providers/ADR-046-provider-credential-managed-identity.md new file mode 100644 index 000000000..cc8178e43 --- /dev/null +++ b/docs/specs/providers/ADR-046-provider-credential-managed-identity.md @@ -0,0 +1,2173 @@ +# ADR 0046 Provider dossier: `credential-managed-identity` + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-credential-managed-identity` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-provider-credential-managed-identity` crate, Credential controller, Nix Credential compiler | +| Depends on | `ADR-046-resources-credential`, `ADR-046-componentsession-and-bus`, `ADR-046-provider-model-and-packaging`, `ADR-046-resource-reconciliation`, `ADR-046-nix-configuration`, `ADR-046-telemetry-audit-and-support` | +| Supersedes | Current v3 `ManagedIdentityRef`, `managed_identity_client_id` ACA config field, `CredentialProvider` trait (status/enrollment-only) in `d2b-realm-provider/src/credential.rs` and `provider.rs` | + +## Purpose + +This dossier specifies the `credential-managed-identity` Provider for d2b +3.0. It covers every aspect required to implement, test, integrate, and +operate the Provider from a fresh baseline: + +- `Provider.spec.config` schema with all field types, bounds, and invariants; +- two-process topology: one Zone-wide secret-free `managed-identity-controller` + plus one `managed-identity-agent` service Process per Credential/SDK-consumer + binding, co-located at the declared `executionRef`; +- SDK-consumer co-location contract for Azure VM and ACA Guest contexts; +- the injected `ManagedIdentityCredentialClient` held exclusively by the agent + process — no ambient IMDS chain, no environment-variable fallback, no + developer-credential path; +- all `d2b.credential.v3` service methods with exact controller/agent + dispatch split, their lease types, and the state machine; +- raw sensitive-output delivery exclusively over a dedicated + `Noise_KK_25519_ChaChaPoly_SHA256` end-to-end ComponentSession terminated + by the agent process; +- canonical Process resource templates for both roles; +- `ExactSdkConsumer` validation on the authenticated bus subject independently + of scope-field declarations; +- placement, RBAC, zero-secret-bytes invariant and redaction; +- status conditions, error codes, audit with Deleted-revision closure, and OTEL; +- Nix authoring shape, eval-time/build-time assertions, and cleanup contract; +- async reconcile loop including agent spawn/teardown lifecycle; +- exact v3 source reuse and main-branch copy targets; +- complete work items; +- `src/`/`tests/`/`integration/`/`README.md` test matrix; +- removal preconditions. + +--- + +## Core design principle: all persistent and observable surfaces are zero-secret + +Normative D089 spec layering: Credential base fields are ResourceType base +`spec.*` fields, including `spec.providerRef`, `audience`, `scope`, +`allowedOperations`, `rotation`, and `revocation`. This Provider's desired-only +extension is the canonical `spec.provider = { schemaId: +"credential-managed-identity.d2bus.org/Credential/spec", schemaVersion, settings }` +envelope; it is manifest-registered/signed, strict deny-unknown, bounded, versioned +and digested, validated against `spec.providerRef` at Nix build and API +admission, implementation-only, and may not shadow base fields. Shared fields +are promoted to the Credential base. The Provider implements the exact base +Credential spec/status version/fingerprint, accepts the canonical minimal valid +base Spec, and rejects unsupported optional base capabilities only through its +signed capability matrix and provider-neutral `unsupported-capability`. +`spec.provider` aligns with `status.provider`; generic CLI/controllers operate on +the base spec and base status only. No secret +bytes or credential material are allowed in any spec layer, including +`spec.provider.settings`; credential bytes are delivered only over Noise_KK +sessions. + +Every surface that may be observed by more than the authorized consumer +Provider process — resource spec, resource status, the redb store, revision +log, d2b-bus routing DTOs, audit records, OTEL spans, metrics, and all log +lines — **never contains secret material** in any field or byte. + +Specifically: + +- `spec` carries only opaque non-secret identifiers and policy fields + (`clientId`, `imdsEndpointAlias`, `maxLeases`). No token, key, bearer + string, connection string, or endpoint URL. +- `status` carries only opaque lease handles, timestamps, outcome codes, and + phase/condition values. No token bytes, no IMDS response content. +- The resource store row, redb WAL, and revision log never contain secret bytes. +- Resource API routing DTOs and d2b-bus envelopes never carry secret bytes. + The sole exception is the dedicated credential-delivery ComponentSession + described in §Credential-delivery endpoint contract: raw token bytes travel + exclusively inside end-to-end `Noise_KK`-encrypted records between the + **`managed-identity-agent` process** and the authorized consumer Provider + process. The `managed-identity-controller` process never touches token bytes, + holds no `ManagedIdentityCredentialClient`, and never opens or terminates a + credential-delivery channel. Bus intermediaries authorize and forward opaque + protected records without decrypting them. +- Error and outcome messages are bounded (max 240 UTF-8 bytes), stripped of + control characters, and must not echo token bytes, IMDS URLs, UUIDs, + subscription IDs, or Azure connection-string shapes. +- Audit records carry only stable codes and opaque digests. +- OTEL spans and metrics never label secret bytes, IMDS URL fragments, + tenant/subscription IDs, or Azure resource URI paths. +- The `contains_sensitive_shape` guard (adapted from + `d2b-realm-provider/src/error.rs:contains_sensitive_shape`, evidence class + `implemented-and-reachable` at baseline) is applied at construction time to + every string field in config, audit, and telemetry output. + +--- + +## Provider resource shape + +### Nix declaration + +```nix +# d2b.zones..resources. = { type = "Provider"; spec = { ... }; } +d2b.zones.dev.resources.credential-managed-identity = { + type = "Provider"; + spec = { + artifactId = "credential-managed-identity-bin"; # d2b.artifacts entry; type = "provider" + config = { + clientId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"; + imdsEndpointAlias = "azure-imds"; + maxLeases = 64; + controllerExecutionRef = "Host/azure-vm-host"; + }; + }; +}; +``` + +`spec.artifactId` is a sibling of `spec.config` on the Provider resource, not +nested inside `spec.config`. `spec.config` is the bounded non-secret runtime +configuration block; `spec.artifactId` is the artifact catalog ID whose store +path and digest are private catalog implementation data never present in the +resource spec, status, audit, or logs. + +### Canonical ResourceSpec JSON + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Provider", + "metadata": { + "name": "credential-managed-identity", + "zone": "dev" + }, + "spec": { + "artifactId": "credential-managed-identity-bin", + "config": { + "clientId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "imdsEndpointAlias": "azure-imds", + "maxLeases": 64, + "controllerExecutionRef": "Host/azure-vm-host" + } + } +} +``` + +`metadata.uid`, `metadata.generation`, `metadata.revision`, `metadata.createdAt`, +`metadata.updatedAt`, `metadata.managedBy`, and `metadata.configurationGeneration` +are filled by core at create/update time and are never authored in Nix. `status` +is controller-managed and is not present in the Nix-rendered bundle output. + +--- + +## Root `spec.config` schema + +### Field reference + +| Field | Type | Required | Bounds | Rules | +| --- | --- | --- | --- | --- | +| `clientId` | string | Yes | 1–128 chars; charset `^[A-Za-z0-9._-]+$` | Opaque Azure Managed Identity client GUID. Validated via `OpaqueAzureRef::parse` (current v3 baseline `d2b-realm-provider/src/credential.rs:OpaqueAzureRef`). The field name is `clientId`, not `clientIdRef`; it is an inline opaque identifier, not a `/` ResourceRef. A secret-shaped value (containing `=`, `+`, `/`, whitespace, or connection-string patterns) fails validation fail-closed. | +| `imdsEndpointAlias` | enum | Yes | closed set | Provider-validated closed alias string resolving to a known IMDS endpoint category. Never an endpoint URL, path, IP address, or hostname. Accepted values and their meanings are defined in §Closed alias set for `imdsEndpointAlias`. | +| `maxLeases` | u32 | No | 1–256; default 64 | Maximum concurrent active leases this Provider instance may hold. Requests beyond the ceiling return `credential-queue-pressure`. | +| `controllerExecutionRef` | ResourceRef | Yes | `Host/` or `Guest/` in same Zone | Execution target for the `managed-identity-controller` Process. Must resolve to a declared system-domain Host or Guest in the same Zone. The controller is secret-free regardless of placement: it holds no `ManagedIdentityCredentialClient`, makes no IMDS calls, and carries no token bytes. | + +### `clientId` validation + +`clientId` is validated using `OpaqueAzureRef::parse` from +`d2b-realm-provider/src/credential.rs` (evidence class: +`implemented-and-reachable`). The charset `^[A-Za-z0-9._-]+$` structurally +rejects secret-shaped values at parse time — the characters `=`, `/`, `+`, and +whitespace that bearer tokens and connection strings carry are all outside the +safe identifier set (fail-closed). A GUID-shaped value +(`aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee`) passes. A SAS token, connection +string, or bearer string fails. Maximum length is 128 chars +(`MAX_AZURE_REF_LEN`). The `clientId` field name matches the target config +schema; the current v3 field `ManagedIdentityRef.client_id` (snake_case) is the +source symbol and maps to camelCase `clientId` in the v3 Provider config. + +### Closed alias set for `imdsEndpointAlias` + +The Provider resolves an alias to its implementation-internal endpoint +configuration. The alias string is a short alphanumeric token; it is not a +URL, path, IP address, or fully-qualified hostname. The closed set is: + +| Alias | Resolved category | Notes | +| --- | --- | --- | +| `azure-imds` | Standard Azure Instance Metadata Service (IMDS) | For Azure VM Hosts or Guests where the standard IMDS endpoint is reachable | +| `azure-imds-aca` | Azure Container Apps sidecar IMDS | For ACA-backed Guest contexts where the ACA sidecar IMDS endpoint is provided to the container | + +Aliases outside this set fail the Provider install validation with a +schema-invalid error. The resolved endpoint is an internal implementation detail +of the Provider's `ManagedIdentityCredentialClient` implementation; it never +appears in resource spec, status, audit, metrics, log lines, or OTEL spans. +Adding a new alias requires a Provider version bump and schema change, not a +runtime string. + +### No endpoint URL or path in config + +`spec.config` must not and cannot contain a raw IMDS endpoint URL, path segment, +IP address, or hostname. The closed `imdsEndpointAlias` field is the only +mechanism for selecting the IMDS category. This prevents: + +- secret-shaped strings masquerading as endpoint values; +- operator-supplied SSRF-risk custom endpoints; +- drift between what audit/telemetry can safely echo and what the field contains. + +The eval-time `contains_sensitive_shape` assertion and the `OpaqueAzureRef` +charset check on `clientId` together enforce this at NixOS build time. The +Provider schema rejects unknown config fields at runtime. + +--- + +## ProviderStateSet and Provider status + +### ProviderStateSet + +**ProviderStateSet** is the optional query-time grouping of a Provider's +declared state Volumes. Under D087, bounded non-secret controller and credential +operational state belongs in the owning resource's `status` subresource and the +core Operation ledger by default. `credential-managed-identity` declares **no +Provider state Volume**: managed-identity token bytes are secret and never +eligible for persistence, while the bounded non-secret lease observation is +revision-suitable status state and does not pass the storage-need test. + +Therefore the ProviderStateSet for `Provider/credential-managed-identity` is +empty. Core ProviderDeployment creates no controller or agent state storage for +this Provider, component Processes receive no state mount, no layout principal +or `Provider/volume-local` state reconciliation is involved, and there is no +bootstrap state Volume mechanism or exception (D086, superseded by D087). + +### Status-first operational state + +Per D088, ResourceType-common Credential observation lives in `status.resource`: +the non-secret lease metadata base that is identical across Credential +implementations. Managed-identity-specific lease observations live only in +`status.provider` with `providerRef`, qualified `schemaId` +`credential-managed-identity.d2bus.org/Credential/status`, `schemaVersion`, +`observedProviderGeneration`, and strict bounded redacted `details` +(≤32 KiB, unknown-field-denied). The controller writes all present layers +atomically in one status mutation; shared +fields are never duplicated into `status.provider`, and the extension schema is +registered and signed in the Provider manifest. + +The owning `Credential/.status` carries only bounded, redacted, +RBAC-readable observation. `status.resource` carries lease phase, credential +readiness, expiry timestamps, audience, and opaque lease IDs that are +non-secret, non-authorizing, bounded, safe for authorized status readers, and +independently revalidated before use. `status.provider.details` carries +managed-identity-specific rotation generation, placement binding, bounded retry +counters, closed-enum error detail, and source observations. The core Operation +ledger carries in-flight idempotency/retry; status carries the latest bounded +lease result/checkpoint. + +Status is revisioned, optimistic-status-writer controlled, observation-only, +reverified against external IMDS/effect-port reality after restart, written only +on material change, and bounded to the D087 status limits (total ≤ 64 KiB, +provider-specific detail ≤ 32 KiB, with `status-oversize` rejection). It must +not contain managed-identity token bytes, keys, PSKs, authority-conferring +credential handles, private path/argv/env/PID/unit data, raw IMDS/cloud error +bodies, large blobs, endpoint private detail, or unbounded/churn-heavy content. +Token bytes are held only transiently in the agent process and delivered solely +over dedicated Noise_KK sensitive sessions. + +### Provider status aggregation + +Core aggregates the `Provider/credential-managed-identity` status from the +controller Process health, agent Process health, and Zone-level resource +conditions. The credential controller writes status only to: + +- `Credential/.status` — lease state, rotation generation, placement + binding, and condition flags received from the agent via status-update RPC; +- `Process/mi-agent-.status` — managed by the Process Provider + (`Provider/system-minijail`); the credential controller reads but does not + write this status. + +The credential controller never writes directly to `Provider/credential-managed-identity.status`. +Core owns Provider status aggregation; the controller contributes only by +keeping its own Process healthy and by updating the Credential resources it +manages. + +--- + +## SDK-consumer co-location model + +### `ManagedIdentityCredentialOwner::ExactSdkConsumer` + +This Provider is constructed for one exact co-located SDK consumer identified +by `spec.consumerRef`. The `ManagedIdentityCredentialOwner` variant is +`ExactSdkConsumer`: only the declared `consumerRef` Provider's authorized +component may invoke `d2b.credential.v3` methods on a Credential resource +backed by this Provider. There is no ambient fallback, no global consumer, and +no unauthenticated consumer path. + +### Azure VM context (Host) + +When placed as `host-system` (system-domain Process under a `Host` resource), +the Provider process runs as a system-domain service on the Azure VM host. +The injected `ManagedIdentityCredentialClient` communicates with the +`azure-imds` IMDS endpoint available on the host. The co-located SDK consumer +is the system-domain Process on the same Host whose `consumerRef` matches this +Provider. + +Example Credential resource for an Azure VM host: + +```yaml +spec: + providerRef: Provider/credential-managed-identity + scope: + executionRef: Host/azure-vm-host + domainFilter: system + audience: https://management.azure.com/ + consumerRef: Provider/runtime-azure-virtual-machine + allowedOperations: + - acquire-token + - refresh-token + - inspect-metadata + rotation: + policy: proactive + proactiveWindowMs: 300000 + maxLeaseLifetimeMs: 3600000 + revocation: + onOwnerDelete: immediate + onProviderGeneration: immediate +``` + +### Azure Container Apps context (Guest) + +When placed as `guest-agent` (system-domain Process under a `Guest` resource +backed by `Provider/runtime-azure-container-apps`), the Provider process runs +inside the container alongside the co-located SDK consumer. The injected client +uses the `azure-imds-aca` alias to reach the ACA sidecar IMDS endpoint provided +to the container. This is the formalized replacement for the current v3 +`d2b-provider-aca/src/lib.rs:managed_identity_client_id` raw `Option` +config field. + +Example Credential resource for an ACA Guest: + +```yaml +spec: + providerRef: Provider/credential-managed-identity + scope: + executionRef: Guest/aca-sandbox + domainFilter: system + audience: https://relay.servicebus.windows.net/ + consumerRef: Provider/runtime-azure-container-apps + allowedOperations: + - acquire-token + - refresh-token + - revoke-token + - inspect-metadata + rotation: + policy: proactive + proactiveWindowMs: 300000 + maxLeaseLifetimeMs: 3600000 + revocation: + onOwnerDelete: immediate + onProviderGeneration: immediate +``` + +### No ambient IMDS chain + +There is no environment credential chain, no `AZURE_CLIENT_ID` environment +variable fallback, no `DefaultAzureCredential` chain, no developer-tool +credential, and no keyring path. The `ManagedIdentityCredentialClient` is +injected during Provider construction with an explicit `imdsEndpointAlias` +binding. The production `ManagedIdentityCredentialProvider::new` constructor +accepts only an injected `impl ManagedIdentityCredentialClient`; it has no +ambient-discovery code path. A test may inject a `FakeClient`; production +injects the real client bound to the alias-resolved endpoint configuration. + +The current v3 ACA path (`d2b-provider-aca/src/lib.rs`) uses an +`Option` raw `managed_identity_client_id` field that is passed through +to the Azure SDK's credential chain. The v3 Provider formalizes this as an +`OpaqueAzureRef`-typed `clientId` config field and replaces the ambient-chain +path with an injected client bound to the closed alias. + +--- + +## Supported placement bindings + +| Placement binding | Accepted | Notes | +| --- | --- | --- | +| `host-system` | Yes | System-domain Process under a `Host` resource with IMDS-compatible endpoint | +| `guest-agent` | Yes | System-domain Process under a `Guest` resource with IMDS-compatible sidecar endpoint | +| `user-agent` | **Rejected** | Managed identity is a machine-level credential, not a user-session credential. Provider install validation and eval-time assertion both reject `domainFilter=user`. | + +The placement binding is derived by the Credential controller from the Credential +resource's `scope` fields and stored in +`status.provider.details.placementBinding`. +The Provider schema declares `supportedPlacementBindings: [host-system, +guest-agent]` and `rejectedPlacementBindings: [user-agent]`. Any Credential +resource whose `scope.domainFilter = "user"` fails the NixOS eval-time assertion +before the bundle is emitted. + +--- + +## Supported operation classes + +| Operation class | Supported | Notes | +| --- | --- | --- | +| `acquire-token` | Yes | Issue a new token from IMDS for the configured `audience` | +| `refresh-token` | Yes | Refresh an existing active lease | +| `revoke-token` | Yes | Mark the lease revoked; no IMDS call required (managed identity tokens are not revocable at the IMDS level; local lease state is marked Revoked) | +| `inspect-metadata` | Yes | Return opaque lease metadata without token delivery | +| `sign-challenge` | **Not supported** | IMDS does not expose a signing primitive; `sign-challenge` is not in the Provider's declared `supportedOperations`; the schema immediately returns `credential-schema-invalid` | + +`allowedOperations` in a Credential resource must be a non-empty subset of +`[acquire-token, refresh-token, revoke-token, inspect-metadata]`. Any element +outside this set, including `sign-challenge`, fails the NixOS eval-time assertion +and the runtime schema check. + +--- + +## Credential-bound service methods + +The `d2b.credential.v3` protobuf/ttrpc service methods are dispatched across +two processes depending on whether IMDS access is required: + +- **`managed-identity-controller`** optionally serves `InspectMetadata` from + stored resource-store state only. It holds no + `ManagedIdentityCredentialClient`, makes no IMDS calls, and never terminates + a credential-delivery session. +- **`managed-identity-agent`** (one per Credential/consumerRef binding) serves + `AcquireToken`, `RefreshToken`, `RevokeToken`, and the live `InspectMetadata` + path. The agent holds the injected `ManagedIdentityCredentialClient`, + accesses the IMDS alias, and terminates the E2E `Noise_KK` delivery session. + The agent is spawned and owned by the controller; its process is co-located + at `Credential.spec.scope.executionRef`. + +d2b-bus derives the exact operation class from the method and routes it to the +correct endpoint: stored `InspectMetadata` goes to the controller; +`AcquireToken`, `RefreshToken`, `RevokeToken`, and live `InspectMetadata` go to +the agent. No request carries a caller-selected operation-class field. + +### Method table + +| Method | Served by | Exact operation/Role subresource | Required Role permission | Outer DTO fields | Sensitive output | +| --- | --- | --- | --- | --- | --- | +| `AcquireToken` | **agent** | `acquire-token` | `use-credential/acquire-token` | `AcquireTokenResponse`: `leaseHandle`, `sourceVersion`, `rotationGeneration`, `expiresAtUnixMs` | Raw token bytes in dedicated `Noise_KK` delivery session (see §Credential-delivery endpoint contract) | +| `RefreshToken` | **agent** | `refresh-token` | `use-credential/refresh-token` | `RefreshTokenResponse`: `leaseHandle`, `sourceVersion`, `rotationGeneration`, new `expiresAtUnixMs` | Raw token bytes in dedicated `Noise_KK` delivery session | +| `RevokeToken` | **agent** | `revoke-token` | `use-credential/revoke-token` | `RevokeTokenResponse`: closed revocation result (`Revoked` or `AlreadyRevoked`), `revokedAtUnixMs` | None | +| `SignChallenge` | admission only | `sign-challenge` | `use-credential/sign-challenge` | `credential-schema-invalid` because this Provider does not declare the operation | None | +| `InspectMetadata` | **agent** (live) / **controller** (stored) | `inspect-metadata` | `use-credential/inspect-metadata` | `InspectMetadataResponse`: `leaseState`, `rotationGeneration`, `sourceVersion`, `expiresAtUnixMs` | None | + +`SignChallenge` is not implemented. A `SignChallenge` call returns +`credential-schema-invalid` immediately, before any IMDS interaction, and +closes the request. + +Every method: + +- rejects an unauthenticated caller or a caller whose **authenticated bus + subject** (established by the ComponentSession from SO_PEERCRED or the + enrolled Noise_KK static key — see §ExactSdkConsumer authentication below) + does not match `spec.consumerRef` when set, with `credential-consumer-mismatch`; +- rejects before Provider dispatch unless the method's one exact operation is + present in both `spec.allowedOperations` and the Role `subresources` under + `use-credential`, with `credential-operation-denied`; +- returns a stable closed error code rather than IMDS response content or + provider-internal diagnostics; +- carries operation/idempotency/correlation IDs from the d2b-bus context; +- enforces a per-call deadline propagated from the d2b-bus context. + +### `ManagedIdentityCredentialClient` trait + +The injected trait is held **exclusively by the agent process** and is the sole +interface to the IMDS endpoint. It accepts and returns typed lease +request/grant/renewal/revocation/inspection values only. No trait method +parameter accepts a raw token string, endpoint URL, bearer header, environment +variable name, or byte buffer. No trait method return value carries a raw token +string to the outer calling context; token bytes flow only through the delivery +session after `AcquireToken` and `RefreshToken`. + +```text +ManagedIdentityCredentialClient: + issue_lease(&ManagedIdentityLeaseRequest) -> Result + refresh_lease(&ManagedIdentityLeaseRef) -> Result + revoke_lease(&ManagedIdentityLeaseRef) -> Result + inspect_lease(&ManagedIdentityLeaseRef) -> Result +``` + +`ManagedIdentityClientState`: `Ready | Unavailable`. + +`ManagedIdentityClientError::Unavailable` maps to +`credential-provider-unavailable`. There is no `InteractionRequired` state +for managed identity; IMDS is either reachable or not. + +### `ManagedIdentityLeaseRequest` + +Fields (all non-secret): + +| Field | Type | Notes | +| --- | --- | --- | +| `audience_opaque_digest` | [u8; 32] | SHA-256 digest of the opaque `spec.audience` string; the raw audience string is not passed to the trait method or recorded in any audit/log surface | +| `requested_expiry_unix_ms` | u64 | Desired expiry; capped by `maxLeaseLifetimeMs` and provider maximum | +| `idempotency_key` | [u8; 32] | Derived from Credential UID + `rotationGeneration` + operation class; no secret material | + +### `ManagedIdentityLeaseGrant` + +Fields (all non-secret except the token bytes held internally): + +| Field | Type | Notes | +| --- | --- | --- | +| `lease_handle` | ManagedIdentityLeaseHandle | Opaque newtype wrapping a bounded string (max 256 chars); not a token; stable across refreshes within one rotation generation | +| `source_version` | String | Opaque, bounded non-secret version string from the IMDS response (e.g. a generation/revision field); max 64 chars | +| `expires_at_unix_ms` | u64 | Token expiry from IMDS response | +| `issued_at_unix_ms` | u64 | Token issue time from IMDS response | + +The raw token bytes are held by the `ManagedIdentityCredentialClient` +implementation internally; they are never present in the `ManagedIdentityLeaseGrant` +struct fields returned to the calling Provider service handler. Token bytes are +delivered exclusively through the `Noise_KK` end-to-end delivery session +established during `AcquireToken` and `RefreshToken` handling. + +### `ManagedIdentityLeaseHandle` + +A bounded newtype wrapping a short opaque string identifier. It must not +resemble, contain, or be derivable from a token, bearer string, IMDS response +fragment, GUID, or connection string. It is safe to include in audit records +and metric labels as an opaque handle. Maximum length: 256 chars. +`ManagedIdentityLeaseHandle` implements a hand-written `Debug` that emits only +`ManagedIdentityLeaseHandle(REDACTED)` — no auto-derived `Debug`. + +### ExactSdkConsumer authentication + +`ExactSdkConsumer` validation is performed by the **agent process** on the +`AuthenticatedSubjectContext` established by the incoming ComponentSession. +This validation is **independent of scope-field declarations** and works as +follows: + +1. The agent's ComponentSession is established over a d2b-bus enrolled channel + using either SO_PEERCRED (for system-domain callers on the same host) or the + consumer Provider's enrolled Noise_KK static key (for enrolled component + processes). +2. d2b-bus maps the authenticated credential to a Zone subject identifier via + the Zone identity registry, yielding a `Provider/` identity + or a `Principal/` for direct SO_PEERCRED callers. +3. d2b-bus provides the resulting `AuthenticatedSubjectContext` to the agent + alongside the forwarded request envelope; the agent does not re-derive the + identity from the request payload. +4. The agent checks: `AuthenticatedSubjectContext.provider_ref == spec.consumerRef` + when `spec.consumerRef` is set. If the authenticated subject does not match, + the method returns `credential-consumer-mismatch`. If `spec.consumerRef` is + null (any authorized Provider), authentication still proceeds through the + enrolled key — there is no unauthenticated path. +5. Additionally, the consumer's signed component descriptor (available through + the enrolled key) must declare `credentialProviderRef: Provider/credential-managed-identity` + for the RBAC `use-credential` check to pass. + +This model means that even if a caller forges a `scope.consumerRef`-shaped +field in the request, the check fails: the validation is on the runtime +authenticated bus identity, not on any caller-supplied field. + +`user-agent` placement is hard-rejected at Placement Validation time (see +§Placement validation); the agent process never receives a request from a +`user-agent` domain caller. + +--- + +## Credential-delivery endpoint contract + +Token bytes for `AcquireToken` and `RefreshToken` are delivered exclusively +through a dedicated end-to-end `Noise_KK_25519_ChaChaPoly_SHA256` +ComponentSession. The `Noise_NN` profile is forbidden for sensitive output +delivery; any anonymous-channel attempt is rejected immediately and the session +is zeroized. + +### Session profile + +Both parties must present enrolled static keys: + +- **Credential Provider key**: registered at Provider installation; the bus + holds only the public key. +- **Consumer Provider key**: extracted from the consumer Provider's signed + component descriptor. The consuming component/Process identity is derived + from the consumer Provider's Role/RoleBinding; no arbitrary component is + permitted. + +### Binding contract + +Each delivery session binds (in the Noise prologue verified by both parties): + +| Field | Description | +| --- | --- | +| `credentialRef` | `Credential/` | +| `credentialUID` | Credential resource UID (stable across spec updates) | +| `credentialGeneration` | Credential resource generation at delivery time | +| `consumerProviderRef` | `Provider/` matching `spec.consumerRef` | +| `consumerComponentGeneration` | Consumer Provider component generation from signed descriptor | +| `audience_digest` | SHA-256 of `spec.audience` — the raw audience string never enters the prologue or any log/audit surface | +| `operationClass` | Closed enum: `acquire-token` or `refresh-token` | +| `expiryUnixMs` | Absolute delivery-session expiry; clipped to `spec.rotation.maxLeaseLifetimeMs` | +| `deadlineUnixMs` | Hard session close deadline; must be ≤ `expiryUnixMs` | +| `routeDigest` | Digest of bus-authorized route parameters (Zone, consumer, provider) | +| `schemaVersion` | Fixed version of this binding contract | +| `maxTokenBytes` | Closed upper bound on sensitive record size for this session | +| `transcriptDigest` | Noise transcript digest after handshake, before first record | + +### Security requirements + +1. **Enrolled keys only**: both static keys enrolled and verified. Any NN/NX/N + attempt is rejected immediately; session closed and zeroized. +2. **Replay-safe sequence**: each delivery session carries a monotonically + increasing per-Credential-UID sequence number. A replay of a prior session's + ciphertext at the same or lower sequence number is rejected. +3. **Bounded output size**: the sensitive record must not exceed `maxTokenBytes`. + Any oversize record is rejected; channel closed and zeroized immediately. No + fragmentation unless each fragment is independently bounded and the + reassembled total is within `maxTokenBytes`. +4. **Zeroizing buffers**: the delivery record's plaintext is zeroed immediately + after the consumer extracts it. The Provider zeros the plaintext source after + encryption. All intermediate buffers are zeroizing types. +5. **Redacted Debug**: all credential-bearing Rust types involved in delivery + implement `Debug` via a hand-written redacted impl. Derived `Debug` is + forbidden for these types. +6. **No automatic success-shaped replay**: after any ambiguous delivery outcome, + the Provider must not automatically retry with the same record. The consumer + must re-initiate, establishing a new delivery session with a fresh sequence + number. +7. **Immediate close/zeroize**: after confirmation, the Provider closes the + delivery channel and zeroizes all session key material. The consumer + similarly closes and zeroizes after extraction. The channel is not reused + across multiple deliveries. + +### RBAC enforcement at bus + +d2b-bus performs the following checks before authorizing the delivery route: + +- RBAC `use-credential` for the authenticated consumer Provider subject, + Credential ResourceRef, and operation class; +- `spec.consumerRef` matches the consumer Provider identity; +- `spec.allowedOperations` includes the operation class; +- current lease state permits the operation class; +- no Role/RoleBinding or Provider generation change has revoked authorization + since the last audit checkpoint. + +After these checks pass, bus forwards opaque `Noise_KK`-encrypted records +between the two endpoints until the delivery session closes. Bus never buffers +or stores the records. + +--- + +## Process components + +| Component | Type | Domain | Binary | Cardinality | +| --- | --- | --- | --- | --- | +| `managed-identity-controller` | controller | system | `d2b-managed-identity-controller` | One per Zone; manages all `credential-managed-identity`-backed Credential resources; **no IMDS access; no token bytes** | +| `managed-identity-agent` | service | system | `d2b-managed-identity-agent` | One per Credential/consumerRef binding; co-located at `Credential.spec.scope.executionRef`; holds injected client; terminates E2E KK delivery | + +The system-domain constraint is unconditional for both processes. +`user-agent` domain is rejected at Provider install validation and again at +Credential controller `reconcile` time. + +### Controller process + +The `managed-identity-controller` process: + +- is owned by `Provider/credential-managed-identity` (`ownerRef`); +- holds **no** `ManagedIdentityCredentialClient`; +- makes **no** IMDS calls; +- never opens, terminates, or forwards a credential-delivery `Noise_KK` session; +- watches and reconciles all `Credential` resources backed by this Provider; +- spawns one `managed-identity-agent` Process resource per Credential binding + when the Credential spec is admitted and its dependencies are ready (not + after Credential `phase=Ready`, which depends on agent readiness); tears it + down on Credential deletion; +- may serve `InspectMetadata` from resource-store state over + `d2b.credential.v3`; +- monitors agent Process health via `ownerChildTrigger` watch; sets + `ProviderUnavailable=True` on sustained agent Process failure. + +The controller descriptor declares: + +```yaml +providerId: Provider/credential-managed-identity +controllerType: Credential +resourceTypes: [Credential] +watchSelectors: + - resourceType: Credential + providerRefFilter: Provider/credential-managed-identity + - resourceType: Provider + nameFilter: credential-managed-identity + - resourceType: Process + ownerRefFilter: Provider/credential-managed-identity +dependencySelectors: + - resourceType: Provider + relationship: providerRef + - resourceType: Host + relationship: scope.executionRef + - resourceType: Guest + relationship: scope.executionRef + - resourceType: Process + relationship: ownerRef # agent processes owned by Credential resources +ownerChildTriggers: [owned-resource-changed, agent-process-health-changed] +reconcileConcurrency: 8 +maxPendingResources: 256 +finalizers: [credential.d2bus.org/provider-revoke] +observeInterval: 30s +``` + +#### Currency and upgrade (D091) + +The `managed-identity-controller` implements `assess_update`, `plan_upgrade`, +and `execute_upgrade`. A Provider generation or signed artifact generation/digest +change updates universal `status.update` with `state: UpdateAvailable` or +`state: UpgradeRequired`, `reasons` including `ProviderGenerationChanged` or +`ArtifactChanged`, observed/target generation or digest IDs, +`disruption: Reload` or `disruption: Restart` for the credential component +realization, `preserveState: true`, bounded `owned`/`dependencies`, and +`lastAssessedAt`. Disruptive changes MUST return `UpgradeRequired` rather than +applying in place; non-disruptive changes reconcile normally. Credential +rotation is not an upgrade and remains the lease lifecycle flow. `status.update` +MUST NOT contain secret bytes, tokens, or lease material; only bounded +non-secret generation/digest IDs and lease metadata already permitted by the +Credential base may appear. Token delivery remains solely over `Noise_KK`. + +#### Expedited reconcile on mutation (D090) + +For `Create`, `UpdateSpec`, and `Delete` with `waitForReconcile`, the +`managed-identity-controller` MUST perform no IMDS/effect-port call, Process +create/delete, finalizer change, or status mutation until core supplies +`CommittedRevisionProof {resourceUid,generation,revision,operationId}`. Abort +before that proof has no effect. After durable commit, the commit is never +rolled back if the reconcile pass times out. The response returns the committed +object, post-pass projected layered status, disposition +(`Converged|Progressing|Blocked|UpgradeRequired|Failed`), and +`statusPersistence: pending|committed`. Effect idempotency keys derive from +`(UID,generation,revision,operationId)` and use the same per-resource +single-flight priority lane. + +### Agent process + +The `managed-identity-agent` process: + +- is owned by the Credential resource it serves (`ownerRef: Credential/`); +- is spawned and destroyed by the controller (never configuration-managed); +- is co-located at `Credential.spec.scope.executionRef` (Host system domain + or Guest agent domain); +- receives exactly one `ManagedIdentityCredentialClient` implementation via an + **effect port** supplied by the co-located runtime Provider at agent start-up; + the client is bound to the IMDS alias resolved from the controller's + LaunchTicket projection (see §Process resource templates); the agent never + opens any direct network connection to an IMDS endpoint; +- serves `AcquireToken`, `RefreshToken`, `RevokeToken`, and the live + `InspectMetadata` path over `d2b.credential.v3` to the exact authorized + consumer process; +- terminates the E2E `Noise_KK_25519_ChaChaPoly_SHA256` delivery session for + token bytes; +- reports lease state changes to the controller via controller-internal + status-update RPC (not via `d2b.credential.v3`); +- holds no other Credentials, no Zone admin token, and no secrets beyond the + IMDS-issued lease bytes held internally by the client implementation. + +--- + +## Process resource templates + +These templates show the canonical shape of the Process resources for both +roles. All fields shown are required unless marked optional; fields excluded +by the instruction set (principalRef, profileRef, endpoint `kind`, Process +`config`, `telemetry.componentRef`, `readiness.probe`/`timeoutMs`, +`network.allowedEffects`) are absent. `budget` uses canonical nested +cpu/memory/pids/fds fields — there is no top-level `class` key on +BudgetSpec. `telemetry` uses canonical metricsEnabled/tracingEnabled/logLevel/ +sensitiveLabels fields — there is no `class` key on TelemetrySpec. +Controller-created agent resources must not appear in Zone bundle Nix +configuration — they are managed exclusively by the controller. + +### Controller Process template + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: managed-identity-controller + zone: + ownerRef: Provider/credential-managed-identity +spec: + processClass: controller + template: managed-identity-controller + providerRef: Provider/system-minijail + executionRef: # Host/ or Guest/ + domain: system + sandbox: + namespaceClasses: [mount, pid, ipc] # inherits execution target network namespace + capabilityClasses: [] + seccompClass: strict + startRoot: false + noNewPrivileges: true + environmentClass: minimal + readOnlyRoot: true + budget: + cpu: + request: "100m" + limit: "500m" + memory: + request: "32Mi" + limit: "128Mi" + pids: + limit: 64 + fds: + limit: 256 + readiness: + class: provider-defined + initialDelay: "1s" + timeout: "10s" + failureThreshold: 3 + successThreshold: 1 + telemetry: + metricsEnabled: true + tracingEnabled: true + logLevel: info + sensitiveLabels: false +``` + +The controller sandbox declares `[mount, pid, ipc]` and inherits the execution +target's network namespace; all communication goes through the authorized +d2b-bus ComponentSession supplied at launch. No `networkUsage` section is needed. +No Process `config` section is needed; +the controller derives its configuration from the Provider resource spec at +registration time. `executionRef` is bound from `Provider.spec.config.controllerExecutionRef` +at spawn time; the controller remains secret-free regardless of placement. + +### Agent Process template + +The controller creates one agent Process resource for each Credential whose spec +is **admitted and whose dependencies are ready** (executionRef resolves to a live +Host or Guest, Provider/credential-managed-identity is Ready). The agent Process +is owned by the Credential resource (`ownerRef: Credential/`): +when the Credential is deleted the controller removes the agent Process before +releasing the `provider-revoke` finalizer. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: mi-agent- # controller-derived, non-configurable + zone: + ownerRef: Credential/ # deleted when Credential is deleted +spec: + processClass: service + template: managed-identity-agent + providerRef: Provider/system-minijail + executionRef: # Host/ or Guest/ + domain: system + sandbox: + namespaceClasses: [mount, pid, ipc] # inherits execution target network namespace + capabilityClasses: [] + seccompClass: strict + startRoot: false + noNewPrivileges: true + environmentClass: minimal + readOnlyRoot: true + networkUsage: + networkRef: null + ports: [] + allowEgress: false + budget: + cpu: + request: "50m" + limit: "250m" + memory: + request: "16Mi" + limit: "64Mi" + pids: + limit: 32 + fds: + limit: 128 + readiness: + class: provider-defined + initialDelay: "1s" + timeout: "5s" + failureThreshold: 3 + successThreshold: 1 + telemetry: + metricsEnabled: true + tracingEnabled: true + logLevel: info + sensitiveLabels: false +``` + +The agent Process produces its stable credential service identity as a separate +owned `Endpoint` resource. The sensitive KK token-delivery session is not an +Endpoint. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: mi-agent--credential-service + zone: + ownerRef: Credential/ +spec: + providerRef: Provider/credential-managed-identity + producerRef: Process/mi-agent- + endpointClass: service + transport: unix + purpose: credential-managed-identity.d2bus.org/credential-service + serviceFingerprint: credential.d2bus.org/CredentialService.v3 + locality: host-local + visibility: zone + attachmentPolicy: component-session + consumerPolicy: + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +## Endpoint resources (D092) + +`Provider/credential-managed-identity` conforms to the standard `Endpoint` base +schema. The stable `d2b.credential.v3`-class ComponentSession service identity is +an owned `Endpoint` resource with `producerRef`; consumers use +`Endpoint/`. Endpoint spec/status never carries IMDS URLs, raw endpoint +locators, fd numbers, token bytes, subscription IDs, credential handles with +authority, or secrets. Resolution occurs only through an authorized +EffectPort/LaunchTicket; unauthorized resolution returns `endpoint-resolve-denied`. +Producer restart bumps `Endpoint.status.endpointGeneration`, causing consumers to +observe `dependency-changed` and reacquire through a fresh authorized ticket. + +## Retained opaque handles (D092 promotion test) + +- pidfds for controller/agent Processes are supervision handles, not resources. +- LaunchTicket fd indexes for the injected IMDS/effect-port client and d2b-bus + connection remain per-launch opaque attachment slots. +- `leaseHandle`, IMDS client aliases, `operationId`, and idempotency keys are + bounded non-secret handles revalidated before use. +- `OwnedTransport`, ComponentSession IDs, and the sensitive Noise_KK token-delivery + session handle are high-churn in-memory capabilities and remain internal. + +#### IMDS access and LaunchTicket projection + +The agent Process spec declares `networkUsage.allowEgress: false`. The agent never +opens any direct network connection. IMDS access is provided exclusively through +the injected `ManagedIdentityCredentialClient` implementation, supplied via an +**effect port** by the co-located runtime Provider at agent start-up. + +The `imdsEndpointAlias` (closed enum `azure-imds | azure-imds-aca`) from +`Provider.spec.config` is projected into a **LaunchTicket** — a signed, +bounded, non-stored launch-time configuration blob — that the controller attaches +to the agent Process resource at spawn time. The runtime Provider reads the +LaunchTicket, resolves the alias to the correct IMDS client configuration, and +constructs the `ManagedIdentityCredentialClient` implementation bound to that +endpoint. The resolved endpoint URL never appears in: + +- the Process resource spec or store entry; +- any resource status, audit record, metric label, OTEL span attribute, or log; +- the agent's environment variables, filesystem, or configuration namespace. + +The `credentialRef` and other non-secret configuration needed by the agent at +start-up are also projected via the LaunchTicket, not stored in the Process spec +`config` section. + +--- + +## Placement validation + +### Eval-time (Nix assertions) + +Applied to every `d2b.zones..resources.` entry whose +`type = "Credential"` and `spec.providerRef = "Provider/credential-managed-identity"`: + +- `spec.scope.domainFilter` must not be `"user"`. Assertion failure with message: + `"credential-managed-identity does not support user-domain placement; use credential-secret-service or credential-entra for user-session credentials"`. +- `spec.scope.executionRef` must resolve to a declared `Host/` or + `Guest/` in the same Zone. +- `spec.allowedOperations` must be a non-empty subset of + `[acquire-token, refresh-token, revoke-token, inspect-metadata]`. Any element + outside this set (including `sign-challenge`) fails with: `"credential-managed-identity + does not support operation class "`. +- `spec.consumerRef`, if set, must resolve a declared `Provider/` in + the same Zone. +- `spec.audience` passes `OpaqueAzureRef`-equivalent charset validation + (`^[A-Za-z0-9._:/@-]+$`, max 256 chars). Any field failing this check + triggers the `contains_sensitive_shape` assertion. + +### Runtime (controller and agent validation) + +**Controller** (`reconcile`): + +- Checks `spec.scope.domainFilter != "user"`. If the constraint is violated + (should not occur after eval-time gate), the controller sets `phase=Failed`, + `outcome=credential-placement-mismatch`, and does not spawn an agent. +- Validates that `spec.scope.executionRef` resolves to a live `Host` or `Guest` + resource; if the target is not ready, the controller sets `phase=Pending`. +- Spawns an agent Process when the Credential spec is **admitted** (domain and + allowedOperations checks pass) and all dependencies are **ready** (executionRef + live, Provider Ready). The controller does not wait for the Credential to reach + `phase=Ready` before spawning — doing so would be circular, because the + Credential only reaches `phase=Ready` after the agent is ready. + +**Agent** (per-method dispatch): + +- `ManagedIdentityCredentialOwner::ExactSdkConsumer` is enforced against the + **`AuthenticatedSubjectContext`** provided by the agent's ComponentSession. + The check compares the runtime authenticated bus identity (established from + SO_PEERCRED or the consumer's enrolled Noise_KK static key, independently of + any caller-supplied field) against `spec.consumerRef`. If the authenticated + subject does not match, the method returns `credential-consumer-mismatch` + before any IMDS interaction. If `spec.consumerRef` is null, authentication + still proceeds through the enrolled channel — there is no unauthenticated + path. +- `sign-challenge` operation class returns `credential-schema-invalid` + immediately before any IMDS interaction. +- `user-agent` domain is hard-rejected: the agent process is never started with + `executionRef` resolving to a user-domain endpoint. + +--- + +## RBAC + +The `credential-managed-identity` Provider participates in the standard +Credential RBAC model defined in `ADR-046-resources-credential`. + +### Required verbs + +| Verb | Who | Meaning | +| --- | --- | --- | +| `get` | Any authorized subject | Read Credential metadata/spec/status; no secret bytes present | +| `list` | Any authorized subject | List managed-identity Credentials | +| `watch` | Any authorized subject; `managed-identity-controller` | Watch Credential events (controller watches all; consumers watch own) | +| `create` plus `admin-credential/create` | `activation-nixos` controller | Create Credential resource from bundle; neither permission implies the other | +| `update-spec` plus `admin-credential/update-spec` | `activation-nixos` controller | Replace Credential spec; neither permission implies the other | +| `update-status` | `managed-identity-controller` only | Update Credential status subresource | +| `update-finalizers` | `managed-identity-controller`, `consumerRef` controller | Add/remove owned finalizers | +| `delete` plus `admin-credential/delete` | `activation-nixos` controller | Request Credential deletion; neither permission implies the other | +| `use-credential` | Consumer subject authorized via `consumerRef`; dispatched by agent | Invoke one admitted `d2b.credential.v3` method under its exact allowed-operation subresource | + +Agent Process creation/deletion uses ordinary `Process` `create`/`delete` +authority plus structural ownership checks. Reading the bound Credential uses +ordinary `Credential` `get`; `spawn-agent` and `get-credential` are not verbs. + +### `use-credential` Role rule shape + +```yaml +rules: + - resourceTypes: [Credential] + verbs: [use-credential] + subresources: [acquire-token, refresh-token] + resourceNames: [aca-mi-relay] + zones: [dev] + executionRefs: [Guest/aca-sandbox] + sessionVerbs: [] +``` + +The effective operation set is the intersection of the Credential resource's +`spec.allowedOperations` and the Role rule's exact `subresources`, further +narrowed by `consumerRef`, scope, and structural Provider/component checks. +Empty, wildcard, unknown, and mismatched subresources deny; there is no +alternate Credential-operation Role field or shorthand operation alias. + +### Consumer descriptor requirement + +The consuming component's signed component descriptor must: + +- declare `credentialProviderRef: Provider/credential-managed-identity`; +- carry an enrolled static public key for the `Noise_KK` delivery channel; +- list the exact Credential ResourceRef and operation classes it will invoke. + +The bus validates the descriptor fingerprint before routing the delivery session. +No arbitrary component within the consumer Provider may receive raw token bytes. + +--- + +## Security invariants + +### OpaqueAzureRef at construction time + +`clientId` is validated by `OpaqueAzureRef::parse` at Provider config parse time. +A malformed or secret-shaped value causes Provider install validation to fail with +`credential-schema-invalid` before the Provider resource reaches `Ready` status. +The Provider process never starts with an invalid `clientId`. + +### No ambient credential discovery + +The `ManagedIdentityCredentialProvider::new` constructor signature — used by +the **`managed-identity-agent` process** at start-up — accepts only: + +- a validated `ManagedIdentityClientConfig` (containing the resolved alias binding); +- an injected `impl ManagedIdentityCredentialClient + Send + Sync + 'static`. + +It has no code path that calls `azure_identity::DefaultAzureCredential::new()`, +reads `AZURE_CLIENT_ID` or any other environment variable, consults a +filesystem path, or calls a credential chain builder. This invariant is +enforced by the absence of these imports in `src/agent.rs` and confirmed by the +`canary.rs` test suite. + +The `managed-identity-controller` process does not import or call +`ManagedIdentityCredentialProvider::new`; it has no IMDS client and no +credential-discovery code path whatsoever. + +### Injection contract for production + +The co-located runtime Provider constructs the `ManagedIdentityCredentialClient` +implementation from the **LaunchTicket projection** attached by the controller at +agent spawn time. The projection contains: + +- the `imdsEndpointAlias` (closed enum, no URL string) from `Provider.spec.config`; +- the `clientId` (already validated `OpaqueAzureRef`) from `Provider.spec.config`. + +The runtime Provider supplies the resulting client implementation to the agent +via an effect port. The agent's `agent/main.rs` entry point receives the injected +client; it never reads `imdsEndpointAlias` or constructs a client from environment +variables or filesystem config. The resolved endpoint URL is a private +implementation detail of the production client and is never serialized, logged, +or placed in any provider output surface. + +### Zero-secret status + +The Credential **controller** writes only the following status fields (received +from the agent via status-update RPC): + +Common `status.resource` lease metadata: + +| Field | Value written | Secret? | +| --- | --- | --- | +| `leaseHandle` | `ManagedIdentityLeaseHandle` opaque newtype value | No | +| `leaseState` | Closed enum: `Active`, `Expired`, `Revoked`, `Unknown` | No | +| `audience` | Opaque bounded audience alias or digest; never raw Azure resource ID | No | +| `expiresAtUnixMs` | Unix milliseconds | No | +| `issuedAtUnixMs` | Unix milliseconds | No | + +Managed-identity-specific `status.provider.details`: + +| Field | Value written | Secret? | +| --- | --- | --- | +| `rotationGeneration` | Monotonic counter | No | +| `sourceVersion` | Opaque bounded string from grant | No | +| `lastRefreshedAt` | RFC 3339 UTC timestamp | No | +| `lastRotatedAt` | RFC 3339 UTC timestamp or null | No | +| `placementBinding` | `host-system` or `guest-agent` | No | + +No token bytes, IMDS response fragments, base64-encoded material, or Azure +resource IDs appear in any status field. The `credential_canary` value used in +the test suite is explicitly confirmed absent from every status field written by +the controller in `canary.rs`. + +--- + +## Redaction + +All Rust types in the Provider that may transitively hold token bytes or IMDS +response content must implement `Debug` via a hand-written redacted impl: + +```rust +impl fmt::Debug for ManagedIdentityLeaseGrant { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ManagedIdentityLeaseGrant") + .field("lease_handle", &self.lease_handle) + .field("expires_at_unix_ms", &self.expires_at_unix_ms) + .field("source_version", &"REDACTED") + .finish() + } +} +``` + +Types that are unconditionally non-secret (e.g., `ManagedIdentityLeaseHandle`, +`ManagedIdentityLeaseState`) may use auto-derived `Debug`. The delivery record +wrapper and any intermediate buffer type used during token extraction must use +a hand-written `Debug` that emits only the type name and `REDACTED`. + +`Display` impls must not expose token bytes. `Error::source()` chains must not +expose provider-internal diagnostic strings that might contain IMDS endpoint +details, subscription IDs, or resource path fragments. + +--- + +## Lease model + +### `ManagedIdentityLeaseState` + +The non-secret lease state below is observed through `Credential.status` and the +core Operation ledger. `status.resource` may include expiry, audience, phase, +and opaque lease IDs that are non-secret and non-authorizing; managed-identity +rotationGeneration and bounded retry/outcome detail live in +`status.provider.details`. Token bytes are never status fields and never persist +to any Provider state Volume. + +``` +Absent (no active lease) + |-- AcquireToken ---------> Active + +Active + |-- proactive window ------> RotationDue (CredentialReady=True, RotationDue=True) + |-- RefreshToken ----------> Active (new expiresAtUnixMs, rotationGeneration unchanged) + |-- [rotation] ------------> Active (rotationGeneration+1, new leaseHandle) + |-- Provider gen change ---> [immediate] -> Revoked | [drain-leases] -> Expired on deadline + |-- RevokeToken -----------> Revoked + |-- expiry deadline -------> Expired + +RotationDue + |-- rotation attempt ------> Active (new rotationGeneration) + |-- rotation fails --------> RotationDue/Degraded (bounded retry) + |-- final retry fails -----> Failed (outcome=rotation-failed) + +Expired + |-- AcquireToken ----------> Active (new lease; rotationGeneration+1) + +Revoked + |-- AcquireToken ----------> Active if policy permits re-acquisition + |-- resource delete -------> provider-revoke finalizer satisfied immediately +``` + +### Idempotency key derivation + +Every `AcquireToken`/`RefreshToken`/`RevokeToken` call carries a stable +`idempotency_key` of type `[u8; 32]` derived as: + +```text +SHA-256(Credential.UID || ':' || rotationGeneration.to_le_bytes() || ':' || operation_class_byte) +``` + +No secret material is present in any component of this derivation. A duplicate +acquire with the same key returns the existing grant without double-issuing an +IMDS token request. + +### Lease cardinality limit + +The controller enforces a maximum of `min(maxLeases, 256)` concurrent active +leases per Provider instance. New requests beyond this ceiling return +`credential-queue-pressure`. The count is maintained in the resource store by +the controller and checked before spawning a new agent Process. + +--- + +## Status derivation + +| Condition | Set `True` when | Set `False` when | +| --- | --- | --- | +| `CredentialReady` | `leaseState=Active` and not within rotation window | `leaseState` ≠ Active, or `RotationDue=True` and rotation failed | +| `RotationDue` | `policy=proactive` and `now + proactiveWindowMs >= expiresAtUnixMs` | Rotation completed successfully | +| `ProviderUnavailable` | Agent `ManagedIdentityClientState=Unavailable` on three consecutive health-check calls to controller, OR agent Process `phase=Failed` | Agent returns successful health-check response | +| `LeaseRevoked` | `leaseState=Revoked` and no replacement has been issued | New active lease acquired | + +Phase transitions: + +| Phase | Condition | +| --- | --- | +| `Pending` | Controller registered but agent Process not yet spawned or not yet `Ready` | +| `Ready` | Agent `phase=Ready`, `leaseState=Active`, `CredentialReady=True` | +| `Degraded` | Agent reachable but returning errors (rotation failed, intermittent IMDS unavailability), or agent Process failing with respawn scheduled | +| `Failed` | Final respawn/retry exhausted; `outcome` code set | +| `Deleted` | `provider-revoke` finalizer cleared; Core has atomically written event-only Deleted revision and removed resource row/index; audit subsystem has appended deletion record | + +--- + +## Errors + +Stable closed error codes for this Provider: + +| Code | Condition | +| --- | --- | +| `credential-not-found` | Credential resource does not exist in this Zone | +| `credential-provider-unavailable` | IMDS endpoint unreachable or returning non-2xx; `ManagedIdentityClientState=Unavailable` | +| `credential-lease-expired` | Lease is past its expiry deadline | +| `credential-lease-revoked` | Lease was explicitly revoked | +| `credential-operation-denied` | Operation class not in `spec.allowedOperations` or RBAC denied | +| `credential-consumer-mismatch` | Authenticated consumer subject does not match `spec.consumerRef`; `ManagedIdentityCredentialOwner::ExactSdkConsumer` guard | +| `credential-placement-mismatch` | `scope.domainFilter=user` rejected at runtime; execution context does not match scope | +| `credential-rotation-failed` | Proactive rotation attempt failed after bounded retries | +| `credential-invariant-failure` | IMDS response failed internal invariant checks (e.g. expiry in the past, malformed lease handle) | +| `credential-schema-invalid` | `sign-challenge` operation class requested; or `spec.config` field fails validation | +| `credential-queue-pressure` | Active lease count at `maxLeases` ceiling; retry after backpressure | + +All error messages: +- maximum 240 UTF-8 bytes; +- stripped of control characters; +- must not contain token bytes, IMDS URLs, IMDS response fragments, UUIDs, + subscription IDs, resource paths, Azure endpoint hostnames, or + connection-string patterns; +- must not echo request arguments (no audience literal, no requested + expiry value, no lease handle value). + +--- + +## Audit + +### Events and field set + +| Event | Fields retained | +| --- | --- | +| Credential resource create/update/delete | Zone, subject digest, `resource_name_digest`, verb, revision result, authorization decision | +| `AcquireToken` | Zone, subject digest, `resource_name_digest`, `operation_class="acquire-token"`, `rotationGeneration`, outcome code, idempotency key digest | +| `RefreshToken` | Zone, subject digest, `resource_name_digest`, `operation_class="refresh-token"`, `rotationGeneration`, outcome code, idempotency key digest | +| `RevokeToken` | Zone, subject digest, `resource_name_digest`, `operation_class="revoke-token"`, `rotationGeneration`, revocation result code | +| `InspectMetadata` | Zone, subject digest, `resource_name_digest`, `operation_class="inspect-metadata"`, outcome code | +| Proactive rotation | Zone, `resource_name_digest`, trigger `proactive-window`, old `rotationGeneration`, new `rotationGeneration`, outcome code | +| Provider generation change revocation | Zone, `resource_name_digest`, policy applied (`immediate` or `drain-leases`), outcome code | +| IMDS unavailability onset | Zone, `resource_name_digest`, consecutive-failure count, outcome code `credential-provider-unavailable` | +| Agent spawn | Zone, `resource_name_digest`, agent Process name, `executionRef`, outcome code | +| Agent Process failure | Zone, `resource_name_digest`, agent Process name, failure reason (closed code), failure count | +| **Deleted-phase closure** | Zone, `resource_name_digest`, `phase=Deleted`, `finalizer=credential.d2bus.org/provider-revoke`, `cleanupLatencyMs`, final `rotationGeneration`, outcome `resource-deleted` | + +`resource_name_digest` is SHA-256 of the Credential resource name, never the +raw name. It is admitted only to the authorization-controlled bounded Zone +audit stream and, for caller-initiated operations, after the authorization +decision. Raw Credential name, ResourceRef, and UID are excluded. The digest +is never copied to telemetry, logs, collector diagnostics, or support +summaries. + +The **Deleted-phase closure** audit record is written by the **audit subsystem**, +not by the controller. The controller's only deletion-time action is to clear +the `provider-revoke` finalizer. The controller never commits any store revision +for Credential deletion and never gates finalizer release on audit completion. +After the finalizer is cleared: + +1. **Core store write**: Core atomically writes an event-only Deleted revision + and removes the Credential row and index entries from the resource store. +2. **Audit subsystem**: The audit subsystem reads the committed Deleted revision + and appends the closure record with exactly-once delivery. A stable dedup key + bound to the committed revision prevents duplicate records on restart or + audit-subsystem recovery. The controller never re-emits or re-enacts this + step. + +### Excluded from all audit records + +Token bytes, raw IMDS response content, IMDS endpoint URL fragments, base64 +material, bearer strings, `clientId` value, `imdsEndpointAlias` string, audience +literal value, tenant/subscription IDs, Azure resource URIs, `leaseHandle` +plaintext value (only the opaque handle digest is permitted), provider-internal +diagnostic strings, host filesystem paths, connection string shapes, and Noise/ +session key material. + +--- + +## OTEL and metrics + +### Span names + +``` +d2b.credential.acquire_token +d2b.credential.refresh_token +d2b.credential.revoke_token +d2b.credential.inspect_metadata +d2b.credential.reconcile +d2b.credential.rotation +d2b.credential.provider_health_check +``` + +### Required span attributes (closed set) + +| Attribute | Value | +| --- | --- | +| `d2b.credential.provider` | `credential-managed-identity` (literal) | +| `d2b.credential.operation_class` | Closed enum string | +| `d2b.credential.placement_binding` | `host-system` or `guest-agent` | +| `d2b.credential.outcome` | Stable closed outcome code | +| `d2b.credential.rotation_generation` | Numeric rotation generation | + +Credential telemetry uses only applicable generic OTEL Resource attributes from +the collector's closed allowlist: + +| Resource attribute | Value | +| --- | --- | +| `d2b.zone` | Zone name, re-stamped at trusted ingress | +| `d2b.provider` | `credential-managed-identity` | +| `d2b.component` | Signed controller/agent component ID | +| `service.name` | Fixed controller or agent service name | +| `service.namespace` | Fixed service namespace | +| `service.version` | Build version | + +No OTEL Resource attribute or span attribute carries a Credential resource +name, ResourceRef, UID, digest (including `resource_name_digest`), or derived +identity token. + +### Forbidden from spans and resource attributes + +Token bytes, audience literal, IMDS URL fragments, `clientId` value, +`imdsEndpointAlias` value, provider-internal diagnostics, host filesystem paths, +resource IDs, tenant/subscription IDs, Azure endpoint URIs, correlation IDs that +embed secret shapes. + +### Metrics + +| Metric | Type | Labels | +| --- | --- | --- | +| `d2b_credential_operations_total` | Counter | `provider="credential-managed-identity"`, `operation_class`, `placement_binding`, `outcome` | +| `d2b_credential_lease_expiry_seconds` | Gauge | `provider`, `placement_binding` | +| `d2b_credential_rotation_total` | Counter | `provider`, `policy`, `outcome` | +| `d2b_credential_provider_health` | Gauge (0/1) | `provider`, `placement_binding` | +| `d2b_credential_active_leases` | Gauge | `provider`, `placement_binding` | +| `d2b_credential_imds_calls_total` | Counter | `provider`, `alias`, `outcome` | + +`alias` in `d2b_credential_imds_calls_total` uses only the closed alias string +(`azure-imds`, `azure-imds-aca`); no resolved endpoint URL appears in this label. +Label cardinality is bounded and semantic. The expiry gauge reports the +minimum seconds remaining across active leases in each provider/placement +aggregate (0 when none). Metric labels carry no Credential resource name, +ResourceRef, UID, digest, or derived identity token. Credential identity is +available only as `resource_name_digest` in authorized bounded audit records, +never telemetry. Generic allowlisted OTEL Resource attributes such as +`d2b.zone`, `d2b.provider`, and `d2b.component` remain available and are not +copied into metric labels or span attributes. + +--- + +## Nix configuration + +### Zone-level Credential declaration examples + +```nix +# Azure VM host placement (host-system) +{ + d2b.zones.prod.resources = { + credential-managed-identity = { + type = "Provider"; + spec = { + artifactId = "credential-managed-identity-bin"; + config = { + clientId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"; + imdsEndpointAlias = "azure-imds"; + maxLeases = 64; + }; + }; + }; + + azure-vm-mi = { + type = "Credential"; + spec = { + providerRef = "Provider/credential-managed-identity"; + scope = { + executionRef = "Host/azure-vm-host"; + domainFilter = "system"; + }; + audience = "https://management.azure.com/"; + consumerRef = "Provider/runtime-azure-virtual-machine"; + allowedOperations = [ "acquire-token" "refresh-token" "inspect-metadata" ]; + rotation = { + policy = "proactive"; + proactiveWindowMs = 300000; + maxLeaseLifetimeMs = 3600000; + }; + revocation = { + onOwnerDelete = "immediate"; + onProviderGeneration = "immediate"; + }; + }; + }; + }; +} +``` + +```nix +# ACA Guest placement (guest-agent) +{ + d2b.zones.prod.resources = { + credential-managed-identity-aca = { + type = "Provider"; + spec = { + artifactId = "credential-managed-identity-bin"; + config = { + clientId = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"; + imdsEndpointAlias = "azure-imds-aca"; + maxLeases = 32; + }; + }; + }; + + aca-relay-mi = { + type = "Credential"; + spec = { + providerRef = "Provider/credential-managed-identity-aca"; + scope = { + executionRef = "Guest/aca-sandbox"; + domainFilter = "system"; + }; + audience = "https://relay.servicebus.windows.net/"; + consumerRef = "Provider/runtime-azure-container-apps"; + allowedOperations = [ "acquire-token" "refresh-token" "revoke-token" "inspect-metadata" ]; + rotation = { + policy = "proactive"; + proactiveWindowMs = 300000; + maxLeaseLifetimeMs = 3600000; + }; + revocation = { + onOwnerDelete = "immediate"; + onProviderGeneration = "immediate"; + }; + }; + }; + }; +} +``` + +### Agent Process resources are controller-managed + +The `managed-identity-agent` Process resources that the controller creates for +each Credential binding are **not present in the Nix-rendered Zone bundle** and +must never be authored in Nix configuration. They are created and managed +exclusively by the `managed-identity-controller` as owned children of each +Credential resource. Their `managedBy` field is set to the controller's process +identity; they do not appear in Nix `d2b.zones..resources` configuration. + +If a Nix configuration entry with `type = "Process"` and a name matching the +controller-generated `mi-agent-` pattern is encountered, the +eval-time assertion raises an error with the message: +`"mi-agent-* Process resources are managed by managed-identity-controller; remove from Nix configuration"`. + +### Artifact catalog entry + +```nix +# Controller and agent are separate binaries in the same package: +d2b.artifacts.credential-managed-identity-bin = { + package = pkgs.d2b-provider-credential-managed-identity; + type = "provider"; + # Exposes both binaries: + # d2b-managed-identity-controller + # d2b-managed-identity-agent +}; +``` + +The `type = "provider"` field is required. The artifact ID must match +`^[a-z][a-z0-9-]*$` (lowercase, starts with a letter, hyphens allowed, max 128 +chars). Store paths and closure metadata +in the emitted artifact catalog are private; they never appear in any resource +spec, status, audit record, or log line. + +### Eval-time assertions + +Applied to every entry with `type = "Credential"` and +`spec.providerRef = "Provider/credential-managed-identity"`: + +1. `spec.scope.domainFilter` must not be `"user"` — assertion failure with + descriptive message. +2. `spec.scope.executionRef` resolves a declared `Host/` or `Guest/` + in the same Zone. +3. `spec.allowedOperations` is a non-empty subset of + `[acquire-token, refresh-token, revoke-token, inspect-metadata]`; `sign-challenge` + triggers an assertion failure. +4. `spec.audience` passes `^[A-Za-z0-9._:/@-]+$` charset; max 256 chars; + `contains_sensitive_shape` guard. +5. `spec.consumerRef`, if set, resolves a declared `Provider/` in the same Zone. +6. `spec.rotation.proactiveWindowMs < spec.rotation.maxLeaseLifetimeMs / 2` when both + are non-zero. +7. `spec.rotation.policy = "proactive"` requires `proactiveWindowMs > 0` and + `maxLeaseLifetimeMs > 0`. +8. No two Credential resources in the same Zone declare the same + `(providerRef, scope.executionRef, audience)` tuple (duplicate-binding conflict). +9. The Provider resource's `spec.artifactId` resolves an artifact catalog entry of + `type = "provider"`; missing or wrong-type entry fails the build. + +Applied to every entry with `type = "Provider"` and +`spec.config.imdsEndpointAlias` present: + +10. `spec.config.imdsEndpointAlias` must be one of the closed alias values + (`azure-imds`, `azure-imds-aca`); an unknown value fails the eval. +11. `spec.config.clientId` passes `OpaqueAzureRef` charset validation; a + secret-shaped value fails `contains_sensitive_shape`. +12. `spec.config.maxLeases` must be 1–256 inclusive. + +### Build-time validation + +- Each Credential resource `spec` validated against + `docs/reference/schemas/v3/credential.json`. +- Provider-specific schema `docs/reference/schemas/v3/provider-credential-managed-identity-config.json` + cross-checked: `clientId` charset, `imdsEndpointAlias` closed enum. +- `make test-drift` regenerates schemas with `cargo xtask gen-schemas` and + asserts `git diff --exit-code`. A schema change diverging from crate-derived + output fails the gate. +- Bundle digest round-trip: sorted resources, digest matches header. +- Artifact catalog round-trip: digest matches header; store paths absent from + resource bundle and status. +- `contains_sensitive_shape` detection covers all emitted bundle derivation outputs. + +### Generation cleanup contract + +When a Credential resource backed by `credential-managed-identity` is removed +from the Nix configuration: + +1. `activation-nixos` issues an async Delete (sets `deletionRequestedAt`). +2. The Credential resource transitions to `phase=Degraded` with `Cleanup=True` / + `reason=nix-generation-removed`. +3. The `managed-identity-controller` processes the `provider-revoke` finalizer: + a. Sends `RevokeToken` to the owned agent Process (or marks all active leases + revoked if the agent is already gone) under `revocation.onOwnerDelete` policy. + b. Deletes the `mi-agent-` Process resource (awaits the + agent's graceful shutdown before removing the finalizer). +4. Once the agent Process deletion watch event is received and lease revocation + is confirmed, the controller clears the `provider-revoke` finalizer. The + controller performs no store write and no audit emission at this step. +5. After the finalizer is cleared, Core atomically writes the event-only Deleted + revision and removes the Credential row and index entries from the store. The + audit subsystem then appends the Deleted-phase closure record from the + committed revision; exactly-once delivery is handled by the audit subsystem's + dedup key, with no controller involvement. +6. Stalled cleanup surfaces as `Degraded` / `nix-cleanup-stalled` with bounded + retry. +7. `activation-nixos` activation completes without blocking on cleanup. +8. Up to `retainedConfigurationMax` (default 3, range 1–16) prior bundles + retained; rollback re-creates Credential resources from retained bundle + (fresh leases acquired; prior IMDS tokens are not restored). +9. Controller-created and API-created Credential resources are never deleted by + the generation cleanup path. + +--- + +## Async reconcile + +### Controller descriptor (wire form) + +```yaml +providerId: Provider/credential-managed-identity +controllerType: Credential +resourceTypes: [Credential] +watchSelectors: + - resourceType: Credential + providerRefFilter: Provider/credential-managed-identity + - resourceType: Provider + nameFilter: credential-managed-identity + - resourceType: Process + ownerRefFilter: Provider/credential-managed-identity +dependencySelectors: + - resourceType: Provider + relationship: providerRef + - resourceType: Host + relationship: scope.executionRef + - resourceType: Guest + relationship: scope.executionRef + - resourceType: Process + relationship: ownerRef +ownerChildTriggers: [owned-resource-changed, agent-process-health-changed] +reconcileConcurrency: 8 +maxPendingResources: 256 +finalizers: [credential.d2bus.org/provider-revoke] +observeInterval: 30s +``` + +### Reconcile lifecycle + +**Create**: + +1. Validate `spec.scope.domainFilter != "user"`. +2. Validate `spec.allowedOperations` ⊆ supported set. +3. Validate `spec.scope.executionRef` resolves a live `Host` or `Guest`. +4. **Admit**: if checks 1–3 pass and the Provider is Ready, create and apply the + `mi-agent-` Process resource (using the agent Process template + in §Process resource templates), owned by this Credential. The controller does + not wait for the Credential to reach `phase=Ready` before spawning the agent; + the Credential reaches `phase=Ready` only after the agent is itself Ready. +5. Wait for the agent Process to reach `phase=Ready`. +6. Agent reports first successful lease acquisition; controller sets + `CredentialReady=True`, transitions Credential `phase=Active`. + +**Agent spawn/teardown lifecycle**: + +- **Spawn**: When the Credential spec is **admitted** (domain/allowedOperations + checks pass) and dependencies are **ready** (executionRef live, Provider Ready), + the controller creates the agent Process resource and attaches a LaunchTicket + projecting the `imdsEndpointAlias` and `credentialRef`. The agent Process is + owned by the Credential (`ownerRef: Credential/`); its lifecycle is bound + to the Credential's lifecycle. The Credential does not reach `phase=Ready` + until the agent is Ready; the spawn is triggered by admission+dependency-ready, + not by `phase=Ready`. +- **Monitor**: Controller watches agent Process health via `ownerChildTrigger`. + On agent Process `phase=Failed` or `phase=Degraded`: controller sets + `ProviderUnavailable=True` on the Credential resource and schedules respawn + after a bounded backoff. +- **Respawn**: Controller deletes the failed agent Process resource and creates + a new one. Consecutive respawn failures (three or more) set `phase=Failed` on + the Credential resource with `outcome=agent-process-failed`. +- **Teardown on deletion**: On Credential deletion, the controller drains + in-flight requests, revokes active leases, then issues a delete on the agent + Process resource. The controller observes the agent Process deletion watch + event. The agent Process has no persisted `phase=Deleted` row; the watch + event fires when the resource is removed from the store. Once the watch event + is received, the controller clears the `provider-revoke` finalizer. Core then + atomically writes the event-only Deleted revision for the Credential and + removes its row/index; the audit subsystem appends the closure record with + exactly-once dedup. The controller never commits a store deletion or emits + the Deleted-phase closure audit record. + +**Spec update**: + +- If `providerRef` changes: revoke old lease (via agent RPC) under + `revocation.onProviderGeneration` policy; tear down old agent; create new + agent with updated config. +- If `audience`, `scope`, or `rotation` changes and re-acquisition is required: + revoke old lease and spawn new agent. +- If only `allowedOperations` or metadata changes: update status without + agent respawn. + +**Dependency-ready**: + +- When `Provider/credential-managed-identity` transitions from `Pending` to + `Ready`, re-attempt agent spawn for all Credentials in `phase=Pending` or + `phase=Degraded` with `ProviderUnavailable=True`. + +**Scheduled-observe** (every 30 s): + +- Send `InspectMetadata` health-check RPC to each active agent Process. +- Agent calls `ManagedIdentityCredentialClient::inspect_lease` and returns + opaque health state to controller (no token bytes in response). +- If `leaseState=Active` and `policy=proactive` and + `now + proactiveWindowMs >= expiresAtUnixMs`: set `RotationDue=True`. +- If agent returns `Unavailable`: increment consecutive-failure count; after + three failures, set `ProviderUnavailable=True`. +- Detects out-of-band revocations (IMDS infrastructure rotation). + +**Proactive rotation** (triggered by `RotationDue=True`): + +1. Send rotation request to agent; agent calls `issue_lease` with new + idempotency key (new `rotationGeneration`). +2. On grant: increment `rotationGeneration`, store new `leaseHandle` and + `expiresAtUnixMs`; commit status; clear `RotationDue`. +3. Old lease valid until new one is active. +4. On failure: bounded retry under `requeue-at`; degrade after final retry with + `outcome=rotation-failed`. + +**Deletion requested** (`deletionRequestedAt` set): + +1. Send graceful-stop signal to agent Process; wait for agent to drain + in-flight requests (bounded deadline). +2. Agent executes revocation: calls `RevokeToken` on all active leases per + `revocation.onOwnerDelete` policy; marks `leaseState=Revoked` and reports + to controller. +3. Controller issues delete on the agent Process resource and observes its + deletion watch event. The agent Process has no persisted `phase=Deleted` + row; the watch event fires when the resource is removed from the store. +4. After the watch event is received and revocation is confirmed (`Revoked` or + `AlreadyRevoked`): controller clears the `provider-revoke` finalizer. + Core then atomically writes the event-only Deleted revision for the Credential + and removes its row/index. The audit subsystem appends the Deleted-phase + closure record from the committed revision with exactly-once dedup. The + controller never commits the store deletion or emits this audit record. +5. On agent `Unavailable` during teardown: mark `leaseState=Revoked` in status + (operator must verify externally); proceed to step 4 with + `outcome=forced-revocation`. + +**Provider generation change**: + +- `revocation.onProviderGeneration=immediate`: send `RevokeToken` to agent + before generation changes. If agent unreachable: mark `leaseState=Revoked` + and emit audit record. +- `revocation.onProviderGeneration=drain-leases`: do not actively revoke; active + leases expire by natural deadline; status remains `Active` until expiry. + +**Unknown/ambiguous state**: + +- Agent Process unreachable or IMDS returning non-recoverable error: set + `phase=Degraded`, `ProviderUnavailable=True`, `CredentialReady=False`; + bounded retry with respawn. After retry exhaustion: set `phase=Failed`. + +### Concurrency and ordering + +- `reconcileConcurrency: 8` — up to 8 Credential resources reconciled in + parallel. +- Independent resources (distinct executionRefs) proceed concurrently. +- Per-resource reconcile tasks serialize: at most one in-flight reconcile per + Credential UID. +- The watch task keeps reading continuously while reconcile tasks run; new + high-water hints are coalesced. +- Long-running IMDS calls (inside the agent) do not block controller + reconciliation of unrelated resources. + +--- + +## Source reuse + +### v3 baseline (current code) + +| Symbol | File | Evidence class | Treatment | +| --- | --- | --- | --- | +| `OpaqueAzureRef` + `AzureRefError` + tests | `packages/d2b-realm-provider/src/credential.rs` | `implemented-and-reachable` | Reused directly; `clientId` field validation delegates to `OpaqueAzureRef::parse` | +| `ManagedIdentityRef` | `packages/d2b-realm-provider/src/credential.rs` | `implemented-and-reachable` | `client_id: OpaqueAzureRef` maps to `clientId` config field (snake_case → camelCase); retained as reuse anchor | +| `CredentialProvider` trait (status/enrollment-only) | `packages/d2b-realm-provider/src/provider.rs` | `implemented-and-reachable` | Superseded by `d2b.credential.v3` service interface; removed only after v3 controller has full parity | +| `ProviderWorkloadIdentity::ManagedIdentity` | `packages/d2b-realm-provider/src/types.rs` | `implemented-and-reachable` (live ACA bootstrap path) | ACA bootstrap migration: v3 ACA Provider config uses `credentialRef` after `credential-managed-identity` controller is live | +| `managed_identity_client_id: Option` | `packages/d2b-provider-aca/src/lib.rs` (line 112) | `implemented-and-reachable` | Superseded by Credential resource + `credentialRef` in ACA Provider config; removed by work item `ADR046-cred-mi-001` removal precondition | +| `managed_identity_client_id` | `packages/d2bd/src/lib.rs` (line 3960, 4173) | `implemented-and-reachable` | Superseded; see above | +| `contains_sensitive_shape` | `packages/d2b-realm-provider/src/error.rs` | `implemented-and-reachable` | Adapted for all string-field guards in Provider config, audit, and telemetry output | +| `no_secrets_or_credentials: bool` | `packages/d2b-core/src/realm_workloads_launcher.rs:LauncherMetadataInvariants` | `implemented-and-reachable` | Provides v3 evidence for the zero-secret-bytes design principle; Credential ResourceType extends this invariant to all resource/store/status/audit/log boundaries | + +### Main-branch reuse (commit `a1cc0b2da4a08ca3240a770a972fe4da6f912bef`) + +| Main source | Selected behavior | Excluded ADR 0045 assumptions | +| --- | --- | --- | +| `packages/d2b-provider-credential-managed-identity/src/lib.rs` | `ManagedIdentityCredentialProvider`, `ManagedIdentityCredentialClient` trait, `ManagedIdentityLeaseRequest/Ref/Grant/Inspection/Renewal/Revocation`, `ManagedIdentityCredentialOwner::ExactSdkConsumer` | v2 `AgentPlacementBinding`, v2 `EndpointRole`, v2 `ProviderFactory`, v2 `ProviderRegistryBuilder`, v2 component-session auth/prologue | +| `packages/d2b-provider-credential-managed-identity/src/tests.rs` | `FakeClient`, `credential_canary` enforcement, `colocated-consumer` tests, unavailable-state tests, idempotency tests | v2 realm/userd process model, v2 session/bootstrap assumptions | + +Reuse action: **copy and adapt**. v3 contract names, module paths, and type +versions are substituted. The `d2b.credential.v3` service interface replaces +the v2 `CredentialProvider` trait. The v3 Provider resource/descriptor/ +controller loop (with agent spawn/teardown) replaces the v2 provider registry. +The `Noise_KK` delivery channel terminated by the **agent process** replaces +any v2 token delivery path. Surrounding ADR 0045 registry, realm model, +endpoint-role, and provider-factory code is explicitly excluded. + +--- + +## Work items + +### ADR046-mi-topology-001 (agent process split) + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-credential-001 and ADR046-credential-002; owner: credential-managed-identity controller/agent topology. This topology contract is consumed by the later managed-identity controller work. | +| Current source | `packages/d2b-realm-provider/src/provider.rs:CredentialProvider` status/enrollment-only trait; main `a1cc0b2d` managed-identity Provider implementation and tests listed in §Source reuse | +| Reuse action | adapt | +| Destination | packages/d2b-provider-credential-managed-identity/src/{controller.rs,agent.rs}; packages/d2b-provider-credential-managed-identity/{controller/main.rs,agent/main.rs}; packages/d2b-provider-credential-managed-identity/tests/topology.rs | +| Detailed design | Implement the controller/agent process split: separate `d2b-managed-identity-controller` binary with no IMDS client and no KK delivery, and `d2b-managed-identity-agent` binary with injected IMDS client via effect port and KK delivery. Controller manages Credential resources, spawns/monitors agent Processes, uses canonical Process templates, attaches LaunchTickets projecting `imdsEndpointAlias` and `credentialRef`, monitors agent Process health with bounded backoff, performs Deleted-phase cleanup without emitting Deleted closure audit, and applies D087 status-first state with no Provider state Volume. Agent validates `ExactSdkConsumer` via `AuthenticatedSubjectContext`, serves token-delivery methods, terminates Noise_KK delivery sessions, reports lease state, declares no direct IMDS egress, and keeps token bytes transient. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt the main managed-identity Provider; replace v2 provider registry/session assumptions with v3 controller/agent Process topology and `d2b.credential.v3` service. | +| Integration | ProviderDeployment starts the controller Process; controller reconciles Credential resources and creates agent Process/Endpoint resources at the declared executionRef; d2b-bus routes `d2b.credential.v3` calls to the agent; co-located runtime Provider injects the IMDS client through the LaunchTicket/effect port; core aggregates Provider status and audit subsystem appends deletion records. | +| Data migration | Full d2b 3.0 reset; no v2 managed-identity process/session state import | +| Validation | `tests/topology.rs`; `integration/host-guest-placement.nix`; `make test-rust`; `make test-integration`; `make test-host-integration` | +| Removal proof | V2 single-process/trait topology is superseded once controller and agent Process split is integrated and all token delivery terminates in the agent | + +### ADR046-cred-mi-001 + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cred-mi-001` | +| Dependency/owner | `ADR046-credential-001` (contracts), `ADR046-credential-002` (service); `ADR046-reconcile-001`; `ADR046-mi-topology-001`; `credential-managed-identity` crate owner | +| Current source | `packages/d2b-realm-provider/src/credential.rs:ManagedIdentityRef` (reachable); `packages/d2b-provider-aca/src/lib.rs:managed_identity_client_id` line 112 (reachable ACA config); `packages/d2bd/src/lib.rs:managed_identity_client_id` lines 3960, 4173 (reachable) | +| Reuse source | main `a1cc0b2d`: `packages/d2b-provider-credential-managed-identity/src/lib.rs` (full implementation); `src/tests.rs` (full test suite) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-credential-managed-identity/src/{lib.rs, controller.rs, agent.rs, service.rs, audit.rs, telemetry.rs}`; `packages/d2b-provider-credential-managed-identity/{controller/main.rs, agent/main.rs}`; `packages/d2b-provider-credential-managed-identity/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs, topology.rs}`; `packages/d2b-provider-credential-managed-identity/integration/{container-service.sh, host-guest-placement.nix, aca-credential-ref.sh, cleanup-rollback.sh}`; `packages/d2b-provider-credential-managed-identity/README.md` | +| Detailed design | (1) Adapt `ManagedIdentityCredentialProvider` to `d2b.credential.v3` service interface; split controller and agent roles (see `ADR046-mi-topology-001`). (2) Enforce `ManagedIdentityCredentialOwner::ExactSdkConsumer` in agent via `AuthenticatedSubjectContext` from ComponentSession, independently of scope fields. (3) Reject `user-agent` placement: `scope.domainFilter=user` returns `credential-placement-mismatch` before agent spawn. (4) Validate `clientId` using `OpaqueAzureRef::parse` from v3 baseline; artifact IDs match `^[a-z][a-z0-9-]*$`. (5) Validate `imdsEndpointAlias` against closed enum `{azure-imds, azure-imds-aca}`; project into LaunchTicket at spawn time (never into Process spec config or env); co-located runtime Provider constructs `ManagedIdentityCredentialClient` from LaunchTicket projection and supplies via effect port; resolved URL never in any output surface. (6) Agent Process declares `networkUsage.allowEgress=false`; uses canonical Process template shape (see `ADR046-mi-topology-001` design item 6). (7) Reject `sign-challenge` with `credential-schema-invalid` immediately. (8) Map `ManagedIdentityClientState::Unavailable` to `credential-provider-unavailable`; no `InteractionRequired` state. (9) Implement `ManagedIdentityLeaseHandle` as opaque bounded newtype with redacted `Debug`. (10) All token bytes held by injected client in agent; delivered only via agent-terminated `Noise_KK` delivery session. (11) Integrate with Provider resource descriptor and controller toolkit. (12) Confirm `credential_canary` never appears in any service response, status field, delivery record outer header, or audit record. (13) Apply D087 status-first state: declare no Provider state Volume, keep ProviderStateSet empty, and write only bounded non-secret lease observation to `Credential.status` plus the Operation ledger. Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | Agent `Process` resource under `Host` or `Guest` executionRef; controller `Process` resource at Zone system host; d2b-bus routes `d2b.credential.v3` token-delivery calls to agent; Credential controller reconciles status; ACA `Provider/runtime-azure-container-apps` holds `credentialRef` pointing to a `credential-managed-identity`-backed Credential resource | +| Data migration | Full v3 reset. `d2b-provider-aca:managed_identity_client_id` raw field migrated to a Credential resource reference in the v3 ACA Provider config; see removal precondition below. | +| Validation | See §Tests. Run `cargo test -p d2b-provider-credential-managed-identity --lib --test lifecycle --test conformance --test faults --test canary --test delivery --test placement --test topology`; run `integration/{container-service.sh,aca-credential-ref.sh,cleanup-rollback.sh}` through `make test-integration` and `integration/host-guest-placement.nix` through `make test-host-integration`. | +| Removal proof | `d2b-provider-aca:managed_identity_client_id` raw field removed only after the `credential-managed-identity` controller and agent are integrated and the ACA Provider config uses `credentialRef` exclusively; `ProviderWorkloadIdentity::ManagedIdentity` bootstrap path superseded only after the ACA Provider controller uses the Credential resource for token acquisition | + +### ADR046-cred-mi-002 (shared with other Credential Providers) + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-credential-001, ADR046-credential-002; ADR046-reconcile-001, ADR046-reconcile-002; ADR046-mi-topology-001; owner: Credential controller toolkit and managed-identity controller | +| Current source | `packages/d2b-realm-provider/src/provider.rs:CredentialProvider` status/enrollment-only trait; main `a1cc0b2d` managed-identity controller/test behavior listed in §Source reuse | +| Reuse action | adapt | +| Destination | packages/d2b-provider-credential-managed-identity/src/controller.rs; packages/d2b-contracts/src/v3/credential_controller.rs | +| Detailed design | Managed-identity-specific controller design: implement async reconcile and agent spawn/teardown from §Async reconcile; enforce system-only domain; spawn agent on Credential admission plus dependency-ready, not on `phase=Ready`; implement `observeInterval=30s` health-check RPC to the agent, which calls `InspectMetadata` on the injected client; controller never calls IMDS; derive idempotency key as `SHA-256(UID \|\| ":" \|\| rotationGeneration.to_le_bytes() \|\| ":" \|\| operation_class_byte)`; enforce `MAX_LOCAL_LEASES=256` in the resource store; implement Deleted-phase closure by clearing `provider-revoke` only after agent Process deletion and revocation confirmation while core/audit own Deleted revision and deletion record. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt shared Credential controller lifecycle to managed-identity controller/agent spawn and teardown. | +| Integration | Shared Credential controller contract produces reconcile events; managed-identity controller consumes them, manages agent Process resources, and writes Credential/agent status; generated controller contracts are consumed by all Credential Providers. | +| Data migration | None — controller lifecycle code only; no runtime state import | +| Validation | Managed-identity reconcile/controller tests; shared Credential reconciliation tests; topology tests validating agent spawn/teardown and Deleted-phase cleanup | +| Removal proof | V2 `CredentialProvider` status/enrollment trait lifecycle is superseded after shared controller reconcile and managed-identity agent lifecycle pass parity | + +This work item is shared with `credential-secret-service` and `credential-entra`; +the table above is the `managed-identity-controller` entry point for this work +item. + +### ADR046-cred-mi-003 (shared Nix + cleanup) + +| Field | Value | +| --- | --- | +| Dependency/owner | Depends on ADR046-credential-001, ADR046-credential-002, and ADR046-cred-mi-001; owner: Nix resource compiler and activation cleanup | +| Current source | `packages/d2b-provider-aca/src/lib.rs:managed_identity_client_id` and `packages/d2bd/src/lib.rs:managed_identity_client_id` are the current raw ACA managed-identity config surfaces; v3 Provider/Credential resource authoring is net-new | +| Reuse action | replace | +| Destination | nixos-modules/options-resources.nix; nixos-modules/activation-nixos-cleanup.nix; integration/aca-credential-ref.sh | +| Detailed design | Shared Nix and cleanup: implement Nix eval-time assertions 1–12 from §Eval-time assertions, closed enum schema for `imdsEndpointAlias`, `clientId` validation via `OpaqueAzureRef` charset, generation cleanup contract, and artifact catalog validation for `credential-managed-identity-bin`. Integration fixture asserts that the migrated ACA Provider config carries `credentialRef: "Credential/aca-relay-mi"` and the raw `managed_identity_client_id` string field is absent from rendered Provider config JSON and ACA runtime bundle. Primary reuse disposition: `replace`. Preserved source-plan detail: replace raw ACA managed identity client-id fields with Credential resource references and v3 Provider/Credential Nix emission. | +| Integration | Nix compiler emits Provider/Credential/ACA resource config; ResourceAPI admission validates it; activation cleanup deletes old generation resources through finalizers; ACA Provider consumes `credentialRef` to obtain tokens from the managed-identity Credential Provider. | +| Data migration | Full d2b 3.0 reset; raw ACA `managed_identity_client_id` config is replaced by a newly authored Credential resource reference rather than imported in place | +| Validation | Nix eval assertion tests; artifact catalog validation tests; `integration/aca-credential-ref.sh` | +| Removal proof | `managed_identity_client_id` raw fields in ACA config and daemon plumbing are removed only after ACA Provider config uses `credentialRef` exclusively | + +### ADR046-cred-mi-004 (shared audit/OTEL) + +| Field | Value | +| --- | --- | +| Dependency/owner | Depends on ADR046-cred-mi-001 and ADR046-mi-topology-001; owner: credential-managed-identity audit/telemetry implementation | +| Current source | `packages/d2b-realm-provider/src/error.rs:contains_sensitive_shape`; `packages/d2b-core/src/realm_workloads_launcher.rs:LauncherMetadataInvariants.no_secrets_or_credentials`; main `a1cc0b2d` managed-identity canary tests listed in §Source reuse | +| Reuse action | adapt | +| Destination | packages/d2b-provider-credential-managed-identity/src/{audit.rs,telemetry.rs}; packages/d2b-contract-tests/tests/credential_audit.rs | +| Detailed design | Shared audit/OTEL: emit audit records for all methods and controller events per §Audit, with Credential identity represented only by the authorized bounded `resource_name_digest`; emit OTEL spans and metrics per §OTEL and metrics with no Credential resource name, ResourceRef, UID, digest, derived identity token, Zone/Credential/resource-name-derived label, or non-allowlisted OTEL Resource attribute; retain applicable generic collector-allowlisted Resource attributes (`d2b.zone`, `d2b.provider`, `d2b.component`, and service fields); report expiry as the minimum for each provider/placement aggregate; add `d2b_credential_imds_calls_total` counter with bounded `alias` label; enforce `contains_sensitive_shape` on all string fields in audit records and metric labels; add canary tests for `managed-identity-canary`, `credential_canary`, `imds-endpoint-canary`, Credential name/ref/UID/digest, and Zone name in `canary.rs`. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing sensitive-shape guard and canary pattern to v3 audit, OTEL, and metric surfaces. | +| Integration | Controller and agent service methods call audit/telemetry helpers; audit subsystem and OTEL exporters consume bounded redacted records; contract tests validate credential audit shape across providers. | +| Data migration | None — audit/telemetry only; no runtime state import | +| Validation | `packages/d2b-contract-tests/tests/credential_audit.rs` requires `resource_name_digest` in authorized audit records and rejects raw Credential name/ResourceRef/UID; managed-identity `canary.rs` and audit/OTEL unit tests structurally assert exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `credential_ref`, `credential_uid`, `credential_digest`, `resource_name_digest`, and every resource-name-derived label key; reject Credential name/ref/UID/digest canaries from all OTEL Resource attributes, span attributes, and metric labels; preserve generic collector-allowlisted Resource attributes including `d2b.zone`, `d2b.provider`, `d2b.component`, and service fields; reject Zone-name span/label canaries and sensitive shapes; pass complete managed-identity metric/span frames through the shared collector ingress validator and prove that adding `d2b.credential.name` or any Credential identity key/value rejects the whole frame | +| Removal proof | None — audit/telemetry helpers are additive; no prior owner to remove | + +--- + +## Tests + +### Required crate layout + +``` +packages/d2b-provider-credential-managed-identity/ + src/ + lib.rs (ManagedIdentityCredentialProvider, client trait, lease types, redaction, unit tests) + controller.rs (async reconcile loop, agent spawn/teardown, status machine, observe, rotation/revocation) + agent.rs (agent service handler: AcquireToken/RefreshToken/RevokeToken/InspectMetadata; no ambient IMDS chain) + service.rs (d2b.credential.v3 service routing; delivery session integration) + audit.rs (audit record emission, Deleted-phase closure, contains_sensitive_shape guards) + telemetry.rs (OTEL span/metric emission, closed label set) + controller/ + main.rs (controller binary entry point: config parse, no IMDS client construction) + agent/ + main.rs (agent binary entry point: config parse, injected IMDS client construction, no ambient chain) + tests/ + lifecycle.rs + conformance.rs + faults.rs + canary.rs + delivery.rs + placement.rs + topology.rs + integration/ + container-service.sh + host-guest-placement.nix + aca-credential-ref.sh + cleanup-rollback.sh + README.md +``` + +Missing any of `src/`, `tests/`, `integration/`, or `README.md` is a workspace +policy failure enforced by `make test-policy` (via `packages/d2b-contract-tests`). + +### `src/` unit tests (`#[cfg(test)]` in `src/` files) + +| Test | Validates | +| --- | --- | +| `OpaqueAzureRef::parse` on `clientId`: valid GUIDs accepted; secret-shaped values rejected | `clientId` fail-closed parse | +| `imdsEndpointAlias` closed enum: `azure-imds` and `azure-imds-aca` accepted; unknown value rejected | Alias validation | +| `ManagedIdentityCredentialOwner::ExactSdkConsumer` guard (via `AuthenticatedSubjectContext`): wrong consumer subject returns `credential-consumer-mismatch`; check is on authenticated bus identity, not caller-supplied field | Owner enforcement via bus identity | +| `ManagedIdentityClientState` transitions: `Ready → Unavailable` and back | State machine transitions | +| `sign-challenge` returns `credential-schema-invalid` immediately without IMDS call | Operation class rejection | +| `ManagedIdentityLeaseHandle` debug output emits only `REDACTED` | Redacted Debug | +| `user-agent` placement construction fails with `credential-placement-mismatch` | Placement rejection | +| `contains_sensitive_shape` on all string fields in config, audit output, metric labels | Secret-shape guard | +| `maxLeases=0` and `maxLeases=257` rejected; `1` and `256` accepted | Bounds validation | + +### `tests/` Cargo integration tests (`cargo test -p d2b-provider-credential-managed-identity`) + +#### `lifecycle.rs` + +| Test | Validates | +| --- | --- | +| Acquire → Active: `FakeClient::issue_lease` called with correct idempotency key; status `leaseState=Active`, `CredentialReady=True` | End-to-end acquire path (via agent) | +| Refresh: `FakeClient::refresh_lease` called (agent); status `expiresAtUnixMs` updated; `rotationGeneration` unchanged | Refresh path | +| Revoke: `FakeClient::revoke_lease` called (agent); status `leaseState=Revoked`, `CredentialReady=False` | Revoke path | +| InspectMetadata: `FakeClient::inspect_lease` called (agent); opaque metadata returned; no token bytes in response DTO | Inspect path | +| Proactive rotation: `RotationDue=True` triggers agent `issue_lease` with incremented `rotationGeneration` | Rotation trigger | +| Idempotent acquire: second call with same idempotency key returns same grant; `FakeClient::issue_lease` called exactly once | Idempotency | +| Re-acquire after expiry: `leaseState=Expired` → `AcquireToken` → `leaseState=Active`, `rotationGeneration` incremented | Expired re-acquire | +| Provider generation change (immediate): agent `RevokeToken` called on old lease before generation change; agent respawned with new config | Generation revocation | +| Provider generation change (drain-leases): no `RevokeToken` call; lease remains Active until natural expiry | Drain-leases policy | + +#### `conformance.rs` + +Calls `d2b_provider_toolkit::conformance::check_provider_conformance` with all +11 standard arms. Every arm must pass. Covers: descriptor validation, schema +fingerprint, placement binding declarations, operation class declarations, +`sign-challenge` absent from supported operations, `user-agent` absent from +supported placement bindings, status subresource auth, finalizer ownership, +audit record field set conformance, delivery session binding contract, RBAC +`use-credential` verb. + +#### `faults.rs` + +| Test | Validates | +| --- | --- | +| `FakeClient` returns `Unavailable` on `issue_lease`: status `ProviderUnavailable=True`, `phase=Degraded`, `CredentialReady=False` | Unavailability | +| Three consecutive `InspectMetadata` `Unavailable` responses: `ProviderUnavailable=True` set | Consecutive-failure threshold | +| `ProviderUnavailable` recovery: `FakeClient` returns success on next call; status `ProviderUnavailable=False` | Recovery path | +| `maxLeases` ceiling: `maxLeases+1`-th acquire returns `credential-queue-pressure`; count decrements on revoke | Cardinality limit | +| Generation mismatch on delivery session: rejected with closed channel | Generation mismatch | +| Provider process unreachable during `provider-revoke` finalizer: `leaseState=Revoked` recorded in status; audit record emitted; finalizer proceeds | Finalizer with unreachable provider | + +#### `canary.rs` + +| Test | Validates | +| --- | --- | +| `"managed-identity-canary"` absent from every `FakeClient` response DTO field, status field, audit record field, metric label value, OTEL Resource attribute, OTEL span attribute, and delivery record outer header | Zero-secret-bytes invariant | +| `"credential_canary"` absent from same surfaces | Cross-provider canary consistency | +| `"imds-endpoint-canary"` (a value that looks like an IMDS endpoint URL) absent from all output surfaces | Endpoint URL exclusion | +| `clientId` value absent from audit records, metric labels, OTEL spans, error messages, and log lines | Config field exclusion | +| `imdsEndpointAlias` value absent from OTEL span attributes, audit records, and error messages | Alias exclusion | +| IMDS response-shaped string absent from status, audit, OTEL, and logs | IMDS response content exclusion | +| Metric descriptors contain no `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `credential_ref`, `credential_uid`, `credential_digest`, `resource_name_digest`, or resource-name-derived key; Credential name/ref/UID/digest canaries are absent from OTEL Resource attributes, span attributes, and metric labels; Zone-name canaries are absent from spans and labels while generic collector-allowlisted Resource attributes remain | Structural telemetry identity exclusion | +| Complete metric/span frames with only generic allowlisted Resource attributes are accepted by the shared collector ingress validator; injecting `d2b.credential.name` or any Credential name/ref/UID/digest key or value rejects the whole frame | Closed collector allowlist gate | + +#### `delivery.rs` + +| Test | Validates | +| --- | --- | +| `AcquireToken` delivery session: binding contract fields all present; outer DTO carries no token bytes; fake delivery record zeroized after extraction | E2E delivery path | +| `RefreshToken` delivery session: new binding, same `credentialRef`; replay-safe sequence incremented | Refresh delivery | +| Oversize delivery record rejected; channel closed and zeroized | Size bound enforcement | +| Replay of prior session's ciphertext at same sequence number rejected | Replay-safe sequence | +| NN-profile delivery attempt rejected immediately | Profile enforcement | +| Delivery session key material zeroized after close; post-close send returns error | Session zeroize | + +#### `placement.rs` + +| Test | Validates | +| --- | --- | +| `scope.domainFilter=user` at construction: returns `credential-placement-mismatch` before any IMDS call or agent spawn | User-agent rejection | +| `host-system` with `azure-imds` alias: agent receives injected `ManagedIdentityCredentialClient` via effect port (LaunchTicket projection); no ambient discovery; `azure_identity::DefaultAzureCredential` absent from agent binary import graph | Host-system placement / ambient-free | +| `guest-agent` with `azure-imds-aca` alias: agent receives correct injected client via effect port | Guest-agent placement | +| `ExactSdkConsumer` with non-matching consumer `AuthenticatedSubjectContext` (forged field in request): `credential-consumer-mismatch` returned by agent; controller does not receive the mismatch | Consumer mismatch via bus identity | +| `ExactSdkConsumer` with matching `AuthenticatedSubjectContext` and null `spec.consumerRef`: authenticated caller accepted | Null consumerRef path | + +#### `topology.rs` + +| Test | Validates | +| --- | --- | +| Credential spec admitted + dependencies ready: controller creates `mi-agent-` Process resource owned by Credential; agent Process transitions `phase=Ready`; controller sets Credential `CredentialReady=True` | Agent spawn on admission+dependency-ready (not on phase=Ready) | +| Credential not yet admitted (executionRef unresolved): controller does not spawn agent; Credential stays `phase=Pending` | No premature spawn | +| Credential `AcquireToken` request routed to agent, not controller; controller has no `FakeClient`; controller's `FakeClient` call count remains zero | Method dispatch to agent | +| Controller receives agent status-update RPC after `FakeClient::issue_lease` succeeds; controller commits status to resource store | Agent-to-controller status reporting | +| Agent Process `phase=Failed`: controller sets `ProviderUnavailable=True` on Credential; schedules respawn after backoff | Agent failure → controller response | +| Agent respawn: controller creates new `mi-agent-` Process resource; new agent transitions `Ready`; controller clears `ProviderUnavailable` | Agent restart recovery | +| Three consecutive agent spawn failures: controller sets Credential `phase=Failed`, `outcome=agent-process-failed` | Respawn exhaustion | +| Credential `Delete`: controller sends graceful-stop to agent; agent drains in-flight requests and revokes leases; controller issues delete on agent Process resource; controller observes agent Process deletion watch event (no persisted `phase=Deleted` row for agent Process); controller clears `provider-revoke` finalizer; Core atomically writes event-only Deleted revision and removes Credential row/index; audit subsystem appends closure record with exactly-once dedup — finalizer cleared before Core deletion, Core deletion before audit, all verified in order | Graceful teardown; finalizer-then-Core-deletion-then-audit ordering | +| Audit exactly-once: simulate Core Deleted revision committed with no corresponding audit record; audit subsystem on recovery appends exactly once using dedup key bound to committed revision; controller does not re-emit; no duplicate in audit log | Audit subsystem exactly-once / no controller re-emit | +| Agent Process `ownerRef` matches Credential UID: controller watch filter correctly associates agent Process events with the owning Credential | ownerRef watch correctness | +| Controller `InspectMetadata` path: returns stored `leaseState` without calling `FakeClient`; agent `FakeClient` call count unchanged | Controller-side metadata inspection | +| Agent Process spec has `networkUsage.allowEgress=false`; agent receives `ManagedIdentityCredentialClient` via effect port (no direct network calls in agent binary) | Effect-port injection; no ambient egress | +| Agent Process template: all required canonical fields present (processClass, template, namespaceClasses `[mount,pid,ipc]`, capabilityClasses `[]`, seccompClass `strict`, startRoot `false`, noNewPrivileges `true`, environmentClass `minimal`, readOnlyRoot `true`, budget nested cpu.request/cpu.limit/memory.request/memory.limit/pids.limit/fds.limit, no inline endpoint fields, owned credential-service Endpoint resource, readiness class `provider-defined`/initialDelay/timeout/failureThreshold/successThreshold, telemetry metricsEnabled/tracingEnabled/logLevel/sensitiveLabels); all excluded fields absent (principalRef, profileRef, endpoint.kind, Process config, telemetry.componentRef, readiness.probe/timeoutMs, network.allowedEffects, budget.class, telemetry.class); agent `networkUsage: {networkRef:null,ports:[],allowEgress:false}` | Canonical template shape | +| Controller binary (`controller/main.rs`) constructed with no `ManagedIdentityCredentialClient` import; `FakeClient` inaccessible from controller entry point | Controller secret-free invariant | + +### `integration/` fixtures + +These are invoked by existing repository test orchestration (`make test-integration`, +`make test-host-integration`). They are shell scripts, Nix expressions, or +container specs; they are **not** run by `cargo test`. + +#### `container-service.sh` + +Container-backed Provider service lifecycle: start controller and agent +services with FakeClient-backed IMDS; confirm `d2b.credential.v3` methods +reachable; stop agent Process; confirm `ProviderUnavailable=True` within +`observeInterval`; controller respawns agent; confirm recovery. + +#### `host-guest-placement.nix` + +`runNixOSTest` fixture verifying: + +- `host-system` placement: `managed-identity-controller` Process launched as + Zone system host; `mi-agent-` Process spawned by controller co-located + under `Host/azure-vm-host`; `AcquireToken` via d2b-bus succeeds (routed to + agent); status `placementBinding=host-system`. Controller never calls IMDS + (verified via FakeClient call-count assertions on the controller side). +- `guest-agent` placement: controller spawns `mi-agent-` Process + co-located under `Guest/aca-sandbox`; `AcquireToken` via d2b-bus succeeds; + status `placementBinding=guest-agent`. +- `user-agent` attempt: Credential resource with `domainFilter=user` fails + NixOS eval-time assertion before build completes. + +#### `aca-credential-ref.sh` + +Verifies the ACA Provider migration path: + +- Renders an ACA Provider config with `credentialRef: "Credential/aca-relay-mi"`. +- Confirms the rendered Provider config JSON does not contain the string + `managed_identity_client_id` or any raw client-ID value. +- Confirms the `aca-relay-mi` Credential resource appears in the Zone resource + bundle with correct `providerRef` and `consumerRef`. +- Confirms `d2b-provider-aca` receives only the opaque `Credential/` ref + at runtime; the raw `managed_identity_client_id` field is absent from the + ACA runtime's bundle artifact. + +#### `cleanup-rollback.sh` + +Nix-generation lifecycle: + +- Generation N declares `aca-relay-mi` Credential resource. +- Generation N+1 removes `aca-relay-mi`. +- Confirms `activation-nixos` completes (new resources Ready) before `aca-relay-mi` + cleanup finalizer finishes. +- Confirms `aca-relay-mi` transitions `phase=Degraded` with `Cleanup=True` / + `reason=nix-generation-removed` during cleanup. +- Confirms that after the `provider-revoke` finalizer completes, a watch event + carrying the event-only Deleted revision is observed for `aca-relay-mi`, and + that a subsequent resource lookup returns not-found — no persisted Deleted row + remains in the store. +- Rollback from generation N+1 to N: `aca-relay-mi` re-created from retained + bundle; fresh lease acquired; prior IMDS token not restored. + +### Fast hermetic execution and test placement (D094) + +Per D094 and `ADR-046-validation-and-delivery` §10.16, this Provider's `src/` +unit tests and `tests/*.rs` hermetic suite are fast, in-process, deterministic, +and parallel-safe: an individual normal test has p95 ≤50 ms with no wall-clock +sleep, and `cargo test -p d2b-provider-credential-managed-identity --lib --tests` +completes in ≤2 s warm-cache execution time (compilation excluded). They use a +deterministic fake clock/RNG and the toolkit fakes/FakeEffectPort only — no +process spawn, container, network, DBus, systemd, broker daemon, Nix eval/build, +KVM, USB/GPU/TPM hardware, or live cloud, and no filesystem tree beyond tiny +temp fixtures. Any scenario needing those lives only in `integration/`, which +keeps a lane timeout/budget, parallel isolation, and fake external services by +default; such a need is re-placed into `integration/`, never given a sleep, +larger timeout, or `#[ignore]`. Bounded crypto/property tests are the only +classified exception, each named with a capped case count and a declared higher +per-test budget. + +--- + +## Removal preconditions + +| Item to remove | Removal precondition | +| --- | --- | +| `d2b-provider-aca/src/lib.rs:managed_identity_client_id: Option` (line 112) and all references | `credential-managed-identity` Provider controller integrated and live; ACA Provider config uses `credentialRef: "Credential/"` exclusively; `integration/aca-credential-ref.sh` passes | +| `d2bd/src/lib.rs:managed_identity_client_id` (lines 3960, 4173) and all references | Same as above | +| `d2b-realm-provider/src/types.rs:ProviderWorkloadIdentity::ManagedIdentity` bootstrap path | ACA Provider controller uses Credential resource for token acquisition; no remaining callsite | +| `d2b-realm-provider/src/provider.rs:CredentialProvider` trait (status/enrollment-only) | All three v3 Credential controllers (`secret-service`, `entra`, `managed-identity`) have full reconcile parity and their `tests/conformance.rs` suites pass | +| Old v2 `d2b-contracts/proto/v2/provider_credential.proto` | All v3 callers migrated to `d2b-contracts/proto/v3/credential.proto` | + +No removal is performed until the live successor passes all required validation. +The current v3 `ManagedIdentityRef` type itself is **not** removed; it is +retained as the `OpaqueAzureRef` reuse anchor and the `clientId` field source. + +Per D094, each replaced current-code test is retired with an explicit +keep/adapt/move/delete disposition and a removal gate: the minimum reusable +semantic assertions migrate into this crate's hermetic `tests/`, and the old +duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest +entries are deleted once successor coverage and the removal proof pass — +updating `tests/layer1-jobs.json`, the closed gate manifests, the +flake/matrix/Nix-unit pins, the generated ledgers, and the CI workflow shards. +Old and new suites never run in parallel indefinitely. + +--- + +## Current-code fit summary + +| Item | Treatment | +| --- | --- | +| Current anchor | `packages/d2b-realm-provider/src/credential.rs:ManagedIdentityRef` (implemented-and-reachable); `d2b-provider-aca/src/lib.rs:managed_identity_client_id` (implemented-and-reachable); `d2bd/src/lib.rs:managed_identity_client_id` (implemented-and-reachable); `CredentialProvider` trait (minimal, implemented-and-reachable); `OpaqueAzureRef` + tests (implemented-and-reachable); `contains_sensitive_shape` (implemented-and-reachable) | +| Evidence class | Opaque ref data model and ACA config path are reachable; full lease provider, `d2b.credential.v3` service, controller, and delivery channel are ADR-only | +| Behavior retained | `OpaqueAzureRef` charset/length validation fail-closed on `clientId`; injected-client pattern keeps token material in the client process; zero-secret-bytes invariant at config/audit/log boundary; `ExactSdkConsumer` ownership model; `contains_sensitive_shape` guard | +| Required delta | Full `ManagedIdentityCredentialProvider` implementation; split `managed-identity-controller` (no IMDS) + `managed-identity-agent` (IMDS client, KK delivery); `d2b.credential.v3` service dispatch split by role; agent spawn/teardown lifecycle in controller reconcile; `Noise_KK` delivery channel terminated by agent; Provider resource/descriptor; agent Process template; `imdsEndpointAlias` closed-alias config; `ExactSdkConsumer` via `AuthenticatedSubjectContext`; D087 status-first state model with no Provider state Volume, empty ProviderStateSet, status/Operation-ledger lease observation, and transient-only token bytes; Nix eval assertions; controller finalizer-release-only deletion path; Core-written event-only Deleted revision; audit subsystem deletion record with exactly-once dedup; audit/OTEL; `aca-credential-ref` migration | +| Feasibility proof | Main `a1cc0b2d` proves the `ManagedIdentityCredentialProvider` implementation; its `FakeClient` test suite covers acquire/refresh/revoke/inspect, idempotency, unavailable state, canary enforcement, `ExactSdkConsumer` enforcement, and lease cardinality | +| Future owner | `ADR046-mi-topology-001` (process split), `ADR046-cred-mi-001` (primary), `ADR046-cred-mi-002`, `ADR046-cred-mi-003`, `ADR046-cred-mi-004` | + +--- + +## Provider README required sections + +The `packages/d2b-provider-credential-managed-identity/README.md` MUST contain +these sections in order: + +1. **Provider identity** — `Provider/credential-managed-identity`, managed + ResourceType (`Credential`), provider generation/versioning policy, + Zone placement constraints (`host-system` and `guest-agent` only). +2. **Config schema** — `spec.config` fields (`clientId`, `imdsEndpointAlias`, + `maxLeases`), types, bounds, constraints, and worked examples for both + Azure VM and ACA placement using the `d2b.zones..resources` Nix + authoring shape. +3. **ResourceTypes managed** — `Credential` only: lifecycle phases, status + conditions owned (`CredentialReady`, `RotationDue`, `ProviderUnavailable`, + `LeaseRevoked`), finalizers owned (`credential.d2bus.org/provider-revoke`). + `Volume` is **not** listed here; this Provider declares no Provider state + Volume under D087 because no managed-identity payload passes the + storage-need test. +4. **Controllers, services, workers, and binaries** — two binaries: + `d2b-managed-identity-controller` (controller; one per Zone; system domain; + no IMDS access; spawns/supervises agent Processes) and + `d2b-managed-identity-agent` (service; one per Credential binding; + co-located at `executionRef`; holds IMDS client; terminates KK delivery; + owned by Credential resource; controller-managed, not Nix-configured). +5. **Placement** — `host-system` and `guest-agent` supported; + `user-agent` rejected with `credential-placement-mismatch`; `sign-challenge` + rejected with `credential-schema-invalid`; agent Process resource + co-located at `scope.executionRef`. +6. **Dependencies and RBAC** — required Zone resources (`executionRef Host|Guest`, + `consumerRef Provider`), `use-credential` verb, `ExactSdkConsumer` enforcement + via `AuthenticatedSubjectContext`, `Noise_KK` enrolled key requirement. +7. **Security, state, and telemetry** — no ambient IMDS chain in agent; no env + fallback; controller holds no IMDS client; zero-secret-bytes invariant; + opaque lease handles only; `credential_canary` and `imds-endpoint-canary` + enforcement; Deleted-phase closure audit record; OTEL spans/metrics. +8. **Build, test, and integration commands** — exact `cargo`/`make` invocations: + `cargo test -p d2b-provider-credential-managed-identity`, + `make test-integration`, `make test-host-integration`. +9. **Standalone-repo usage** *(mandatory before first release to a sibling flake)* — + flake input pattern; `nixpkgs`/toolkit input-follows boilerplate; compatibility + constraints; `d2b.artifacts` catalog entry pattern. diff --git a/docs/specs/providers/ADR-046-provider-credential-secret-service.md b/docs/specs/providers/ADR-046-provider-credential-secret-service.md new file mode 100644 index 000000000..f340d9326 --- /dev/null +++ b/docs/specs/providers/ADR-046-provider-credential-secret-service.md @@ -0,0 +1,1608 @@ +# ADR 0046 Provider dossier: `credential-secret-service` + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-credential-secret-service` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 1 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `packages/d2b-provider-credential-secret-service/`, Credential controller (user-domain), Nix Credential compiler | +| Depends on | `ADR-046-resources-credential`, `ADR-046-provider-model-and-packaging`, `ADR-046-componentsession-and-bus`, `ADR-046-resource-reconciliation`, `ADR-046-nix-configuration`, `ADR-046-telemetry-audit-and-support`, `ADR-046-resource-api-and-authorization`, `ADR-046-components-processes-and-sandbox` | +| Supersedes | v2 `SecretServiceCredentialProvider` / `SecretServiceCredentialProviderFactory` in `d2b-realm-provider`; v2 `CredentialProvider` trait | +| Work items | ADR046-cred-ss-003 (primary), ADR046-cred-ss-001, ADR046-cred-ss-002, ADR046-cred-ss-004, ADR046-cred-ss-005, ADR046-cred-ss-006 | + +--- + +## 1. Provider identity + +| Field | Value | +| --- | --- | +| `providerRef` | `Provider/credential-secret-service` | +| Implements | `Credential` ResourceType | +| Placement bindings | `user-agent` only | +| System/user domain | `user` only | +| Crate | `packages/d2b-provider-credential-secret-service/` | +| Binary | `d2b-provider-credential-secret-service` | +| Component type | `controller` | +| Cardinality | One controller process per `(Zone, User, executionRef)` triple | +| Provider generation | Monotonic; incremented on package version, config, or descriptor change | +| Compatibility | `credential` Provider API major exact; minor additive only | + +**Zone placement constraint**: `scope.executionRef` resolves a `Host/` or +`Guest/` in the same Zone. Both Host and Guest execution contexts are +supported when `scope.domainFilter=user`. + +**Domain restriction**: this Provider supports `domainFilter=user` only. Any +Credential or Provider resource with `domainFilter=system` referencing this +Provider fails at Nix eval time, Provider install validation, and controller +construction. There is no system-domain path, no host-daemon path, no keyring +file path, and no environment-fallback. + +--- + +## 2. Crate layout + +Every `packages/d2b-provider-credential-secret-service/` path is enforced by +the workspace policy gate (`make test-policy`). Missing any path is a policy +failure. + +```text +packages/d2b-provider-credential-secret-service/ + src/ + lib.rs — port trait, lease DTOs, owner placement guard, and unit tests + controller.rs — Credential controller: reconcile/observe/finalize/drain/health handlers + service.rs — d2b.credential.v3 service handler bound to controller state + main.rs — binary entry point; constructs process context and drives controller + tests/ + lifecycle.rs — acquire/refresh/revoke/inspect end-to-end with FakeOo7Port + conformance.rs — all check_provider_conformance arms pass + faults.rs — locked state → credential-provider-unavailable; unavailable; cardinality limit + canary.rs — credential_canary and object_path_canary absent from every response, status field, and delivery record + delivery.rs — delivery-session binding contract; zeroizing buffer; replay-safe sequence + placement.rs — user-agent on Host and Guest accepted; system-domain and guest-agent (system-domain on Guest) rejected + integration/ + container-service.sh — container-backed Provider service start/stop/drain + host-placement.nix — user-domain Host/Process placement (executionRef=Host) in runNixOSTest + guest-placement.nix — user-domain Guest/Process placement (executionRef=Guest) in runNixOSTest + cleanup-rollback.sh — Nix-generation removal triggers async Delete and provider-revoke finalizer + README.md — integration fixture descriptions and invocation instructions (optional; root README.md is the mandated policy gate) + README.md — all §Provider README required sections (see §17) + Cargo.toml +``` + +--- + +## 3. Provider resource Nix shape + +`spec.artifactId` is a sibling of `spec.config` on the Provider resource. + +```nix +d2b.zones.dev.resources.credential-secret-service = { + type = "Provider"; + spec = { + artifactId = "credential-secret-service-bin"; # d2b.artifacts entry; type = "provider" + config = { + collectionAlias = "login"; # provider-validated Secret Service collection alias + maxLeases = 64; # max concurrent active leases; range 1..256 + lockPolicy = "fail-closed"; # fail-closed | fail-degraded + }; + }; +}; + +d2b.artifacts.credential-secret-service-bin = { + package = pkgs.d2b-provider-credential-secret-service; + type = "provider"; +}; +``` + +`artifactId` is a plain bounded identifier matching `^[a-z][a-z0-9-]*$`, +not a `*Ref` field. `Artifact` is not a ResourceType. + +--- + +## 4. Root `spec.config` schema + +Runtime configuration only. `artifactId` is a sibling on the Provider spec, not +inside `config`. No `config` field accepts a secret-shaped value. + +| Field | Type | Default | Rules | +| --- | --- | --- | --- | +| `collectionAlias` | string | `"login"` | Provider-validated Secret Service collection alias; charset: printable ASCII except `"`, `\`, `\n`, `\r`, `\0`; max 128 chars; not an `OpaqueAzureRef` (collection aliases may include spaces); must not be empty | +| `maxLeases` | u32 | `64` | Maximum concurrent active leases in the local lease table; range 1..=256; matches `MAX_LOCAL_LEASES` in the crate | +| `lockPolicy` | enum | `"fail-closed"` | `fail-closed` = return `credential-provider-unavailable` when keyring is locked; `fail-degraded` = set `ProviderUnavailable=True` condition and continue with degraded status | + +Provider-specific constraint: `credential-secret-service` does not have a +`tenantId`, `authorityUrl`, `clientId`, or `imdsEndpointAlias` field. Those +fields belong on other credential Providers and are rejected here. + +Nix eval-time secret-shape assertion (`contains_sensitive_shape`) runs on all +string fields of this `config` block before the NixOS build completes. + +--- + +## 5. Credential resource spec (as consumed by this Provider) + +The `Credential` ResourceType spec is defined canonically in +`ADR-046-resources-credential`. + +Normative D089 spec layering: Credential base fields are ResourceType base +`spec.*` fields, including `spec.providerRef`, `audience`, `scope`, +`allowedOperations`, `rotation`, and `revocation`. This Provider's desired-only +extension is the canonical `spec.provider = { schemaId: +"credential-secret-service.d2bus.org/Credential/spec", schemaVersion, settings }` +envelope; it is manifest-registered/signed, strict deny-unknown, bounded, versioned +and digested, validated against `spec.providerRef` at Nix build and API +admission, implementation-only, and may not shadow base fields. Shared fields +are promoted to the Credential base. The Provider implements the exact base +Credential spec/status version/fingerprint, accepts the canonical minimal valid +base Spec, and rejects unsupported optional base capabilities only through its +signed capability matrix and provider-neutral `unsupported-capability`. +`spec.provider` aligns with `status.provider`; generic CLI/controllers operate on +the base spec and base status only. No secret +bytes or credential material are allowed in any spec layer, including +`spec.provider.settings`; credential bytes are delivered only over Noise_KK +sessions. + +Fields specific to this Provider: + +### 5.1 Full Nix example + +```nix +d2b.zones.dev.resources.local-keyring = { + type = "Credential"; + metadata = { + labels = { "purpose" = "user-session"; }; + }; + spec = { + providerRef = "Provider/credential-secret-service"; + scope = { + executionRef = "Host/host-system"; + domainFilter = "user"; + userRef = "User/alice"; + }; + audience = "user-session"; + consumerRef = "Provider/shell-terminal"; # optional + allowedOperations = [ "acquire-token" "refresh-token" "revoke-token" "inspect-metadata" ]; + rotation = { + policy = "on-demand"; + }; + revocation = { + onOwnerDelete = "immediate"; + onProviderGeneration = "immediate"; + }; + }; +}; +``` + +### 5.2 Spec field reference (Provider-specific constraints) + +| Field | Provider constraint | +| --- | --- | +| `providerRef` | Must be `Provider/credential-secret-service` for this Provider | +| `scope.domainFilter` | Must be `"user"`; `"system"` and `"guest"` rejected at eval time and Provider construction | +| `scope.userRef` | Required (domainFilter=user rule) | +| `scope.executionRef` | Must resolve a `Host/` or `Guest/` in the same Zone; both supported with `domainFilter=user` | +| `audience` | Provider-validated non-secret opaque value; charset `^[A-Za-z0-9._:/@-]+$`, max 256 chars | +| `allowedOperations` | Subset of `{ acquire-token, refresh-token, revoke-token, inspect-metadata }`; `sign-challenge` is not supported and schema-rejects it at eval and Provider install | +| `consumerRef` | Optional; restricts the acquiring Provider; no arbitrary component fallback | + +### 5.3 Canonical rendered ResourceSpec JSON + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Credential", + "metadata": { + "name": "local-keyring", + "zone": "dev", + "labels": { "purpose": "user-session" }, + "annotations": {}, + "ownerRef": null + }, + "spec": { + "providerRef": "Provider/credential-secret-service", + "scope": { + "executionRef": "Host/host-system", + "domainFilter": "user", + "userRef": "User/alice" + }, + "audience": "user-session", + "consumerRef": "Provider/shell-terminal", + "allowedOperations": ["acquire-token", "inspect-metadata", "refresh-token", "revoke-token"], + "rotation": { + "policy": "on-demand" + }, + "revocation": { + "onOwnerDelete": "immediate", + "onProviderGeneration": "immediate" + } + } +} +``` + +`allowedOperations` is sorted alphabetically and deduplicated in the emitted +JSON. + +--- + +## 6. Credential controller + +### 6.1 Controller descriptor + +```yaml +providerId: Provider/credential-secret-service +controllerType: Credential +resourceTypes: [Credential] +watchSelectors: + - resourceType: Credential + providerRefFilter: Provider/credential-secret-service + - resourceType: Provider + nameFilter: credential-secret-service +dependencySelectors: + - resourceType: Provider + relationship: providerRef + - resourceType: Host + relationship: scope.executionRef + - resourceType: Guest + relationship: scope.executionRef + - resourceType: User + relationship: scope.userRef +ownerChildTriggers: [owned-resource-changed] +reconcileConcurrency: 8 +maxPendingResources: 256 +finalizers: + - credential.d2bus.org/provider-revoke +observeInterval: 30s +``` + +### 6.2 Controller handler methods + +All handlers are async. They return a typed `ReconcileResult` carrying the +next disposition: `Ok(Ready)`, `Degrade(reason, requeue_at)`, `Fail(outcome)`, +`Finalizing`, or `Blocked(reason, requeue_at)`. + +#### Currency and upgrade (D091) + +The controller implements `assess_update`, `plan_upgrade`, and +`execute_upgrade`. A Provider generation or signed artifact generation/digest +change updates universal `status.update` with `state: UpdateAvailable` or +`state: UpgradeRequired`, `reasons` including `ProviderGenerationChanged` or +`ArtifactChanged`, observed/target generation or digest IDs, +`disruption: Reload` or `disruption: Restart` for the credential component +realization, `preserveState: true`, bounded `owned`/`dependencies`, and +`lastAssessedAt`. Disruptive changes MUST return `UpgradeRequired` rather than +applying in place; non-disruptive changes reconcile normally. Credential +rotation is not an upgrade and remains the §6.5 flow. `status.update` MUST NOT +contain secret bytes, tokens, or lease material; only bounded non-secret +generation/digest IDs and lease metadata already permitted by the Credential +base may appear. Token delivery remains solely over `Noise_KK`. + +#### Expedited reconcile on mutation (D090) + +For `Create`, `UpdateSpec`, and `Delete` with `waitForReconcile`, the controller +MUST perform no Secret Service effect, finalizer change, or status mutation +until core supplies `CommittedRevisionProof {resourceUid,generation,revision,operationId}`. +Abort before that proof has no effect. After durable commit, the commit is never +rolled back if the reconcile pass times out. The response returns the committed +object, post-pass projected layered status, disposition +(`Converged|Progressing|Blocked|UpgradeRequired|Failed`), and +`statusPersistence: pending|committed`. Effect idempotency keys derive from +`(UID,generation,revision,operationId)` and use the same per-resource +single-flight priority lane. + +#### `reconcile(resource, context)` + +Called when a Credential resource assigned to this Provider has been created, +updated, or depends-on-trigger fires. Steps: + +1. Verify the resource's `spec.providerRef` matches this Provider instance + generation; reject if generation mismatch. +2. Validate `scope.domainFilter = "user"` and `scope.executionRef` resolves a + ready `Host/` or `Guest/` in the same Zone. If not, set + `phase=Degraded`, `ProviderUnavailable=True`, return `Degrade`. +3. If `spec.allowedOperations` includes `acquire-token` and `leaseState ∉ {Active}`: + a. Derive idempotency key: + `sha256(credential_uid || rotation_generation.to_le_bytes() || b"acquire")`, + encoded as hex; max 64 chars; no secret material. + b. Call `Oo7SecretServicePort::issue_lease(&SecretServiceLeaseRequest)`. + c. On grant: write status (`leaseState=Active`, `leaseHandle`, `sourceVersion`, + `expiresAtUnixMs`, `rotationGeneration`, `issuedAtUnixMs`, `CredentialReady=True`). + d. On `SecretServicePortError::Locked`: apply `lockPolicy`; set + `ProviderUnavailable=True`, `leaseState=Unknown`. Return + `Degrade("keyring-locked", requeue_at)`. + e. On `SecretServicePortError::Unavailable`: set `ProviderUnavailable=True`. + Return `Degrade("provider-unavailable", requeue_at)`. + f. On `SecretServicePortError::CompletionUnknown`: set + `ProviderUnavailable=True`. Do NOT auto-retry with the same record. + Return `Degrade("completion-unknown", requeue_at)`. +4. If `rotation.policy = "proactive"` and `RotationDue=True` condition is set: + execute the rotation algorithm (§6.5). +5. Commit status mutation batch with optimistic revision precondition. + +#### `observe(resource, context)` + +Called every `observeInterval` (30 s) to detect out-of-band lease changes. Steps: + +1. Call `Oo7SecretServicePort::inspect_lease(&SecretServiceLeaseRef)`. +2. On success: update `leaseState`, `expiresAtUnixMs`, `lastRefreshedAt`; if + `leaseState` changed from `Active` to `Revoked/Expired`, clear + `CredentialReady`, set `LeaseRevoked` or check expiry. +3. If `policy=proactive` and remaining lifetime < `proactiveWindowMs`: set + `RotationDue=True`, requeue for rotation. +4. On `SecretServicePortError::Locked`: update status per `lockPolicy`; requeue. +5. Commit status batch. + +#### `finalize(resource, context)` + +Called when `deletionRequestedAt` is set and `credential.d2bus.org/provider-revoke` +finalizer is present. Canonical sequence: revoke/drain → delete scoped Process +(if applicable) → clear `provider-revoke` → (core) event-only `Deleted` revision ++ row/index removal → (audit subsystem) closure audit with dedup. + +1. If `spec.revocation.onOwnerDelete = "immediate"`: + a. If `leaseState=Active` or `leaseState=Unknown`: call + `Oo7SecretServicePort::revoke_lease(&SecretServiceLeaseRef)`. + b. On `Revoked` or `AlreadyRevoked`: set `leaseState=Revoked`, + `LeaseRevoked=True`, commit status. + c. On `SecretServicePortError::Unavailable` or `Locked`: set + `ProviderUnavailable=True`; return `Blocked("provider-unavailable", + requeue_at)`. +2. If `spec.revocation.onOwnerDelete = "drain-leases"`: + - If `leaseState=Expired`: proceed. + - If `leaseState=Active`: set `LeaseRevoked=False` condition; requeue until + `expiresAtUnixMs` passes. +3. If `leaseState=Revoked` or `leaseState=Expired`: proceed without further + network calls (terminal state satisfies the finalizer). +4. Emit bounded revoke outcome audit record (Zone, `resource_name_digest`, + operation class, rotationGeneration, revocation outcome code; no token + bytes, paths, or provider diagnostics). The controller MUST NOT emit a + resource-deleted closure audit; that is appended by the audit subsystem after + the core event-only `Deleted` revision with dedup/exactly-once recovery. +5. Issue a controller-initiated Delete for the scoped `Process` resource owned + by `Provider/credential-secret-service` for this `(Zone, User, executionRef)` + triple (if one was created). This is a regular resource delete via the + controller's write authority; the Process controller completes it through its + own lifecycle before the Credential row is removed. +6. Clear the `credential.d2bus.org/provider-revoke` finalizer. After this, core + writes the event-only `Deleted` revision and removes the Credential row and + indexes atomically. + +#### `drain(resource, context)` + +Called before a Provider generation change. Steps: + +1. Apply `spec.revocation.onProviderGeneration` policy. +2. `immediate`: call `revoke_lease`; on success, set `leaseState=Revoked`. +3. `drain-leases`: do nothing; leases expire by natural deadline. +4. Emit bounded audit record. + +#### `health() -> ControllerHealth` + +Returns `{ state: Ready | Degraded, provider_process_reachable: bool, +active_leases: u32, locked_count: u32 }`. Does not expose collection object +paths, user-identifying data, or token bytes. Core reads this health response +to aggregate Provider-level status; the controller writes only its scoped +Credential and Process health, never Provider-level status directly. + +### 6.3 Service methods (`d2b.credential.v3`) + +The controller process serves the `d2b.credential.v3` protobuf/ttrpc service +routed through d2b-bus. All methods are async. + +| Method | Port call | Exact operation/Role subresource | Required Role permission | Outer DTO | Sensitive output | +| --- | --- | --- | --- | --- | --- | +| `AcquireToken` | `issue_lease` | `acquire-token` | `use-credential/acquire-token` | `AcquireTokenResponse` (leaseHandle, sourceVersion, rotationGeneration, expiresAtUnixMs) | raw token bytes in dedicated Noise_KK delivery record | +| `RefreshToken` | `refresh_lease` | `refresh-token` | `use-credential/refresh-token` | `RefreshTokenResponse` (leaseHandle, sourceVersion, rotationGeneration, new expiresAtUnixMs) | raw token bytes in dedicated Noise_KK delivery record | +| `RevokeToken` | `revoke_lease` | `revoke-token` | `use-credential/revoke-token` | `RevokeTokenResponse` (Revoked or AlreadyRevoked, revokedAtUnixMs) | none | +| `SignChallenge` | none | `sign-challenge` | `use-credential/sign-challenge` | `credential-schema-invalid` because this Provider does not declare the operation | none | +| `InspectMetadata` | `inspect_lease` | `inspect-metadata` | `use-credential/inspect-metadata` | `InspectMetadataResponse` (leaseState, rotationGeneration, sourceVersion, expiresAtUnixMs) | none | + +`sign-challenge` is not implemented. A `SignChallenge` call returns +`credential-schema-invalid` immediately without consulting the port. The method +selects the operation class; requests do not carry a caller-selected +operation-class field. + +Every method: +- rejects a request where the authenticated subject is not the declared + `consumerRef` (when set); +- rejects before Provider dispatch unless the method's one exact operation is + present in both `spec.allowedOperations` and Role `subresources` under + `use-credential`; +- returns a stable closed error code, never provider-internal diagnostics; +- carries operation/idempotency/correlation IDs from d2b-bus context; +- enforces a per-call deadline propagated from the d2b-bus context. + +### 6.4 `Oo7SecretServicePort` trait (injected interface) + +```rust +#[async_trait] +pub trait Oo7SecretServicePort: Send + Sync { + async fn state(&self) -> Result; + + async fn issue_lease( + &self, + request: &SecretServiceLeaseRequest, + ) -> Result; + + async fn inspect_lease( + &self, + lease: &SecretServiceLeaseRef, + ) -> Result; + + async fn refresh_lease( + &self, + lease: &SecretServiceLeaseRef, + ) -> Result; + + async fn revoke_lease( + &self, + lease: &SecretServiceLeaseRef, + ) -> Result; +} +``` + +None of these methods accepts or returns a password, secret value, token, +endpoint, path, file descriptor, or byte buffer. The port owns all interaction +with the FreeDesktop.org Secret Service D-Bus API (GNOME Keyring, KWallet, or +equivalent) and retains all credential material. Only bounded opaque lease +metadata crosses the `Oo7SecretServicePort` boundary. + +The production implementation of this trait is the `oo7`-backed D-Bus adapter +in `src/lib.rs`. It operates exclusively over the inherited portal FD delivered +in the LaunchTicket; it must not call `Session::new()`, connect to any ambient +D-Bus address or socket path, or read `DBUS_SESSION_BUS_ADDRESS`. Tests use +`FakeOo7Port` (§10.2). + +### 6.5 Rotation algorithm + +For `rotation.policy = "proactive"`: + +1. Controller's `observe` handler sets `RotationDue=True` when + `now_unix_ms + proactiveWindowMs >= expiresAtUnixMs`. +2. `reconcile` detects `RotationDue=True` and calls `issue_lease` with a new + idempotency key: + `sha256(credential_uid || (rotation_generation + 1).to_le_bytes() || b"acquire")`. +3. On grant: increment `rotationGeneration`; store new `leaseHandle` and + `expiresAtUnixMs`; clear `RotationDue`; commit status; emit rotation audit + record (Zone, `resource_name_digest`, trigger reason, old + rotationGeneration, new rotationGeneration, outcome code). +4. Old lease remains valid until the new lease is confirmed active. +5. A duplicate `issue_lease` with the same idempotency key returns the same + grant without double-issuing. +6. On failure: bounded retry under `requeue-at` disposition; after retry + exhaustion, `phase=Failed`, `outcome=rotation-failed`. + +For `rotation.policy = "on-demand"`: controller does not auto-rotate. Consumer +explicitly calls `RefreshToken` or `RevokeToken`+`AcquireToken`. Controller only +monitors expiry for status conditions. + +For `rotation.policy = "on-expiry"`: controller calls `issue_lease` only after +`leaseState` transitions to `Expired`. + +### 6.6 Provider-internal state machine + +``` +SecretServiceState: Locked | Unlocked +SecretServiceLeaseState: Active | Revoked | Expired +``` + +The state machine is process-local and reconstructed from the owning +`Credential.status`, core Operation ledger entries, and live Secret Service +observation after restart. Under D087 the Provider declares **no Provider state +Volume**: bounded non-secret operational state belongs in status by default, and +this Provider does not satisfy the storage-need test. ProviderStateSet remains +only the optional query-time grouping of declared state Volumes and is empty for +`credential-secret-service` (D086, superseded by D087). + +When `SecretServiceState = Locked`: +- `AcquireToken` returns `credential-provider-unavailable` + (from `SecretServicePortError::Locked`). +- Status reflects `ProviderUnavailable=True`, `leaseState=Unknown`. +- Controller applies `lockPolicy`: + - `fail-closed`: return error immediately on every call while locked. + - `fail-degraded`: set `phase=Degraded` + `ProviderUnavailable=True`; requeue. +- The port is polled at `observeInterval` to detect unlock. + +When `SecretServiceState = Unlocked`: +- All service methods proceed normally. +- `CredentialReady=True` when `leaseState=Active`. + +--- + +## 7. User-agent placement + +### 7.1 Process component + +| Component | Type | Domain | Binary | Cardinality | +| --- | --- | --- | --- | --- | +| `secret-service-controller` | controller | user | `d2b-provider-credential-secret-service` | One per `(Zone, User, executionRef)` triple | + +The controller process is a user-domain `Process` resource under the Host or +Guest declared in `scope.executionRef`. It is launched and supervised by +`Provider/system-systemd`. + +### 7.2 `SecretServiceOwner` + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SecretServiceOwner { + /// The exact user's d2b user-domain process. + Userd, +} +``` + +The owner is `SecretServiceOwner::Userd`. The controller is constructed only +for a user-domain Process authorized by the matching `scope.userRef`. Construction +for any other owner type fails with `SecretServiceProviderError::InvalidConsumer`. + +`SecretServiceProviderError::NotColocated` is returned when the consumer +Provider and Secret Service provider are not co-located in the same user session. + +The consumer Provider identity (from `spec.consumerRef`, checked by d2b-bus RBAC +against the authenticated ComponentSession subject) and the user identity (from +`spec.scope.userRef`, checked against the authenticated user-domain context of +the requesting Process) are enforced independently by d2b-bus; neither derives +from the other. + +### 7.3 Canonical controller Process resource template + +One Process resource is created per `(Zone, User, executionRef)` triple. +`metadata.ownerRef` is `Provider/credential-secret-service`; the controller +creates and manages it during provider reconciliation and does not expose it in +the public Nix authoring surface. + +`spec.template` is a plain string naming the component descriptor. The component +descriptor for `secret-service-controller` declares the required authenticated +`dbus-session` FD attachment; the Process controller validates this attachment +and threads it into the LaunchTicket's inherited FD table before the process is +spawned. `spec` has no generic `attachments` array. Stable service identities +are separate owned `Endpoint` resources, not inline Process fields. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + # Generated name; pattern: credential-ss-; no secret component. + name: credential-ss- + zone: + ownerRef: Provider/credential-secret-service +spec: + providerRef: Provider/system-systemd + executionRef: | Guest/> # mirrors Credential spec.scope.executionRef + domain: user + userRef: > # mirrors Credential spec.scope.userRef + processClass: controller + template: secret-service-controller # component descriptor ID; plain string + sandbox: + namespaceClasses: [mount, pid, ipc] # Provider/system-systemd rejects user namespace class; + # same-UID execution is guaranteed by spec.userRef + capabilityClasses: [] # zero capability classes; no host caps + seccompClass: strict # closed strict seccomp class + noNewPrivileges: true + startRoot: false # does not require elevated start + environmentClass: minimal # no inherited environment variables + readOnlyRoot: true # read-only root filesystem + budget: + cpu: + request: "10m" # 10 millicores baseline + limit: "500m" # 0.5 core ceiling + memory: + request: "16Mi" # 16 MiB baseline + limit: "64Mi" # 64 MiB hard ceiling + pids: + limit: 32 # max 32 PIDs in the cgroup + fds: + limit: 64 # max 64 open file descriptors + networkUsage: null # no ambient network; all comms via d2b-bus portal + readiness: + initialDelay: 2s + timeout: 30s + failureThreshold: 5 + successThreshold: 1 + class: provider-defined # provider verifies d2b-bus registration +``` + +The controller's stable credential service identity is a separate owned +`Endpoint` resource: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: credential-ss--credential-service + zone: + ownerRef: Provider/credential-secret-service +spec: + providerRef: Provider/credential-secret-service + producerRef: Process/credential-ss- + endpointClass: service + transport: unix + purpose: credential-secret-service.d2bus.org/credential-service + serviceFingerprint: credential.d2bus.org/CredentialService.v3 + locality: host-local + visibility: zone + attachmentPolicy: component-session + consumerPolicy: + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +## Endpoint resources (D092) + +`Provider/credential-secret-service` conforms to the standard `Endpoint` base +schema. The stable `d2b.credential.v3`-class ComponentSession service identity is +an owned `Endpoint` resource with `producerRef`; consumers use +`Endpoint/`. Endpoint spec/status never carries Secret Service object +paths, D-Bus addresses, fd numbers, lease handles with authority, token bytes, +passwords, credential values, or other secrets. Resolution occurs only through +an authorized EffectPort/LaunchTicket; unauthorized resolution returns +`endpoint-resolve-denied`. Producer restart bumps +`Endpoint.status.endpointGeneration`, causing consumers to observe +`dependency-changed` and reacquire through a fresh authorized ticket. + +## Retained opaque handles (D092 promotion test) + +- pidfds for the user-domain controller Process are supervision handles, not + stable endpoint identities. +- The pre-opened D-Bus fd index is a LaunchTicket-local attachment slot and + remains opaque. +- `OwnedTransport`, ComponentSession IDs, named streams, and the sensitive + Noise_KK delivery session handle are in-memory per-session capabilities. +- `leaseHandle` and `operationId` values remain bounded, non-secret, + non-authorizing status/idempotency handles and are revalidated before use. + +### 7.4 Injected port: pre-opened D-Bus FD + +The controller is constructed with an injected `Arc`. +The production implementation wraps the pre-opened Secret Service connection +port delivered by the fixed user supervisor as the user portal FD in the +LaunchTicket's inherited FD table. The component descriptor for +`secret-service-controller` declares a required authenticated `dbus-session` +FD attachment; the Process controller validates this attachment and threads it +into the LaunchTicket before the process is spawned. There is no +`$DBUS_SESSION_BUS_ADDRESS` lookup, no ambient socket path discovery, no +Volume-mounted socket path, no environment credential chain, no keyring file +path, and no host-daemon fallback. The user portal FD is the only path from +the controller to the keyring; absent it, the controller fails closed. + +None of the `Oo7SecretServicePort` methods accepts or returns a password, secret +value, token, endpoint, path, or byte buffer; only opaque non-secret identifiers +and bounded metadata cross the port boundary. + +--- + +## 8. Noise KK delivery: end-to-end token delivery + +Token bytes (`AcquireToken`, `RefreshToken`) are never present in the outer +`d2b.credential.v3` response DTO, resource status, audit records, OTEL spans, +or any log line. + +### 8.1 Session profile + +The delivery channel MUST use `Noise_KK_25519_ChaChaPoly_SHA256`. `Noise_NN` +is forbidden for sensitive output delivery. + +- **Credential Provider key**: registered at Provider installation; d2b-bus + holds only the public key. +- **Consumer Provider key**: extracted from the consumer Provider's signed + component descriptor; the consuming component and Process identity are derived + from the consumer Provider's Role/RoleBinding. + +### 8.2 Delivery session binding + +Each delivery session binds these fields (from +`ADR-046-componentsession-and-bus` §Credential-delivery endpoint contract): + +| Field | Source | +| --- | --- | +| `credentialRef` | `Credential/` | +| `credentialUID` | Credential resource UID | +| `credentialGeneration` | Credential generation at delivery time | +| `consumerProviderRef` | `Provider/` matching `spec.consumerRef` | +| `consumerComponentGeneration` | Consumer Provider component generation | +| `audience` | `spec.audience` value (opaque; not echoed in logs or spans) | +| `operationClass` | `acquire-token` or `refresh-token` | +| `expiryUnixMs` | Clipped to `spec.rotation.maxLeaseLifetimeMs` | +| `deadlineUnixMs` | Hard session close deadline (≤ `expiryUnixMs`) | +| `routeDigest` | Digest of bus-authorized route parameters | +| `schemaVersion` | Fixed version of the binding contract | +| `maxTokenBytes` | Closed upper bound on sensitive output size | +| `transcriptDigest` | Noise transcript digest after handshake | + +Both parties MUST verify the full binding before accepting records. The binding +is conveyed in the Noise handshake prologue. + +### 8.3 Security requirements for the delivery session + +1. **Enrolled keys only**: both static keys must be enrolled at session + initiation. Any NN/NX/N pattern is rejected immediately; session is closed + and zeroized. +2. **Replay-safe sequence**: monotonically increasing per-credential-UID sequence + number. A replay at the same or lower sequence number is rejected. +3. **Bounded output**: sensitive output record MUST NOT exceed `maxTokenBytes`. + Oversize records reject immediately; channel closed and zeroized. +4. **Zeroizing buffers**: plaintext is zeroed in memory immediately after the + consumer extracts it. The provider zeroizes after encryption. All intermediate + buffers are zeroizing types. +5. **Redacted Debug**: all credential-bearing Rust types involved in delivery + (request, response, record wrapper, buffer) MUST implement `Debug` via a + hand-written impl emitting only the type name and a placeholder. Derived + `Debug` is forbidden for these types. +6. **No automatic success-shaped replay**: after any ambiguous delivery outcome + (timeout, partial write, disconnection), the provider MUST NOT auto-retry + with the same record. The consumer must re-initiate via `AcquireToken` or + `RefreshToken`. +7. **Immediate close/zeroize**: after the delivery record is confirmed received, + the provider closes and zeroizes the delivery channel. The channel is not + reused across multiple deliveries. + +### 8.4 RBAC enforcement point + +d2b-bus performs the following checks before authorizing the delivery route: + +- RBAC `use-credential` for the authenticated consumer Provider subject, + Credential ResourceRef, and operation class; +- `spec.consumerRef` matches the consumer Provider identity from the signed + component descriptor; +- `spec.allowedOperations` includes the operation class; +- current lease state permits the operation class; +- no Role/RoleBinding or Provider generation change has revoked authorization + since the last audit checkpoint. + +After these checks pass, d2b-bus forwards opaque Noise-encrypted records between +the two endpoints. The bus never buffers or stores the records. + +--- + +## 9. Lease lifecycle + +### 9.1 State machine + +The latest bounded lease checkpoint is status/Operation-ledger state, not a +Provider state Volume payload. Per D088, Credential lease metadata common to all +implementations (`leaseState`, opaque non-authorizing `leaseHandle`, `audience`, +expiry/issue timestamps, and phase) lives in `status.resource`; Secret +Service-specific observations (`rotationGeneration`, `sourceVersion`, retry +counters, and closed-enum outcome) live in `status.provider.details`. Any +`leaseHandle` written to status is opaque, non-authorizing, bounded, safe for +authorized status readers, and independently revalidated against the Secret +Service port before use. Secret Service object paths and credential bytes never +enter status, audit, telemetry, or storage. + +``` +Absent + └── AcquireToken ──────────► Active (CredentialReady=True) + +Active + ├── [proactive window] ────► RotationDue (CredentialReady=True, RotationDue=True) + ├── RefreshToken ──────────► Active (rotationGeneration unchanged, new expiresAtUnixMs) + ├── [rotation] ────────────► Active (rotationGeneration+1, new leaseHandle) + ├── RevokeToken ───────────► Revoked (CredentialReady=False, LeaseRevoked=True) + ├── expiry deadline ───────► Expired (CredentialReady=False) + └── Provider gen change ───► [onProviderGeneration=immediate] → Revoked + [onProviderGeneration=drain-leases] → Expired on deadline + +RotationDue + ├── rotation success ───────► Active (rotationGeneration+1) + ├── rotation failure ───────► RotationDue/Degraded (bounded retry) + └── retry exhaustion ───────► Failed (outcome=rotation-failed) + +Expired + └── AcquireToken ──────────► Active (new lease; rotationGeneration+1) + +Revoked + ├── AcquireToken ──────────► Active (if policy permits re-acquisition) + └── resource delete ───────► provider-revoke finalizer satisfied immediately +``` + +### 9.2 Idempotency + +Every `issue_lease` / `refresh_lease` / `revoke_lease` call carries a stable +`idempotency_key` derived from: + +``` +idempotency_key = sha256(credential_uid || rotation_generation.to_le_bytes() || operation_tag) +``` + +Where `operation_tag` is one of: `b"acquire"`, `b"refresh"`, `b"revoke"`. The +key is hex-encoded; max 64 chars; contains no secret material. A duplicate call +with the same key returns the existing result without double-issuing. + +### 9.3 Lease cardinality + +`MAX_LOCAL_LEASES = 256`. The controller maintains a bounded in-memory lease +table. When the table is full, `issue_lease` returns +`credential-queue-pressure`. The `maxLeases` Provider config value (range 1..256) +further restricts the effective limit per Provider instance. + +### 9.4 Finalizers + +| Finalizer ID | Owner | Meaning | +| --- | --- | --- | +| `credential.d2bus.org/provider-revoke` | Credential controller | Revoke all active leases before deletion; honors `revocation.onOwnerDelete` policy | +| `credential.d2bus.org/consumer-drain` | `consumerRef` controller | Drain in-flight operations before Provider releases lease handle | + +Execution order: `consumer-drain` completes before `provider-revoke`. A missing +`consumerRef` removes `consumer-drain` automatically. A terminal `leaseState` +(Revoked or Expired) satisfies `provider-revoke` immediately without further +network calls. + +--- + +## 10. RBAC and security + +### 10.1 Resource verbs + +| Verb | Authorized subjects | +| --- | --- | +| `get` | Any authorized subject | +| `list` | Any authorized subject | +| `watch` | Any authorized subject | +| `create` plus `admin-credential/create` | Deployer/system-core configuration controller; neither permission implies the other | +| `update-spec` plus `admin-credential/update-spec` | Deployer/system-core configuration controller; neither permission implies the other | +| `update-status` | Credential controller (exact registered process generation) only | +| `update-finalizers` | Credential controller; `consumerRef` controller | +| `delete` plus `admin-credential/delete` | Deployer/system-core configuration controller; neither permission implies the other | +| `use-credential` | Consumer subject authorized via `consumerRef` and Role/RoleBinding | + +`use-credential` Role rule shape: + +```yaml +rules: + - resourceTypes: [Credential] + verbs: [use-credential] + subresources: [acquire-token, refresh-token] + resourceNames: [local-keyring] + zones: [dev] + executionRefs: [Host/host-system] + sessionVerbs: [] +``` + +The effective operation set is the intersection of `spec.allowedOperations` and +the Role's exact `subresources`, further narrowed by `consumerRef`, scope, and +structural Provider/component checks. Empty, wildcard, unknown, or mismatched +subresources deny; there is no alternate Credential-operation Role field or +shorthand operation alias. + +### 10.2 Secret isolation invariants + +1. **Zero-secret-bytes invariant**: `spec`, `status`, the resource store row, + redb WAL, revision log, bus routing DTOs, error messages, audit records, + OTEL spans, metrics, and all log lines never contain secret bytes in any + field. +2. **Port boundary**: the `Oo7SecretServicePort` trait is the sole boundary + through which the controller interacts with the keyring. No method accepts or + returns a password, secret value, token, endpoint, path, file descriptor, or + byte buffer. +3. **Delivery channel**: token bytes travel exclusively in the end-to-end + Noise_KK delivery record between the Credential Provider process and the + authorized consumer Provider process. d2b-bus never terminates or stores + delivery record content. +4. **Redacted Debug**: `SecretServiceLeaseRequest` and `SecretServiceLeaseRef` + implement hand-written `Debug` that omits `operation`, `credential_provider_id`, + and `consumer_provider_id`; they emit only generation, placement binding, + operation count, and expiry fields. +5. **`object_path_canary` enforcement**: the `object_path_canary` value held + by `FakeOo7Port` in tests MUST NOT appear in any service response, status + field, audit record, log line, OTEL span attribute, or delivery record. This + is enforced by `tests/canary.rs`. +6. **`credential_canary` enforcement**: the `credential_canary` value held by + `FakeOo7Port` MUST NOT appear in any service response, status field, audit + record, log line, OTEL span, metric label, or delivery record. +7. **No ambient portal fallback**: the Secret Service connection port reaches the + controller exclusively through the fixed user supervisor/user portal: the + framework delivers it as a pre-opened FD in the LaunchTicket's inherited FD + table. The crate adapter operates only over this inherited portal FD; it must + not discover D-Bus addresses, connect to ambient sockets, read + `DBUS_SESSION_BUS_ADDRESS`, or open any socket path to reach the keyring. + Absent the portal FD, the controller fails closed. +8. **Status-first operational state (D087)**: bounded non-secret controller + and lease observation lives in the owning `Credential.status` subresource + and the core Operation ledger by default. In-flight idempotency/retry is an + Operation-ledger concern; the latest bounded lease result/checkpoint lives in + status. `credential-secret-service` declares **no Provider state Volume** + because it has no payload that passes the storage-need test: no secret + private recovery data may enter storage, there is no large/binary/file + content, no private data unsafe for authorized status readers is persisted, + and bounded recovery state is suitable for revisioned status plus external + revalidation. ProviderStateSet is optional query-time grouping and is empty + for this Provider (D086, superseded by D087); there is no bootstrap state + Volume mechanism or exception. Status is revisioned, + optimistic-status-writer controlled, RBAC-readable, redacted, + observation-only, written only on material change, and bounded to the core + status limits (total ≤ 64 KiB, provider-specific detail ≤ 32 KiB, with + `status-oversize` rejection). It must not contain secrets, tokens, keys, PSKs, + authority-conferring credential handles, Secret Service object paths, + private path/argv/env/PID/unit data, raw provider error bodies, large blobs, + or churn-heavy content. Any opaque handle in status is non-secret, + non-authorizing, bounded, safe for authorized status readers, and + independently revalidated before use. Raw credential delivery remains + transient process memory over dedicated Noise_KK sensitive sessions only. + +### 10.3 Canary tests (`tests/canary.rs`) + +Every test in `canary.rs` constructs a `FakeOo7Port` with a non-empty +`credential_canary` and a non-empty `object_path_canary`, runs a complete +operation (acquire, refresh, revoke, inspect), and asserts that neither canary +string appears anywhere in: + +- the outer `d2b.credential.v3` response DTO; +- any `status` field serialized to JSON; +- any audit record serialized to JSON; +- any log line captured by the test subscriber; +- any OTEL span attribute captured by the test subscriber; +- the delivery session binding parameters. + +A failing canary test is a hard error that blocks the PR. + +--- + +## 11. Status, errors, audit, and OTEL + +Per D088, ResourceType-common Credential observation lives in `status.resource`: +the non-secret lease metadata base that is identical across Credential +implementations. Secret Service-specific lease observations live only in +`status.provider` with `providerRef`, qualified `schemaId` +`credential-secret-service.d2bus.org/Credential/status`, `schemaVersion`, +`observedProviderGeneration`, and strict bounded redacted `details` +(≤32 KiB, unknown-field-denied). The controller writes all present layers +atomically in one status mutation; shared +fields are never duplicated into `status.provider`, and the extension schema is +registered and signed in the Provider manifest. No secret bytes appear in any +status layer. + +### 11.1 Status conditions + +| Condition type | Meaning | +| --- | --- | +| `CredentialReady` | `True` when `leaseState=Active` and not within rotation window | +| `RotationDue` | `True` when `policy=proactive` and remaining lifetime < `proactiveWindowMs` | +| `ProviderUnavailable` | `True` when port returns `Locked` or `Unavailable` consistently | +| `LeaseRevoked` | `True` when `leaseState=Revoked` and no replacement issued | + +### 11.2 Stable error codes + +| Code | Meaning | +| --- | --- | +| `credential-not-found` | Credential resource does not exist in this Zone | +| `credential-provider-unavailable` | Port returned `Locked` or `Unavailable` | +| `credential-lease-expired` | Lease is past expiry deadline | +| `credential-lease-revoked` | Lease was explicitly revoked | +| `credential-operation-denied` | Operation class not in `allowedOperations` or RBAC denied | +| `credential-consumer-mismatch` | Requesting subject does not match `consumerRef` | +| `credential-placement-mismatch` | Scope domain is not `user` or Host constraint violated | +| `credential-rotation-failed` | Proactive rotation failed after bounded retries | +| `credential-invariant-failure` | Port returned a response failing invariant checks | +| `credential-schema-invalid` | `sign-challenge` requested (unsupported); or spec fails validation | +| `credential-queue-pressure` | Lease table at capacity (`maxLeases`) | + +All error messages are bounded (max 240 UTF-8 chars), stripped of control +characters, and must not contain token bytes, URLs, UUIDs, provider diagnostics, +host paths, or connection string shapes. + +### 11.3 Audit records + +| Event | Retained fields | +| --- | --- | +| Credential resource create/update/delete | Zone, subject digest, `resource_name_digest`, verb, revision result, authorization decision | +| `AcquireToken` | Zone, subject digest, `resource_name_digest`, operation class, `rotationGeneration`, outcome code, idempotency key digest | +| `RefreshToken` | Zone, subject digest, `resource_name_digest`, operation class, `rotationGeneration`, outcome code, idempotency key digest | +| `RevokeToken` | Zone, subject digest, `resource_name_digest`, operation class, `rotationGeneration`, revocation result code | +| Rotation | Zone, `resource_name_digest`, trigger reason, old `rotationGeneration`, new `rotationGeneration`, outcome code | +| Provider generation change revocation | Zone, `resource_name_digest`, policy applied, outcome code | +| Finalize (`provider-revoke`) | Zone, `resource_name_digest`, revocation outcome, `revokedAtUnixMs` | +| Bundle activated | Zone, `activationGeneration`, digest, create/update/skip/removed counts | +| Cleanup complete *(audit subsystem only; appended post-core-deletion; not emitted by controller)* | Zone, `resource_name_digest`, event-only Deleted revision committed and row/indexes removed atomically, `activationGeneration`, `cleanupLatencyMs` | + +`resource_name_digest` is SHA-256 of the Credential resource name, never the +raw name. It is admitted only to the authorization-controlled bounded Zone +audit stream and, for caller-initiated operations, after the authorization +decision. Raw Credential name, ResourceRef, and UID are excluded. The digest +is never copied to telemetry, logs, collector diagnostics, or support +summaries. + +Excluded from all audit records: token bytes, key material, passwords, bearer +strings, provider-internal diagnostics, host paths, connection strings, audience +literals, user-identifying path components, Noise/session key material, and +Secret Service object paths. + +### 11.4 OTEL spans + +Span names follow `d2b.credential.`: + +- `d2b.credential.acquire_token` +- `d2b.credential.refresh_token` +- `d2b.credential.revoke_token` +- `d2b.credential.inspect_metadata` +- `d2b.credential.reconcile` +- `d2b.credential.rotation` + +Required span attributes (closed set): + +| Attribute | Value | +| --- | --- | +| `d2b.credential.provider` | `credential-secret-service` | +| `d2b.credential.operation_class` | Closed enum string | +| `d2b.credential.placement_binding` | `user-agent` | +| `d2b.credential.outcome` | Stable closed outcome code | +| `d2b.credential.rotation_generation` | Numeric rotation generation | + +Credential telemetry uses only applicable generic OTEL Resource attributes from +the collector's closed allowlist: + +| Resource attribute | Value | +| --- | --- | +| `d2b.zone` | Zone name, re-stamped at trusted ingress | +| `d2b.provider` | `credential-secret-service` | +| `d2b.component` | Signed controller/service component ID | +| `service.name` | Fixed controller/service name | +| `service.namespace` | Fixed service namespace | +| `service.version` | Build version | + +No OTEL Resource attribute or span attribute carries a Credential resource +name, ResourceRef, UID, digest (including `resource_name_digest`), or derived +identity token. Also forbidden: token bytes, audience literals, provider +diagnostics, host paths, Secret Service object paths, collection names, +resource IDs, and correlation IDs embedding secret shapes. + +### 11.5 Metrics + +| Metric | Type | Labels | +| --- | --- | --- | +| `d2b_credential_operations_total` | Counter | `provider=credential-secret-service`, `operation_class`, `placement_binding=user-agent`, `outcome` | +| `d2b_credential_lease_expiry_seconds` | Gauge | `provider=credential-secret-service`, `placement_binding=user-agent` | +| `d2b_credential_rotation_total` | Counter | `provider=credential-secret-service`, `policy`, `outcome` | +| `d2b_credential_provider_health` | Gauge (0/1) | `provider=credential-secret-service` | +| `d2b_credential_active_leases` | Gauge | `provider=credential-secret-service`, `placement_binding=user-agent` | + +The expiry gauge reports the minimum seconds remaining across active +user-agent leases (0 when none). Label cardinality is bounded and semantic; +metric labels carry no Credential resource name, ResourceRef, UID, digest, or +derived identity token. Credential identity is available only as +`resource_name_digest` in authorized bounded audit records, never telemetry. +Generic allowlisted OTEL Resource attributes such as `d2b.zone`, +`d2b.provider`, and `d2b.component` remain available and are not copied into +metric labels or span attributes. Secret-shape assertions run on all label +values. + +--- + +## 12. Nix artifact and build + +### 12.1 Artifact catalog entry + +```nix +d2b.artifacts.credential-secret-service-bin = { + package = pkgs.d2b-provider-credential-secret-service; + type = "provider"; +}; +``` + +The ID `credential-secret-service-bin` matches `^[a-z][a-z0-9-]*$`. It is not +a `*Ref`. The catalog entry: + +- is integrity-pinned at build time alongside the resource bundle; +- is emitted to the global private catalog `/etc/d2b/artifact-catalog.json` + (owner `root:d2bd`, mode 0640), shared across all Zones; +- carries `id`, `type`, `sha256`, `storePath` (private; implementation data + only), and bounded closure metadata; +- `storePath` is private implementation data used by `activation-nixos` for + package staging; it never appears in resource spec, status, audit records, + or any log line; +- has its own SHA-256 digest header verified by `activation-nixos` before any + create/update. + +### 12.2 Eval-time assertions (Nix, `nixos-modules/assertions.nix` pattern) + +Applied to every `d2b.zones..resources.` entry with +`type = "Credential"` and `spec.providerRef = "Provider/credential-secret-service"`: + +- `spec.scope.domainFilter` must be `"user"`; `"system"` and `"guest"` fail + the assertion. +- `spec.scope.executionRef` must resolve a declared `Host/` or + `Guest/` in the same Zone. +- `spec.scope.userRef` must be set and resolve a declared `User/` in the + same Zone. +- `spec.allowedOperations` must be a non-empty subset of + `{ acquire-token, refresh-token, revoke-token, inspect-metadata }`; + `sign-challenge` fails the assertion. +- `spec.audience` must pass `^[A-Za-z0-9._:/@-]+$` charset and max 256 chars. +- `spec.consumerRef`, if set, must resolve a declared `Provider/` in the + same Zone. +- `contains_sensitive_shape` runs on all string fields; secrets in any field + fail the eval. +- Duplicate `(providerRef, executionRef, userRef, audience)` tuple in the same + Zone is rejected. +- `spec.providerRef` must resolve a Provider resource whose `spec.artifactId` + resolves an artifact catalog entry of `type = "provider"`. + +### 12.3 Build-time validation + +- Each Credential spec is validated against + `docs/reference/schemas/v3/credential.json`. +- The Provider-specific schema cross-check confirms `audience` charset and + `collectionAlias` format match the Provider's declared constraints. +- Drift gate (`make test-drift`): `cargo xtask gen-schemas` + `git diff + --exit-code`. A committed schema change not matching the crate-derived schema + fails the gate. +- Nix options module drift is checked in the same gate. +- Bundle and artifact catalog: the bundle digest round-trip test verifies the + sorted-resources digest matches the bundle header. The global artifact catalog + (`/etc/d2b/artifact-catalog.json`) round-trip test verifies the catalog digest + header and asserts that `storePath` values are absent from the resource bundle, + resource status, and log outputs. +- Process spec golden: the `controller_process_spec_golden` unit test in + `src/controller.rs` serializes the Process resource generated by `reconcile()` + and asserts exact field shapes: `template = "secret-service-controller"`; + `sandbox.namespaceClasses = [mount, pid, ipc]` (no `user` class; + `Provider/system-systemd` rejects it; same-UID execution via `spec.userRef`), + `sandbox.capabilityClasses = []`, `sandbox.seccompClass = "default-strict"`, + `sandbox.noNewPrivileges = true`, `sandbox.startRoot = false`, + `sandbox.readOnlyRoot = true`; `budget.memory.limit = "64Mi"`, + `budget.pids.limit = 32`, `budget.fds.limit = 64`; `networkUsage = null`; + owned `Endpoint` resource for the credential service; `readiness.class = "provider-defined"`. Any drift from this + shape fails the test and blocks the PR. + +### 12.4 Generation transition and cleanup contract + +When a Nix configuration generation removes a Credential resource that +referenced `Provider/credential-secret-service`: + +1. `activation-nixos` verifies the new resource bundle and artifact catalog + SHA-256 digests. +2. It creates/updates the new desired set without blocking on cleanup. +3. It issues async Delete for the removed resource + (`metadata.managedBy` is set exclusively by core, never by ownerRef, labels, + or bundle-authored fields; `deletionRequestedAt` set). +4. The Credential controller runs the `provider-revoke` finalizer (see §6.2): + `RevokeToken` is called; the controller emits a revoke outcome audit but MUST + NOT emit the resource-deleted closure audit. The scoped `Process` resource + (if one was created for this Credential) is deleted by the controller before + clearing the finalizer. The `provider-revoke` finalizer is then cleared. The + core store transaction writes the event-only `Deleted` revision and removes + the resource row and indexes atomically. The audit subsystem appends the + closure audit after that committed revision with dedup/exactly-once recovery. + No `phase=Deleted` row persists. Finalizers are never force-cleared. +5. Resources with `metadata.managedBy = "controller"` or `"api"` are never + touched by this path. +6. Prior bundles are retained up to `retainedGenerations` (default 3, + range 1..16). Rollback re-creates removed resources from a retained bundle; + fresh leases are acquired after re-creation (prior secrets are not restored). + +Removed resource status during cleanup: + +```yaml +phase: Degraded +conditions: + - type: Cleanup + status: "True" + reason: nix-generation-removed + message: "credential removed from nix configuration; pending provider-revoke finalizer" +``` + +--- + +## 13. Async reconcile loop + +All controller handlers are async. The reconcile loop follows +`ADR-046-resource-reconciliation`: + +- A dedicated watch task reads the Credential, Provider, Host, Guest, and User + watch streams concurrently. +- Per-resource reconcile tasks run independently; independent resources + reconcile in parallel within `reconcileConcurrency = 8`. +- A long-running `observe` or `finalize` task does not block the next ready + reconcile task. +- There is no fixed poll or debounce delay. Core delivers bounded hints + immediately after durable commit. +- Retry backoff is exponential with jitter; max 5 retries before `Fail`. +- `observeInterval = 30s` is a lightweight `InspectMetadata` poll; it does not + re-acquire leases or mutate the resource store on success. +- Idempotency keys prevent double-issuing on concurrent or redundant reconcile + triggers. +- The reconcile loop cancels outstanding tasks cleanly on controller drain. + +--- + +## 14. Current-code fit and reuse + +| Item | Treatment | +| --- | --- | +| Current anchor (v3 baseline `b5ddbed6`) | `d2b-realm-provider/src/provider.rs:CredentialProvider` (status-only trait, `implemented-and-reachable`); `credential.rs` (three-plane opaque refs, `implemented-and-reachable`); `packages/d2b-core/src/realm_workloads_launcher.rs:LauncherMetadataInvariants.no_secrets_or_credentials` (`implemented-and-reachable`) | +| Evidence class | Full lease model, controller, controller descriptor, d2b-bus routing, and async reconciliation are `ADR-only`; `Oo7SecretServicePort` trait, lease DTOs, and `FakeOo7Port` test suite are adapted from main | +| Main reuse source | main `a1cc0b2d`: `packages/d2b-provider-credential-secret-service/src/lib.rs` (`Oo7SecretServicePort`, `SecretServiceLeaseRequest/Ref/Grant/Inspection/Renewal/Revocation`, `SecretServiceState/LeaseState`, `SecretServiceProviderError`, `SecretServiceOwner`, `SecretServiceCredentialProvider`, `SecretServiceCredentialProviderFactory`); `src/tests.rs` (`FakeOo7Port`, `credential_canary`, `object_path_canary`, lease lifecycle, locked-state tests, cardinality limits) | +| Reuse action | copy and adapt (revert v2 types; replace v2 `CredentialProvider` trait with v3 `d2b.credential.v3` service; replace v2 ProviderFactory/registry with Provider resource/descriptor) | +| Behavior retained | Zero-secret-bytes invariant structurally enforced at port boundary; `SecretServiceOwner::Userd` placement restriction; bounded opaque lease metadata only crosses the port boundary; `credential_canary`/`object_path_canary` enforcement; hand-written `Debug` on request/ref types; injected-port pattern; `MAX_LOCAL_LEASES` cardinality cap | +| Required delta | v3 contract names/versions; Provider resource and signed controller descriptor; d2b-bus routing; Zone/Resource placement/scope; async reconcile loop and handler methods; `Noise_KK` delivery session; OTEL/audit emission; Nix resource compiler integration; workspace layout (`src/`, `tests/`, `integration/`, `README.md`); D087 status-first state model: `credential-secret-service` declares no Provider state Volume, ProviderStateSet is optional/query-time and empty, and bounded non-secret lease/acquisition/retry observation lives in `Credential.status` plus the core Operation ledger; no bootstrap state Volume mechanism (D086, superseded by D087); any status handle is non-secret, non-authorizing, bounded, safe for authorized status readers, and independently revalidated; `networkUsage: null` in Process spec; finalize sequence separates revoke/drain + Process deletion from closure audit (audit subsystem only); core aggregates Provider status (controller writes only scoped Credential/Process health) | +| Excluded main assumptions | v2 `EndpointRole`/`Realm`/`userd` process model; v2 `ProviderFactory`/`ProviderRegistryBuilder`; v2 component-session auth and prologue; v2 `AgentPlacementBinding`; v2 `CredentialLease`/`CredentialLeaseState` from `d2b-contracts::v2_provider`; v2 `CredentialPlacementBinding::UserAgent` struct (replaced by v3 `PlacementBinding::UserAgent` enum variant) | +| Replacement/deletion | Old `CredentialProvider` trait (`d2b-realm-provider/src/provider.rs`) and `CredentialStatus` enum removed only after all three v3 Credential Provider controllers reach full reconcile parity | +| Feasibility proof | Main `a1cc0b2d` proves: `Oo7SecretServicePort` trait API; `FakeOo7Port` with canary enforcement; acquire/refresh/revoke/inspect lifecycle; locked-state → unavailable mapping; cardinality limits; check_provider_conformance pattern | + +--- + +## 15. Implementation work items + +### ADR046-cred-ss-003 (primary) + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-cred-ss-003` | +| Dependency/owner | `ADR046-cred-ss-001` (contract types); `ADR046-cred-ss-002` (service proto); `ADR046-reconcile-001`; credential-secret-service owner | +| Current source | `packages/d2b-realm-provider/src/provider.rs:CredentialProvider` (minimal v3 baseline) | +| Reuse source | main `a1cc0b2d`: `packages/d2b-provider-credential-secret-service/src/lib.rs` (full implementation); `src/tests.rs` (full test suite including `FakeOo7Port`, lease lifecycle, locked state, canary enforcement, cardinality limits) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-credential-secret-service/src/{lib.rs, controller.rs, service.rs, main.rs}`; `packages/d2b-provider-credential-secret-service/tests/{lifecycle.rs, conformance.rs, faults.rs, canary.rs, delivery.rs, placement.rs}`; `packages/d2b-provider-credential-secret-service/integration/{container-service.sh, host-placement.nix, guest-placement.nix, cleanup-rollback.sh}`; `packages/d2b-provider-credential-secret-service/README.md` | +| Detailed design | Adapt `SecretServiceCredentialProvider` and `SecretServiceCredentialProviderFactory` to v3 `d2b.credential.v3` service; replace v2 `CredentialProvider` trait with v3 controller/service handler; retain `Oo7SecretServicePort` trait methods unchanged; ensure `SecretServiceOwner::Userd` placement guard rejects system-domain and guest-agent construction; validate `collectionAlias` against provider-internal charset (not `OpaqueAzureRef`; collection aliases may include spaces); integrate with Provider resource descriptor and controller toolkit; test that `credential_canary` never appears in any service response; create a Process resource per `(Zone, User, executionRef)` triple with `template = "secret-service-controller"` (plain string), canonical `sandbox` fields (`namespaceClasses`, `capabilityClasses`, `seccompClass`, `noNewPrivileges`, `startRoot`, `environmentClass`, `readOnlyRoot`), `budget` with nested `cpu`/`memory`/`pids`/`fds` sub-fields, `networkUsage: null`, no inline endpoint fields, an owned credential-service `Endpoint` resource, and `readiness.class = "provider-defined"`; component descriptor declares the required authenticated `dbus-session` FD attachment carried privately by the LaunchTicket; D087 status-first state model: no Provider state Volume is declared, ProviderStateSet is optional/query-time and empty, no Volume mount or layout principal is required, and the storage-need test is not met; bounded non-secret lease/acquisition/retry observation lives in `Credential.status` plus the core Operation ledger; any opaque status handle is non-secret, non-authorizing, bounded, safe for authorized status readers, and independently revalidated; no token/object-path/lease bytes persist anywhere; finalize() emits revoke outcome audit but MUST NOT emit the resource-deleted closure audit (audit subsystem only); controller writes only scoped Credential/Process health (core aggregates Provider status) Primary reuse disposition: `adapt`. Preserved source-plan detail: copy and adapt. | +| Integration | Target: user-domain `Process` resource under `Host` or `Guest` (ADR-only ResourceType); d2b-bus routes `d2b.credential.v3` calls to this process; Credential controller reconciles status. Current v3 has no user-credential host process: v3 `d2b-userd` is a guest exec stub (exits 78 in service mode; no credential functionality; `test-only-or-preview`). This integration path is fully new (ADR-only) work. | +| Data migration | Full reset; no migration from old `CredentialProvider` trait | +| Validation | See §16 | +| Removal proof | Old `d2b-realm-provider:CredentialProvider` trait removed only after all three v3 Credential Provider controllers reach full reconcile parity | + +### ADR046-cred-ss-001 (dependency: contract types) + +| Field | Value | +| --- | --- | +| Dependency/owner | Dependency for ADR046-cred-ss-003; owner: `packages/d2b-contracts` Credential ResourceType contract | +| Current source | `packages/d2b-realm-provider/src/provider.rs:CredentialProvider`; `packages/d2b-realm-provider/src/credential.rs` opaque credential refs and `OpaqueAzureRef` helpers | +| Reuse action | adapt | +| Destination | packages/d2b-contracts/src/v3/credential.rs | +| Detailed design | Contract types: define `CredentialSpec`, `CredentialStatus`, `CredentialLeaseHandle`, `OperationClass`, `PlacementBinding`, `CredentialConditionType`, and serde/validation/redaction helpers. Reuse `OpaqueAzureRef` from the v3 baseline. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt current credential/status concepts into v3 ResourceType DTOs; reuse `OpaqueAzureRef` directly where applicable. | +| Integration | Nix compiler emits these DTOs; ResourceAPI stores them; credential-secret-service controller/service consumes them; CLI and conformance tests validate base Credential spec/status behavior. | +| Data migration | Full d2b 3.0 reset; no v2 CredentialProvider status/config import | +| Validation | Credential ResourceType schema/serde/redaction tests from `ADR-046-resources-credential`; credential-secret-service conformance consumes the shared types | +| Removal proof | Old `d2b-realm-provider:CredentialProvider` trait and `CredentialStatus` enum are removed only after all three v3 Credential Provider controllers reach full reconcile parity | + +Defines `CredentialSpec`, `CredentialStatus`, `CredentialLeaseHandle`, +`OperationClass`, `PlacementBinding`, `CredentialConditionType`, and all +serde/validation/redaction helpers in +`packages/d2b-contracts/src/v3/credential.rs`. Reuses `OpaqueAzureRef` +directly from v3 `d2b-realm-provider/src/credential.rs`. Full detail in +`ADR-046-resources-credential` §Implementation work items. + +### ADR046-cred-ss-002 (dependency: service proto) + +| Field | Value | +| --- | --- | +| Dependency/owner | Dependency for ADR046-cred-ss-003; owner: credential service contract/codegen | +| Current source | None — net-new v3 `d2b.credential.v3` service; no pre-ADR45 baseline service proto equivalent | +| Reuse action | create | +| Destination | packages/d2b-contracts/proto/v3/credential.proto; packages/d2b-credential-service/ | +| Detailed design | Service proto: define the `d2b.credential.v3` protobuf/ttrpc service and generate typed client/server code. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `create`. Preserved source-plan detail: net-new service contract replacing the v2 in-process `CredentialProvider` trait. | +| Integration | d2b-bus routes Credential service calls to credential-secret-service Process instances; generated client/server types bind the controller/service implementation to ComponentSession delivery. | +| Data migration | Full d2b 3.0 reset; no v2 service state import | +| Validation | Generated-code compile tests and credential service contract tests from `ADR-046-resources-credential`; credential-secret-service lifecycle/delivery tests consume the generated service | +| Removal proof | V2 `CredentialProvider` trait calls are superseded by `d2b.credential.v3` only after all credential providers reach parity | + +Defines `d2b.credential.v3` protobuf/ttrpc service in +`packages/d2b-contracts/proto/v3/credential.proto` and generates typed +client/server in `packages/d2b-credential-service/`. Full detail in +`ADR-046-resources-credential` §Implementation work items. + +### ADR046-cred-ss-004 (dependency: controller toolkit) + +| Field | Value | +| --- | --- | +| Dependency/owner | Dependency for ADR046-cred-ss-003; owner: common Credential controller/reconciliation toolkit | +| Current source | ADR-only controller pattern from `ADR-046-resource-reconciliation`; no concrete secret-service baseline controller to import | +| Reuse action | create | +| Destination | packages/d2b-provider-credential-/src/controller.rs | +| Detailed design | Controller toolkit: implement the common Credential controller handler conforming to the `ADR-046-resource-reconciliation` async loop. Secret-service-specific controller code plugs into this handler while keeping provider bytes out of status/store/audit. Primary reuse disposition: `create`. Preserved source-plan detail: net-new shared controller handler pattern specialized by each Credential Provider. | +| Integration | Resource watches and Operation ledger drive the controller loop; credential-secret-service handler uses the toolkit to reconcile Credential status, finalizers, Process health, and service lifecycle. | +| Data migration | None — controller toolkit code only; no runtime state migration | +| Validation | Shared reconciliation tests from `ADR-046-resources-credential`; credential-secret-service lifecycle/fault tests verify the handler integration | +| Removal proof | None — shared toolkit is additive; v2 trait removal is tracked by ADR046-cred-ss-003/001 parity | + +Implements the common Credential controller handler conforming to the +`ADR-046-resource-reconciliation` async loop in +`packages/d2b-provider-credential-/src/controller.rs`. Full detail in +`ADR-046-resources-credential` §Implementation work items. + +### ADR046-cred-ss-005 (dependency: Nix compiler) + +| Field | Value | +| --- | --- | +| Dependency/owner | Dependency for ADR046-cred-ss-003; owner: Nix resource compiler and activation cleanup | +| Current source | None — net-new v3 `d2b.zones..resources.` Credential/Provider authoring surface; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | nixos-modules/options-resources.nix; nixos-modules/activation-nixos-cleanup.nix | +| Detailed design | Nix compiler: implement `d2b.zones..resources.` authoring, eval-time assertions, canonical JSON emission, artifact catalog, bundle digest, and generation cleanup contract. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `create`. Preserved source-plan detail: net-new Nix resource emission and cleanup contract. | +| Integration | Nix emits Provider/Credential resource JSON and artifact catalog entries; ResourceAPI admission and credential-secret-service controller consume the rendered resources; activation cleanup issues async Delete/finalizer flow on generation removal. | +| Data migration | Full d2b 3.0 reset; no old credential config is imported into v3 resources | +| Validation | Nix eval/assertion/golden tests from `ADR-046-resources-credential`; credential-secret-service cleanup rollback integration fixture | +| Removal proof | None — new v3 Nix resource surface; old trait removal waits for controller parity | + +Implements `d2b.zones..resources.` Nix authoring, eval-time +assertions, canonical JSON emission, artifact catalog, bundle digest, and +generation cleanup contract in `nixos-modules/options-resources.nix` and +`nixos-modules/activation-nixos-cleanup.nix`. Full detail in +`ADR-046-resources-credential` §Implementation work items. + +### ADR046-cred-ss-006 (dependency: audit/OTEL) + +| Field | Value | +| --- | --- | +| Dependency/owner | Dependency for ADR046-cred-ss-003; owner: credential-secret-service audit and telemetry implementation | +| Current source | `packages/d2b-core/src/realm_workloads_launcher.rs:LauncherMetadataInvariants.no_secrets_or_credentials`; secret-service main reuse canary tests listed in §14 | +| Reuse action | adapt | +| Destination | packages/d2b-provider-credential-secret-service/src/{audit.rs,telemetry.rs} | +| Detailed design | Audit/OTEL: emit authorized bounded audit records with Credential identity represented only by `resource_name_digest`, and emit OTEL spans/metrics for all credential service methods and controller events with canary enforcement, expiry aggregated across user-agent leases, no Credential resource name, ResourceRef, UID, digest, derived identity token, Zone/Credential/resource-name-derived metric label, or non-allowlisted OTEL Resource attribute; retain applicable generic collector-allowlisted Resource attributes (`d2b.zone`, `d2b.provider`, `d2b.component`, and service fields); no token/object-path/lease bytes in status, delivery outer headers, audit, metrics, spans, or logs. Full detail remains in `ADR-046-resources-credential` §Implementation work items. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt zero-secret invariant and canary test pattern to credential-secret-service audit/OTEL surfaces. | +| Integration | Controller and service methods call audit/telemetry helpers; audit subsystem and OTEL exporters consume bounded event/span/metric records; canary tests verify every public observable surface stays secret-free. | +| Data migration | None — audit/telemetry only; no runtime state migration | +| Validation | Credential audit/OTEL tests from `ADR-046-resources-credential` require `resource_name_digest` in authorized audit records and reject raw Credential name/ResourceRef/UID; `tests/canary.rs` structurally asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `credential_ref`, `credential_uid`, `credential_digest`, `resource_name_digest`, and every resource-name-derived metric key; Credential name/ref/UID/digest canaries are absent from all OTEL Resource attributes, span attributes, and metric labels; Zone-name canaries are absent from spans and labels while generic collector-allowlisted Resource attributes remain; complete secret-service metric/span frames pass the shared collector ingress validator, while adding `d2b.credential.name` or any Credential identity key/value rejects the whole frame; `tests/delivery.rs` covers credential-secret-service delivery | +| Removal proof | None — audit/telemetry helpers are new; no prior owner to remove | + +Implements audit record and OTEL span/metric emission for all credential +service methods and controller events in +`packages/d2b-provider-credential-secret-service/src/{audit.rs, telemetry.rs}`. +Full detail in `ADR-046-resources-credential` §Implementation work items. + +--- + +## 16. Tests + +### 16.1 `src/` unit tests (`#[cfg(test)]` in `src/lib.rs`, `src/controller.rs`) + +| Test | Validates | +| --- | --- | +| `Oo7SecretServicePort` trait API surface | All five port methods are async and take/return only non-secret types | +| `SecretServiceOwner` placement guard | Construction with non-Userd owner returns `SecretServiceProviderError::InvalidConsumer` | +| `collectionAlias` charset | Accepts valid aliases (with spaces); rejects empty string and control chars | +| `lockPolicy` state transitions | `fail-closed` returns error; `fail-degraded` sets Degraded status | +| `SecretServiceLeaseRequest` Debug | Emits only generation/placement/operation_count/expiry; no provider_id or operation field | +| `SecretServiceLeaseRef` Debug | Same redacted shape as request | +| `controller_process_spec_golden` | `reconcile()` generates a Process resource with `template = "secret-service-controller"`, `sandbox.namespaceClasses = [mount, pid, ipc]` (no `user` class; `Provider/system-systemd` rejects it; same-UID execution via `spec.userRef`), `sandbox.capabilityClasses = []`, `sandbox.seccompClass = "default-strict"`, `sandbox.noNewPrivileges = true`, `sandbox.startRoot = false`, `sandbox.readOnlyRoot = true`, `budget.memory.limit = "64Mi"`, `budget.pids.limit = 32`, `budget.fds.limit = 64`, `networkUsage = null`, no inline endpoint fields, an owned credential-service `Endpoint` resource, `readiness.class = "provider-defined"` | +| `finalize_does_not_emit_closure_audit` | `finalize()` emits a revoke outcome audit record but does not produce a `resource-deleted` closure event; verified by asserting no `Cleanup complete` record is captured by the test audit subscriber (closure record is audit-subsystem-only) | +| `status_first_state_golden` | Provider descriptor declares `resourceTypes: [Credential]` only and no Provider state Volume; ProviderStateSet query returns an empty grouping; Process specs contain no state mount; bounded non-secret lease/acquisition/retry fields are written only to `Credential.status` and the core Operation ledger; status rejects oversize/provider-detail overrun and excludes token bytes, keys, PSKs, Secret Service object paths, private paths, raw provider error bodies, and authority-conferring handles | + +### 16.2 `tests/` Cargo integration tests (`cargo test -p d2b-provider-credential-secret-service`) + +#### `tests/lifecycle.rs` + +| Test | Validates | +| --- | --- | +| `acquire_token_unlocked` | `FakeOo7Port` unlocked; `AcquireToken` returns leaseHandle, sourceVersion, rotationGeneration=1, expiresAtUnixMs; `leaseState=Active`; `CredentialReady=True` | +| `refresh_token_extends_expiry` | After acquire; `RefreshToken` returns new expiresAtUnixMs; rotationGeneration unchanged; `lastRefreshedAt` updated | +| `revoke_token_idempotent` | `RevokeToken` returns `Revoked`; second call returns `AlreadyRevoked`; `leaseState=Revoked`; `CredentialReady=False` | +| `inspect_metadata_reflects_state` | `InspectMetadata` returns current leaseState, sourceVersion, rotationGeneration, expiresAtUnixMs | +| `proactive_rotation_success` | `rotation.policy=proactive`; `observe` sets `RotationDue=True` at window; `reconcile` issues new lease; `rotationGeneration` increments; old lease valid until new confirmed | +| `on_demand_no_auto_rotation` | `rotation.policy=on-demand`; controller does not auto-rotate; `RotationDue` never set | +| `on_expiry_rotation` | `rotation.policy=on-expiry`; controller acquires new lease after `leaseState=Expired`; `rotationGeneration` increments | +| `idempotency_key_no_double_issue` | Duplicate `issue_lease` with same idempotency key returns same grant; `issue_calls` count does not increment on duplicate | +| `revocation_on_provider_generation_immediate` | `revocation.onProviderGeneration=immediate`; `drain` handler calls `revoke_lease`; `leaseState=Revoked` | +| `revocation_on_provider_generation_drain` | `revocation.onProviderGeneration=drain-leases`; drain handler does not call `revoke_lease`; lease expires by deadline | +| `process_resource_created_with_correct_spec` | After first `reconcile()`, the owned Process resource exists with `template = "secret-service-controller"`, `sandbox.namespaceClasses = [mount, pid, ipc]` (no `user` class), `sandbox.capabilityClasses = []`, `sandbox.seccompClass = "default-strict"`, `sandbox.startRoot = false`, `sandbox.readOnlyRoot = true`, `budget.cpu` and `budget.memory` nested sub-fields present, `budget.pids.limit = 32`, `budget.fds.limit = 64`, `networkUsage = null`, no inline endpoint fields, an owned credential-service `Endpoint` resource, and `readiness.class = "provider-defined"` | + +#### `tests/conformance.rs` + +All `check_provider_conformance` arms pass for `d2b-provider-credential-secret-service`. + +#### `tests/faults.rs` + +| Test | Validates | +| --- | --- | +| `locked_state_fail_closed` | `FakeOo7Port.state = Locked`; `lockPolicy=fail-closed`; `AcquireToken` returns `credential-provider-unavailable`; status `ProviderUnavailable=True`, `leaseState=Unknown` | +| `locked_state_fail_degraded` | `lockPolicy=fail-degraded`; `phase=Degraded`; `ProviderUnavailable=True`; does not return error to caller | +| `unavailable_port` | `FakeOo7Port::issue_lease` returns `SecretServicePortError::Unavailable`; `credential-provider-unavailable` returned; bounded retry | +| `completion_unknown_no_auto_retry` | `FakeOo7Port::issue_lease` returns `CompletionUnknown`; provider does NOT retry with same record; consumer must re-initiate | +| `cardinality_limit` | `maxLeases=2`; third `AcquireToken` returns `credential-queue-pressure` | +| `lease_expired_reacquire` | `leaseState=Expired`; `AcquireToken` opens new lease; `rotationGeneration` increments | +| `rotation_failure_exhaustion` | Proactive rotation fails `maxRetries` times; `phase=Failed`, `outcome=rotation-failed` | +| `sign_challenge_schema_invalid` | `sign-challenge` operation class returns `credential-schema-invalid` immediately | + +#### `tests/canary.rs` + +| Test | Validates | +| --- | --- | +| `canary_absent_acquire_response` | `credential_canary` absent from `AcquireTokenResponse` outer DTO | +| `canary_absent_refresh_response` | `credential_canary` absent from `RefreshTokenResponse` outer DTO | +| `canary_absent_revoke_response` | `credential_canary` absent from `RevokeTokenResponse` | +| `canary_absent_inspect_response` | `credential_canary` absent from `InspectMetadataResponse` | +| `canary_absent_status_json` | `credential_canary` absent from status JSON serialization | +| `object_path_absent_all_responses` | `object_path_canary` absent from all response DTOs | +| `canary_absent_audit_records` | `credential_canary` and `object_path_canary` absent from all audit record JSON | +| `canary_absent_telemetry_attributes` | Neither secret canary nor any Credential name/ref/UID/digest canary is present in OTEL Resource or span attributes; generic collector-allowlisted Resource attributes remain | +| `metric_identity_labels_absent` | No descriptor key is `vm`, `zone`, `zone_id`, `zone_uid`, `credential_name`, `credential_ref`, `credential_uid`, `credential_digest`, `resource_name_digest`, or resource-name-derived; Credential name/ref/UID/digest and Zone-name canaries are absent from label values; Zone-name canaries are also absent from span attributes | +| `collector_allowlist_frame_accepted` | Complete metric/span frames with only generic allowlisted Resource attributes are accepted; injecting `d2b.credential.name` or any Credential name/ref/UID/digest key or value rejects the whole frame | +| `canary_absent_delivery_binding` | Neither canary present in delivery session binding parameters | + +#### `tests/delivery.rs` + +| Test | Validates | +| --- | --- | +| `delivery_session_binding_fields` | All binding contract fields (§8.2) present and correct for a successful `AcquireToken` | +| `delivery_zeroizing_buffer` | Sensitive output buffer is a zeroizing type; plaintext zeroed after extraction | +| `delivery_replay_safe_sequence` | A replayed delivery record at the same sequence number is rejected | +| `delivery_enrolled_keys_only` | NN-profile delivery attempt rejected immediately | +| `delivery_max_token_bytes_enforced` | Record exceeding `maxTokenBytes` causes immediate rejection and channel close | +| `delivery_single_use_channel` | Delivery channel closed and zeroized after confirmation; not reused | +| `delivery_no_auto_retry_on_ambiguous` | `CompletionUnknown` during delivery does not trigger automatic retry | + +#### `tests/placement.rs` + +| Test | Validates | +| --- | --- | +| `system_domain_rejected` | Construction with `domainFilter=system` returns `SecretServiceProviderError::InvalidConsumer` | +| `guest_agent_binding_rejected` | Construction with `placementBinding=guest-agent` (system-domain on Guest) returns `SecretServiceProviderError::InvalidConsumer` | +| `host_system_binding_rejected` | Construction with `placementBinding=host-system` returns `SecretServiceProviderError::InvalidConsumer` | +| `user_agent_on_host_accepted` | Construction with `domainFilter=user`, `executionRef=Host/`, and `placementBinding=user-agent` succeeds | +| `user_agent_on_guest_accepted` | Construction with `domainFilter=user`, `executionRef=Guest/`, and `placementBinding=user-agent` succeeds | +| `not_colocated_rejected` | Consumer and provider on different users returns `SecretServiceProviderError::NotColocated` | + +### 16.3 `integration/` fixtures (invoked by `make test-integration` / `make test-host-integration`) + +Files in `integration/` are shell scripts, Nix expressions, or container specs. +They are NOT run by `cargo test`. + +#### `integration/container-service.sh` + +Starts a container-backed `credential-secret-service` Provider process with a +`FakeOo7Port`-equivalent canary D-Bus stub, exercises the full d2b-bus routing +path for `AcquireToken`/`RefreshToken`/`RevokeToken`/`InspectMetadata`, and +verifies that canary values do not appear in any captured bus message or log. + +#### `integration/host-placement.nix` + +`runNixOSTest` scenario for Host execution context: +- Declares `Provider/credential-secret-service`, `Host/host-system`, + `User/alice`, and `Credential/local-keyring` with + `scope.executionRef=Host/host-system` in the Zone resource bundle. +- Verifies the controller process is launched as `User/alice` in the user + domain of `Host/host-system`. +- Verifies `leaseState=Active` and `CredentialReady=True` after acquisition. +- Verifies that `d2b zone inspect dev Credential/local-keyring` shows no secret + bytes in status output. + +#### `integration/guest-placement.nix` + +`runNixOSTest` scenario for Guest execution context: +- Declares `Provider/credential-secret-service`, `Guest/work-vm`, + `User/alice`, and `Credential/guest-keyring` with + `scope.executionRef=Guest/work-vm` in the Zone resource bundle. +- Verifies the controller process is launched as `User/alice` in the user + domain of `Guest/work-vm`. +- Verifies `leaseState=Active` and `CredentialReady=True` after acquisition. +- Verifies that `d2b zone inspect dev Credential/guest-keyring` shows no secret + bytes in status output. + +#### `integration/cleanup-rollback.sh` + +1. NixOS generation N declares `Credential/local-keyring`. +2. Generation N+1 removes it. +3. Verifies the resource row is atomically removed from the store after an + event-only `Deleted` revision is written (no persisted `phase=Deleted` row + remains in the store). +4. Verifies activation of generation N+1 does not block on cleanup finalizer. +5. Verifies rollback to generation N re-creates `local-keyring` from the + retained bundle; a fresh lease is acquired. + +#### `integration/README.md` + +An optional `integration/README.md` may document fixture descriptions and +invocation instructions (`make test-integration`, `make test-host-integration`), +canary enforcement, and fake Secret Service usage. It is not separately mandated +by the workspace policy gate. The policy gate enforces only the four root items: +`src/`, `tests/`, `integration/`, and `README.md` at the crate root. + +### Fast hermetic execution and test placement (D094) + +Per D094 and `ADR-046-validation-and-delivery` §10.16, this Provider's `src/` +unit tests and `tests/*.rs` hermetic suite are fast, in-process, deterministic, +and parallel-safe: an individual normal test has p95 ≤50 ms with no wall-clock +sleep, and `cargo test -p d2b-provider-credential-secret-service --lib --tests` +completes in ≤2 s warm-cache execution time (compilation excluded). They use a +deterministic fake clock/RNG and the toolkit fakes/FakeEffectPort only — no +process spawn, container, network, DBus, systemd, broker daemon, Nix eval/build, +KVM, USB/GPU/TPM hardware, or live cloud, and no filesystem tree beyond tiny +temp fixtures. Any scenario needing those lives only in `integration/`, which +keeps a lane timeout/budget, parallel isolation, and fake external services by +default; such a need is re-placed into `integration/`, never given a sleep, +larger timeout, or `#[ignore]`. Bounded crypto/property tests are the only +classified exception, each named with a capped case count and a declared higher +per-test budget. + +--- + +## 17. `README.md` required sections + +The `packages/d2b-provider-credential-secret-service/README.md` MUST contain +these sections in order: + +### Section 1: Provider identity + +`providerRef = Provider/credential-secret-service`; implements `Credential`; +`user-agent` only; one controller binary; cardinality; Zone placement +constraints; versioning policy. + +### Section 2: Config schema + +`spec.config` fields (`collectionAlias`, `maxLeases`, `lockPolicy`), types, +defaults, constraints, and the worked Nix example from §3 above. + +### Section 3: ResourceTypes managed + +`Credential` lifecycle phases; status conditions owned (`CredentialReady`, +`RotationDue`, `ProviderUnavailable`, `LeaseRevoked`); finalizers owned +(`credential.d2bus.org/provider-revoke`). + +### Section 4: Controllers, services, workers, and binaries + +`secret-service-controller`: binary `d2b-provider-credential-secret-service`, +controller type, user domain, Host or Guest placement, cardinality +(`(Zone, User, executionRef)` triple). + +### Section 5: Placement + +Supported: `user-agent` on Host or Guest (when `domainFilter=user`). Rejected: +`host-system` and `guest-agent` (system-domain bindings). Error codes for each +rejected binding. + +### Section 6: Dependencies and RBAC + +Required Zone resources: `Host/` or `Guest/` (executionRef), +`User/` (userRef), optional `Provider/` (consumerRef); RBAC verbs +consumed; consumer Provider requirements; cross-resource ordering (Host or Guest +and User must be Ready before controller acquires first lease). + +### Section 7: Security, state, and telemetry + +Secret isolation model; D087 status-first state model; no Provider state Volume +for this credential Provider; ProviderStateSet is optional/query-time and empty; +bounded non-secret status fields (opaque non-authorizing lease handle, source +version, rotation generation, expiry timestamps, retry/outcome enums) plus core +Operation ledger state; no token bytes, Secret Service object paths, or private +runtime details in status, audit, telemetry, or storage; audit events; OTEL spans +and metrics; canary enforcement (`credential_canary`, `object_path_canary`). + +### Section 8: Build, test, and integration commands + +```bash +# Unit tests (src/ inline) +cargo test -p d2b-provider-credential-secret-service + +# Hermetic Cargo integration tests (tests/) +cargo test -p d2b-provider-credential-secret-service --tests + +# Container integration fixtures +make test-integration # see integration/README.md + +# NixOS runNixOSTest placement and cleanup fixtures +make test-host-integration # see integration/README.md +``` + +### Section 9: Standalone-repo usage *(required before first release)* + +How to consume outside the monorepo; flake input pattern; `inputs.d2b.inputs.nixpkgs.follows` +boilerplate; compatibility constraints; minimum toolkit version. + +--- + +## 18. Removal contract + +The following current symbols are removed only after the stated live successor +conditions are met: + +| Symbol to remove | Location | Successor condition | +| --- | --- | --- | +| `CredentialProvider` trait | `d2b-realm-provider/src/provider.rs` | All three v3 Credential Provider controllers (`credential-secret-service`, `credential-entra`, `credential-managed-identity`) have tested replacement controllers consuming `d2b.credential.v3` service | +| `CredentialStatus` enum | `d2b-realm-provider/src/provider.rs` | Same as above | +| v2 `CredentialLease` / `CredentialLeaseState` | `d2b-contracts/src/v2_provider.rs` | All v3 callers migrate to `d2b-contracts/src/v3/credential.rs` | +| `d2b-provider-aca:managed_identity_client_id` raw field | `d2b-provider-aca/src/lib.rs` | `credential-managed-identity` Provider controller integrated; ACA Provider config uses `credentialRef` | +| v2 `CredentialProviderService` proto | `d2b-contracts/proto/v2/provider_credential.proto` | All v3 callers migrate to `d2b.credential.v3` | + +Per D094, each replaced current-code test is retired with an explicit +keep/adapt/move/delete disposition and a removal gate: the minimum reusable +semantic assertions migrate into this crate's hermetic `tests/`, and the old +duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest +entries are deleted once successor coverage and the removal proof pass — +updating `tests/layer1-jobs.json`, the closed gate manifests, the +flake/matrix/Nix-unit pins, the generated ledgers, and the CI workflow shards. +Old and new suites never run in parallel indefinitely. diff --git a/docs/specs/providers/ADR-046-provider-device-gpu.md b/docs/specs/providers/ADR-046-provider-device-gpu.md new file mode 100644 index 000000000..0ad6c481d --- /dev/null +++ b/docs/specs/providers/ADR-046-provider-device-gpu.md @@ -0,0 +1,1788 @@ +# ADR 0046 Provider dossier: device-gpu + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-device-gpu` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 8 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-provider-device-gpu` crate, GPU/video controller contracts, Nix graphics/video emitters | +| Depends on | `ADR-046-resources-device`, `ADR-046-resource-object-model`, `ADR-046-primitive-resource-composition`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-provider-model-and-packaging`, `ADR-046-components-processes-and-sandbox`, `ADR-046-telemetry-audit-and-support` | +| Supersedes | `ProcessRole::Gpu`, `ProcessRole::GpuRenderNode`, `ProcessRole::Video` in `packages/d2b-core/src/processes.rs`; Nix `nixos-modules/components/graphics.nix`; Nix `nixos-modules/components/video/guest.nix`; `d2b.vms..graphics.*` options | + +## Purpose + +`Provider/device-gpu` is the combined GPU-acceleration and hardware-video-decode +Provider for d2b v3. It is a single crate (`packages/d2b-provider-device-gpu/`) +that manages: + +- **Full virtio-gpu**: exclusive physical DRM device claim, crosvm `device gpu` + sidecar, cross-domain Wayland channel, Vulkan/EGL/virglrenderer acceleration. +- **Render-node-only**: exclusive or explicitly shared DRM render-node claim, + user-namespace crosvm GPU worker, no full virtio-gpu bind-mount. +- **Hardware video decode**: `crosvm device video-decoder --backend vaapi` sidecar + (vhost-user-media, virtio-media, `virtio_id=48`), optionally with NVIDIA + device passthrough. Always separate from the GPU worker Process; always + depends on a full-GPU Device claim (`renderNodeOnly=false`). + +The Provider owns: GPU/video worker Process lifecycle and sequencing; physical +DRM device probe and hotplug observation; exclusive/shared arbitration +enforcement; broker operation orchestration; and Nix option migration from +`d2b.vms..graphics.*` to `d2b.zones..resources.*`. + +VFIO and SR-IOV passthrough are **not** part of the standard GPU device claim +and are reserved for a future Provider. + +## Identity + +```text +Provider/device-gpu +``` + +Crate: `packages/d2b-provider-device-gpu/` + +### Crate layout + +```text +packages/d2b-provider-device-gpu/ + src/ + lib.rs Controller entry point; exports controller/worker binaries + controller.rs Async reconcile loop; ResourceClient; spec/status owner + probe.rs GpuEffectPort::probe_drm_device call; hotplug observe scheduler; three-strike counter + arbitration.rs Exclusive vs shared claim arbitration; conflict detection + worker_gpu.rs Full GPU and render-node Process creation/teardown + worker_video.rs Video-decoder Process creation/teardown; wire-contract check + argv.rs Thin re-export of d2b-host gpu_argv / video_argv generators + broker.rs Device claim registration; GpuEffectPort claim state (no execution authority; claim authority is Device resource status + core Operation ledger) + status.rs Status writer; condition builder; phase state machine + audit.rs Path-free audit record emitter + error.rs Typed error enum; stable closed-set error slugs + tests/ + combined_reconcile.rs GPU+video combined state machine; fake broker/supervisor + render_node_enforcement.rs shared+renderNodeOnly=false spec rejected at controller + wire_constant_snapshot.rs Byte-stable wire-contract constants vs video_argv.rs + conformance.rs Spec/settings serde round-trip; ResourceTypeSchema; deviceUsage/budget/endpoints/readiness field validation + arbitration_conflict.rs Exclusive claim from second Guest rejected + video_dependency.rs Video Process not started until GPU Process is Ready + seccomp_policy_ref.rs GPU/video/render-node seccomp policy ref names stable + integration/ + gpu_worker_start/ GPU worker Process obtains broker tokens, reaches Ready + render_node_shared/ Two Guests share same render-node Device simultaneously + video_dependency/ Video Process starts only after GPU Process is Ready + README.md How to invoke integration fixtures; hardware test note + README.md +``` + +Workspace policy rejects the crate if `src/`, `tests/`, `integration/`, or +`README.md` is missing (these four paths only). The layout must allow moving +the crate to its own GitHub repository without splitting semantics or copying +daemon internals. + +### Controller Process (static, created by core ProviderDeployment) + +When the framework processes a `Provider` resource with +`spec.config.controllerExecutionRef`, core `ProviderDeployment` creates one +static controller Process resource. The device-gpu controller **does not** +create or own this Process; core creates and manages it, aggregates its status +into the Provider status, and deletes it when the Provider is deleted. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: device-gpu-controller + zone: dev + ownerRef: Provider/device-gpu + managedBy: core # created by core ProviderDeployment; not by the controller itself +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system # resolved from Provider.spec.config.controllerExecutionRef + domain: system + processClass: controller + template: device-gpu-controller + sandbox: + namespaceClasses: [mount, pid, ipc, uts, cgroup] + capabilityClasses: [] + seccompClass: w1-provider-controller + startRoot: false + userNamespace: null + budget: + cpu: + request: "50m" + limit: "500m" + memory: + request: "32Mi" + limit: "256Mi" + pids: + limit: 64 + fds: + limit: 32 + networkUsage: null + deviceUsage: [] + mounts: [] + readiness: + class: provider-defined + initialDelay: "1s" + timeout: "10s" + failureThreshold: 3 + successThreshold: 1 + restartPolicy: + class: on-failure + backoffBase: "5s" + backoffMax: "5m" + backoffMultiplier: 2 + maxRestarts: 10 + resetAfter: "1h" +``` + +Core aggregates the controller Process status (phase, conditions) into the +`Provider/device-gpu` status. Per D087, `device-gpu` declares no Provider state +Volume; bounded non-secret controller operational state lives in Device/Provider +status and the core Operation ledger, and the controller has no `/state` mount. + +## Device spec served by this Provider + +### Canonical ResourceSpec + +Normative D089 spec layering: Device base fields are ResourceType base +`spec.*` fields, including `spec.providerRef`, `deviceClass`, +`inventory.selector`, attachments, and arbitration. This Provider's +desired-only extension is the canonical `spec.provider = { schemaId: +"device-gpu.d2bus.org/Device/spec", schemaVersion, settings }` envelope; it is +manifest-registered/signed, strict deny-unknown, bounded, versioned and +digested, +validated against `spec.providerRef` at Nix build and API admission, +implementation-only, and may not shadow base fields. Shared fields are promoted +to the Device base. The Provider implements the exact base Device spec/status +version/fingerprint, accepts the canonical minimal valid base Spec, and rejects +unsupported optional base capabilities only through its signed capability matrix +and provider-neutral `unsupported-capability`. `spec.provider` aligns with +`status.provider`; generic CLI/controllers operate on the base spec and base +status only. No secret bytes are allowed in any spec layer, and no +credential material is allowed in `spec.provider.settings`. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Device +metadata: + name: corp-vm-gpu + zone: dev + uid: + generation: 1 + revision: + ownerRef: Guest/corp-vm + finalizers: [device-gpu.d2bus.org/worker-stopped] + deletionRequestedAt: null + createdAt: 2026-07-22T00:00:00Z + updatedAt: 2026-07-22T00:00:00Z +spec: + providerRef: Provider/device-gpu + deviceClass: physical + arbitration: exclusive + maxConcurrentClaims: 1 + inventory: + selector: + busClass: drm + label: host-gpu + pciSlot: null + provider: + schemaId: "device-gpu.d2bus.org/Device/spec" + schemaVersion: "1.0.0" + settings: + renderNodeOnly: false + videoSidecar: false + videoNvidiaDecode: false + contextTypes: [cross-domain, virgl, virgl2] + displays: [{hidden: true}] + egl: true + vulkan: true + crossDomainTrusted: false + virglVideo: false +status: + observedGeneration: 1 + phase: Ready + conditions: [] + lastReconciledAt: 2026-07-22T00:00:01Z + device: + present: true + health: healthy + holderRefs: [Guest/corp-vm] + claims: + - holderRef: Guest/corp-vm + claim: exclusive + passthrough: gpu-virtio + claimedAt: 2026-07-22T00:00:01Z + health: healthy + provisionedAt: null + lastProbedAt: 2026-07-22T00:00:00Z + providerDiagnostic: null +``` + +## Root config schema + +| Field | Type | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | +| `renderNodeOnly` | bool | `false` | — | If true, render-node-only mode; no full virtio-gpu bind-mount. Shared arbitration is permitted only when this is `true`. | +| `videoSidecar` | bool | `false` | — | If true, spawn a crosvm video-decoder Process alongside the GPU worker. Requires `renderNodeOnly=false`. | +| `videoNvidiaDecode` | bool | `false` | — | If true, include `nvidia-ctl`, `nvidia-device`, `nvidia-uvm` allowlist tokens for the video worker in addition to `dri`. No effect when `videoSidecar=false`. | +| `contextTypes` | list\ | `[cross-domain, virgl, virgl2]` | closed set | GPU context types: `virgl`, `virgl2`, `cross-domain`. Order is preserved and emitted in sorted order in canonical JSON. | +| `displays` | list\ | `[{hidden: true}]` | 0–8 entries | Virtual display config. Each entry: `{hidden: bool}`. | +| `egl` | bool | `true` | — | EGL rendering via virglrenderer. | +| `vulkan` | bool | `true` | — | Vulkan rendering via venus. | +| `crossDomainTrusted` | bool | `false` | — | Enable the `cross-domain` virtio-gpu context type for cross-domain Wayland forwarding. Default `false`; set `true` only for VMs where Wayland forwarding is the primary use case (e.g., a Wayland-forwarding launchpad VM). Must be `false` for VMs running Docker or container workloads. | +| `virglVideo` | bool | `false` | — | Experimental virglrenderer/rutabaga video forwarding (separate from `videoSidecar`). Requires a crosvm build with `use_video=true` patch. Mutually exclusive with `videoSidecar=true` on the same Guest. | + +The crosvm binary path and the crosvm video-decoder binary path are resolved +from the signed `d2b-provider-device-gpu` package closure. They are not +configurable fields in the Device spec. + +## Discovery and probe + +### DRM device probe via GpuEffectPort + +The device-gpu controller probes physical GPU presence through its injected +opaque `GpuEffectPort`. The controller does **not** directly read `/dev/` or +`/sys/` paths; the effect port handles device inventory and allowlist fd opens +on the controller's behalf. + +The `inventory.selector` for GPU devices always uses `busClass: drm`. + +**Probe sequence on each observe trigger:** + +1. Controller calls `GpuEffectPort::probe_drm_device(selector)`. The effect port + matches the `label` (and optional `pciSlot`) from the selector against the + trusted device table and returns a presence/health result. +2. A DRM card node present and accessible: `DevicePresent=True`, `health=healthy`. +3. If the DRM card node is absent at probe time: begin consecutive-failure + counting (see Physical probe failure semantics below). +4. Render-node availability is checked in the same probe call; the effect port + includes it in the returned result. + +**Observe interval:** default 30 s; maximum 60 s. Configurable in the Provider +root config via `observeIntervalSecs: uint (10–60)`. + +### Physical probe failure semantics + +The standard three-strike semantics from `ADR-046-resources-device` apply +unchanged: + +| Consecutive probe failures | Status transition | +| --- | --- | +| 1 (first) | phase → `Unknown`; `DevicePresent` status=`Unknown` | +| 2 | `DevicePresent` remains `Unknown`; phase remains `Unknown` | +| 3 | phase → `Degraded`; `DevicePresent` status=`False`, reason=`device-consecutive-probe-failures-exceeded` | +| Device returns | phase → `Ready`; `DevicePresent` status=`True` | + +A single probe failure does not set `DevicePresent=False` or stop the GPU or +video workers. After three consecutive failures, all claimant Guest controllers +receive a `dependency-changed` trigger through the normal resource watch path. +The GPU and video worker Processes transition per the `owned-resource-changed` +reconcile trigger handling; the Guest controller may stop or degrade the Guest. + +When the device returns, the Device controller sets phase `Ready` and +re-triggers all claimants. + +## Arbitration + +### Full GPU — always exclusive + +A Device resource with `settings.renderNodeOnly=false` always uses +`arbitration: exclusive` and `maxConcurrentClaims: 1`. This covers: + +- Full virtio-gpu passthrough: `crosvm device gpu` with card-node access, + `kvm`, `dri`, `udmabuf`, and optionally `nvidia-ctl`/`nvidia-device`/`nvidia-uvm` + broker allowlist tokens. +- Any video-decoder sidecar (`settings.videoSidecar=true`) that depends on a + full-GPU claim. + +A spec that sets `arbitration: shared` with `settings.renderNodeOnly=false` +is rejected at admission with error `shared-arbitration-requires-render-node-only` +and fails the NixOS eval. + +### Render-node-only — exclusive default, explicitly shared + +A Device resource with `settings.renderNodeOnly=true` may use either +`arbitration: exclusive` (default) or `arbitration: shared` (must be explicitly +set by the operator). When `arbitration: shared`, `maxConcurrentClaims` may be +1–16. + +Render-node-only mode: +- Provider/system-minijail validates the LaunchTicket and requests + `OpenDevice(dri)` via its injected `MinijailProcessEffectPort`; the core + executor pre-opens the DRM render node fd. No full-card or auxiliary device + tokens are included. +- Does **not** include `nvidia-ctl`, `nvidia-device`, `nvidia-uvm`, or + `udmabuf` allowlist tokens. Those root:video-owned character devices are + inaccessible inside the single-entry user namespace where in-NS UID/GID 0 + maps to the allocator-assigned worker principal's stable UID; they appear as + UID 65534 (overflow) and DAC access is denied. +- Omits the full virtio-gpu display/cross-domain plumbing. +- Always uses `settings.videoSidecar=false`; `videoSidecar` requires + `renderNodeOnly=false`. + +The render node fd is inherited by the crosvm process via the privileged broker's +private fd-inheritance protocol (`packages/d2b-priv-broker/src/sys.rs` +`clone3_spawn_runner`). The fd survives the user-NS pivot without losing access +semantics because the kernel checks permissions at `openat2` time only. + +### VFIO/SR-IOV — not included, reserved + +VFIO and SR-IOV passthrough are **not** part of the `device-gpu` claim. They +are reserved for a future Device Provider that uses `busClass: pci` selectors +and a distinct broker operation. No existing code path or admitted Device spec +exercises VFIO; implementing it would require a separate hardware-validated +Provider dossier. + +### Admission invariants + +| Rule | Error slug | +| --- | --- | +| `arbitration=shared` requires `settings.renderNodeOnly=true` for `Provider/device-gpu` | `shared-arbitration-requires-render-node-only` | +| `settings.videoSidecar=true` requires `settings.renderNodeOnly=false` | `video-sidecar-requires-full-gpu` | +| `settings.virglVideo=true` and `settings.videoSidecar=true` on the same Guest | `virgl-video-and-sidecar-mutually-exclusive` | +| `settings.videoNvidiaDecode=true` requires `settings.videoSidecar=true` | `nvidia-decode-requires-video-sidecar` | + +## Per-Guest claims + +### Device attachment on Guest spec + +```yaml +# Guest spec (desired state) — standard deviceAttachments; no custom claim/passthrough array +spec: + deviceAttachments: + - deviceRef: Device/corp-vm-gpu # full virtio-gpu + + - deviceRef: Device/dev-vm-render # shared render-node +``` + +GPU mode (full virtio-gpu vs render-node) and arbitration are determined by +the Device resource `spec.provider.settings` managed by the operator. The +common Guest +spec carries only standard `deviceAttachments` referencing Device resources; no +provider-specific `passthrough` or `claim` fields appear in the Guest spec. + +Adding or removing a `deviceAttachments[]` entry on a Guest spec is a normal +`update-spec` RBAC verb. The device-gpu controller detects the change via its +reconcile loop, performs arbitration, and writes result to Device status. There +is no separate `claim-device` or `release-device` verb. + +### Process device dependency entry + +A Process that requires the GPU (e.g., the CH runner) declares a dependency: + +```yaml +spec: + deviceUsage: + - deviceRef: Device/corp-vm-gpu + access: shared + purpose: gpu-socket +``` + +The Process controller verifies the Device is `Ready` and claimed by the owning +Guest before launching the Process. The GPU sidecar surface is an owned +`Endpoint` resource consumed as `Endpoint/` by the CH runner; no socket +locator is expressed as a raw path in any resource spec or status. + +## GPU worker Process: full GPU + +### Process resource shape + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: device--gpu + zone: dev + ownerRef: Device/corp-vm-gpu + managedBy: controller +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + processClass: worker + template: gpu-worker # plain template ID matching ^[a-z][a-z0-9-]*$; declares mapped-root user-NS requirement + sandbox: + namespaceClasses: [mount, pid, ipc, uts, cgroup, user] + capabilityClasses: [] + seccompClass: w1-gpu + startRoot: false + userNamespace: + mappingClass: process-principal-root # uid/gid resolved privately by core from signed worker template + budget: + cpu: + request: "500m" + limit: "4000m" + memory: + request: "256Mi" + limit: "2Gi" + pids: + limit: 32 + fds: + limit: 16 + networkUsage: null + deviceUsage: + - deviceRef: Device/corp-vm-gpu + access: exclusive + purpose: gpu-virtio + readiness: + class: provider-defined + initialDelay: "2s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + restartPolicy: + class: on-failure + backoffBase: "5s" + backoffMax: "5m" + backoffMultiplier: 2 + maxRestarts: 10 + resetAfter: "1h" +``` + +The worker produces the stable GPU sidecar Endpoint separately: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: corp-vm-gpu-sidecar + zone: dev + ownerRef: Device/corp-vm-gpu +spec: + providerRef: Provider/device-gpu + producerRef: Process/device--gpu + endpointClass: device + transport: unix + purpose: gpu-sidecar + serviceFingerprint: device-gpu.d2bus.org/gpu-sidecar/v1 + locality: cross-domain + visibility: provider + attachmentPolicy: launch-ticket-only + consumerPolicy: + allowedSubjects: [Provider/runtime-cloud-hypervisor] + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +``` + +`uid-short` = first 12 hex characters of the owner Device resource UID. +VM or Guest human names never appear in Process resource names. Resolved +principal, cgroup placement, fd numbers, and socket paths are private +LaunchTicket/broker state not expressed in the resource spec. + +### Broker Device allowlist (full GPU) + +The broker opens device fds exclusively from the declared Device allowlist. +No `/dev` path crosses the public wire. Maximum 8 fds per Process launch. + +| Allowlist token | Always | Conditional | +| --- | --- | --- | +| `kvm` | ✓ | | +| `dri` (render node) | ✓ | | +| `nvidia-ctl` | | when `videoNvidiaDecode=true` or NVIDIA graphics VM | +| `nvidia-device` | | when `videoNvidiaDecode=true` or NVIDIA graphics VM | +| `nvidia-uvm` | | when `videoNvidiaDecode=true` or NVIDIA graphics VM | +| `udmabuf` | ✓ | | + +Source: `packages/d2b-core/src/bundle_resolver.rs` lines 1888–1894 (ProcessRole::Gpu +and ProcessRole::GpuRenderNode broker device token comment). Source: +`nixos-modules/minijail-profiles.nix` gpu profile `deviceBinds` list. + +The full GPU profile uses **zero host capabilities** (`capabilities = []`). +CAP_SYS_NICE was previously listed in the capability matrix; it was confirmed +unnecessary at runtime on NVIDIA Quadro T1000 (virgl/venus/cross-domain operate +under SCHED_OTHER). The `sandbox.seccompClass` for this profile is `w1-gpu`, +enforced by `system-minijail` before exec; if the compiled BPF policy is +unavailable in the Provider closure, the launch fails closed. + +### crosvm GPU sidecar argv (full GPU) + +> **Current-source evidence** (`packages/d2b-host/src/gpu_argv.rs`, +> `implemented-and-reachable`). The argv shape below reflects the v3 baseline. +> Socket paths and binary path are resolved by Provider/system-minijail from +> the signed LaunchTicket; they are private LaunchTicket state and do not +> appear in Process resource specs, status, audit, or telemetry. The +> `--wayland-sock` argument is filled from the opaque display endpoint FD +> delivered by d2b-bus/ProviderSupervisor via the `display-wayland` dependency; +> the device-gpu Provider performs no host compositor socket resolution. + +```text +crosvm device gpu \ + --socket \ + --wayland-sock \ + --params '{"context-types":"cross-domain:virgl:virgl2","displays":[{"hidden":true}],"egl":true,"vulkan":true}' +``` + +Source: `packages/d2b-host/src/gpu_argv.rs` (implemented-and-reachable). + +The `GpuArgvInput` struct from `d2b-host` is re-exported from `argv.rs` in +the Provider crate. The provider builds the input struct from resolved Device +spec settings and the signed component descriptor paths; it never constructs +raw argv strings. + +**`crossDomainTrusted=false` enforcement:** The signed component descriptor is +static and is not rewritten per Device. `crossDomainTrusted` is a validated +Device setting projected into the private LaunchTicket by Provider/system-minijail +at resolution time. When `settings.crossDomainTrusted=false` (the default), +system-minijail's argv builder omits `GpuContextType::CrossDomain` from the +`contextTypes` list in the runtime `--params` arg. When `crossDomainTrusted=true`, +`cross-domain` is included. This mirrors the Nix shell shim in +`nixos-modules/components/graphics.nix` that strips `cross-domain` at eval time. + +**`implicit-render-server: true` and `external-blob: true`** are always emitted +in the `--params` JSON payload to enable the virglrenderer render server for +VA-API video decode and blob texture transfer. Source: +`nixos-modules/components/graphics.nix` `crosvmWithRenderServer` wrapper. + +**`--gpu-device-node`** is appended when the render node fd is present. +This enables virglrenderer's `get_drm_fd` callback for VA-API video decode on +the host. Without it, rutabaga_gfx logs "no valid GPU path provided" and video +decode falls back to software. The privileged broker pre-opens the fd and passes +it via the private fd-inheritance protocol from the privileged broker; no device path appears +in the resource spec or argv. + +**Venus Vulkan** is added to `context-types` when `vulkan=true`: +`contextTypes` output appends `venus` via the same `unique | join(":")` logic +as the Nix wrapper. The provider's argv builder handles this addition. + +### Wayland cross-domain endpoint (full GPU only) + +The GPU worker Process consumes an opaque cross-domain display endpoint from +its configured `display-wayland` dependency. The device-gpu Provider has **no +compositor authority** and performs no host socket resolution: + +- The `display-wayland` dependency is declared in the GPU worker's signed + component descriptor. d2b-bus/ProviderSupervisor resolves the dependency and + routes the opaque display endpoint FD directly into the GPU worker LaunchTicket + at execution time. The device-gpu controller and Provider never hold or request + compositor socket handles. +- Device status exposes typed EndpointRefs such as + `crossDomainEndpointRef: Endpoint/` and + `gpuSidecarEndpointRef: Endpoint/`; no socket path, Wayland + display name, UID-scoped path, or opaque endpoint ID appears in status, audit, + or telemetry. +- The GPU worker uses the endpoint FD to expose the cross-domain Wayland channel + to the Guest via the virtio-gpu context. The GPU worker produces virtio-gpu + context/output consumed by the Guest Runtime; the host-side display endpoint + is an input, not an output of this Provider. + +The **video worker Process** does **not** hold this endpoint. See § Video +worker: denied host sockets. + +### Endpoint resources (D092) + +`Provider/device-gpu` declares conformance to the standard `Endpoint` base +schema. Stable GPU sidecar, video sidecar, and cross-domain display identities +are owned `Endpoint` resources with `producerRef` to the producing +`Process`/`Device`, closed `endpointClass`/`transport`, and no raw locator in +spec/status/CLI. Consumers use `Endpoint/` ResourceRefs. +Core/ProviderSupervisor resolves Unix sockets or fd attachments only through +authorized EffectPort/LaunchTicket flows; unauthorized resolve fails +`endpoint-resolve-denied`. A producer restart bumps `endpointGeneration`, which +triggers CH/video dependencies through `dependency-changed`. + +### Retained opaque handles + +Retained opaque values are pidfds, LaunchTicket fd indexes, render-node fd +leases, transient DRM inventory handles, `OwnedTransport`, operation IDs, and +per-session compositor connection handles. They are high-churn, internal to the +controller/effect port, or have no independent lifecycle, so D092 does not +promote them to resources. + +### Cloud Hypervisor connection + +Cloud Hypervisor connects to the GPU sidecar via the `--gpu socket=...` flag, +appended by the CH runner Process via the signed provider component descriptor. +The private socket locator is resolved from `Endpoint/` only through +the authorized EffectPort/LaunchTicket path and is not a spec field, status +value, audit record, or telemetry attribute. + +## GPU worker Process: render-node-only + +### Process resource shape + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: device--render-node + zone: dev + ownerRef: Device/dev-vm-render + managedBy: controller +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + processClass: worker + template: render-node-worker # plain template ID matching ^[a-z][a-z0-9-]*$; declares mapped-root user-NS requirement + sandbox: + namespaceClasses: [mount, pid, ipc, uts, cgroup, user] + capabilityClasses: [] + seccompClass: w1-gpu-render-node + startRoot: false + userNamespace: + mappingClass: process-principal-root # uid/gid resolved privately by core from signed worker template + budget: + cpu: + request: "100m" + limit: "2000m" + memory: + request: "64Mi" + limit: "1Gi" + pids: + limit: 16 + fds: + limit: 8 + networkUsage: null + deviceUsage: + - deviceRef: Device/dev-vm-render + access: shared + purpose: gpu-render-node + readiness: + class: provider-defined + initialDelay: "2s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + restartPolicy: + class: on-failure + backoffBase: "5s" + backoffMax: "5m" + backoffMultiplier: 2 + maxRestarts: 10 + resetAfter: "1h" +``` + +Resolved principal, cgroup placement, and fd numbers are private LaunchTicket/broker +state not expressed in the resource spec. + +### User namespace (ADR 0021 model) + +The render-node-only mode uses the ADR 0021 broker-pre-NS model. Upon +receiving a `SpawnRunner(gpu-render-node)` effect request (routed via +`MinijailProcessEffectPort` → core EffectPort adapter → privileged broker), +the **privileged broker** performs: + +1. `OpenDevice` for the render node fd in the parent process (before + `clone3(CLONE_NEWUSER)`). +2. `clone3(CLONE_NEWUSER | CLONE_PIDFD)` to create the child with a new user + namespace. +3. Writes a single-entry `uid_map`/`gid_map`: in-NS UID/GID 0 → the + allocator-assigned worker principal's stable host UID/GID (private broker + state, not expressed in the resource spec). +4. Transfers the pre-opened render node fd to the child via the private + fd-inheritance protocol before signaling ready. +5. crosvm receives the render device node via the inherited fd; the specific + fd number and path form are private broker-layer state not expressed in the + resource spec. +6. The crosvm process runs fake-root inside the user NS with **zero host + capabilities**. No bind-mount is performed for the render node; the fd is + passed via fd inheritance. + +Source: `nixos-modules/minijail-profiles.nix` `gpu-render-node` profile (lines +490–545 approximately); `packages/d2b-priv-broker/src/sys.rs` +(`clone3_spawn_runner`); `packages/d2b-core/src/bundle_resolver.rs` +(test `gpu_render_node_user_namespace_propagates_to_resolved_intent` at line 4419). + +**Key constraint**: no `deviceBinds` in the render-node profile. The render +node fd is pre-opened by the privileged broker and passed via private +fd-inheritance; no bind-mount action is executed for user-NS spawns. + +**Shared arbitration**: When `arbitration: shared`, multiple Guest Processes +may each hold a `gpu-render-node` passthrough claim simultaneously. The device-gpu +controller creates one `device--render-node` Process per active claim. +Provider/system-minijail sends a separate `SpawnRunner` effect request via +`MinijailProcessEffectPort` for each Process; the privileged broker opens a +separate render-node fd for each. The render node DRM device supports concurrent +unprivileged readers. + +## Video decoder Process + +### Process resource shape + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: device--video + zone: dev + ownerRef: Device/corp-vm-gpu + managedBy: controller +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + processClass: worker + template: video-worker # plain template ID matching ^[a-z][a-z0-9-]*$ + sandbox: + namespaceClasses: [mount, pid, ipc, uts, cgroup] + capabilityClasses: [] + seccompClass: w1-video + startRoot: false + userNamespace: null # video worker does not use a user namespace; tested invariant + budget: + cpu: + request: "250m" + limit: "2000m" + memory: + request: "128Mi" + limit: "1Gi" + pids: + limit: 16 + fds: + limit: 8 + networkUsage: null + deviceUsage: + - deviceRef: Device/corp-vm-gpu + access: exclusive + purpose: video-decode + readiness: + class: provider-defined + initialDelay: "2s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + restartPolicy: + class: on-failure + backoffBase: "5s" + backoffMax: "5m" + backoffMultiplier: 2 + maxRestarts: 10 + resetAfter: "1h" +``` + +The video worker produces the stable video sidecar Endpoint separately: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: corp-vm-video-sidecar + zone: dev + ownerRef: Device/corp-vm-gpu +spec: + providerRef: Provider/device-gpu + producerRef: Process/device--video + endpointClass: data + transport: unix + purpose: video-sidecar + serviceFingerprint: device-gpu.d2bus.org/video-sidecar/v1 + locality: cross-domain + visibility: provider + attachmentPolicy: launch-ticket-only + consumerPolicy: + allowedSubjects: [Provider/runtime-cloud-hypervisor] + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +``` + +The video and GPU workers carry distinct allocator-assigned principals, enforced +as a LaunchTicket invariant by Provider/system-minijail at resolution time and +carried in the effect request to the privileged broker. This invariant confines the +video worker to the `w1-video` seccomp class and its declared Device allowlist, +with no access to host Wayland, PipeWire, or Pulse sockets. Principal names are +private broker state and are not expressed in the resource spec. + +### Dependency on GPU Process readiness + +The video decoder Process is created only **after** the GPU worker Process +reaches phase `Ready`. The controller's reconcile loop enforces this: + +1. Controller receives `spec-generation-changed` with `settings.videoSidecar=true`. +2. Controller creates `device--gpu` Process. +3. Controller watches for `owned-resource-changed` on the GPU Process. +4. When GPU Process phase transitions to `Ready`, controller creates + `device--video` Process. +5. If the GPU Process fails or restarts, the video Process is stopped first, + then re-created after the GPU Process is Ready again. + +This ordering is tested in `tests/video_dependency.rs` (fake supervisor) and +`integration/video_dependency/` (container fixture). + +### crosvm video-decoder sidecar argv + +> **Current-source evidence** (`packages/d2b-host/src/video_argv.rs`, +> `implemented-and-reachable`). Runtime socket path and binary path are +> resolved by Provider/system-minijail from the signed LaunchTicket; they +> are private LaunchTicket state and do not appear in Process resource specs, +> status, audit, or telemetry. + +```text +crosvm device video-decoder \ + --socket-path \ + --backend vaapi +``` + +Source: `packages/d2b-host/src/video_argv.rs` (implemented-and-reachable). + +The video Process uses `VideoBackend::Vaapi` always. No other backend is +currently supported; the enum is open for future backends. The binary path +is resolved from the signed Provider descriptor; it points to a crosvm build +with `cargoBuildFeatures += [video-decoder, vaapi, media]`. The stock nixpkgs +crosvm binary is **never** used for the video sidecar; the controller fails +closed if the signed descriptor does not supply the patched binary path. +Source: `packages/d2b-core/src/bundle_resolver.rs` +`video_runner_has_no_stock_crosvm_legacy_fallback` (test at line 4235). + +### Cloud Hypervisor vhost-user-media connection + +Cloud Hypervisor connects to the video decoder via the `--vhost-user-media +socket=...` flag, appended by the CH runner Process via the signed component +descriptor. The private socket locator is resolved from +`Endpoint/` only through the authorized EffectPort/LaunchTicket +path and is not a spec field, status value, audit record, or telemetry +attribute. + +### Broker Device allowlist (video worker) + +The broker opens device fds exclusively from the declared Device allowlist. +No `/dev` path crosses the public wire. Maximum 8 fds per Process launch. + +| Allowlist token | Always | Conditional | +| --- | --- | --- | +| `dri` (render node) | ✓ | | +| `nvidia-ctl` | | when `settings.videoNvidiaDecode=true` | +| `nvidia-device` | | when `settings.videoNvidiaDecode=true` | +| `nvidia-uvm` | | when `settings.videoNvidiaDecode=true` | + +Source: `nixos-modules/minijail-profiles.nix` video profile `deviceBinds` list, +`lib.optionals (vm.graphics.videoNvidiaDecode or false)` guard. + +The video profile uses **zero host capabilities** (`capabilities = []`), +`namespaces.pid = true`. The `sandbox.seccompClass` for this profile is +`w1-video`, enforced by `system-minijail` before exec; if the compiled BPF +policy is unavailable in the Provider closure, the launch fails closed. + +### NVIDIA opt-in — explicit operator action required + +NVIDIA hardware video decode (`nvidia-ctl`, `nvidia-device`, `nvidia-uvm` +allowlist tokens) is gated behind **two** explicit opt-in fields that must +both be `true`: + +1. `settings.videoSidecar: true` — enable the video decoder Process. +2. `settings.videoNvidiaDecode: true` — add NVIDIA device tokens to the video + worker's `deviceUsage[]` entries. + +Neither field defaults to `true`. An operator who sets only `videoSidecar=true` +gets a video decoder with VA-API via Mesa virtio-GPU; NVIDIA device tokens are +**not** added unless `videoNvidiaDecode=true` is also set. Both fields must +appear explicitly in the Device spec and in the Nix configuration. + +Source: `nixos-modules/minijail-profiles.nix` video profile +`lib.optionals (vm.graphics.videoNvidiaDecode or false)`. + +### Denied host sockets (video worker) + +The video worker does NOT receive Wayland, PipeWire, or PulseAudio socket +access: + +- The `template: video-worker` descriptor declares no + cross-domain or audio endpoint capability. +- The video minijail profile has no `bindMounts` for Wayland or audio sockets. +- The distinct allocator-assigned principal maintained by the LaunchTicket + invariant ensures broker and activation ACLs can deny these sockets to the + video worker without affecting the GPU worker. + +This is a load-bearing architectural invariant. From +`AGENTS.md §Critical subsystems — handle with care` (Video sidecar): "The +video runner MUST use the dedicated `d2b--video` principal, not +`d2b--gpu`, so broker/activation ACLs can deny host Wayland/PipeWire/Pulse +sockets to video without breaking GPU cross-domain." The principal names are +private broker state; the invariant is enforced by the LaunchTicket, not +expressed in the resource spec. + +## Frozen wire-contract constants + +The following constants from `packages/d2b-host/src/video_argv.rs` are frozen +virtio-media wire contract values. Changing any of them requires updating the +CH patch `pkgs/spectrum-ch/cloud-hypervisor/0003-vhost-user-media-device.patch` +and a separate hardware-validated review: + +| Constant | Value | Source in CH patch | +| --- | --- | --- | +| `VIRTIO_ID_MEDIA` | `48` | `const VIRTIO_ID_MEDIA: u32 = 48` | +| `VHOST_USER_MEDIA_NUM_QUEUES` | `2` | `const NUM_QUEUES: u16 = QUEUE_SIZES.len() as _` | +| `VHOST_USER_MEDIA_QUEUE_SIZE` | `256` | `const QUEUE_SIZES: &[u16] = &[256, 256]` | +| `VHOST_USER_MEDIA_SHM_REGION_BYTES` | `268435456` (256 MiB) | `VhostSharedMemoryRegion { length: 256 * 1024 * 1024 }` | +| `VHOST_USER_MEDIA_VRING_BASE` | `0` | `activate()` SET_VRING_BASE override | +| `VHOST_USER_MEDIA_PROTOCOL_FLAGS` | `BACKEND_REQ\|REPLY_ACK\|SHMEM_MAP_CROSVM` | `acked_protocol_features` | +| `VHOST_USER_MEDIA_MMIO_ALLOCATOR` | `pci-mem64` | PCI MMIO allocator for SHM region | + +The `wire_contract_snapshot()` function in `video_argv.rs` renders these as a +deterministic single line; `tests/wire_constant_snapshot.rs` byte-compares the +output against a committed golden vector. Any future drift in the CH patch +surfaces as a CI golden diff before any argv change. + +## Runtime Cloud Hypervisor dependency + +### CH/crosvm compatibility contract + +The device-gpu Provider has a hard dependency on the patched Cloud Hypervisor +build (`pkgs/spectrum-ch`) and the matching crosvm revision: + +- **CH patch set**: `pkgs/spectrum-ch/cloud-hypervisor/0003-vhost-user-media-device.patch` + introduces `vhost-user-media` with `virtio_id=48`. Any CH rev bump must + update this patch and the frozen wire-contract constants above. +- **crosvm compatibility**: the vhost-user-gpu sidecar uses standardized + vhost-user shmem message numbers (`GET_SHMEM_CONFIG=44`, `SHMEM_MAP=9`, + `SHMEM_UNMAP=10`) matching `rust-vmm/vhost @ vhost-user-backend-v0.22.0`. + The `spectrumCH.passthru.testedWithCrosvmRev` assertion in + `nixos-modules/components/graphics.nix` enforces crosvm revision parity. +- **Eval guard**: if `spectrumCH.passthru.testedWithCrosvmRev ≠ pkgs.crosvm.src.rev`, + the NixOS evaluation fails with a clear message before any hardware is touched. +- **v3 target**: this compatibility contract is preserved. The provider crate + ships a `RuntimeCompatibilityDescriptor` that records the expected + `testedWithCrosvmRev` digest; the controller validates it at startup against + the installed CH package descriptor. A mismatch fails closed. + +### seccomp enforcement + +The `w1-gpu`, `w1-video`, and `w1-gpu-render-node` seccomp classes are enforced +by `system-minijail` before exec. If the compiled BPF policy for a class is +unavailable in the Provider closure, the launch fails closed; there is no +policy-less fallback and no runtime flag wiring is required. + +## Process execution boundary + +The `device-gpu` controller creates and manages Process resource records with +`deviceUsage[]` entries. It does **not** have broker authority or fd access of +any kind. It never calls `SpawnRunner`, `OpenDevice`, or any fd-inheritance +operation. + +Provider/system-minijail, when processing a Process with `template: gpu-worker`, +`render-node-worker`, or `video-worker`, uses its injected **`MinijailProcessEffectPort`** +to request execution effects. The core EffectPort adapter maps opaque intents from +`MinijailProcessEffectPort` to broker requests. The **privileged broker alone** +performs `OpenDevice`, `clone3`, `uid_map`/`gid_map` writes, and FD transfer; +neither system-minijail nor the device-gpu controller has direct broker access. + +### Effect operations (requested via MinijailProcessEffectPort; executed by privileged broker) + +Provider/system-minijail sends the following effect requests through its injected +`MinijailProcessEffectPort`; the core EffectPort adapter routes them to the +privileged broker which executes them: + +| Effect op | Effect | Audit | Rate limit | +| --- | --- | --- | --- | +| `SpawnRunner` (gpu role) | Privileged broker spawns crosvm GPU worker in broker-pre-NS | Yes | 1 per Device per Guest start cycle | +| `SpawnRunner` (gpu-render-node role) | Privileged broker spawns crosvm render-node worker in user NS via fd inheritance | Yes | 1 per active claim | +| `SpawnRunner` (video role) | Privileged broker spawns crosvm video-decoder Process | Yes | 1 per Device | +| `OpenDevice` (kvm, dri, udmabuf, nvidia*) | Privileged broker opens GPU device fds before clone; passes to worker | Yes | ≤8 fds per Process launch | + +Source: `packages/d2b-contracts/src/broker_wire.rs` `RunnerRole::Gpu`, +`RunnerRole::Video`; `packages/d2b-core/src/bundle_resolver.rs` device token +sets (lines 1882–1894). + +### No blanket device grant + +No Provider process receives a blanket device-path grant, raw device node string, +or ambient host capability. The **privileged broker**: + +1. Validates all inputs against the trusted bundle before any effect. +2. Opens fds in the parent process for GPU workers (before `clone3`). +3. Passes fds to the child via the private fd-inheritance protocol; no device + path crosses the wire. + +No device path crosses the public wire or any effect port as a string. + +### User namespace pre-spawn (ADR 0021) + +Full GPU workers run inside a broker-pre-established user namespace where: +- in-NS UID/GID 0 maps to the allocator-assigned worker principal's stable host UID. +- Device fds are pre-opened before `clone3(CLONE_NEWUSER)`. +- The worker has zero ambient host capabilities. + +Source: ADR 0021; `nixos-modules/minijail-profiles.nix` gpu profile; +`packages/d2b-priv-broker/src/sys.rs` `clone3_spawn_runner` (privileged broker implementation). + +The render-node-only worker always uses the user-NS model (ADR 0021). +The full-GPU worker uses it as well (no `userNamespace: null` exception; +the full GPU profile also transitions to broker-pre-NS in v3, aligned with +the render-node model). + +## Status, conditions, and phase semantics + +### Status shape + +Per D088, ResourceType-common Device observation lives in +`status.resource`: the provider-neutral claim/arbitration/presence base that is +identical across Device implementations. GPU-specific observations (DRM/render +node mode and availability, worker refs/readiness, video sidecar and +wire-contract observations, bounded diagnostics) live only in `status.provider` +with `providerRef`, qualified `schemaId` `device-gpu.d2bus.org/Device/status`, +`schemaVersion`, `observedProviderGeneration`, and strict bounded redacted +`details` +(≤32 KiB, unknown-field-denied). The controller writes all present layers +atomically in one status mutation; shared fields are never duplicated +into `status.provider`, and the extension schema is registered and signed in the +Provider manifest. + +### Currency and expedited reconcile (D091/D090) + +D091 currency is universal status, not GPU provider detail. The controller +implements `assess_update`, `plan_upgrade`, and `execute_upgrade`, populates +universal `status.update`, and keeps shared currency fields out of +`status.provider`; GPU-specific observations may appear only under +`status.provider.details`. Driver/provider generation, artifact, spec, or +security-policy changes that require interrupting active claimants MUST set +`status.update.state = Blocked` while dependent `Process`/`Guest` resources are +running, then `UpgradeRequired` when an upgrade operation is planned, with +`reasons = [ProviderGenerationChanged]`, `[ArtifactChanged]`, `[SpecChanged]`, +or `[SecurityPolicyChanged]`, `disruption = Recycle`, and +`preserveState = true`. Non-disruptive changes reconcile normally. The +dependency-aware planner drains dependent Processes/Guests, recycles the GPU +realization, and restarts dependents; no surprise disruption is permitted and +device identity is preserved. + +D090 expedited `waitForReconcile` on `Create`/`UpdateSpec`/`Delete` performs no +external effect, finalizer change, or status mutation until Core supplies +`CommittedRevisionProof {resourceUid,generation,revision,operationId}`. The +one-pass response returns the committed object, projected layered status, +disposition `Converged|Progressing|Blocked|UpgradeRequired|Failed`, and +`statusPersistence = pending|committed`; the durable commit is never rolled back +after a reconcile timeout. Effect idempotency keys derive from +`(UID,generation,revision,operationId)`, and the expedited pass uses the bounded +priority lane inside the same per-resource single-flight. + +```yaml +status: + observedGeneration: 1 + phase: Ready | Pending | Degraded | Failed | Unknown + conditions: + - type: DevicePresent + status: "True" | "False" | "Unknown" + reason: device-probed-present | device-not-found | device-consecutive-probe-failures-exceeded | device-probe-failed + - type: DeviceClaimed + status: "True" | "False" + reason: exclusive-claim-held | no-active-claim + - type: DeviceHealthy + status: "True" | "False" | "Unknown" + reason: worker-healthy | worker-failed | worker-not-started + - type: GpuWorkerReady + status: "True" | "False" | "Unknown" + reason: gpu-worker-ready | gpu-worker-starting | gpu-worker-failed | gpu-worker-not-created + - type: VideoWorkerReady + status: "True" | "False" | "Unknown" + reason: video-worker-ready | video-worker-starting | video-worker-failed | video-worker-not-created | video-sidecar-disabled + - type: ClaimConflict + status: "True" | "False" + reason: exclusive-claim-conflict | no-conflict + - type: GpuEffectAvailable + status: "True" | "False" + reason: gpu-effect-available | gpu-effect-unavailable + lastReconciledAt: null + resource: + present: true | false | null + health: healthy | degraded | failed | unknown + holderRefs: [] + claims: [] + provisionedAt: null + lastProbedAt: null + provider: + providerRef: Provider/device-gpu + schemaId: "device-gpu.d2bus.org/Device/status" + schemaVersion: "1.0.0" + observedProviderGeneration: 1 + details: + gpu: + mode: full | render-node + renderNodeAvailable: true | false | null + workerRefs: [] + videoWorkerRef: null + wireContract: ok | failed | unknown + providerDiagnostic: null # bounded redacted one-line; never paths/secrets +``` + +### Phase semantics + +| Phase | Meaning | +| --- | --- | +| `Pending` | Device spec committed; probe not yet complete or GPU effect port not yet available | +| `Ready` | Device present, claim held, GPU worker (and video worker if enabled) healthy | +| `Degraded` | One condition impaired (e.g., three consecutive probe failures, video worker failed but GPU still running) | +| `Failed` | Current spec generation cannot complete under retry policy (e.g., arbitration conflict, three probe failures with no device recovery) | +| `Unknown` | Controller cannot currently prove device state (e.g., first or second probe failure) | + +### Condition types (GPU-specific) + +| Type | Meaning | +| --- | --- | +| `DevicePresent` | DRM render node sysfs-visible | +| `DeviceClaimed` | At least one active claim held | +| `DeviceHealthy` | GPU worker (and video worker if enabled) are responsive | +| `GpuWorkerReady` | crosvm GPU/render-node Process in phase `Ready` | +| `VideoWorkerReady` | crosvm video-decoder Process in phase `Ready`; always `False` when `videoSidecar=false` | +| `ClaimConflict` | Exclusive Device received a second concurrent claim request | +| `GpuEffectAvailable` | Injected `GpuEffectPort` is available for device probing and execution | + +## Errors + +Stable GPU-specific error classes (subset of common device errors): + +| Error | Meaning | +| --- | --- | +| `device-not-found` | DRM device absent from sysfs/udev at probe time | +| `device-claim-conflict` | Exclusive GPU device already claimed by another Guest | +| `device-arbitration-violation` | Shared claim attempted on `renderNodeOnly=false` Device | +| `shared-arbitration-requires-render-node-only` | `arbitration=shared` set with `renderNodeOnly=false` | +| `video-sidecar-requires-full-gpu` | `videoSidecar=true` with `renderNodeOnly=true` | +| `virgl-video-and-sidecar-mutually-exclusive` | Both `virglVideo=true` and `videoSidecar=true` on same Guest | +| `nvidia-decode-requires-video-sidecar` | `videoNvidiaDecode=true` without `videoSidecar=true` | +| `device-worker-failed` | GPU or video worker Process entered `Failed` phase | +| `device-wire-contract-mismatch` | Video or GPU wire constant diverged from frozen snapshot | +| `gpu-effect-unavailable` | Injected `GpuEffectPort` returned unavailable; device probe and worker launch cannot proceed | +| `render-node-fd-inheritance-failed` | Broker fd pre-open or private fd-inheritance transfer for render-node worker failed | +| `ch-crosvm-rev-mismatch` | `testedWithCrosvmRev` diverges from installed crosvm; controller refuses to spawn | +| `video-binary-not-found` | Signed descriptor missing patched video-decoder binary; fails closed | + +All error messages are bounded, UTF-8 validated, and must not contain device +paths, sysfs node names, `/dev/dri/` entries, GPU socket paths, or credential +material. + +## Audit records + +Each broker operation emits a path-free audit record: + +| Field | Value | +| --- | --- | +| `subject` | Provider Process identity digest | +| `zone` | Zone name | +| `op` | Broker op tag (`SpawnRunnerGpu`, `SpawnRunnerRenderNode`, `SpawnRunnerVideo`, `OpenDeviceGpu`) | +| `resource_type` | `Device` | +| `resource_name_digest` | SHA-256 of the Device resource name; never a raw path or device node | +| `outcome` | `success` \| `failure` \| `denied` | +| `error_class` | Stable error slug from the table above | +| `correlation_id` | Operation/trace ID | +| `timestamp` | RFC 3339 UTC | + +The audit record excludes: raw GPU device paths, DRM card node names, render +node paths, GPU socket paths, CH argv, crosvm argv, nvidia device paths, +video socket paths, and any credential material. + +## OTEL telemetry + +All telemetry placement — span vs resource attribute classification and full +label-set boundaries — is defined in +`ADR-046-telemetry-audit-and-support`. `d2b.zone` and +`d2b.provider="device-gpu"` are bounded OTEL resource attributes, never metric +labels. This dossier does not compete with those constraints. + +GPU-specific label rules: + +- No device path, DRM card node, render node path, PCI slot, GPU socket path, + video socket path, or process PID may appear in any OTEL span attribute or + metric label. +- `mode` ∈ `{full, render-node}` is a closed semantic metric label. +- `video_sidecar` ∈ `{enabled, disabled}` is a closed semantic metric label. +- `arbitration` ∈ `{exclusive, shared}` is a closed semantic metric label. +- No `vm_name`, `guest_name`, or human-readable VM identifier appears in metric + labels; VM identity is carried only in OTEL resource attributes and trace context. + +## RBAC + +The device-gpu Provider uses the standard Device RBAC roles from +`ADR-046-resources-device`: + +| Role | Verbs | Scope | Subjects | +| --- | --- | --- | --- | +| `device-manager` | get, list, watch, create, update-spec, delete | Zone | `Provider/device-gpu` controller | +| `device-status-owner` | update-status | Zone | `Provider/device-gpu` controller only | +| `device-finalizer-owner` | update-finalizers | Zone | `Provider/device-gpu` controller only | +| `device-reader` | get, list, watch | Zone | Guest runtime Provider, CH runner, CLI | +| `device-claimant` | get, watch | Zone | Guest runtime Provider holding a GPU claim | + +No Role grants wildcard `*` over all Device resources. The `device-gpu` +controller's RoleBinding covers only Device resources whose `spec.providerRef` +resolves to `Provider/device-gpu`. + +The `device-gpu` controller also requires `process-manager` and +`process-status-reader` RBAC for the Process resources it creates (`device--gpu` +and `device--video`). It holds no other resource authority. + +## Async reconcile loop + +The controller implements the standard async reconcile contract from +`ADR-046-resource-reconciliation`. All reconciliation APIs are asynchronous. +A dedicated watch task reads while per-resource tasks reconcile in parallel. + +### Reconcile triggers and handler actions + +| Trigger | Handler | +| --- | --- | +| `spec-generation-changed` | Re-evaluate settings; update argv/broker-token set; if `renderNodeOnly` or `videoSidecar` changed, stop/restart affected Processes; update arbitration mode. | +| `deletion-requested` | Issue Delete on owned video Process (if running); issue Delete on GPU/render-node Process; wait for both to commit `phase=Deleted`; release OS resources; clear finalizer `device-gpu.d2bus.org/worker-stopped`. | +| `dependency-changed` | If owning Guest stops or degrades, release active claims; set phase `Degraded`. If GPU Process transitions to `Ready` and `videoSidecar=true`, create video Process. If GPU Process fails, stop video Process, update `VideoWorkerReady=False`. | +| `scheduled-observe` | Probe DRM sysfs presence; update `DevicePresent` condition; apply three-strike failure semantics. | +| `owned-resource-changed` | GPU Process phase changed: update `GpuWorkerReady`; trigger video dependency check. Video Process phase changed: update `VideoWorkerReady`; set `DeviceHealthy` accordingly. | + +### Process fast-path + +GPU and video worker Processes follow the standard Process fast-path: +commit-to-controller-handler ≤5 ms p95; launch-attempt ≤20 ms p95. +The device-gpu controller creates the Process resource; the system-minijail +Process controller manages the launch. + +### Deletion sequence (finalizer `device-gpu.d2bus.org/worker-stopped`) + +1. `deletionRequestedAt` set on Device resource. +2. Controller receives `deletion-requested` trigger. +3. Controller issues Delete requests on owned video Process (if present) and + GPU/render-node Process; waits for their finalizers to clear. +4. Controller clears finalizer `device-gpu.d2bus.org/worker-stopped`. +5. Core commits one revision event with `phase=Deleted` for the Device, removes + the resource row and all indexes atomically in one redb transaction, then + emits the audit record after the commit. No `phase=Deleted` row persists in + the store. + +GPU device state (DRM card, render node) is physical; there is no persistent +emulated state to preserve or delete. The Device finalizer does not delete any +Volume resource. There is no force-cleanup path; the finalizer sequence is the +only deletion path. + +## Provider state + +### ProviderStateSet (query-time grouping) + +Per D087, `device-gpu` declares **no Provider state Volume**. A +`ProviderStateSet(zone, device-gpu)` is the optional query-time grouping of +Provider state Volumes declared by the Provider descriptor; for this Provider it +is empty. + +```text +ProviderStateSet(zone, device-gpu) = {} +``` + +GPU device state (DRM card, render node, video sidecar attachment) is physical +or Process/Device runtime state, not a durable Provider payload. GPU has no +Device-payload Volume; render-node access is a Device attachment resolved through +LaunchTicket and the privileged broker, not a Provider state Volume. + +### Status-first operational state + +Bounded non-secret operational state is written to the owning `Device.status`, +`Provider.status`, and the core Operation ledger. The common Device +claim/arbitration/presence summary lives in `status.resource`; GPU-specific +render-node/card availability, Process references, readiness observations, +finalizer progress, restart/adoption observations, and wire-contract check +results live in `status.provider.details.gpu`. Status is revisioned, +optimistic-status-writer controlled, RBAC-readable, redacted, +observation-only, bounded, and written only on material change. After restart, +the controller re-lists Device and Process resources, revalidates external +DRM/render-node and worker reality, and updates status; status never acts as +host-mutation or repair authority. + +Worker Processes (`gpu-worker`, `render-node-worker`, `video-worker`) declare no +Provider state Volume and no `/state` mount. Their live fds and process-local +observations remain transient runtime data. Genuine runtime/device attachments +remain intact; only the former identity-only Provider state Volume is removed. + +Storage-need test rationale: device-gpu operational state contains no durable +secret recovery payload, no large/binary/file content, no private data unsafe for +authorized status readers, and no bounded-but-revision-unsuitable data with a +demonstrated recovery need. Physical GPU state is reobserved, and claim/adoption +state is already represented by Device status and Operation rows. + +## Nix configuration + + +### Authoring shape + +```nix +# Full GPU, exclusive, with video sidecar +d2b.zones..resources."-gpu" = { + type = "Device"; + metadata.ownerRef = "Guest/"; + spec = { + providerRef = "Provider/device-gpu"; + deviceClass = "physical"; + arbitration = "exclusive"; + inventory.selector = { + busClass = "drm"; + label = "host-gpu"; + }; + provider = { + schemaId = "device-gpu.d2bus.org/Device/spec"; + schemaVersion = "1.0.0"; + settings = { + videoSidecar = true; + videoNvidiaDecode = false; # must be explicit when videoSidecar=true + contextTypes = ["cross-domain" "virgl" "virgl2"]; + crossDomainTrusted = false; + }; + }; + }; +}; + +# Render-node only, shared (up to 4 concurrent Guests) +d2b.zones..resources."-render" = { + type = "Device"; + metadata.ownerRef = "Guest/"; + spec = { + providerRef = "Provider/device-gpu"; + deviceClass = "physical"; + arbitration = "shared"; + maxConcurrentClaims = 4; + inventory.selector = { + busClass = "drm"; + label = "host-gpu"; + }; + provider = { + schemaId = "device-gpu.d2bus.org/Device/spec"; + schemaVersion = "1.0.0"; + settings = { + renderNodeOnly = true; + contextTypes = ["virgl2"]; + egl = true; + vulkan = false; + }; + }; + }; +}; +``` + +`spec` field names, types, bounds, and defaults in Nix are identical to the +canonical ResourceTypeSchema. A `spec` field absent from the schema fails eval +with `invalid-provider-settings`. + +### Eval-time assertions (GPU-specific) + +Added to `nixos-modules/assertions.nix`: + +| Assertion | Error | +| --- | --- | +| `settings.videoSidecar=true` requires `settings.renderNodeOnly=false` | `video-sidecar-requires-full-gpu` | +| `settings.virglVideo=true` and `settings.videoSidecar=true` on the same Guest | `virgl-video-and-sidecar-mutually-exclusive` | +| `settings.videoNvidiaDecode=true` requires `settings.videoSidecar=true` | `nvidia-decode-requires-video-sidecar` | +| `arbitration=shared` requires `settings.renderNodeOnly=true` for this Provider | `shared-arbitration-requires-render-node-only` | +| No two Device resources in the same Zone have the same `inventory.selector.label` | `duplicate-device-label` | +| Guest with `graphics.enable=true` must declare a GPU Device resource | `graphics-enable-requires-device-gpu-resource` | + +### Canonical ResourceSpec JSON + +**Full GPU, exclusive, with video sidecar:** + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Device", + "metadata": { + "name": "corp-vm-gpu", + "ownerRef": "Guest/corp-vm", + "zone": "dev" + }, + "spec": { + "arbitration": "exclusive", + "deviceClass": "physical", + "inventory": { + "selector": { + "busClass": "drm", + "label": "host-gpu", + "pciSlot": null + } + }, + "maxConcurrentClaims": 1, + "providerRef": "Provider/device-gpu", + "settings": { + "contextTypes": ["cross-domain", "virgl", "virgl2"], + "crossDomainTrusted": false, + "displays": [{"hidden": true}], + "egl": true, + "renderNodeOnly": false, + "videoNvidiaDecode": false, + "videoSidecar": true, + "virglVideo": false, + "vulkan": true + } + } +} +``` + +**Render-node only, shared, up to 4 Guests:** + +```json +{ + "apiVersion": "resources.d2bus.org/v3", + "type": "Device", + "metadata": { + "name": "dev-vm-render", + "ownerRef": "Guest/dev-vm", + "zone": "dev" + }, + "spec": { + "arbitration": "shared", + "deviceClass": "physical", + "inventory": { + "selector": { + "busClass": "drm", + "label": "host-gpu", + "pciSlot": null + } + }, + "maxConcurrentClaims": 4, + "providerRef": "Provider/device-gpu", + "settings": { + "contextTypes": ["virgl2"], + "crossDomainTrusted": false, + "displays": [], + "egl": true, + "renderNodeOnly": true, + "videoNvidiaDecode": false, + "videoSidecar": false, + "virglVideo": false, + "vulkan": false + } + } +} +``` + +Array elements sorted lexicographically. `null`-default optional selector fields +included explicitly. Absent settings fields receive schema defaults. + +### Artifact catalog + +The `d2b-provider-device-gpu` package closure is referenced from the Zone's +Provider resource via `spec.artifactId`: + +```nix +d2b.artifacts.provider-device-gpu = { + package = pkgs.d2b-provider-device-gpu; + type = "provider"; +}; +``` + +The Provider resource spec: + +```nix +d2b.zones..resources."provider-device-gpu" = { + type = "Provider"; + spec = { + artifactId = "provider-device-gpu"; + config.controllerExecutionRef = "Host/host-system"; + }; +}; +``` + +`spec.config.controllerExecutionRef` declares the execution context for the +controller Process. The `ProviderStateSet` for `Provider/device-gpu` is empty: +`device-gpu` declares no Provider state Volume, does not export `Volume` as a +ResourceType, and has no controller `/state` mount. Device resource status, +Provider status, and core Operation rows are the authority for bounded +non-secret operational state. + +The Device spec carries **no** `artifactId` field. Binary paths for crosvm +(GPU and video) are resolved from the **signed component descriptor** inside the +`d2b-provider-device-gpu` package closure. Store paths never appear in Device +`spec.provider.settings`, resource status, audit records, or telemetry. + +### Activation and publication path + +1. NixOS build produces `/etc/d2b/zones//resources.json` (root:d2bd 0640). +2. Zone runtime reads the bundle at startup and on generation-change signal. +3. Resources with `managedBy=configuration` absent from the new generation + receive `deletionRequestedAt`; the Zone transitions to `Degraded/pending-cleanup`. +4. GPU Device resources are reconciled asynchronously; new workloads start + immediately. + +### Removed configured-resource cleanup + +When a GPU Device resource is removed from the Nix config: + +1. Zone activates new generation; Device gets `deletionRequestedAt`. +2. Device-gpu controller handles `deletion-requested` trigger: + stops video Process, then GPU Process, clears finalizer. +3. Zone status transitions `Degraded/pending-cleanup` → `Ready` after the + Device's `phase=Deleted` revision event is committed and the row is removed. +4. Associated controller-created Processes (`managedBy=controller`) are deleted + by the Device finalizer, never by generation cleanup. +5. The guest-side virtio-media kernel module and CH `--vhost-user-media` arg + remain in the Guest runtime Nix module until the Guest's next generation + change. + +## Crate and source layout detail + +### `src/` — implementation + +| File | Content | +| --- | --- | +| `lib.rs` | Crate entry; re-exports controller, worker binaries; Provider identity const | +| `controller.rs` | Async reconcile loop; ResourceClient; `spec-generation-changed`, `deletion-requested`, `dependency-changed`, `scheduled-observe`, `owned-resource-changed` handlers | +| `probe.rs` | `GpuEffectPort::probe_drm_device(selector)` caller; `observe_interval_secs` scheduler; three-strike failure counter | +| `arbitration.rs` | Exclusive vs shared claim check; `ClaimConflict` condition; second-claim rejection | +| `worker_gpu.rs` | Full GPU and render-node Process resource builder; argv construction via `d2b-host::gpu_argv`; broker token set | +| `worker_video.rs` | Video-decoder Process resource builder; argv construction via `d2b-host::video_argv`; NVIDIA opt-in gating; wire-contract check at startup | +| `argv.rs` | Re-export `GpuArgvInput`, `VideoArgvInput`, `GpuContextType` from `d2b-host`; no new argv logic | +| `broker.rs` | Device claim registration; tracks in-memory claim admission state per Device/Guest via `GpuEffectPort`; claim authority is `Device` resource `spec`/`status` (holderRefs, conditions) and the core Operation ledger managed via `ResourceClient` — no file-backed allocation table and no Volume writes; does **not** hold execution authority (Provider/system-minijail sends effect requests via `MinijailProcessEffectPort`; the core EffectPort adapter routes them to the privileged broker which executes them) | +| `status.rs` | `StatusWriter`; condition builder; phase state machine; bounded `providerDiagnostic` | +| `audit.rs` | Path-free `GpuAuditRecord` builder; correlation ID threading | +| `error.rs` | `DeviceGpuError` enum; closed-set slug strings | + +### `tests/` — hermetic Cargo integration + +| File | Coverage | +| --- | --- | +| `combined_reconcile.rs` | Full GPU+video state machine: probe → claim → gpu-Process Ready → video-Process Ready; fake broker and supervisor | +| `render_node_enforcement.rs` | `shared + renderNodeOnly=false` spec rejected at controller admission; `exclusive + renderNodeOnly=true` accepted | +| `wire_constant_snapshot.rs` | `video_argv::wire_contract_snapshot()` byte-matches committed golden string; any constant change surfaces here | +| `conformance.rs` | `DeviceSpec` and `DeviceGpuSettings` serde round-trip vs ResourceTypeSchema golden; unknown-field denial; `deviceUsage`, `budget`, `endpoints`, `readiness`, and `mounts` field names and structure validated against canonical schema | +| `arbitration_conflict.rs` | Second Guest attempts exclusive claim; controller writes `ClaimConflict` condition, sets second Device phase `Degraded` | +| `video_dependency.rs` | Video Process not created until GPU Process reaches `Ready`; video Process stopped when GPU Process fails | +| `seccomp_policy_ref.rs` | `sandbox.seccompClass` for gpu/video/render-node Process templates are `w1-gpu`, `w1-video`, `w1-gpu-render-node` (stable regression guard; confirms system-minijail enforces correct class before exec) | +| `status_state.rs` | Provider descriptor declares no Provider state Volume; ProviderStateSet query is empty; controller and worker Process templates have no `/state` mounts; no GPU Device-payload Volume exists; bounded operational observations are written to Device/Provider status and Operation rows; render-node access remains a Device attachment rather than Provider state | + +### `integration/` — container/Host/Guest fixtures + +| Path | Scenario | +| --- | --- | +| `gpu_worker_start/` | Full GPU worker Process obtains broker device tokens (`kvm`, `dri`, `udmabuf`), crosvm starts, Process reaches `Ready`. Requires x86_64 container host. | +| `render_node_shared/` | Two Guests each claim the same render-node Device with `arbitration=shared`; both worker Processes reach `Ready` simultaneously. Requires x86_64 + `/dev/dri/renderD128`. | +| `video_dependency/` | `videoSidecar=true`: video Process created only after GPU Process `Ready`; GPU Process crash causes video Process stop and re-sequenced restart. | +| `README.md` | How to invoke integration fixtures (`make test-integration`); hardware test note (see § Hardware tests). | + +### `integration/README.md` content requirements + +The `integration/README.md` file must include, at minimum: + +1. How to run the container integration fixtures: + ``` + make test-integration + # or directly: + cargo test -p d2b-provider-device-gpu --test integration + ``` +2. The host requirements for each fixture (x86_64, `/dev/dri/renderD128`, + `/dev/kvm`). +3. The explicit statement that hardware-GPU and NVIDIA tests are **not** part + of the container fixtures; they are manual-only and described in + `tests/host-integration/hardware/` (see § Hardware tests below). +4. A note that the `render_node_shared` fixture uses a mock render node fd and + does not require a live GPU; the `gpu_worker_start` fixture requires + `/dev/dri/renderD128` to be present. + +### Fast hermetic execution and test placement (D094) + +Per D094 and `ADR-046-validation-and-delivery` §10.16, this Provider's `src/` +unit tests and `tests/*.rs` hermetic suite are fast, in-process, deterministic, +and parallel-safe: an individual normal test has p95 ≤50 ms with no wall-clock +sleep, and `cargo test -p d2b-provider-device-gpu --lib --tests` completes in +≤2 s warm-cache execution time (compilation excluded). They use a deterministic +fake clock/RNG and the toolkit fakes/FakeEffectPort only — no process spawn, +container, network, DBus, systemd, broker daemon, Nix eval/build, KVM, +USB/GPU/TPM hardware, or live cloud, and no filesystem tree beyond tiny temp +fixtures. Any scenario needing those lives only in `integration/`, which keeps +a lane timeout/budget, parallel isolation, and fake external services by +default; such a need is re-placed into `integration/`, never given a sleep, +larger timeout, or `#[ignore]`. Bounded crypto/property tests are the only +classified exception, each named with a capped case count and a declared higher +per-test budget. + +Per D094, each replaced current-code test is retired with an explicit +keep/adapt/move/delete disposition and a removal gate: the minimum reusable +semantic assertions migrate into this crate's hermetic `tests/`, and the old +duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest +entries are deleted once successor coverage and the removal proof pass — +updating `tests/layer1-jobs.json`, the closed gate manifests, the +flake/matrix/Nix-unit pins, the generated ledgers, and the CI workflow shards. +Old and new suites never run in parallel indefinitely. + +## Hardware tests (explicit and manual only) + +The following tests require a physical GPU and are **not** part of any +automated CI lane. They are manual-only and must be invoked explicitly: + +```bash +# Requires: x86_64-linux host, /dev/dri/renderD128, KVM, running d2b Zone +D2B_LIVE=1 bash tests/host-integration/hardware/gpu-minijail-live.sh + +# Requires: x86_64-linux, NVIDIA GPU, /dev/nvidiactl + /dev/nvidia-uvm +D2B_LIVE=1 bash tests/host-integration/hardware/gpu-nvidia-video-decode.sh + +# Requires: full d2b host with a running Guest using a GPU Device +D2B_LIVE=1 bash tests/host-integration/hardware/gpu-virtio-guest-connect.sh +``` + +Hardware tests: +- Are gated on `D2B_LIVE=1`; without it they print a skip message and exit 0. +- Write evidence to `/var/lib/d2b/validated/gpu-.json` after passing. +- Are never required for PR merge eligibility or automated CI. +- Require a deployed d2b host with the matching hardware. + +The `integration/README.md` must document these tests and their hardware +requirements explicitly. + +## ProcessRole disposition + +| Current v3 ProcessRole | v3 resource target | Evidence class | +| --- | --- | --- | +| `ProcessRole::Gpu` (`packages/d2b-core/src/processes.rs`) | `Process/device--gpu` owned by `Provider/device-gpu` | implemented-and-reachable | +| `ProcessRole::GpuRenderNode` (`packages/d2b-core/src/processes.rs`) | `Process/device--render-node` owned by `Provider/device-gpu` | implemented-and-reachable | +| `ProcessRole::Video` (`packages/d2b-core/src/processes.rs`) | `Process/device--video` owned by `Provider/device-gpu` | implemented-and-reachable | +| `d2b.vms..graphics.*` Nix options | `d2b.zones..resources.` Device spec; settings mirror | generated-or-eval-contract | +| `nixos-modules/components/graphics.nix` | Subset of functionality moves to Guest `runtime-cloud-hypervisor` Nix module (CH arg injection, virglVideo crosvm patch, seccomp policies); Provider controller owns worker spawn | implemented-and-reachable (Nix) | +| `nixos-modules/components/video/guest.nix` | Guest-side `virtio_media` module, CH `--vhost-user-media` arg: stay in Guest `runtime-cloud-hypervisor` Nix module | generated-or-eval-contract | + +No ProcessRole variant or Nix component is removed until the Provider/Process +resource successor integration is live and all current tests pass against the +new model. The removal condition is: `d2b-provider-device-gpu` crate +integration tests pass; GPU and video worker Processes reach `Ready` in a live +Zone; the NixOS eval test `device-gpu-eval.nix` passes; the ProcessRole +disposition contract test passes. + +## Implementation work items + +### ADR046-gpu-001: Create `d2b-provider-device-gpu` crate scaffold + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-gpu-001` | +| Dependency/owner | `ADR046-resources-device` accepted; `ADR046-provider-model-and-packaging` accepted; workspace root must add crate | +| Current source | No v3 source; `packages/d2b-host/src/gpu_argv.rs` and `packages/d2b-host/src/video_argv.rs` (implemented-and-reachable) provide argv generators | +| Reuse source | `packages/d2b-host/src/gpu_argv.rs` (baseline `b5ddbed`), `packages/d2b-host/src/video_argv.rs` (baseline `b5ddbed`) | +| Reuse action | extract | +| Destination | `packages/d2b-provider-device-gpu/` with `src/`, `tests/`, `integration/`, `README.md`; add to workspace `Cargo.toml` members list (alphanumerically sorted) | +| Detailed design | Crate scaffold: `Cargo.toml` with `d2b-host`, `d2b-contracts`, `d2b-provider-toolkit`, `d2b-core` dependencies; `lib.rs` exporting controller binary entry points; `error.rs` with `DeviceGpuError` closed-set enum; placeholder `controller.rs` Primary reuse disposition: `extract`. Preserved source-plan detail: `extract` both argv files into `d2b-provider-device-gpu/src/argv.rs` as re-exports; do not copy logic. | +| Integration | Workspace policy test must pass; crate must build; `src/`, `tests/`, `integration/`, `README.md` must exist | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | `cargo build -p d2b-provider-device-gpu`; workspace policy crate-layout check passes | +| Removal proof | N/A (new crate) | + +### ADR046-gpu-002: Implement async reconcile controller + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-gpu-002` | +| Dependency/owner | ADR046-gpu-001; `ADR-046-resource-reconciliation` implementation present; Provider toolkit `ResourceClient` available | +| Current source | `packages/d2bd/src/usbip_state_machine.rs` (implemented-and-reachable) as reconcile loop pattern reference. GPU/video reconcile state is `ADR-only`. | +| Reuse source | Pattern only: `packages/d2bd/src/usbip_state_machine.rs` (baseline). No code copy. | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-device-gpu/src/{controller.rs,telemetry.rs}` | +| Detailed design | Five triggers: `spec-generation-changed`, `deletion-requested`, `dependency-changed`, `scheduled-observe`, `owned-resource-changed`. Each trigger handler writes optimistic `ResourceMutationBatch`. Status writer in `status.rs`. Async watch task + per-resource reconcile tasks. Independent resources in parallel. Telemetry uses only closed `mode`/`video_sidecar`/`arbitration` semantics; `d2b.zone` and `d2b.provider` remain resource attributes and no Zone/resource-name-derived key is a metric label. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — implement the five-trigger reconcile loop using Provider toolkit async reconciler. | +| Integration | Resource API (ADR046 store) must be present; fake ResourceClient available from Provider toolkit; `tests/combined_reconcile.rs` validates trigger dispatch | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | `cargo test -p d2b-provider-device-gpu --test combined_reconcile`; all five trigger handlers must reach their expected output state; structural metric descriptor test asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys plus GPU Device/Zone-name canary absence while preserving `d2b.zone` resource attributes | +| Removal proof | Current ProcessRole::Gpu/Video/GpuRenderNode retained until this test passes; see ProcessRole disposition table | + +### ADR046-gpu-003: DRM sysfs probe and observe scheduler + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-gpu-003` | +| Dependency/owner | ADR046-gpu-002 | +| Current source | `nixos-modules/assertions.nix` x86_64-linux guard; `packages/d2b-core/src/processes.rs` ProcessRole::Gpu/GpuRenderNode; no existing sysfs probe module | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-gpu/src/probe.rs` | +| Detailed design | Call `GpuEffectPort::probe_drm_device(selector)` on each `scheduled-observe` trigger; the effect port resolves device presence against the trusted device table and returns a presence/health result without exposing raw sysfs or device paths to the controller. Three-strike failure counter; `observe_interval_secs` (10–60, default 30); emit `DevicePresent` condition and update `lastProbedAt`. | +| Integration | `scheduled-observe` trigger from reconcile loop calls `probe::check_drm_device` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | `tests/conformance.rs` contains probe-mock path; `cargo test` passes | +| Removal proof | N/A (new module) | + +### ADR046-gpu-004: Exclusive/shared arbitration enforcement + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-gpu-004` | +| Dependency/owner | ADR046-gpu-002 | +| Current source | `packages/d2b-core/src/bundle_resolver.rs` `validate_graphics_vm_invariants` (assertion guard) — `ADR-only` for resource-level arbitration | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-gpu/src/arbitration.rs` | +| Detailed design | On `spec-generation-changed` and each new claim: check `arbitration` vs `maxConcurrentClaims` vs current `holderRefs` length. Exclusive: reject any second claim with `ClaimConflict` condition, set requesting Device phase `Degraded`. Shared render-node: accept up to `maxConcurrentClaims`. Admission: `shared + renderNodeOnly=false` fails with `shared-arbitration-requires-render-node-only`. | +| Integration | Tested by `tests/arbitration_conflict.rs`; integration fixture `render_node_shared/` | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | `cargo test -p d2b-provider-device-gpu --test arbitration_conflict`; `cargo test -p d2b-provider-device-gpu --test render_node_enforcement` | +| Removal proof | N/A (new module) | + +### ADR046-gpu-005: GPU and render-node worker Process management + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-gpu-005` | +| Dependency/owner | ADR046-gpu-002; `ADR046-components-processes-and-sandbox` (Provider/system-minijail present and able to handle Process resources with `gpu-worker`/`render-node-worker` templates) | +| Current source | `packages/d2b-host/src/gpu_argv.rs` (implemented-and-reachable); `packages/d2b-core/src/bundle_resolver.rs` lines 1888–1894 (device token set); `packages/d2b-core/src/processes.rs` `ProcessRole::Gpu`, `ProcessRole::GpuRenderNode` (implemented-and-reachable); `nixos-modules/minijail-profiles.nix` gpu/gpu-render-node profiles (implemented-and-reachable) | +| Reuse source | `packages/d2b-host/src/gpu_argv.rs` (baseline `b5ddbed`): `GpuArgvInput`, `GpuParams`, `GpuContextType`, `GpuDisplayConfig`; `packages/d2b-core/src/bundle_resolver.rs` device token constant comment | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-device-gpu/src/worker_gpu.rs` | +| Detailed design | Build and commit `Process` resource record with `template: gpu-worker` or `template: render-node-worker`; set `sandbox.seccompClass` (`w1-gpu` or `w1-gpu-render-node`), `sandbox.userNamespace: {mappingClass: process-principal-root}` (uid/gid resolved privately by core from signed worker template — controller does NOT write numeric values), `sandbox.namespaceClasses`, `sandbox.capabilityClasses=[]`, `sandbox.startRoot=false`; set `deviceUsage[{deviceRef,access,purpose}]`, `networkUsage: null`, `endpoints[{name,transport,purpose}]`, `budget` (including `pids` and `fds` bounded limits), `readiness` (with `class`, `initialDelay`, `timeout`, `failureThreshold`, `successThreshold`), and `restartPolicy` (with `class`, `backoffBase`, `backoffMax`, `backoffMultiplier`, `maxRestarts`, `resetAfter`). Provider/system-minijail validates and resolves the LaunchTicket and sends effect requests via `MinijailProcessEffectPort`; the core EffectPort adapter routes them to the **privileged broker** which performs `SpawnRunner`, `OpenDevice`, `clone3`, `uid_map`/`gid_map` writes, and fd transfer — the device-gpu controller does not have execution authority or fd access. `crossDomainTrusted` gating: the signed descriptor is static; `crossDomainTrusted` is projected from the Device setting into the LaunchTicket by Provider/system-minijail, which omits `GpuContextType::CrossDomain` from runtime argv when false. Primary reuse disposition: `adapt`. Preserved source-plan detail: `extract` argv builder logic into `argv.rs` as re-export from `d2b-host` (used by Provider/system-minijail at LaunchTicket resolution time; the signed component descriptor is static and is not rewritten per Device); `adapt` device allowlist token set from `bundle_resolver.rs` into `worker_gpu.rs` `GPU_DEVICE_ALLOWLIST` constant for `deviceUsage` population. | +| Integration | `integration/gpu_worker_start/`; `integration/render_node_shared/`; `packages/d2b-contract-tests/tests/minijail_gpu.rs` (reused existing test) | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | `cargo test -p d2b-provider-device-gpu`; `cargo test -p d2b-contract-tests --test minijail_gpu` continues to pass | +| Removal proof | `ProcessRole::Gpu` and `ProcessRole::GpuRenderNode` removed from `processes.rs` only after both integration tests pass and the ProcessRole disposition contract test confirms zero remaining references | + +### ADR046-gpu-006: Video decoder Process management and wire-contract check + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-gpu-006` | +| Dependency/owner | ADR046-gpu-005 (video depends on GPU Process being Ready) | +| Current source | `packages/d2b-host/src/video_argv.rs` (implemented-and-reachable): `VideoArgvInput`, `VideoBackend`, `wire_contract_snapshot()`; `packages/d2b-contract-tests/tests/video_binary_contract.rs` (implemented-and-reachable); `packages/d2b-contract-tests/tests/minijail_swtpm_video.rs` video section (implemented-and-reachable); `nixos-modules/minijail-profiles.nix` video profile (implemented-and-reachable) | +| Reuse source | `packages/d2b-host/src/video_argv.rs` (baseline `b5ddbed`): argv generator, wire-contract constants, `wire_contract_snapshot()` | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-device-gpu/src/worker_video.rs`, `tests/wire_constant_snapshot.rs` | +| Detailed design | Controller creates `Process/device--video` only after `GpuWorkerReady=True`. `worker_video.rs` builds `VideoArgvInput` from resolved device spec and signed descriptor binary path. Validates `wire_contract_snapshot()` matches committed golden at startup; fails closed if mismatch (error `device-wire-contract-mismatch`). NVIDIA device gating: include `nvidia-ctl`, `nvidia-device`, `nvidia-uvm` tokens in `deviceUsage[]` entries only when `videoNvidiaDecode=true`; the **privileged broker** opens the fds when executing the effect request from the core EffectPort adapter. Distinct allocator-assigned principal enforced by LaunchTicket (internal invariant; not expressed in the resource spec); `template: video-worker` descriptor declares no Wayland/audio endpoint capability. `sandbox.seccompClass: w1-video`; `sandbox.namespaceClasses` includes `pid`; `userNamespace: null` (explicit, tested invariant). Primary reuse disposition: `adapt`. Preserved source-plan detail: `extract` argv generator (re-export from `argv.rs`); `copy-unchanged` wire-contract constants into `tests/wire_constant_snapshot.rs` golden comparison. | +| Integration | `integration/video_dependency/`; `packages/d2b-contract-tests/tests/video_binary_contract.rs` (reused); `packages/d2b-contract-tests/tests/minijail_swtpm_video.rs` video section (reused) | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | `cargo test -p d2b-provider-device-gpu --test video_dependency`; `cargo test -p d2b-provider-device-gpu --test wire_constant_snapshot`; `cargo test -p d2b-contract-tests --test video_binary_contract` continues to pass | +| Removal proof | `ProcessRole::Video` removed from `processes.rs` only after `integration/video_dependency/` passes and the video Process reaches `Ready` in a live Zone | + +### ADR046-gpu-007: Nix option migration and eval validation + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-gpu-007` | +| Dependency/owner | ADR046-gpu-001; ADR 0046 Zone Nix emitter wired; `ADR-046-nix-configuration` Nix emitter present | +| Current source | `nixos-modules/options-realms-workloads.nix` `d2b.vms..graphics.*` options (generated-or-eval-contract); `nixos-modules/assertions.nix` graphics assertions; `nixos-modules/components/graphics.nix` (host-side); `nixos-modules/components/video/guest.nix` (guest-side) | +| Reuse source | Settings schema field names/defaults/bounds from `nixos-modules/options-realms-workloads.nix` options documentation | +| Reuse action | adapt | +| Destination | `nixos-modules/assertions.nix` (new GPU Device eval assertions); `tests/unit/nix/cases/device-gpu-eval.nix` (new Nix eval case); committed settings schema `docs/reference/schemas/v3/providers/device-gpu.settings.json` | +| Detailed design | Eval assertions as documented in § Nix configuration / Eval-time assertions. Canonical JSON golden as documented. Settings schema drift gate via `make test-drift`. `d2b.vms..graphics.*` options are deprecated (emit deprecation warning) until a transition generation removes them; they are not removed in the same commit that adds the Device spec option. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — map old `d2b.vms..graphics.*` fields to `d2b.zones..resources.` Device spec settings fields; add eval assertions. | +| Integration | `nix flake check`; `tests/unit/nix/cases/device-gpu-eval.nix`; `make test-drift` | +| Data migration | Consumer config migration guide: replace `d2b.vms..graphics.enable = true` with a Device resource declaration. Old options emit deprecation warnings, not hard failures, during the transition window. | +| Validation | `nix-unit tests/unit/nix/cases/device-gpu-eval.nix`; `make test-drift`; `make test-flake` | +| Removal proof | `d2b.vms..graphics.*` options removed only after migration guide ships and the deprecation warning has been live for one minor release | + +### ADR046-gpu-008: Assert status-first Provider state + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-gpu-008` | +| Dependency/owner | ADR046-gpu-001; D087 status-first state model present in the foundational ADR-046 specs | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-gpu/` component descriptor; controller/status tests | +| Detailed design | Do **not** declare a controller Provider state Volume. The device-gpu component descriptor declares an empty ProviderStateSet; controller and worker Process templates contain no `/state` mount. Bounded non-secret operational state is published to Device/Provider status and the core Operation ledger. GPU has no Device-payload Volume; render-node access remains a Device attachment resolved by LaunchTicket and broker policy. Primary reuse disposition: `create`. Preserved source-plan detail: `new` — status-first state assertions in the component descriptor and controller tests. | +| Integration | `tests/status_state.rs`; `integration/gpu_worker_start/` verifies controller startup is gated by resource dependencies and status writer authority, not by a Provider state Volume | +| Data migration | None — no Provider state Volume exists to migrate. | +| Validation | `cargo test -p d2b-provider-device-gpu --test status_state`; component descriptor golden has no Provider state Volume declaration; controller Process template has no `/state` mount; ProviderStateSet query is empty; status/core-ledger fields carry bounded operational observations | +| Removal proof | `StorageRoot`/`StoragePathSpec` lifecycle tracking entries for GPU/video roles in `d2b-core/src/storage.rs` removed after Device/Process status-first lifecycle and restart-adoption integration tests pass in a live Zone | + +### ADR046-gpu-009: Provider `README.md` + + +| Field | Value | +| --- | --- | +| Work item ID | `ADR046-gpu-009` | +| Dependency/owner | ADR046-gpu-001 | +| Current source | None; new file | +| Reuse source | None | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-gpu/README.md` | +| Detailed design | Must include: Provider identity, supported ResourceTypes, controller/service/worker binary descriptions, placement (Host, system domain), dependencies (system-minijail, volume-local, observability-otel), RBAC roles, security model summary, state/telemetry contract, build command (`cargo build -p d2b-provider-device-gpu`), test commands (`cargo test -p d2b-provider-device-gpu`), integration command (`make test-integration`), hardware test note (see `integration/README.md`), standalone-repository consumption stub. | +| Integration | Workspace policy checks for `README.md` presence | +| Data migration | None — full d2b 3.0 reset; no prior state to migrate | +| Validation | `make test-policy` (workspace crate layout policy check) | +| Removal proof | N/A (new file) | + +## Current-code fit + +| Item | Treatment | +| --- | --- | +| Current anchor | **GPU/video process roles**: `packages/d2b-core/src/processes.rs` `ProcessRole::Gpu`, `ProcessRole::GpuRenderNode`, `ProcessRole::Video` (`implemented-and-reachable`). **GPU argv**: `packages/d2b-host/src/gpu_argv.rs` `GpuArgvInput`, `GpuParams`, `GpuContextType`, `GpuDisplayConfig` (`implemented-and-reachable`). **Video argv + wire constants**: `packages/d2b-host/src/video_argv.rs` `VideoArgvInput`, `VideoBackend`, `wire_contract_snapshot()`, all `VHOST_USER_MEDIA_*` constants (`implemented-and-reachable`). **GPU device token set**: `packages/d2b-core/src/bundle_resolver.rs` lines 1882–1894, ProcessRole::Gpu/GpuRenderNode arm (`implemented-and-reachable`). **Minijail profiles**: `nixos-modules/minijail-profiles.nix` gpu, video, gpu-render-node profiles with device binds, seccomp refs, user NS config (`implemented-and-reachable`). **Broker ops**: `packages/d2b-contracts/src/broker_wire.rs` `RunnerRole::Gpu`, `RunnerRole::Video` (`implemented-and-reachable`). **Nix host graphics**: `nixos-modules/components/graphics.nix` (crosvm wrapper, virglVideo patch, CH rev guard, crossDomainTrusted enforcement) (`implemented-and-reachable`). **Nix guest video**: `nixos-modules/components/video/guest.nix` (`virtio_media` module, CH `--vhost-user-media` arg) (`generated-or-eval-contract`). **Contract tests**: `packages/d2b-contract-tests/tests/minijail_gpu.rs`, `minijail_swtpm_video.rs`, `video_binary_contract.rs` (`implemented-and-reachable`). **Provider crate**: `packages/d2b-provider-device-gpu/` (`ADR-only`). | +| Evidence class | GPU/video process role enum and argv generators: `implemented-and-reachable`. GPU device token set and minijail profiles: `implemented-and-reachable`. Broker RunnerRole::Gpu/Video: `implemented-and-reachable`. CH/crosvm version guard: `implemented-and-reachable`. Video wire-contract constants: `implemented-and-reachable`. Device ResourceType schema: `ADR-only`. Provider crate and reconcile loop: `ADR-only`. | +| Behavior retained | GPU device allowlist token set (kvm/dri/udmabuf/nvidia*); video wire-contract constants frozen; distinct allocator-assigned video vs GPU worker principal (LaunchTicket invariant; private broker state); render-node fd pre-opened by the **privileged broker** and inherited via private fd-inheritance protocol; user-namespace zero-host-caps (ADR 0021); no Wayland/audio sockets for video role; EndpointRef-based cross-domain trust projected from Device setting into LaunchTicket at resolution time; argv builder omits CrossDomain from runtime args when false; NVIDIA opt-in gating for video; CH/crosvm rev compatibility guard; `videoSidecar` + `videoNvidiaDecode` mutual independence; `virglVideo` + `videoSidecar` mutual exclusion. | +| Required delta | `d2b-provider-device-gpu` crate, async reconcile controller, Device ResourceType schema for GPU settings, Provider resource registration, process-name templates from Device UID, wire-contract check at startup, shared render-node arbitration enforcement, generation-based lifecycle via Zone resource plane; D087 status-first state assertion in the component descriptor — no Provider state Volume, empty ProviderStateSet, no controller `/state` mount, bounded operational state in status and Operation rows (ADR046-gpu-008). | +| Reuse path | Re-export `gpu_argv.rs` and `video_argv.rs` from `d2b-host` unmodified. Adapt device token set constant from `bundle_resolver.rs` into `worker_gpu.rs` `GPU_DEVICE_ALLOWLIST` for `deviceUsage` population. Adapt minijail profile field names to `Process` resource spec fields. uid/gid mapping is resolved privately by core from the signed worker template — the device-gpu controller does not write hostUid/hostGid into any resource spec field. | +| Replacement/deletion | `ProcessRole::Gpu`, `ProcessRole::GpuRenderNode`, `ProcessRole::Video` in `processes.rs` retained until Provider integration parity. `d2b.vms..graphics.*` Nix options deprecated (with warning) until consumer migration window closes. Nix `components/graphics.nix` host-side worker-spawn logic removed after `worker_gpu.rs` is live; CH arg injection and crosvm patches stay in Guest runtime Nix module. `StorageRoot`/`StoragePathSpec` entries for GPU/video roles in `d2b-core/src/storage.rs` removed after status-first Device/Process lifecycle integration passes. | +| Feasibility proof | GPU worker process broker token set: `packages/d2b-contract-tests/tests/minijail_gpu.rs` (existing, reachable). Video wire-contract constant snapshot: `packages/d2b-host/src/video_argv.rs` `wire_contract_snapshot()` + `tests/video_binary_contract.rs` (existing, reachable). Render-node user-NS propagation: `packages/d2b-core/src/bundle_resolver.rs` test `gpu_render_node_user_namespace_propagates_to_resolved_intent` (existing, reachable). | +| Future owner | `packages/d2b-provider-device-gpu/` crate; work items ADR046-gpu-001 through ADR046-gpu-009 | diff --git a/docs/specs/providers/ADR-046-provider-device-security-key.md b/docs/specs/providers/ADR-046-provider-device-security-key.md new file mode 100644 index 000000000..369e45790 --- /dev/null +++ b/docs/specs/providers/ADR-046-provider-device-security-key.md @@ -0,0 +1,2490 @@ +# ADR 0046 Provider dossier: device-security-key + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-device-security-key` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 6 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `packages/d2b-provider-device-security-key/`, `SecurityKeyService`/`SecurityKeyBinding` controllers, physical Device integration, Nix resource emitters | +| Depends on | `ADR-046-resources-device`, `ADR-046-resource-object-model`, `ADR-046-primitive-resource-composition`, `ADR-046-resource-api-and-authorization`, `ADR-046-resource-reconciliation`, `ADR-046-provider-model-and-packaging`, `ADR-046-components-processes-and-sandbox`, `ADR-046-componentsession-and-bus`, `ADR-046-telemetry-audit-and-support`, `ADR-046-nix-configuration`, `ADR-046-provider-state` | +| Supersedes | `ProcessRole::SecurityKeyFrontend` daemon-internal accept loop (`packages/d2bd/src/security_key.rs`), `nixos-modules/components/security-key-guest.nix` untracked guest `d2b-sk-frontend.service` unit | + +## Purpose + +This dossier exhaustively specifies the `device-security-key` Provider in d2b 3.0. +It covers the local physical hidraw `Device`; the provider-neutral +`security-key.d2bus.org.SecurityKeyService` authority/projection resource; +the provider-neutral `security-key.d2bus.org.SecurityKeyBinding` consumer-intent +resource, initially implemented by `Provider/device-security-key`; the +one-ceremony-at-a-time fair lease model; the unprivileged Host relay +Process and Guest frontend Process; the broker-only fd handoff path; +ComponentSession-protected service and stream surfaces with descriptor validation; +lifecycle, disconnect, and cancel semantics; RBAC and security invariants; root +configuration, the standard Device schema, provider-neutral Service/Binding +schemas, and their signed Provider extensions; status, conditions, and phase +transitions; error classes; audit and OTEL placement; Nix declaration and +eval-time constraints; async reconcile loop triggers; all reuse work items mapping +baseline code to v3 destinations; required tests; and the current-code removal +sequence. + +No raw key data, physical hidraw node path, sysfs bus path, vendor/product +string concatenation, or device serial number ever appears in any public or +broker-wire surface. All external claims use opaque stable labels or +session-scoped digests only. + +## Identity + +```text +Provider/device-security-key +``` + +Crate: `packages/d2b-provider-device-security-key/` + +Provider implements: + +- standard `Device` for the **real local physical hidraw backing only** + (`deviceClass: physical`, `busClass: hidraw`); +- provider-neutral + `security-key.d2bus.org.SecurityKeyService` for the owner-Zone authority and + the core-owned local import projection; and +- provider-neutral + `security-key.d2bus.org.SecurityKeyBinding` for one consuming + Guest/user attachment and its policy. + +`Device` is never an import projection and never represents the UHID frontend. +`ResourceExport` targets `SecurityKeyService`; `ResourceImport` creates one local +projection `SecurityKeyService`; ordinary consumer intent is always a +Nix/operator-authored `SecurityKeyBinding` that references a same-Zone Service. +The base Service and Binding schemas carry only semantic security-key authority, +target, attachment, and lifecycle fields. `spec.provider` and `status.provider` +select and describe the initial `Provider/device-security-key` implementation; +physical Device, hidraw, CTAPHID, relay, UHID, queue, and ceremony details do not +enter the provider-neutral base. No provider-named ResourceType alias is +registered or accepted. A future Provider implements these same exact semantic +types through a different `providerRef` and its own signed extension; it does not +mint another Service or Binding type name. + +## Resource catalog and cardinality + +| ResourceType | Author | Cardinality and role | +| --- | --- | --- | +| `Device` | Nix/operator | One real local physical hidraw backing per selector. It owns discovery/presence status only and is referenced by an owner-Zone authority Service. It is not exported and is not projected. | +| `security-key.d2bus.org.SecurityKeyService` | Nix/operator for `mode: authority`; Core projection factory for `mode: projection` | Exactly one authority Service per opaque security key across the Host authority scope. Its base carries the semantic D097 `AuthorityDescriptor`; the authored owner extension references the same-Zone physical `Device` and local relay `Endpoint`. A projection has `ownerRef: ResourceImport/`, `providerRef`, semantic base/import fields, and no `spec.provider`, physical Device, or hidraw authority. | +| `security-key.d2bus.org.SecurityKeyBinding` | Nix/operator only | One per consuming Guest/user attachment/policy. Its base references one same-Zone `SecurityKeyService` plus the Guest/User target. The initial Provider extension realizes its UHID frontend `Process` and private frontend `Endpoint`. | +| `Process`, `Endpoint` | Controllers/adapters | Owned realization children. They are not substitute service/binding resources and are never authored as ceremony records. | + +Individual CTAP ceremonies, `LeaseId` values, CIDs, queue entries, and cancellation +handles are bounded high-churn session records. They are **not Resource objects**. + +## Crate layout + +```text +packages/d2b-provider-device-security-key/ + src/ + tests/ + integration/ + README.md +``` + +Workspace policy rejects the crate if any of `src/`, `tests/`, `integration/`, +or `README.md` is absent. Detailed module and test file layout is normative in the +respective sections below; the directory names are the fixed root contract. + +## Root config schema + +Root configuration appears in `Provider/device-security-key` `spec.config`, +validated against the signed JSON Schema in the Provider package descriptor. + +| Field | Type | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | +| `devices` | list | `[]` | 0–16 entries | Per-selector entries; may be empty when Zone configures no security-key Devices | +| `devices[].label` | string | — | `^[a-z][a-z0-9-]{0,62}$` | Stable operator-defined selector label; unique within the config | +| `devices[].vendorId` | uint16 | — | 0x0001–0xFFFE | USB vendor ID | +| `devices[].productId` | uint16 | — | 0x0001–0xFFFE | USB product ID | +| `devices[].serial` | string \| null | `null` | ≤ 128 UTF-8 chars, no NUL | Optional serial filter; null matches any serial | +| `sessionRingSize` | uint | `32` | 8–256 | Maximum recent ceremony-session records per SecurityKeyBinding; oldest entry evicted when full; records are not Resources | +| `leaseTimeoutSecs` | uint | `120` | 30–3600 | Provider ceiling/default for per-Binding ceremony timeout; preserves baseline `CEREMONY_TIMEOUT` | +| `queueWaitTimeoutSecs` | uint | `15` | 5–120 | Maximum wait for a busy lease before the relay returns `ERR_CHANNEL_BUSY`; maps to `QUEUE_WAIT_TIMEOUT` | + +**Prohibited fields:** `devices[].hidrawPath`, `devices[].sysfsPath`, any field +containing a raw filesystem path. The Provider derives the physical node from +the trusted bundle device table at runtime; no path is accepted in config. + +**Duplicate labels** are rejected at Provider spec admission. Labels that do not +match `^[a-z][a-z0-9-]{0,62}$` are rejected. An empty `devices` list is valid; +the controller remains installed but creates no Device sub-resources. + +## Physical Device spec contract + +Normative D089 spec layering: Device base fields are ResourceType base +`spec.*` fields, including `spec.providerRef`, `deviceClass`, +`inventory.selector`, attachments, and arbitration. This Provider's +desired-only extension is the canonical `spec.provider = { schemaId: +"device-security-key.d2bus.org/Device/spec", schemaVersion, settings }` +envelope; it is manifest-registered/signed, strict deny-unknown, bounded, versioned +and digested, validated against `spec.providerRef` at Nix build and API +admission, implementation-only, and may not shadow base fields. Shared fields +are promoted to the Device base. The Provider implements the exact base Device +spec/status version/fingerprint, accepts the canonical minimal valid base Spec, +and rejects unsupported optional base capabilities only through its signed +capability matrix and provider-neutral `unsupported-capability`. +`spec.provider` aligns with `status.provider`; generic CLI/controllers operate on +the base spec and base status only. No secret bytes are allowed +in any spec layer, and no credential material is allowed in +`spec.provider.settings`. + +```yaml +spec: + providerRef: Provider/device-security-key + deviceClass: physical + arbitration: exclusive + maxConcurrentClaims: 1 + inventory: + selector: + busClass: hidraw + label: yubikey-primary # must resolve to a label in Provider root config + vendorId: "1050" # 4 lower-cased hex digits + productId: "0407" # 4 lower-cased hex digits + serial: null # optional; null matches any serial + provider: + schemaId: device-security-key.d2bus.org/Device/spec + schemaVersion: "1.0.0" + settings: {} +``` + +The `inventory.selector.label` must match exactly one entry in `Provider/device-security-key` +`spec.config.devices[].label`. At admission, the controller verifies the label is +present in the Provider's installed config. An unresolvable label fails Device +spec admission with `device-not-found` condition. + +`arbitration` and `maxConcurrentClaims` for this Provider are always `exclusive` +and `1`. Any other value is rejected at admission. + +`busClass` must be `hidraw`. Any other `busClass` is rejected at admission. + +The physical `Device` does not identify a consuming Guest, own a relay/frontend, +or carry cross-Zone import metadata. Its only consumers are same-Zone authority +`SecurityKeyService` resources. An imported security key therefore never creates a +`Device`, and no projection has a `deviceClass`, `inventory.selector`, or +DeviceGrant. + +## `SecurityKeyService` spec and status contract + +The provider-neutral ResourceType has a strict discriminated `spec.mode` union. +Its base declares only semantic security-key authority. The initial +`Provider/device-security-key` extension names the physical backing and relay: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: security-key.d2bus.org.SecurityKeyService +metadata: + name: yubikey-primary + zone: devices +spec: + providerRef: Provider/device-security-key + mode: authority + authority: + authorityScope: physical-device + authorityKeyClass: semantic-security-key + cardinality: zero-or-one + arbitration: exclusive + authorityRef: security-key.d2bus.org.SecurityKeyService/yubikey-primary + duplicateConflict: security-key-authority-conflict + updateStrategy: drain-recycle + exportability: explicit-export + provider: + schemaId: device-security-key.d2bus.org/SecurityKeyService/spec + schemaVersion: "1.0.0" + settings: + deviceRef: Device/yubikey-primary + relayEndpointRef: Endpoint/yubikey-primary-ctaphid-relay + authorityDerivation: physical-fido-selector + ownerProof: service-and-relay-process-identity + fairnessPolicy: bounded-fifo +status: + phase: Pending | Ready | Degraded | Failed | Unknown + observedGeneration: 1 + conditions: + - type: BackingAuthorityReady + status: "True" + reason: backing-authority-claimed + resource: + authority: + available: true + holderCount: 0 + arbitration: exclusive + opaqueOwnerDigest: sha256: + provider: + providerRef: Provider/device-security-key + schemaId: device-security-key.d2bus.org/SecurityKeyService/status + schemaVersion: "1.0.0" + observedProviderGeneration: 1 + details: + physicalBackingClaim: ready + relayProcessRef: Process/device--sk-relay + relayEndpointRef: Endpoint/yubikey-primary-ctaphid-relay + relayReady: true + queueDepth: 0 + observedDeviceGeneration: 1 + observedEndpointGeneration: 1 + lastCeremonyOutcome: success | timeout | cancelled | busy | error | null +``` + +The base `authorityKeyClass: semantic-security-key` identifies the +service-specific security-key authority. The Provider extension's +`authorityDerivation: physical-fido-selector` selects its initial derivation, +but the spec never carries the trusted bundle `device_token`, a raw digest +chosen by the caller, a path, serial, address, or fd. In addition, after trusted +physical-USB identity resolution, Core mandatorily derives the +`physical-usb-backing/v1` digest and claims +`(Host, physical-usb-backing, opaqueKeyDigest)`. Every USB and security-key +Provider backed by the same token submits that identical tuple; neither this +service-specific authority nor a Provider-private class can replace it. Core +admits both required claims before it permits the relay DeviceGrant/open. The +Provider setting `relayEndpointRef` is a reserved desired-child ref: the Service +controller creates that same-name owned Endpoint and the Service remains +`Pending` until it is Ready. + +The projection branch is Core-only: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: security-key.d2bus.org.SecurityKeyService +metadata: + name: yubikey-primary + zone: work + ownerRef: ResourceImport/yubikey-primary +spec: + providerRef: Provider/device-security-key + mode: projection +status: + phase: Pending | Ready | Degraded | Failed | Unknown + observedGeneration: 1 + resource: + import: + leaseState: pending | bound | degraded | revoked + remoteGeneration: 7 + remoteFingerprint: sha256: + provider: + providerRef: Provider/device-security-key + schemaId: device-security-key.d2bus.org/SecurityKeyService/status + schemaVersion: "1.0.0" + observedProviderGeneration: 1 + details: + relayEndpointRef: Endpoint/yubikey-primary-import-ctaphid + observedEndpointGeneration: 3 +``` + +A projection permits only `providerRef`, semantic base/import fields, and +ResourceImport ownership. It rejects `spec.provider`, `deviceRef`, `authority`, +physical selectors, and DeviceGrant fields. Core creates and owns it by +invoking the signed projection factory, always with `metadata.ownerRef: +ResourceImport/`. Routing derives from that signed local Provider +descriptor, `providerRef`, and the ResourceImport record; the Provider may +report local import-route Endpoint/lease observations only in +`status.provider`. Nix and ordinary API callers cannot create +`mode: projection`. + +The signed Provider descriptor's D096 factory publishes +`serviceType: security-key.d2bus.org.SecurityKeyService`, the SHA-256 +`projectionSchemaFingerprint` of the canonical committed projection schema, and +a `factoryFingerprint` over the semantic factory fields plus projection-protocol +version. Provider and export/import adapter identity are authenticated +separately by the signed descriptor and never affect the semantic factory +fingerprint. The authority relay Endpoint and projection import-route Endpoint +remain Service-owned implementation children; neither is an Export or Import +field. + +## `SecurityKeyBinding` spec and status contract + +One Binding expresses one consuming Guest/user attachment and policy. It is authored +by Nix or an authorized operator after either an authority Service or projection +Service exists in the **same Zone**: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: security-key.d2bus.org.SecurityKeyBinding +metadata: + name: corp-vm-yubikey + zone: work + ownerRef: Guest/corp-vm +spec: + providerRef: Provider/device-security-key + serviceRef: security-key.d2bus.org.SecurityKeyService/yubikey-primary + target: + guestRef: Guest/corp-vm + userRef: User/alice + policy: + enabled: true + provider: + schemaId: device-security-key.d2bus.org/SecurityKeyBinding/spec + schemaVersion: "1.0.0" + settings: + ceremonyTimeoutSecs: 120 + queueWaitTimeoutSecs: 15 +status: + phase: Pending | Ready | Degraded | Failed | Unknown + observedGeneration: 1 + resource: + attachment: detached | connecting | ready | degraded + provider: + providerRef: Provider/device-security-key + schemaId: device-security-key.d2bus.org/SecurityKeyBinding/status + schemaVersion: "1.0.0" + observedProviderGeneration: 1 + details: + frontendProcessRef: Process/binding--sk-frontend + frontendEndpointRef: Endpoint/binding--sk-frontend + frontendReady: true + observedServiceGeneration: 1 + activeSession: false + queued: false + lastCeremonyOutcome: success | timeout | cancelled | busy | error | null + providerDiagnostic: null +``` + +`serviceRef`, `target.guestRef`, and `target.userRef` are local ResourceRefs. The +User must belong to the target Guest. Exactly one Binding may exist for the tuple +`(serviceRef, guestRef, userRef, attachment-policy-key)`; duplicate intent is +rejected deterministically. Base Binding status carries only semantic attachment +state. The bounded, non-secret implementation observations live in +`status.provider.details`; neither layer may carry CTAP bytes, credential IDs, +PINs, signatures, raw CIDs, a `LeaseId`, or an unbounded ceremony history. +Individual ceremony records remain in the bounded relay/controller session table +and Core Operation/audit surfaces; no `SecurityKeySession` ResourceType exists. +`SecurityKeyState` is neither a ResourceType nor a compatibility alias. Consumer +state exists only in `SecurityKeyBinding.status`. + +## hidraw discovery and identity + +### Discovery contract + +Physical FIDO/hidraw device discovery is split between the Core/broker adapter +(which owns sysfs matching, device-token resolution, and any related audit) and the +Provider controller (which receives only opaque `InventoryObservation` results through +the injected `SecurityKeyEffectPort`). The Provider process never reads `/sys`, +opens hidraw device nodes, manages OFD lock paths, calls broker operations, or +receives raw device paths or UIDs. + +### Bundle device table (trusted source — Core/adapter) + +During NixOS activation, the Provider's activation component records +`(zone, device_label)` → `device_token` mappings in the private bundle device table. +The `device_token` is an opaque reference used exclusively by the Core/adapter; it +is never surfaced to the Provider crate or any public interface. + +### Core LaunchTicket DeviceGrant (relay Process startup) + +When Core launches the relay Process, it resolves the `deviceUsage` entry +`{deviceRef: Device/, access: exclusive}` into a DeviceGrant. Core: + +1. Looks up `device_token` from the bundle device table for the Zone + label. +2. Opens the referenced node with `O_RDWR | O_NONBLOCK | O_NOFOLLOW` (no iterative + sysfs scan; targeted open using `device_token`). +3. Revalidates the opened fd: `fstat` → `S_IFCHR`; `ioctl(HIDIOCGRDESC)` → FIDO + usage page `0xF1D0`; `ioctl(HIDIOCGRAWINFO)` → vendor/product match. +4. Passes the pre-opened fd to the relay as part of its LaunchTicket (inherited fd; + no path in the relay's filesystem namespace). +5. Holds the corresponding OFD lease for the relay's process lifetime. Lease releases + automatically when the relay exits for any reason. + +No path or UID crosses any wire. No Provider code participates in steps 1–5. + +### Identity and FIDO usage page + +A hidraw node is FIDO-class if its report descriptor contains usage page `0xF1D0` +(`[0x06, 0xD0, 0xF1]` — usage-page item type 0x04, 2-byte little-endian `[0xD0, +0xF1]`). This check is performed by Core during revalidation. It is the sole +hardware-identity signal; Core does not verify attestation, AAGUID, or authenticator +model at open time. + +### Probe semantics + +The Provider controller schedules `scheduled-observe` for physical Devices at the configured interval +(default 30 s, max 60 s). On each trigger the controller calls: + +```rust +effect_port.observe_inventory(&device_id, &policy_id).await +``` + +where `device_id` and `policy_id` are the opaque values injected by Core at +controller startup. The injected Core adapter performs any sysfs/udev observation +required; the controller receives only `InventoryObservation { present, fido_confirmed }`. +Neither `DeviceId` nor `ObservationPolicyId` is logged anywhere. No zone string, +label string, sysfs access, device open, or broker call occurs inside the +Provider process. + +Probe outcome handling: + +1. On success (`present=true, fido_confirmed=true`): reset consecutive-failure + counter; `DevicePresent=True`; if phase was `Unknown`/`Degraded` due to probe + failures, transition to `Ready` (if all other conditions clear). +2. On first failure: increment counter; transition to `Unknown` if currently `Ready`. +3. On second failure: remain `Unknown`. +4. On third consecutive failure: `DevicePresent=False`; transition to `Degraded`. + Dependent Guests receive `dependency-changed` through the normal watch path. +5. When device returns: reset counter; `DevicePresent=True`; retransition to `Ready`. + +## SecurityKeyEffectPort + +`SecurityKeyEffectPort` is an injected async trait through which the Provider +controller performs all inventory observations that cross outside its own process +boundary. The trait and its associated opaque types live in a **neutral contract +crate** (`d2b-contracts`) so both the Provider crate and the Core adapter crate +depend on it without a circular edge. The Provider crate itself never reads sysfs, +opens device nodes, manages OFD lock paths, calls broker operations, or receives +raw device UIDs, paths, or node names. + +```rust +// In d2b-contracts (neutral; no Provider-crate or Core-crate dependency) + +use std::fmt; + +/// Opaque stable device identity token issued by Core at controller startup. +/// Custom Debug impl redacts content so this type never appears in logs. +#[derive(Clone, PartialEq, Eq)] +pub struct DeviceId(/* opaque bytes; not a path, label, or UID string */); + +impl fmt::Debug for DeviceId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("DeviceId([redacted])") + } +} + +/// Opaque observation policy identity issued by Core at controller startup. +/// Encodes zone, interval, and selector context — Core resolves these; the +/// Provider never sees zone or label strings. Custom Debug redacts content. +#[derive(Clone, PartialEq, Eq)] +pub struct ObservationPolicyId(/* opaque bytes */); + +impl fmt::Debug for ObservationPolicyId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ObservationPolicyId([redacted])") + } +} + +/// Opaque result of a single inventory observation. No path, UID, or node name. +pub struct InventoryObservation { + pub present: bool, // true iff a matching FIDO hidraw node exists + pub fido_confirmed: bool, // true iff FIDO usage page 0xF1D0 confirmed +} + +#[async_trait] +pub trait SecurityKeyEffectPort: Send + Sync { + /// Observe whether a FIDO hidraw node matching this device identity is + /// currently present. Core resolves inventory; the Provider only sees the + /// opaque InventoryObservation result. Neither DeviceId nor + /// ObservationPolicyId is logged; both are redacted by their Debug impls. + async fn observe_inventory( + &self, + device_id: &DeviceId, + policy_id: &ObservationPolicyId, + ) -> Result; +} +``` + +The Core/broker adapter is the sole concrete implementation. It is the only +location where sysfs matching, `device_id`-to-sysfs resolution, and any related +audit occur. The Provider controller is injected with a `Box` +plus a `DeviceId` and `ObservationPolicyId` per Device at startup; Core resolves +the Zone + label context to these opaque values before injecting. The Provider +passes them back to the port without inspecting their contents. + +The Provider crate's `src/effect_port.rs` re-exports `SecurityKeyEffectPort`, +`DeviceId`, `ObservationPolicyId`, and `InventoryObservation` from `d2b-contracts`. +No additional trait methods are defined in the re-export. + +**Relay receives no port.** The relay receives the pre-opened hidraw fd from its +LaunchTicket DeviceGrant at process start and never calls any port or broker +method. + +## Process model + +The Provider owns one static controller, one Host relay Process per authority +`SecurityKeyService`, and one Guest frontend Process per `SecurityKeyBinding`. +Projection Services never own a hidraw relay. The controller and authority relay +are host-side system processes; each Binding-owned frontend is a user-domain process +inside its target Guest. + +### Controller Process (created by Core ProviderDeployment) + +```yaml +type: Process +metadata: + name: device-security-key-controller + zone: + ownerRef: Provider/device-security-key +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + processClass: controller + template: sk-controller + sandbox: + namespaceClasses: [mount, ipc, pid] + capabilityClasses: [] + seccompClass: sk-controller + environmentClass: provider-defined + startRoot: false + noNewPrivileges: true + readOnlyRoot: true + budget: + pids: + limit: 16 + fds: + limit: 64 + memory: + limit: "64Mi" + restartPolicy: + class: always + backoffBase: "1s" + backoffMax: "30s" + backoffMultiplier: 2.0 + readiness: + initialDelay: "0s" + timeout: "10s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined +``` + +The controller exists for the lifetime of the Provider installation. +Core creates it once via ProviderDeployment and restarts it on failure. It holds +the narrow Zone resource API authorities listed in §RBAC for physical `Device`, +`SecurityKeyService`, `SecurityKeyBinding`, and their owned `Process`/`Endpoint` +children. It never creates a physical Device or a projection Service. It realizes +an authority Service as one relay Process plus relay Endpoint and realizes a Binding +as one frontend Process plus private frontend Endpoint. It receives relay session +lifecycle events via `device-security-key.relay-ctrl.v1`. + +### Host relay Process + +```yaml +type: Process +metadata: + name: device--sk-relay + zone: + ownerRef: security-key.d2bus.org.SecurityKeyService/ +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + processClass: service + template: sk-relay + deviceUsage: + - deviceRef: Device/ + access: exclusive + purpose: hidraw-fido + sandbox: + namespaceClasses: [mount, ipc, pid] + capabilityClasses: [] + seccompClass: sk-relay + environmentClass: provider-defined + startRoot: false + noNewPrivileges: true + readOnlyRoot: true + budget: + pids: + limit: 32 + fds: + limit: 64 + memory: + limit: "32Mi" + restartPolicy: + class: on-failure + backoffBase: "1s" + backoffMax: "30s" + backoffMultiplier: 2.0 + maxRestarts: 5 + readiness: + initialDelay: "0s" + timeout: "10s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined +``` + +`uid-short` is the first 12 hex characters of the owner Service resource UID. The +Process name never contains a Guest or User name. The Service UID is stable across +daemon restarts. + +The relay Process: + +- Receives the hidraw fd from its LaunchTicket DeviceGrant at process start. Core + opens, revalidates, and passes the pre-opened fd. The relay **never calls any + broker operation** and never sees a device path or UID. +- The `deviceUsage: access: exclusive` DeviceGrant IS the exclusive lock. Core + holds the corresponding OFD lease for the relay's process lifetime; it releases + automatically when the relay exits for any reason (clean or crash). +- Serves the `d2b.security-key.v3` ComponentSession over the + `Endpoint/-ctaphid-relay` resource resolved through + Provider/transport-vsock (see §ComponentSession). +- Accepts a bounded set of authenticated Binding frontend connections. Exactly one + CTAP ceremony across all connected local/imported Bindings may hold the Service + lease. Other ceremony requests enter a bounded FIFO fair queue for at most + `Binding.spec.provider.settings.queueWaitTimeoutSecs`, then receive + `ERR_CHANNEL_BUSY`. +- Proxies 64-byte CTAPHID reports bidirectionally between the Guest frontend + (over the ComponentSession named CTAPHID stream) and the physical token (over + the hidraw fd from the LaunchTicket). +- Translates CIDs: guest-provided channel IDs are replaced with host-assigned + monotonically-incrementing CIDs before forwarding to the token; responses are + translated back to the guest CID (see §CID translation). The relay never + inspects raw vsock CIDs; transport-vsock/ComponentSession authenticates the + expected Guest endpoint and passes only a canonical authenticated subject to the + relay. The relay uses that canonical subject, not any raw transport address. +- Connects to the Provider controller's manifest-declared internal service + `device-security-key.relay-ctrl.v1` using the bound internal channel FD from its + LaunchTicket. Reports session lifecycle events and + receives `CancelSession` signals. +- Has narrow ComponentSession service authority: responder of + `d2b.security-key.v3` and client of `device-security-key.relay-ctrl.v1` only. + No Zone resource API authority, no broker connection, no write access to Zone + resource store. +- On crash, Core releases the DeviceGrant and OFD lease; the Provider controller + observes `owned-resource-changed`, marks the owner Service unavailable, and + degrades dependent Bindings. The relay is restarted after back-off. + +The Host relay produces the stable CTAPHID relay service as an owned `Endpoint` +resource: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: -ctaphid-relay + zone: + ownerRef: security-key.d2bus.org.SecurityKeyService/ +spec: + providerRef: Provider/device-security-key + producerRef: Process/device--sk-relay + endpointClass: device + transport: vsock + purpose: device-security-key.d2bus.org/ctaphid-relay + serviceFingerprint: device-security-key.d2bus.org/SecurityKeyCtapRelay.v3 + locality: cross-domain + visibility: zone + attachmentPolicy: component-session + consumerPolicy: + allowedProviderComponents: [device-security-key.d2bus.org/frontend] + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +## Endpoint resources (D092) + +`Provider/device-security-key` conforms to the standard `Endpoint` base schema. +An authority Service owns its stable relay Endpoint. A projection Service owns a +local import Endpoint created through the signed import adapter. Each +`SecurityKeyBinding` owns a private frontend Endpoint. The Binding controller resolves +only the same-Zone Service Endpoint; cross-Zone routing is behind the projection +Endpoint and never appears as a remote Ref. Service Endpoints remain +implementation details and never appear in `ResourceExport` or `ResourceImport`. +Endpoint spec/status never carries raw vsock CIDs, ports, hidraw/UHID paths, fd +numbers, CTAP/session bytes, PINs, CBOR payloads, signatures, or credentials. +Resolution occurs only through an authorized EffectPort/LaunchTicket; +unauthorized resolution returns `endpoint-resolve-denied`. Producer restart +bumps `Endpoint.status.endpointGeneration`, causing frontend consumers to observe +`dependency-changed` and reconnect through a fresh authorized ticket. + +## Retained opaque handles (D092 promotion test) + +- pidfds for relay/frontend supervision stay process-local identity handles. +- LaunchTicket fd indexes for hidraw, UHID, and internal controller channels stay + opaque per-launch attachment slots. +- CTAPHID CIDs, relay `sessionId` values, and per-connection ComponentSession + handles are high-churn ceremony/session records and are never promoted to + Resources. +- The manifest-declared relay-controller channel handle is controller-internal; + it is not independently consumed outside this Provider. +- `OwnedTransport` and named CTAPHID streams are in-memory transport + capabilities behind Endpoint resolution, not Endpoint identities. +- `operationId` values remain opaque audit/idempotency correlation handles. + +**Current implementation note (implemented-and-reachable):** In the baseline the +relay is NOT a separate process — it is a daemon-internal loop in +`packages/d2bd/src/security_key.rs`. `ProcessRole::SecurityKeyFrontend` is a +readiness-only tracking node, not a spawned process. The v3 Provider extracts +this logic into `d2b-provider-device-security-key` as a proper unprivileged +`system`-domain Process receiving its hidraw fd from the LaunchTicket. + +### Binding-owned frontend realization + +There is no virtual or projected `Device` for UHID. The standard `Device` remains +the owner-Zone physical hidraw backing only. The frontend template requests the +Guest-scoped `uhid` authority subresource from `Provider/system-core`; Core +resolves that request as a LaunchTicket DeviceGrant, opens `/dev/uhid` inside the +target Guest execution context, revalidates it, and passes only the fd. This is +the same authority-subresource pattern used for shared kernel devices and does not +create a new ResourceType or a Device row. + +The `SecurityKeyBinding` owns both frontend realization children. Its private +Endpoint is: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: binding--sk-frontend + zone: + ownerRef: security-key.d2bus.org.SecurityKeyBinding/ +spec: + providerRef: Provider/device-security-key + producerRef: Process/binding--sk-frontend + endpointClass: device + transport: component-session + purpose: device-security-key.d2bus.org/ctaphid-frontend + serviceFingerprint: device-security-key.d2bus.org/SecurityKeyCtapRelay.v3 + locality: zone-local + visibility: provider + attachmentPolicy: launch-ticket-only + consumerPolicy: + allowedProviderComponents: [device-security-key.d2bus.org/service] + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +``` + +### Guest frontend Process + +```yaml +type: Process +metadata: + name: binding--sk-frontend + zone: + ownerRef: security-key.d2bus.org.SecurityKeyBinding/ +spec: + providerRef: Provider/system-systemd + executionRef: Guest/ + domain: user + processClass: service + template: sk-frontend + userRef: User/ # required; Guest executionRef must have defaultUserRef if absent + sandbox: + namespaceClasses: [mount, ipc, pid] + capabilityClasses: [] + seccompClass: sk-frontend + environmentClass: provider-defined + startRoot: false + noNewPrivileges: true + readOnlyRoot: true + budget: + pids: + limit: 16 + fds: + limit: 32 + memory: + limit: "16Mi" + restartPolicy: + class: on-failure + backoffBase: "2s" + backoffMax: "60s" + backoffMultiplier: 2.0 + maxRestarts: 5 + readiness: + initialDelay: "2s" + timeout: "15s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined +``` + +`executionRef` and `userRef` are copied from the Binding target. The Provider +controller creates this Process only after the same-Zone Service is Ready and the +Guest/User relationship is verified. + +The frontend `userRef` is required. If absent, the `executionRef` Guest must +declare a `defaultUserRef`; the controller fails Process admission if neither is +present (user-domain Process requires a resolved user identity). + +`providerRef: Provider/system-systemd` manages the frontend as a transient +user-scope systemd unit. The `sandbox` spec fields compile into systemd service +hardening directives (no minijail profile for the frontend). + +Core resolves the template's Guest-scoped UHID authority-subresource request as a +LaunchTicket DeviceGrant. No standard Device resource is created. No udev rule, +wildcard device permission, or ambient `/dev` access is needed: the guest `/dev` +is masked in the frontend sandbox and only the pre-opened UHID fd is available. + +The frontend Process: + +- Runs as a user-domain process inside the Guest under `Provider/system-systemd`. +- Receives the pre-opened UHID fd from its `Provider/system-core` Guest-substrate + LaunchTicket DeviceGrant. +- Creates a virtual FIDO2 CTAPHID HID device (UHID_CREATE event) on the received + UHID fd with the FIDO usage descriptor. The virtual device is visible to libfido2, + browsers, and `pamu2fcfg` inside the Guest. +- Resolves `Binding.spec.serviceRef` in its own Zone and connects as initiator of + `d2b.security-key.v3` through that Service's local Endpoint. For a projection + Service, the local Endpoint carries the bounded encrypted named stream to the + owner authority. It reconnects on ComponentSession/import generation change. +- Reads UHID_OUTPUT events from the Guest kernel and sends them to the relay over + the named CTAPHID stream. Reads relay responses and injects them via UHID_INPUT2. +- Has narrow ComponentSession client authority: initiator of `d2b.security-key.v3` + only. No resource API authority, no Zone bus access. +- Binary: `packages/d2b-sk-frontend/` (static binary; implemented-and-reachable). + +**Current implementation note:** Baseline `d2b-sk-frontend.service` is an +untracked Guest systemd unit. The v3 target removes that unit when the Process +resource is live (see ADR046-security-key-020). + +## ComponentSession: relay server endpoint + +The Host relay Process has two channel types: an external ComponentSession serving +authenticated Binding frontends, and a manifest-declared typed internal +ComponentSession service to the Provider controller. + +### CTAPHID relay ComponentSession (relay ↔ Guest frontend) + +The relay Process serves the `d2b.security-key.v3` ComponentSession over +`Provider/transport-vsock`. This is the sole CTAPHID transport in v3; no parallel +raw AF_VSOCK framing exists. + +**Transport allocation:** the Binding resolves only its same-Zone +`SecurityKeyService` and the Service's local Endpoint. For an authority Service, +`Provider/transport-vsock` resolves that owned relay Endpoint into opaque +attachments for relay/frontend LaunchTickets. For a projection Service, the +import adapter resolves its local Endpoint into a per-import bounded encrypted +named stream over ZoneLink. The relay does not bind a raw vsock port. Handles are +opaque, never configurable as port numbers, and never appear in resource +spec/status. `vsockPort` does not exist in v3; no FD or raw transport locator +crosses a Zone. + +**Noise profile:** enrolled KK (`Noise_KK_25519_ChaChaPoly_SHA256`). Relay and +Binding frontend static keys are enrolled at Process provisioning before the first +connection. The relay acts as responder; the frontend acts as initiator. The +cross-Zone named stream retains end-to-end record encryption so intermediate +controllers see ciphertext only. + +**Named CTAPHID stream:** one bounded bidirectional named stream `ctaphid` within +the session: +- Maximum message size: 64 bytes (one CTAPHID report); any message exceeding 64 + bytes is a protocol error — both ends close the session on receipt. +- Relay → frontend: host-CID-translated CTAPHID reports from the physical token. +- Frontend → relay: guest CTAPHID reports (relay translates CID before forwarding; + see §CID translation). +- The relay never logs raw packet bytes, CTAP payload content, PINs, CBOR + payloads, assertion bytes, or signature material. + +**Session authority:** +- Relay: responder of `d2b.security-key.v3` service only; no resource API + authority; no Zone bus method access beyond this service. +- Frontend: initiator of `d2b.security-key.v3` service only; no resource API + authority. + +The baseline `vsock.sock_14320` (port 14320) and `packages/d2b-sk-frontend/src/framing.rs` +raw-frame protocol are obsolete under v3. The frontend +`packages/d2b-sk-frontend/src/vsock.rs` is replaced by the ComponentSession +vsock client from `d2b-session-unix/src/vsock.rs` (see ADR046-security-key-003). + +### Manifest-declared relay ↔ controller service + +The relay connects to the Provider controller's typed internal ComponentSession +service `device-security-key.relay-ctrl.v1`. This service is declared in the +Provider's package descriptor (manifest); there is no ambient filesystem socket +path. The bound internal channel FD is injected into the relay's LaunchTicket as a +controller-internal attachment — the relay never resolves a path to find the +controller. + +- **Service name:** `device-security-key.relay-ctrl.v1` (internal; not + addressable by external Providers or the CLI). +- **Role:** relay is client (initiator); controller is server (responder). +- **Noise profile:** NN (`Noise_NN_25519_ChaChaPoly_SHA256`); both ends verify + the Provider descriptor digest before handshake. +- The relay sends bounded typed messages (`SessionStarted`, `SessionCompleted`, + `SessionTimeout`) and receives `CancelSession { session_id }` from the + controller. +- **Descriptor validation:** relay verifies the controller component's descriptor + digest against the signed Provider package before the Noise handshake. Mismatch + causes the relay to refuse the connection and enter `Degraded` state. +- **SO_PEERCRED check:** the relay verifies the connected peer's uid maps to + the `d2b-provider-device-security-key` controller principal. +- **Record bounds:** messages ≤ 512 bytes. Oversized messages are discarded and + the connection is closed. +- No CTAP session bytes, device paths, guest VM identity strings, or secret + material appear in this channel. + +## Lease, CID, and session lifecycle + +### Session state machine + +Each authority relay holds the hidraw fd from its LaunchTicket DeviceGrant for its +entire lifetime; the DeviceGrant is the physical-open lock. It may retain bounded +authenticated frontend connections, but its fair queue grants exactly one +ceremony lease at a time. The state machine is per ceremony: + +``` +Idle (relay alive; hidraw fd held via DeviceGrant) + │ (Binding frontend submits a ceremony request) + ▼ +Queued (holderRef: SecurityKeyBinding/; bounded FIFO; deadline attached) + │ (head of queue; relay allocates monotonic LeaseId) + ▼ +Active (holderRef: SecurityKeyBinding/, opaque sessionId + LeaseId) + │ (one CTAPHID ceremony may be in-flight; at most one) + ├──────────────────────────────────────────────────────┐ + │ CTAPHID op complete or CTAPHID_CANCEL received │ CEREMONY_TIMEOUT elapsed + ▼ ▼ +Completed TimedOut + │ │ + └──────────────────────▼──────────────────────────────┘ + Idle +(DeviceGrant/OFD lease released only when relay process exits) +``` + +Transitions: + +| Transition | Trigger | Effect | +| --- | --- | --- | +| `Idle → Queued` | Binding frontend submits a CTAP ceremony | Relay authenticates the Service/Binding pairing, allocates an opaque session ID, and enqueues by bounded FIFO order | +| `Queued → Active` | Request reaches queue head before `QUEUE_WAIT_TIMEOUT` | Relay allocates a monotonic `LeaseId`, reports `SessionStarted` with the Binding UID digest, and marks only bounded Binding/Service observations | +| `Queued → Completed` | Queue deadline expires | Relay returns `ERR_CHANNEL_BUSY`; records a bounded `busy` outcome; no physical CTAP bytes are written | +| `Active → Completed` | CTAP op complete or CTAPHID_CANCEL received | Relay reports `SessionCompleted`; updates status; ready for next connection | +| `Active → TimedOut` | `CEREMONY_TIMEOUT` (120 s) elapsed | Same as Completed; audit reason `session-timeout` | +| `Active → Completed` via controller | Controller `CancelSession { session_id, lease_id }` | Relay verifies the current `LeaseId`, cancels all active translated CIDs, transitions to Completed (reason=operator-cancel), and reports `SessionCompleted` | +| `Completed/TimedOut → Idle` | (internal transition) | Relay ready for next frontend connection | + +**One active ceremony per Service:** while `Active`, requests from other local or +imported Bindings wait in bounded FIFO order for up to +`Binding.spec.provider.settings.queueWaitTimeoutSecs` (15 s) +and then receive `ERR_CHANNEL_BUSY`. A monotonic `LeaseId` prevents a stale +release/cancel from affecting a later ceremony. Exactly one relay authority per +physical Device is enforced by the Service `AuthorityDescriptor`, authority index, +and exclusive DeviceGrant before any open. + +**Session ID format:** `sk--` where `uid-short` is +the first 12 hex chars of the authority Service UID and `counter` is a +per-relay-process monotonic u64. Session IDs and `LeaseId` values are opaque. Each +Binding has at most `sessionRingSize` bounded non-secret ceremony records for +session queries and cancellation bookkeeping. Those rows are high-churn session +records, not Resource objects and not child resources of the Binding. + +### CID translation + +The relay maintains a `CidTranslator` (v3 counterpart to the type of the same +name in `packages/d2bd/src/security_key.rs`). On each incoming guest CTAPHID +packet: + +- Bytes 0–3 are the guest-assigned CID (u32, big-endian per CTAPHID spec). +- The relay looks up the guest CID in the translation table. If absent, it + allocates a new host-assigned CID from a monotonic counter and records the + mapping. +- The outgoing packet to the token uses the host-assigned CID. +- Incoming token responses use the host-assigned CID; the relay reverses the + lookup and replaces with the guest CID before forwarding to the frontend. + +CID translation is per active ceremony lease, not per Zone or long-lived +frontend connection. The table is discarded when the ceremony ends. Different +Bindings and Services never share a CID namespace. Disconnect/cancel invokes +`build_cancel_packet` for **all** active translated CIDs before dropping the map. + +### Disconnect and cancel semantics + +**Guest frontend disconnect (clean close):** + +1. The relay detects graceful ComponentSession shutdown from the frontend. +2. If a ceremony is in-flight (`Active` state), the relay sends `CTAPHID_CANCEL` + to the physical token (via the hidraw fd from its LaunchTicket). +3. The relay waits up to 500 ms for the token's cancel acknowledgment. +4. The relay transitions to `Completed` (reason: `client-disconnect`). +5. The relay reports `SessionCompleted` to the Provider controller with only the + opaque Binding/session/lease correlation. +6. DeviceGrant/OFD lease remains held (relay is still running and Idle). + +**Guest frontend disconnect (crash/abrupt):** + +1. The relay detects broken ComponentSession (transport error or POLLHUP). +2. Same cancel-and-complete sequence as clean close. + +**Operator cancel (Binding cancel action; the compatibility device-cancel command +resolves the active Binding):** + +1. The CLI emits an admin-only cancel action for the active + `SecurityKeyBinding`. +2. The Provider controller sends `CancelSession { session_id, lease_id }` to the relay via + the manifest-declared internal service `device-security-key.relay-ctrl.v1`. +3. The relay sends `CTAPHID_CANCEL` to the token, transitions from `Active` to + `Completed` (reason: `operator-cancel`), and reports `SessionCompleted`. +4. The controller updates bounded Service/Binding status and emits an audit record. +5. `CancelSession` is a protocol-level operation on the typed internal service; + it never implies finalizer removal or DeviceGrant release (the grant persists + while the relay process lives). + +**Relay crash:** + +1. Core detects relay process exit; DeviceGrant and OFD lease release automatically. +2. The Provider controller observes `owned-resource-changed` → relay Process `Failed`. +3. The controller marks the authority Service unavailable and dependent Bindings + `Degraded`; the physical Device presence observation is unchanged. +4. Controller restarts relay Process after back-off (initial 1 s, ×2, max 30 s, + max 5 attempts before `Failed`). New relay Process gets a new LaunchTicket with + a fresh DeviceGrant. + +## Security invariants + +All invariants below are normative and must be preserved in every implementation +change. Violation of any invariant is a security defect regardless of test pass +status. + +### I-1: Core is the only hidraw opener + +Core opens the physical hidraw node as part of the relay Process LaunchTicket +DeviceGrant resolution. Neither the Provider controller nor the relay process +ever opens any `/dev/hidraw*` path, reads sysfs, manages OFD lock paths, or +receives raw device UIDs. The Provider controller calls +`SecurityKeyEffectPort::observe_inventory()` for probe observations; the injected +Core adapter is the only concrete site of sysfs access. The relay receives a +pre-opened hidraw fd from its LaunchTicket and has no path to any device node. + +### I-2: No path on any public surface + +No raw hidraw path, sysfs path, vendor/product string, or device serial number +appears in: +- Device, SecurityKeyService, SecurityKeyBinding, Endpoint, or Process spec/status. +- Audit records (path-free; device_label digest only). +- OTEL spans, metrics, or log lines. +- Public wire DTOs (`SecurityKeyStatusResponse`, `SecurityKeySessionsResponse`). +- Broker request or response body. +- Any ComponentSession record or error message. + +Session IDs and authority/owner digests are opaque. They are never the hidraw +node name, sysfs path, serial, or a concatenation of vendor/product IDs. + +### I-3: Exclusive DeviceGrant per relay Process + +At most one authority Service relay can hold the exclusive DeviceGrant for a +given physical Device. Before Core opens hidraw, the Host-global D097 authority +index admits both the Service's semantic security-key authority and the shared +Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` claim, rejecting a +duplicate with no effect. Core then enforces that no two concurrent exclusive +grants are issued for the Device. The relay spec uses the initial Provider extension: +`deviceUsage: [{deviceRef: Service.spec.provider.settings.deviceRef, access: +exclusive}]`. On relay +exit, Core releases the DeviceGrant/OFD lease automatically; there is no +in-relay OFD lock management. Frontend UHID access is a separate Guest-substrate +authority-subresource DeviceGrant from `Provider/system-core`, not a physical, +virtual, or projected Device. + +### I-4: Security-key proxy and USBIP are mutually exclusive + +A Service under `Provider/device-security-key` and a Service under any USB +Provider that resolve to the same physical USB device cannot hold authority +anywhere on the same Host. + +This is enforced: + +**At Nix eval time:** `assertions.nix` emits a hard error if the compiled Host +bundle contains a security-key authority Service/device pair and a USB +Service/device pair whose trusted selector inputs are known to resolve to the +same physical USB device, even when authored in different Zones. This preflight +blocks NixOS activation but is not runtime authority. + +**At authority admission:** after trusted USB identity resolution, Core derives +one `physical-usb-backing/v1` opaque digest. Security-key and every USB Provider +must claim the exact Host-global +`(Host, physical-usb-backing, opaqueKeyDigest)` tuple before effects. The second +Service transitions to `Failed` with `BackingAuthorityReady=False`, +`AuthorityConflict=True`, and `physical-usb-backing-conflict`. Provider-private +labels, key classes, or digests cannot bypass the collision. No hidraw open, +USBIP withhold/bind, module, relay, or attachment effect occurs. + +### I-5: One ceremony lease, many explicit consumer Bindings + +Multiple local or imported `SecurityKeyBinding` resources may reference one +Service. Each represents explicit Guest/User intent, but exactly one ceremony +holds the physical token lease at a time. Other requests wait in the bounded FIFO +fair queue and time out with `ERR_CHANNEL_BUSY`; they do not cause a second open +or terminate the active ceremony. Duplicate Binding intent for the same +Service/Guest/User/policy tuple is rejected before frontend creation. + +### I-6: Peer authentication + +The relay accepts ComponentSession connections only from a frontend static key +enrolled for a Ready Binding bound to the same Service. Peer authentication is +performed by the Noise KK handshake on the `d2b.security-key.v3` session; +the relay rejects any initiator whose static key is not enrolled for the +expected frontend Process. Transport-vsock/ComponentSession authenticates the +expected Binding/Guest endpoint and passes a canonical authenticated subject to the relay; +the relay uses only this canonical subject for identity decisions. The relay never +inspects or verifies raw vsock CIDs — those are a transport-internal detail owned +entirely by the transport layer. A connection from an unenrolled key is refused; +the relay does not accept in-band identity claims from the peer. + +### I-7: No credential material in log/audit/OTEL + +Raw CTAP payloads, PINs, CBOR assertions, credential IDs, WebAuthn responses, +and signature bytes are never logged, audited, or included in OTEL spans or +metrics. Authorized bounded audit records may carry the target, +Service/Binding, and session digests specified below. OTEL spans and metrics +carry only fixed semantic operation, phase, outcome, and error-class values; +they carry no target or resource identity, including opaque digests. + +### I-8: Relay has narrow ComponentSession service authority + +The relay Process holds narrow ComponentSession service authority: responder of +`d2b.security-key.v3` and client of the manifest-declared internal service +`device-security-key.relay-ctrl.v1` only. It does not hold a Zone resource API +client, a broker connection, or write access to the Zone resource store. The +internal service channel FD is injected via LaunchTicket; no ambient path is +used. + +### I-9: No guest udev rules required + +Core pre-opens `/dev/uhid` before the Binding-owned frontend starts, using the +`Provider/system-core` Guest-substrate authority and passing the fd via a +LaunchTicket DeviceGrant. No Device Resource is created for UHID. The frontend +sandbox masks `/dev`, so it accesses only the pre-opened fd. No udev rules, +`plugdev` membership, or wildcard device permissions are required. The +`SecurityKeyApplyUdevRules` operation is removed. + +### I-10: ProviderStateSet is empty under status-first state + +Per D087, `device-security-key` declares **no Provider state Volume** for the +controller, relay, or frontend components. `ProviderStateSet` is the optional +query-time grouping of declared Provider state Volumes; for +`Provider/device-security-key` the set is empty. + +Bounded non-secret operational state belongs to the Resource that owns it: +physical presence in `Device.status`, authority/relay/queue aggregates in +`SecurityKeyService.status`, and consumer/frontend/attachment aggregates in +`SecurityKeyBinding.status`, plus the Core Operation ledger. Individual ceremony +rows, CTAP bytes, stream content, CIDs, `LeaseId` values, UHID/hidraw fds, session +keys, and cancellation handles are never Resource objects and must not be +persisted or exposed through status. + +Storage-need test rationale: v1 security-key controller, relay, and frontend +state has no durable secret recovery payload, no large or binary file content, no +private data that belongs outside authorized status readers, and no +bounded-but-revision-unsuitable recovery payload. Transient CTAP and relay bytes +fail the persistence side of the storage-need test and remain in memory only. + +## RBAC + + +Physical Device resources use the standard Device RBAC verbs from +`ADR-046-resources-device`. Qualified resource and child bindings are: + +| Role | Verbs | Scope | Subjects | +| --- | --- | --- | --- | +| `security-key-device-observer` | `get`, `list`, `watch`, `update-status` | physical `Device` rows for this Provider | Provider controller only; no create/delete/finalizer authority | +| `security-key-service-controller` | `get`, `list`, `watch`, `create`, `update-spec`, `update-status`, `update-finalizers`, `delete` | authority `SecurityKeyService` with `providerRef=Provider/device-security-key` and its owned Process/Endpoint children | Provider controller only | +| `security-key-projection-controller` | `create`, `get`, `watch`, `update-spec`, `update-status`, `update-finalizers`, `delete` | projection `SecurityKeyService` with `providerRef=Provider/device-security-key`, `ownerRef: ResourceImport/`, and its Endpoint | Core export/import controller + signed import adapter only | +| `security-key-binding-author` | `get`, `list`, `watch`, `create`, `update-spec`, `delete` | `SecurityKeyBinding` with an admitted providerRef | authorized Nix/operator subjects; never relay/frontend | +| `security-key-binding-controller` | `get`, `list`, `watch`, `update-status`, `update-finalizers`; child `create`, `update-spec`, `delete` | Binding with `providerRef=Provider/device-security-key` and its owned Process/Endpoint children | Provider controller only | +| `security-key-reader` | `get`, `list`, `watch` | Service/Binding in the caller's Zone | authorized CLI/runtime readers | +| `security-key-cancel` | action `cancel` | `SecurityKeyBinding/` | Admin-only (`d2b` group + SO_PEERCRED admission) | + +No Role grants wildcard `*`. Ordinary operators cannot create projection +Services, controllers cannot author consumer Binding intent, and import controllers +cannot create physical Devices or authority Services. A `ResourceExport` may +target an authority Service only. + +The relay Process has no RoleBinding to the Zone bus. It cannot invoke any +resource API method. Its typed internal ComponentSession service connection to the +Provider controller is manifest-declared and socket-FD-injected via LaunchTicket; +no ambient path is used and the relay cannot open arbitrary Zone bus connections. + +## Provider state + +Per D087, `device-security-key` declares **no Provider state Volume**. The +controller, relay, and frontend component descriptors contain no state namespace, +no Provider state Volume template, and no `/state` mount. The ProviderStateSet is +empty because no declared Volume passes the storage-need test. + +### Controller operational state + +Device, SecurityKeyService, SecurityKeyBinding, their owned Process/Endpoint +children, and the Core Operation ledger are the authority for controller +decisions. The controller writes only each Resource's bounded observations. On +restart it re-lists those resources, adopts an exact Service authority by D097 +owner proof without speculative reopen, revalidates external reality, and writes +material changes; it never recovers from a private controller state directory. + +### Relay operational state + +Each relay Process keeps CTAPHID session state, CID translation maps, cancellation +state, and hidraw fd ownership in process memory and inherited LaunchTicket +DeviceGrant fds. These values are transient and authority-conferring; they are not +Volume payloads. Bounded queue/holder counts and outcomes may appear in Service or +Binding status, while CTAP bytes, CIDs, LeaseIds, session keys, and raw identities +never do. Individual ceremonies remain bounded session records, not Resources. + +### Frontend operational state + +Each frontend Process keeps UHID/ComponentSession stream state in process memory +and inherited DeviceGrant fds. No frontend Provider state Volume or host-side +attachment Volume exists in v3. Bounded readiness and error summaries are written +to the owning Binding status and Operation ledger. + +### Lifecycle + +Core ProviderDeployment has no Provider state Volumes to create, mount, migrate, +or delete for `device-security-key`. Process lifecycle still uses LaunchTicket +DeviceGrants for the authority Service's physical Device and the Binding frontend's +Guest-substrate UHID authority subresource; neither is Provider state. Service +finalizers drain/cancel ceremonies, stop the relay, release the physical +DeviceGrant/OFD lease, revoke exports, and delete the relay Endpoint. Binding +finalizers cancel its ceremony, stop the frontend, release UHID, and delete its +private Endpoint. projection teardown is Core-owned and child-first. There is no frontend Device +or Volume cleanup step. + +The storage-need test is not met by controller, relay, or frontend operational +state: durable status/operation records cover bounded non-secret observations, +while CTAP bytes, fd handles, session keys, and relay/frontend stream state are +transient data that must never be persisted. + +The Provider controller consumes + **no** broker operations directly. Core resolves +the relay Process's `deviceUsage: exclusive` DeviceGrant internally when launching +the relay, using the trusted bundle `device_token` to open the hidraw node. The +operations below are internal to Core's LaunchTicket machinery and are not +called by the Provider at runtime. + +| Internal operation | Effect | Caller | Audit | +| --- | --- | --- | --- | +| `SecurityKeyOpenDevice` | Resolve FIDO hidraw node from trusted bundle `device_token`; open `O_RDWR\|O_NONBLOCK\|O_NOFOLLOW`; fstat/HIDIOCGRDESC/HIDIOCGRAWINFO revalidation; pass fd to relay LaunchTicket | Core LaunchTicket (DeviceGrant resolution) | Yes — path-free; device_label digest, zone, outcome | + +The Provider controller never calls `SecurityKeyOpenDevice`. Core emits a +path-free `device-grant` audit record when the hidraw fd is opened; the Provider +controller does not emit this record. + +`SecurityKeyApplyUdevRules` is removed from the architecture. Guest Nix loads the +`uhid` kernel module, but supplies no security-key udev rule; no runtime broker op +writes or applies udev rules. + +**SecurityKeyOpenDevice internal request (from `packages/d2b-contracts/src/security_key.rs`):** + +```rust +pub struct SecurityKeyOpenDeviceRequest { + pub device_label: SecurityKeyDeviceLabel, // opaque stable label; no path + pub session_id: SecurityKeySessionId, // for audit correlation; not a path + pub zone: String, // Zone name +} +``` + +No path, sysfs node, or device string is accepted. Unknown fields are rejected. +The broker ignores any other field. + +## Resource status, ownership, and finalizers + +Per D088, every Resource writes its common `status.resource` base and its own +strict signed provider extension atomically. The provider-neutral Service and +Binding base schemas are +`security-key.d2bus.org/{SecurityKeyService,SecurityKeyBinding}/status`. The +initial implementation extensions are +`device-security-key.d2bus.org/{Device,SecurityKeyService,SecurityKeyBinding}/status`, +bounded to 32 KiB, deny unknown fields, and never duplicate common or semantic +base fields. + +### Physical Device status + +The standard Device carries physical inventory only: + +```yaml +status: + phase: Ready | Pending | Degraded | Failed | Unknown + conditions: + - type: DevicePresent + status: "True" | "False" | "Unknown" + reason: device-probed-present | device-probe-failed + | device-consecutive-probe-failures-exceeded + resource: + present: true | false | null + health: healthy | degraded | failed | unknown + holderRefs: + - security-key.d2bus.org.SecurityKeyService/ + lastProbedAt: "2026-07-22T00:05:00Z" + provider: + providerRef: Provider/device-security-key + schemaId: device-security-key.d2bus.org/Device/status + schemaVersion: "1.0.0" + observedProviderGeneration: 1 + details: + fidoUsagePageConfirmed: true +``` + +It has no frontend, import, Guest/User, session, relay, or ceremony fields. The +holder is the local authority Service, never a remote consumer. + +### Service status + +Authority Service base status carries the D097 provider-neutral authority fields +(`available`, holder count, arbitration, opaque owner digest, update currency). +`status.provider.details` carries queue depth, relay Process/Endpoint refs, +observed Device/Endpoint generations, the initial implementation's shared +physical-backing claim state, relay readiness, and the last non-secret ceremony +outcome. All semantic authority fields are nested under `status.resource`. +Projection `status.resource` carries import lease state and remote +generation/fingerprint; its local import-route Endpoint/ref and readiness remain +in `status.provider.details`. It has no Device or DeviceGrant observation. +Binding attachment state is likewise nested under `status.resource`; no +semantic authority/import/attachment field appears directly under `status`. + +Closed semantic Service conditions are `AuthorityReady`, `ImportBound`, +`AuthorityConflict`, `BackingAuthorityReady`, and `ConsumersDrained`. +Initial-Provider relay readiness and reasons remain in +`status.provider.details`. A duplicate security-key authority sets +`AuthorityConflict=True`; a shared physical USB collision additionally sets +`BackingAuthorityReady=False` with `physical-usb-backing-conflict` and `Failed` +before any effect. Link loss/export revocation sets a projection +`ImportBound=False` and `Degraded`/`Failed` according to disconnect policy; +dependent Bindings observe it. + +An authority Service owns the relay Process and relay Endpoint and installs +`device-security-key.d2bus.org/authority-drained`. It clears that finalizer only +after exports stop admitting, queued/active ceremonies are cancelled or drained, +the relay child is deleted, and Core confirms DeviceGrant/OFD release. A +projection Service is owned by its `ResourceImport`; Core's import finalizer +stops dependent consumers, releases the remote lease, deletes the projection +Endpoint, then deletes the projection Service. + +### Binding status + +Binding `status.resource` carries only semantic attachment state; common +phase/update/conditions remain universal top-level fields. Closed semantic conditions are `ServiceReady`, +`TargetReady`, and `AttachmentReady`. `status.provider.details` carries Service +generation, frontend Process/Endpoint refs, frontend readiness, whether this +Binding is queued or active, and the last closed ceremony outcome. Neither layer +embeds CTAP data, CIDs, LeaseIds, credentials, paths, or an unbounded session +list. + +A Binding owns exactly one frontend Process and private Endpoint and installs +`device-security-key.d2bus.org/frontend-released`. Deletion cancels only that +Binding's queued/active ceremony, waits up to five seconds for a lease-identity +matched completion, deletes the frontend Process and Endpoint, confirms the UHID +DeviceGrant closed, then clears the finalizer. Deleting a Binding never deletes its +Service, physical Device, ResourceImport, or ResourceExport. + +### Currency and update (D091) + +The controller implements `assess_update`, `plan_upgrade`, and `execute_upgrade` +for Device observation and Service/Binding realization and populates only universal +`status.update`. A disruptive authority Service update returns +`UpgradeRequired`, stops new queue admission, drains/cancels active consumers, +then recycles the relay while preserving the Service/Device identities. A +projection update revalidates import generation/fingerprint before rebinding. +A Binding update cancels only its ceremony and recycles only its frontend. +Currency propagates Device → authority Service → export/import → projection +Service → Binding → frontend. Non-disruptive policy/status changes reconcile in +place. No update field carries session or credential material. + +## Async reconcile loop + +The Provider controller implements the standard async reconcile interface for the +physical Device and both provider-neutral semantic ResourceTypes when +`spec.providerRef=Provider/device-security-key`. For `Create`, `UpdateSpec`, or +`Delete` with `waitForReconcile` (D090), it performs no external effect, finalizer, +or status mutation until Core supplies +`CommittedRevisionProof {resourceUid,generation,revision,operationId}`. `Abort` +means no effect. Idempotency keys derive from the proof in one bounded +per-resource single-flight lane. + +### Watch plan and indexes + +The controller uses bounded indexed watches, never an unfiltered cross-Zone list: + +| Watched type | Filter/index | Reconcile target | +| --- | --- | --- | +| `Device` | same Zone + `providerRef=Provider/device-security-key` | that Device and authority Services whose `spec.provider.settings.deviceRef` matches | +| `SecurityKeyService` | same Zone + providerRef; authority/projection branch | that Service and Bindings indexed by `spec.serviceRef` | +| `SecurityKeyBinding` | same Zone + providerRef | that Binding | +| `Process`, `Endpoint` | `ownerRef` index for Service or Binding | owning Service/Binding | +| `Guest`, `User` | `dependencyRefIn` from Binding target | dependent Bindings only | +| `ResourceExport`, `ResourceImport`, `ZoneLink` | local Service/export/import-owner indexes | authority/projection Service and its Bindings | + +Core performs projection creation and import lease writes under its own RBAC; the +Provider watch observes those committed revisions but cannot synthesize a +projection or mutate ResourceImport. + +### `spec-generation-changed` + +- **Physical Device:** validate the root-config label, `busClass=hidraw`, physical + class, exclusive arbitration, and USBIP mutual exclusion; schedule observation. + Do not create relay, frontend, Endpoint, Service, Binding, or import resources. +- **Authority Service:** require same-Zone physical + `spec.provider.settings.deviceRef`, require its reserved same-Zone + `spec.provider.settings.relayEndpointRef`, verify the semantic D097 base and + signed initial-Provider extension, and admit the Host-scoped opaque authority + before any effect. Install + `authority-drained`; create/repair the owned relay Process with exclusive + `deviceUsage` for that exact Device and the owned relay Endpoint. A duplicate + authority or USBIP owner fails without opening hidraw. +- **Projection Service:** accept creation only from Core/import adapter with + `ownerRef: ResourceImport/` after the adapter matches + `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and + `expectedFactoryFingerprint` to the signed factory; reject base or + Provider-extension `deviceRef`, authority, or physical selectors; + create/repair only the local projection-Service-owned Endpoint and import + binding. + The Provider controller never opens hidraw for this branch. +- **Binding:** require a same-Zone Ready Service and a valid Guest/User target, + reject duplicate attachment intent, install `frontend-released`, and + create/repair the Binding-owned frontend Process plus private Endpoint. Resolve + UHID through the Guest-substrate DeviceGrant; never create a Device. + +### `deletion-requested` + +- **Binding:** cancel its exact `{session_id, lease_id}` if queued/active, delete the + frontend Process then private Endpoint, confirm UHID grant closure, clear + `frontend-released`, and let Core delete the Binding. +- **Authority Service:** stop export admission, mark dependent local Bindings + draining, cancel/drain the fair queue, delete relay Process then relay Endpoint, + confirm physical DeviceGrant/OFD release, clear `authority-drained`, and let + Core delete the Service. The physical Device remains independently authored. +- **Projection Service:** Core's ResourceImport finalizer first stops dependent + Bindings, releases the remote lease, deletes the local import-route Endpoint, then deletes + the Service. Provider code cannot bypass or reorder this child-first sequence. +- **Physical Device:** deletion is blocked while an authority Service references + it. Once unreferenced, no Provider-owned child remains to delete. + +### `dependency-changed` / `execution-status-changed` + +- Device absence/degradation makes the authority Service unavailable and + propagates through export/import/projection to every dependent Binding. +- Export revocation, ZoneLink loss, or generation/fingerprint mismatch revokes the + import lease, degrades the projection Service, cancels its Binding ceremonies, + and requires revalidation before reconnect. +- Guest/User stop or policy denial cancels only the affected Binding, deletes its + frontend children, and marks that Binding `Degraded`/`Unknown`; it does not stop + the authority relay or other consumers. + +### `scheduled-observe` + +For a physical Device, call +`SecurityKeyEffectPort::observe_inventory(&device_id, &policy_id)` and record +`lastProbedAt`. Success resets failures and sets `DevicePresent=True`; one or two +failures produce `Unknown`; three produce `DevicePresent=False` and `Degraded`. +Recovery resets the counter. The resulting Device change propagates to its +authority Service and downstream Bindings. The Provider never reads sysfs. + +### `owned-resource-changed` + +A relay failure marks its owner authority Service unavailable, degrades all +dependent Bindings, and restarts the relay after bounded backoff. A frontend +Process/Endpoint failure marks only its owner Binding degraded and restarts that +frontend after bounded backoff. A local import-route Endpoint failure degrades the +projection Service and triggers import revalidation. Logs use opaque Resource +names/digests only. + +## Errors + +Stable error classes for this Provider (subset of Device common errors plus +security-key-specific additions): + +| Error slug | Condition type | Meaning | +| --- | --- | --- | +| `device-not-found` | `DevicePresent=False` | Physical hidraw node absent or label not in bundle table | +| `security-key-authority-conflict` | `AuthorityConflict=True` | A second semantic security-key authority resolved to the same service-specific key; no open occurred | +| `physical-usb-backing-conflict` | `BackingAuthorityReady=False`, `AuthorityConflict=True` | Another USB or security-key Service owns the Core-derived Host-global physical USB tuple; no backing effect occurred | +| `security-key-binding-conflict` | `AttachmentReady=False` | Duplicate Binding intent exists for the same Service/Guest/User/policy tuple | +| `device-grant-denied` | `BackingReady=False` | Core physical DeviceGrant open denied (revalidation failed or device absent) | +| `device-session-timeout` | — | CTAP ceremony exceeded `leaseTimeoutSecs` | +| `device-session-cancelled` | — | Binding ceremony was cancelled with a matching LeaseId | +| `device-session-busy` | `status.provider.details.queued=false` | Fair-queue wait exceeded the Provider extension's `queueWaitTimeoutSecs`; `ERR_CHANNEL_BUSY` returned | +| `device-worker-failed` | Provider detail `relayReady=false` or `frontendReady=false` | Owned relay/frontend Process failed after retry exhaustion | +| `device-cid-collision` | — | Internal CID allocation overflow (monotonic counter wraparound; effectively unreachable at u64) | +| `device-selector-label-unresolvable` | `DevicePresent=False` | `inventory.selector.label` does not match any entry in Provider root config | +| `security-key-import-invalid` | `ImportBound=False` | Projection owner, generation, fingerprint, or capability ceiling failed validation | +| `security-key-cross-zone-ref` | — | A Service/Binding/export/import attempted to carry a remote ResourceRef | + +All error messages are bounded (≤ 256 UTF-8 chars), must not contain device +paths, hidraw node names, sysfs bus IDs, raw CTAP bytes, vendor/product strings, +or any credential material. + +## Audit and OTEL + +### Audit records + +Audit records from this Provider and the Core device-grant operations it triggers: + +**Core `device-grant` audit** (emitted by Core's LaunchTicket machinery, not by +the Provider controller; path-free): + +```json +{ + "kind": "device-grant", + "op": "SecurityKeyOpenDevice", + "zone": "", + "resource_type": "Device", + "resource_name_digest": "sha256:", + "subject_digest": "sha256:", + "session_id_digest": "sha256:", + "outcome": "success | failure | denied", + "error_class": "", + "correlation_id": "", + "timestamp": "" +} +``` + +`resource_name_digest` is permitted only in this bounded Core audit record, +after the caller has already been authorized for the DeviceGrant operation. It +is not a telemetry field and must never be copied into a metric label, span +attribute, OTEL log, collector diagnostic, or support summary. + +Excluded: hidraw node path, sysfs bus ID, vendor/product string, serial, device +file descriptor number, CTAP payload, guest VM name. The Provider controller does +not emit this record; Core emits it at DeviceGrant resolution time. + +**Service/Binding ceremony-lifecycle controller audit** (emitted by the Provider +controller, not Core; uses the Zone runtime audit stream): + +```json +{ + "kind": "device-lease", + "event": "acquired | released | timeout | cancelled | conflict", + "zone": "", + "resource_type": "security-key.d2bus.org.SecurityKeyBinding", + "service_digest": "sha256:", + "binding_digest": "sha256:", + "holder_digest": "sha256:", + "session_id_digest": "sha256:", + "correlation_id": "", + "timestamp": "" +} +``` + +### OTEL telemetry + +OTEL span attributes and metric labels follow `ADR-046-telemetry-audit-and-support`. +Constraints specific to this Provider: + +- OTEL resource attributes include `d2b.provider="device-security-key"` and + `d2b.zone=`; neither is copied into metric labels. +- **`phase`** metric label: `"Ready"` | `"Pending"` | `"Degraded"` | `"Failed"` | `"Unknown"`. +- Metric `d2b_device_sk_session_total{outcome}`: counter; `outcome` ∈ `{success, timeout, cancelled, busy, conflict, error}`. +- Metric `d2b_device_sk_ceremony_duration_seconds`: histogram; bucketed 0–120 s. +- Metric `d2b_device_sk_relay_restarts_total`: counter. +- No metric label or span attribute carries a device/resource/Service/Binding + name, UID, ref, digest (including `resource_name_digest`), session ID, guest + name, hidraw path, serial, or derived identity token. Spans use only fixed + semantic operation, phase, outcome, and closed error-class attributes. +- `resource_name_digest` remains audit-only under the authorization rule above. +- OTEL emitter: lightweight bounded ring (no OTEL SDK in the Provider process; + tracing crate only). The `observability-otel` Provider drains and forwards. + +## Security-key authority and cross-Zone sharing (D096/D097) + +**One authority Service (D097).** The owner-Zone +`security-key.d2bus.org.SecurityKeyService` in `mode: authority` is the +stable semantic authority Resource. Its provider-neutral D097 +`AuthorityDescriptor` uses `authorityScope: physical-device`, opaque +`authorityKeyClass: semantic-security-key`, `cardinality: zero-or-one`, +`arbitration: exclusive`, `authorityRef` to itself, and `exportability: +explicit-export`. The initial `Provider/device-security-key` extension references +one same-Zone physical `Device` and its owned local relay `Endpoint`; that relay +is the sole holder of the physical hidraw FD. Its +`authorityDerivation: physical-fido-selector`, `ownerProof: +service-and-relay-process-identity`, and `fairnessPolicy: bounded-fifo` remain +inside `spec.provider.settings`. Core derives the non-authorizing key digest from +the trusted bundle `device_token`/FIDO selector. Separately and mandatorily, Core +resolves trusted physical USB identity and derives the +`physical-usb-backing/v1` digest used in the exact Host-global tuple +`(Host, physical-usb-backing, opaqueKeyDigest)`. Every physical-USB-backed +security-key and USB Provider claims that same tuple before effects; a +service-specific authority remains additive and cannot replace it. Restart +adopts the exact Service + relay Process DeviceGrant and shared backing claim by +the Provider owner proof; ambiguity quarantines. The physical Device is backing +inventory, not the service authority. + +**Preserved reusable semantics** (grounded in +`packages/d2bd/src/security_key.rs` and +`packages/d2b-priv-broker/src/ops/security_key.rs`): sole hidraw opener (Core via +`SecurityKeyOpenDevice`/`live_open_hidraw_security_key`); post-open double +`fstat` + FIDO usage-page (0xF1D0) + HID raw-info revalidation on the pre-opened +`O_RDWR|O_NONBLOCK|O_NOFOLLOW` fd; async cancellation-safe fd relay I/O; +per-session `CidTranslator` (`alloc_host_cid`/`guest_to_host`/`host_to_guest`/ +`release_guest_cid`); `LeaseId` stale-release guard; cancel of **all** active +CIDs (`build_cancel_packet`) on disconnect; exactly one CTAP ceremony at a time +(`CEREMONY_TIMEOUT` 120 s); a bounded fair wait for a busy lease +(`QUEUE_WAIT_TIMEOUT` 15 s, then `ERR_CHANNEL_BUSY`); and the guest-side UHID +frontend (`packages/d2b-sk-frontend/`: `/dev/uhid` virtual FIDO2 CTAPHID device, +64-byte report relay). + +**Service export/import (D096).** The owner Zone declares a `ResourceExport` +whose local `resourceRef` is the authority `SecurityKeyService`, whose +`serviceType` is `security-key.d2bus.org.SecurityKeyService`, and whose +`projectionSchemaFingerprint` and `factoryFingerprint` match the signed +semantic projection factory. The Service's relay Endpoint remains its local +implementation child and is not copied into the Export. A physical `Device` is +never an export target. A consumer Zone declares a `ResourceImport` naming only +its local ZoneLink plus export key and supplies the corresponding +`expectedServiceType`, `expectedProjectionSchemaFingerprint`, and +`expectedFactoryFingerprint`. Core and the signed import adapter create exactly +one local projection +`SecurityKeyService` with `ownerRef: ResourceImport/`. The projection has +no physical Device ref, hidraw FD, DeviceGrant, selector, or open path. + +Nix/an authorized operator then creates one or more local `SecurityKeyBinding` +resources. Each Binding references the projection Service and its own Guest/User +target and owns the UHID frontend Process/private Endpoint. `SecurityKeyBinding` is +consumer intent and is never auto-created by ResourceImport. Ordinary consumers +never use ResourceImport or a projected Device as the typed security-key handle. + +CTAPHID reports flow over a per-import bounded **encrypted named stream** with +credit backpressure, per-import session generation, deadline, idempotency, and +cancel; only the trusted authority relay and exact Binding frontend see plaintext. +Intermediate controllers see ciphertext. The authority Service serializes all +local and cross-Zone ceremonies through the same `LeaseId`-guarded fair queue, +per-session `CidTranslator`, timeout, and cancel-all-CIDs semantics. No FD, USBIP, +raw hidraw access, remote Ref, or token path crosses a Zone. + +Core owns export/import routing, base lifecycle, and projection ownership; the +Provider adapters own semantic admission and observation. Export removal or +ZoneLink loss revokes the import lease, degrades the projection Service, and +cancels/degrades its Bindings child-first. Reconnect revalidates generation and +fingerprint before frontend replay. D091 drains all Bindings before recycling the +single authority Service. + +**No legacy shortcuts; legacy removed after successor.** The relay adds **no** +new `ProcessRole`, no direct broker path, and no per-VM state file: the hidraw fd +arrives via the D077 EffectPort/LaunchTicket DeviceGrant, observed state is D087 +status-first, the relay endpoint is a D092 `Endpoint`, and cross-Zone sharing is +D096. The legacy daemon-internal accept loop (`ProcessRole::SecurityKeyFrontend` +in `packages/d2bd/src/security_key.rs`), the raw CTAPHID framing over a fixed +vsock port (`SK_VSOCK_PORT`), and the broker sysfs `/sys/class/hidraw/` scan +fallback are removed only after the successor relay/`Endpoint`/named-stream path +and the `device_token`-only broker open are green (see the work items and removal +schedule). + +## Nix configuration + +### Nix authoring shape + +Security-key resources are declared under `d2b.zones..resources`. The +Provider must be installed in every Zone that owns an authority Service or a +consumer Binding. The physical Device and authority Service are authored only in +the owner Zone. A consuming Zone authors ResourceImport plus Binding; Core creates +the projection Service. + +```nix +# Install the Provider +d2b.zones.devices.resources."sk-provider" = { + type = "Provider"; + spec = { + artifactId = "device-security-key"; # selects the signed package + config = { + devices = [ + { + label = "yubikey-primary"; + vendorId = 4176; # 0x1050 — Yubico + productId = 1031; # 0x0407 — YubiKey 5 + serial = null; + } + ]; + sessionRingSize = 32; + leaseTimeoutSecs = 120; + queueWaitTimeoutSecs = 15; + }; + }; +}; + +d2b.zones.work.resources."sk-provider" = { + type = "Provider"; + spec = { + artifactId = "device-security-key"; + config = { + devices = [ ]; + sessionRingSize = 32; + leaseTimeoutSecs = 120; + queueWaitTimeoutSecs = 15; + }; + }; +}; + +# Owner Zone: real physical backing only. +d2b.zones.devices.resources."yubikey-primary-device" = { + type = "Device"; + spec = { + providerRef = "Provider/device-security-key"; + deviceClass = "physical"; + arbitration = "exclusive"; + inventory.selector = { + busClass = "hidraw"; + label = "yubikey-primary"; + vendorId = "1050"; # 4 lower-cased hex digits + productId = "0407"; + serial = null; + }; + provider = { + schemaId = "device-security-key.d2bus.org/Device/spec"; + schemaVersion = "1.0.0"; + settings = { }; + }; + }; +}; + +# Owner Zone: exactly one D097 authority Service for that physical key. +d2b.zones.devices.resources."yubikey-primary" = { + type = "security-key.d2bus.org.SecurityKeyService"; + spec = { + providerRef = "Provider/device-security-key"; + mode = "authority"; + authority = { + authorityScope = "physical-device"; + authorityKeyClass = "semantic-security-key"; + cardinality = "zero-or-one"; + arbitration = "exclusive"; + authorityRef = + "security-key.d2bus.org.SecurityKeyService/yubikey-primary"; + duplicateConflict = "security-key-authority-conflict"; + updateStrategy = "drain-recycle"; + exportability = "explicit-export"; + }; + provider = { + schemaId = "device-security-key.d2bus.org/SecurityKeyService/spec"; + schemaVersion = "1.0.0"; + settings = { + deviceRef = "Device/yubikey-primary-device"; + relayEndpointRef = "Endpoint/yubikey-primary-ctaphid-relay"; + authorityDerivation = "physical-fido-selector"; + ownerProof = "service-and-relay-process-identity"; + fairnessPolicy = "bounded-fifo"; + }; + }; + }; +}; + +d2b.zones.devices.resources."yubikey-primary-export" = { + type = "ResourceExport"; + spec = { + providerRef = "Provider/device-security-key"; + resourceRef = + "security-key.d2bus.org.SecurityKeyService/yubikey-primary"; + serviceType = "security-key.d2bus.org.SecurityKeyService"; + projectionSchemaFingerprint = + "sha256:"; + factoryFingerprint = "sha256:"; + arbitration = "exclusive"; + operations = [ "security-key-ceremony" ]; + consumerZonePolicy = { + zones = [ "Zone/work" ]; + capabilityCeiling = [ "security-key-ceremony" ]; + }; + visibility = "named-zones"; + }; +}; + +# Consumer Zone: import creates the projection Service; do not author a Device +# or an Endpoint. +d2b.zones.work.resources."yubikey-primary-import" = { + type = "ResourceImport"; + spec = { + providerRef = "Provider/device-security-key"; + zoneLinkRef = "ZoneLink/work-uplink"; + exportKey = "devices/yubikey-primary-export"; + expectedServiceType = "security-key.d2bus.org.SecurityKeyService"; + expectedProjectionSchemaFingerprint = + "sha256:"; + expectedFactoryFingerprint = "sha256:"; + projectionName = "yubikey-primary"; + requestedCapabilities = [ "security-key-ceremony" ]; + disconnectPolicy = { mode = "degrade"; }; + }; +}; + +# Consumer intent: one Binding per Guest/User attachment and policy. +d2b.zones.work.resources."corp-vm-yubikey" = { + type = "security-key.d2bus.org.SecurityKeyBinding"; + metadata.ownerRef = "Guest/corp-vm"; + spec = { + providerRef = "Provider/device-security-key"; + serviceRef = + "security-key.d2bus.org.SecurityKeyService/yubikey-primary"; + target = { + guestRef = "Guest/corp-vm"; + userRef = "User/alice"; + }; + policy = { + enabled = true; + }; + provider = { + schemaId = "device-security-key.d2bus.org/SecurityKeyBinding/spec"; + schemaVersion = "1.0.0"; + settings = { + ceremonyTimeoutSecs = 120; + queueWaitTimeoutSecs = 15; + }; + }; + }; +}; +``` + +**Eval-time invariants:** + +1. A physical Device label resolves exactly once in the same owner Zone's + Provider config; `deviceClass=physical`, `busClass=hidraw`, and exclusive + arbitration are mandatory. +2. An authority Service's semantic D097 descriptor is complete and its + `authorityRef` names that Service. The initial Provider extension references + one same-Zone physical Device and one same-Zone reserved relay Endpoint. +3. Two Services/USBIP resources that resolve to the same Host-scoped opaque + physical key fail activation, including conflicts authored in different Zones. +4. A `ResourceExport` names only an authority `SecurityKeyService` through + `resourceRef`, `serviceType`, `projectionSchemaFingerprint`, and + `factoryFingerprint`. Its Service-owned Endpoint is not an Export field. + Exporting a Device or Binding is rejected, as are the obsolete Export + `endpointRef`, `exportedType`, `baseSchemaFingerprint`, and `exportKey` + fields. +5. A ResourceImport's `expectedServiceType`, + `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint` + match the Export and signed local factory. The obsolete `expectedType`, + `expectedBaseSchemaFingerprint`, and `projectionType` fields are rejected. + Nix cannot author `mode=projection`; only Core creates it with `ownerRef: + ResourceImport/`, and it has no Device or hidraw fields. +6. Every Binding base references a same-Zone Service and same-Guest User target; + duplicate `(serviceRef, guestRef, userRef, policy-key)` intent is rejected. +7. No cross-Zone ResourceRef, `hidrawPath`, sysfs path, serial-derived authority + key, fd, USBIP field, or unknown field may appear in any + Service/Binding/export/import base or Provider extension. The initial Provider + extension admits only its strict same-Zone Device/Endpoint refs and bounded + implementation settings. + +**Eval-time derivations:** + +| Field | Derived from | +| --- | --- | +| `metadata.name` | Resource attribute key | +| `metadata.zone` | Zone attribute key | +| `apiVersion` | Constant `"resources.d2bus.org/v3"` | +| `metadata.uid`, `generation`, `revision` | Core-assigned on creation | +| `metadata.finalizers` | Written by Provider controller | +| `status` | Entirely read-only | + +### Guest Nix module (preserved wiring) + +The guest NixOS module `nixos-modules/components/security-key-guest.nix` +continues to wire the following under v3. These module declarations remain in +the Guest Provider's `runtime-cloud-hypervisor` Nix module and are not replaced +by the Device resource: + +- `boot.kernelModules = ["uhid"]` — required for the UHID kernel interface to + exist at all; must be loaded before Core opens `/dev/uhid` for the frontend's + LaunchTicket DeviceGrant. +- The static `d2b-sk-frontend` binary in the Guest store closure. + +**Removed from v3:** `services.udev.extraRules` and +`users.users..extraGroups = ["plugdev"]` are no longer needed. +Core pre-opens `/dev/uhid` with masked `/dev` and passes only the fd; the +frontend has no ambient device path access, so no udev rule or group is required. + +**v3 change:** The `d2b-sk-frontend.service` systemd unit declared in +`security-key-guest.nix` is removed when the Binding-owned Process +`binding--sk-frontend` is live. The Process controller manages the +frontend lifecycle. The Nix module removes the unit declaration behind a +`d2b.securityKey._legacySystemdUnit = false` option gate, defaulting to false +once the Provider is installed. + +## Work items + +All items are New (not yet implemented) unless marked with the baseline evidence +class. + +### Reuse from baseline + +### ADR046-security-key-001 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR-046 provider-device-security-key session/relay owner; depends on ADR046-security-key-008 and the ComponentSession/Process contracts. | +| Current source | `packages/d2bd/src/security_key.rs` — baseline internal `SecurityKeyState` (renamed `RelaySessionTable` in v3 so state terminology remains reserved for Resource status), `LeaseState`, `LeaseId`, `CidTranslator`, `try_acquire_lease`, `release_lease`, `CEREMONY_TIMEOUT`, `QUEUE_WAIT_TIMEOUT` (implemented-and-reachable) | +| Reuse action | adapt | +| Destination | Move to `packages/d2b-provider-device-security-key/src/session.rs` and `cid.rs`; adapt to Provider Process model (remove daemon Mutex wrapping, add async relay protocol) | +| Detailed design | Extract the baseline lease/session constants and CID mapping into provider-local modules. Preserve `LeaseId` stale-release protection, cancel-all-active-CIDs, `CEREMONY_TIMEOUT`, `QUEUE_WAIT_TIMEOUT`, and bounded fair queue semantics; remove daemon-global `Mutex` ownership; keep the authority relay's DeviceGrant/OFD lease for its lifetime. Ceremony rows remain high-churn session records, never Resources. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | ADR046-security-key-010 owns the relay loop; ADR046-security-key-011/ADR046-security-key-012 consume this extracted foundation; ADR046-security-key-009 consumes lifecycle events and writes bounded Service/Binding observations; ComponentSession/encrypted named streams carry CTAPHID bytes. | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `session_state_machine.rs`, `session_ring.rs`, `cancel_propagation.rs`, `session_timeout.rs`, `fair_queue.rs`, and `cid_isolation.rs` verify queue/active/completed/timeout transitions, ring eviction, LeaseId stale-release denial, cancel-all-CIDs, fair timeout, and per-session CID isolation with no daemon-global lease state or ceremony Resource. | +| Removal proof | ADR046-security-key-030 deletes the superseded daemon-internal `packages/d2bd/src/security_key.rs` `SecurityKeyState`, `LeaseState`, `SkRegistry`, and accept-loop ownership after the provider relay/session tests pass. | + +### ADR046-security-key-002 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR-046 provider-device-security-key relay extraction owner; depends on ADR046-security-key-008, ADR046-security-key-001, and the frozen ComponentSession/Endpoint contracts. | +| Current source | `packages/d2bd/src/security_key.rs` — CTAPHID relay loop, `SkAcceptHandle`, `relay_one_ceremony` (implemented-and-reachable) | +| Reuse action | adapt | +| Destination | Move to `packages/d2b-provider-device-security-key/src/relay.rs`; replace daemon-internal Unix socket proxy with ComponentSession over the owned Service Endpoint | +| Detailed design | Extract the CTAPHID ceremony relay behavior into the provider relay binary. Preserve one-ceremony-at-a-time proxy semantics and CTAPHID cancel handling, but replace daemon-internal Unix socket proxying with the `d2b.security-key.v3` ComponentSession over the owned CTAPHID Endpoint and named `ctaphid` stream. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | Core launches the relay Process with a LaunchTicket DeviceGrant and Endpoint attachment; transport-vsock resolves `Endpoint/-ctaphid-relay`; frontend Process connects as ComponentSession initiator; controller receives session events over the manifest-declared internal channel. | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `host_relay_guest_frontend/` integration fixture, `device_grant_no_path.rs`, `descriptor_validation.rs`, and `cancel_propagation.rs` prove relay fd injection, ComponentSession transport, cancel propagation, and absence of daemon-internal socket proxying. | +| Removal proof | ADR046-security-key-030 and ADR046-security-key-031 remove `start_sk_accept_loop`, `SkAcceptHandle`, `relay_one_ceremony`, and the daemon-internal Unix socket proxy bind from `packages/d2bd/src/security_key.rs` and `packages/d2bd/src/lib.rs`. | + +### ADR046-security-key-003 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR-046 provider-device-security-key frontend extraction owner; depends on ADR046-security-key-008 and frozen Process/ComponentSession contracts. | +| Current source | `packages/d2b-sk-frontend/src/` — `main.rs`, `uhid.rs` (implemented-and-reachable); `framing.rs` and `vsock.rs` are obsolete under v3 (replaced by ComponentSession transport) | +| Reuse action | adapt | +| Destination | Adopt `main.rs` and `uhid.rs` as the v3 Process binary entry point; replace `framing.rs`/`vsock.rs` with ComponentSession client from `d2b-session-unix/src/vsock.rs`; wire as Process service in Provider crate | +| Detailed design | Retain UHID creation and frontend entry behavior, but run it as a Binding-owned v3 user-domain Process receiving a pre-opened `/dev/uhid` fd from the `Provider/system-core` Guest-substrate DeviceGrant. Delete raw frame/vsock protocol and use the ComponentSession client/named `ctaphid` stream. No virtual/projected Device exists. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt. | +| Integration | ADR046-security-key-026 defines Service/Binding ownership; ADR046-security-key-020/N17 wire the Binding-owned frontend Process/private Endpoint and same-Zone Service resolution; Core injects UHID from the Guest substrate. | +| Data migration | Full d2b 3.0 reset; no frontend session state import | +| Validation | `host_relay_guest_frontend/`, `device_grant_no_path.rs`, `descriptor_validation.rs`, and guest Nix migration tests prove UHID fd injection, no `/dev/uhid` path, ComponentSession client use, and no raw `framing.rs`/`vsock.rs` protocol. | +| Removal proof | ADR046-security-key-032 removes the legacy `d2b-sk-frontend.service` unit declaration, and the v3 frontend excludes the obsolete `packages/d2b-sk-frontend/src/framing.rs` and `vsock.rs` raw transport behavior. | + +### ADR046-security-key-004 + +| Field | Value | +| --- | --- | +| Dependency/owner | Core LaunchTicket/privileged broker reuse owner; depends on ADR-046-resources-device and ADR046-security-key-013 probe/device-token population. | +| Current source | `packages/d2b-priv-broker/src/ops/security_key.rs` — `live_open_hidraw_security_key`, FIDO usage page revalidation, group validation, `ALLOWED_GROUPS` (implemented-and-reachable) | +| Reuse action | adapt | +| Destination | Preserve revalidation logic; update `SecurityKeyOpenDevice` to use bundle device table `device_token` as sole open target (no iterative sysfs scan); add zone-field handling; remove sysfs fallback. **Core's LaunchTicket calls this internally; the Provider does not call it.** | +| Detailed design | Keep the FIDO usage-page and post-open revalidation logic, but make the trusted private bundle `device_token` the only open target. Add Zone-aware request handling, reject path/sysfs fallback inputs, and keep the operation internal to Core LaunchTicket DeviceGrant resolution rather than callable by the Provider controller. | +| Integration | Provider activation records label-to-`device_token` mappings; Core LaunchTicket resolves `deviceUsage` for the relay Process; broker opens and revalidates the hidraw fd; relay receives only an inherited fd; Core emits path-free `device-grant` audit. | +| Data migration | Full d2b 3.0 reset; no v2 device state import | +| Validation | `packages/d2b-priv-broker/tests/security_key_broker.rs` updates for bundle table lookup and zone-field round trip; `device_grant_no_path.rs` proves Provider code does not call the broker and sees no device path; audit tests prove path-free grant records. | +| Removal proof | The superseded iterative sysfs scan/fallback behavior in `packages/d2b-priv-broker/src/ops/security_key.rs` is removed once bundle-token lookup and revalidation tests pass. | + +### ADR046-security-key-005 + +| Field | Value | +| --- | --- | +| Dependency/owner | `d2b-contracts` security-key ceremony/effect DTO owner; depends on ADR046-provider-004, ADR-046-resource-object-model, ADR-046-resources-device, and ADR046-security-key-008. | +| Current source | `packages/d2b-contracts/src/security_key.rs` — `SecurityKeySessionId`, `SecurityKeyDeviceLabel`, `SecurityKeySession`, `SecurityKeySessionResult`, `SecurityKeyStatusResponse`, `SecurityKeySessionsResponse`, `SecurityKeyOpenDeviceRequest`, `SecurityKeyEvent` (implemented-and-reachable) | +| Reuse action | adapt | +| Destination | Adapt to v3 Zone/ResourceRef identifiers; preserve serde shapes for zero downstream breakage where possible; remove `SecurityKeyApplyUdevRulesRequest` (ADR046-security-key-035) | +| Detailed design | Rebase wire DTOs onto v3 Zone/ResourceRef identifiers; consume the shared ADR046-provider-004 `security-key.d2bus.org` Service/Binding bases and define only strict `device-security-key.d2bus.org` Provider-extension DTOs; reject `spec.provider` on Core projections; place authority/import/attachment semantic observations only under `status.resource` and implementation observations only under `status.provider`; preserve opaque bounded ceremony records as non-Resource DTOs; add `zone` to `SecurityKeyOpenDeviceRequest`; drop the udev-rules request because UHID comes from the Guest-substrate DeviceGrant. No provider-named ResourceType alias is admitted. | +| Integration | Core LaunchTicket, broker open op, Provider controller Service/Binding status/audit, CLI session readers, and provider tests consume the v3 DTOs. | +| Data migration | Full d2b 3.0 reset; no v2 DTO compatibility migration beyond serde-shape preservation where possible | +| Validation | DTO serde round trips, exact provider-neutral ResourceType identity, provider-named alias rejection, canonical minimal base acceptance, Core projection `spec.provider` rejection, D088 `status.resource`/`status.provider` layering, base/Provider-extension field separation, unknown-field denial, zone-field round trip, path-redaction tests, and updated `usb_sk_contract.rs` assertions in the provider crate. | +| Removal proof | ADR046-security-key-035 removes `SecurityKeyApplyUdevRulesRequest`, the `SecurityKeyApplyUdevRules` broker op, and related broker code after UHID DeviceGrant coverage is live. | + +### ADR046-security-key-006 + +| Field | Value | +| --- | --- | +| Dependency/owner | Provider crate test owner; depends on ADR046-security-key-005 v3 DTOs and ADR046-security-key-008 provider crate layout. | +| Current source | `packages/d2b-contract-tests/tests/usb_sk_contract.rs` — DTO serde round-trips, unknown-field denial, broker capability set (implemented-and-reachable) | +| Reuse action | adapt | +| Destination | Move to `packages/d2b-provider-device-security-key/tests/`; update imports and v3 type names | +| Detailed design | Move the reusable semantic assertions for security-key DTO serde, unknown-field denial, and broker capability shape into the provider crate's hermetic `tests/` suite, updating imports and names to the v3 contract modules without weakening assertions. Primary reuse disposition: `adapt`. Preserved source-plan detail: move and adapt. | +| Integration | `cargo test -p d2b-provider-device-security-key --lib --tests` runs the moved contract tests with the provider's DTO/controller test matrix; old contract-test manifests point to the successor coverage before deletion. | +| Data migration | None — test-only move; no runtime state | +| Validation | Moved tests pass under the provider crate; contract assertions are retained; D094 disposition records moved/adapted coverage before old duplicate tests are deleted. | +| Removal proof | ADR046-security-key-033 deletes `packages/d2b-contract-tests/tests/usb_sk_contract.rs` only after the provider-crate successor test covers all prior assertions. | + +### ADR046-security-key-007 + +| Field | Value | +| --- | --- | +| Dependency/owner | Provider crate test/minijail adaptation owner; depends on ADR046-security-key-008 and the frozen Process sandbox contract. | +| Current source | `packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` — minijail profile shape, `ProcessRole::SecurityKeyFrontend` (implemented-and-reachable) | +| Reuse action | adapt | +| Destination | Move to `packages/d2b-provider-device-security-key/tests/`; update for v3 Process resource minijail profile; retain zero-capabilities assertion | +| Detailed design | Move the reusable minijail/sandbox assertions into the provider crate and retarget them from `ProcessRole::SecurityKeyFrontend` to the v3 Process resource templates and relay/controller minijail profiles. Preserve zero-capabilities and seccomp-class assertions while recognizing the frontend uses `Provider/system-systemd` hardening rather than a minijail profile. Primary reuse disposition: `adapt`. Preserved source-plan detail: move and adapt. | +| Integration | Provider tests validate Nix minijail profile entries, Process resource sandbox templates, and system-minijail/system-systemd conformance expectations before old contract tests are retired. | +| Data migration | None — test-only move; no runtime state | +| Validation | Provider-crate tests retain zero-capability and seccomp assertions for relay/controller and assert no minijail profile is used for the frontend Process. | +| Removal proof | ADR046-security-key-033 deletes `packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` only after the provider-crate successor test covers all prior assertions. | + +### New items + +### ADR046-security-key-008 + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR-046 provider-device-security-key crate owner; depends on provider-model/package workspace policy. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | New crate `packages/d2b-provider-device-security-key/` with `src/`, `tests/`, `integration/`, `README.md` (workspace policy requires all four) | +| Detailed design | New crate `packages/d2b-provider-device-security-key/` with `src/`, `tests/`, `integration/`, `README.md` (workspace policy requires all four) | +| Integration | Workspace/package descriptor expose the crate to Core; ADR046-security-key-009 through ADR046-security-key-029 add controllers, resource contracts, relay/frontend, adapters, tests, and docs. | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | Workspace package-policy check rejects missing `src/`, `tests/`, `integration/`, or `README.md`; `cargo test -p d2b-provider-device-security-key --lib --tests` discovers the hermetic suite; README acceptance criteria from the provider crate standard layout are satisfied. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-security-key-009 + +| Field | Value | +| --- | --- | +| Dependency/owner | Provider controller owner; depends on ADR046-security-key-013 probe, ADR046-security-key-016 templates, ADR046-security-key-025 effect port, ADR046-security-key-026 Service/Binding contracts, ADR046-security-key-027 status contract, and ADR-046-resource-reconciliation. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-security-key/src/controller.rs` | +| Detailed design | One controller implements standard reconcile for local physical Devices, authority/projection SecurityKeyServices, and SecurityKeyBindings. It observes Devices; realizes an authority Service as relay Process/Service-owned Endpoint; accepts projection Services only from Core/import after signed-factory admission; realizes each Binding as frontend Process/private Endpoint; enforces child-first finalizers and never creates an import or Device projection. Export/Import routing never treats an Endpoint as exported identity. | +| Integration | Watches Device and both provider-neutral semantic types filtered by `providerRef=Provider/device-security-key`, plus Process, Endpoint, Guest/User, ResourceExport/Import, and Service/export/import-owner dependency indexes; writes semantic base plus signed Provider-extension status/finalizers; drives relay-control messages. | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `controller_reconcile.rs`, `service_binding_projection.rs`, `mutual_exclusion.rs`, `status_binding.rs`, and deletion/finalizer tests cover authority/projection/Binding branches, signed-factory admission before projection reconcile, Service-owned Endpoint isolation from Export/Import identity, no Device projection, and no Volume API calls. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-security-key-010 + +| Field | Value | +| --- | --- | +| Dependency/owner | Authority Service relay owner; depends on ADR046-security-key-001, ADR046-security-key-002, ADR046-security-key-011, ADR046-security-key-012, ADR046-security-key-014, ADR046-security-key-016, and ADR046-security-key-018. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-security-key/src/relay.rs` | +| Detailed design | Authority Service-owned relay entry point: bounded authenticated Binding connections, one LeaseId-guarded fair ceremony queue, CID translation/cancel-all-CIDs, hidraw fd from Core DeviceGrant, CTAPHID named stream, and internal relay-control channel. | +| Integration | Core injects the Service's physical DeviceGrant, relay Endpoint, and controller channel; Bindings that reference authority or projection Services connect through same-Zone Service Endpoints; Core releases grant on relay exit. | +| Data migration | Full d2b 3.0 reset; no relay session state import | +| Validation | `host_relay_guest_frontend/`, `fair_queue.rs`, `device_grant_no_path.rs`, `descriptor_validation.rs`, `cancel_propagation.rs`, and `cid_isolation.rs` prove one authority open, multi-Binding fair serialization, fd-only access, LeaseId cancel, and CID isolation. | +| Removal proof | Supersedes daemon-internal relay behavior removed by ADR046-security-key-030/ADR046-security-key-031 after relay Process tests pass. | + +### ADR046-security-key-011 + +| Field | Value | +| --- | --- | +| Dependency/owner | Relay ceremony-session foundation owner; depends on ADR046-security-key-001 and ADR046-security-key-008. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-security-key/src/session.rs` | +| Detailed design | `SessionStateMachine` with Idle/Queued/Active/Completed/TimedOut, bounded FIFO queue, monotonic LeaseId stale-release guard, per-Binding session ring, timeout/cancel, and ring eviction. Ceremony rows are non-Resource records; DeviceGrant remains held for relay lifetime. | +| Integration | ADR046-security-key-010 consumes it; controller receives lifecycle messages; Service/Binding status receives aggregates only; session query/audit consumes bounded non-secret rows. | +| Data migration | Full d2b 3.0 reset; no session ring import | +| Validation | `session_state_machine.rs`, `session_ring.rs`, `fair_queue.rs`, `session_timeout.rs`, and `cancel_propagation.rs` cover queue fairness, eviction, stale LeaseId rejection, timeout, and cancel. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-security-key-012 + +| Field | Value | +| --- | --- | +| Dependency/owner | Relay CID-translation foundation owner; depends on ADR046-security-key-001 and ADR046-security-key-008. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-security-key/src/cid.rs` | +| Detailed design | CID translator: per-active-ceremony u32→u64 host-CID allocation, bimap, cancel-all-active-CIDs, and eviction on ceremony end | +| Integration | Relay rewrites frontend CTAPHID CIDs before sending to hidraw fd and reverses responses before writing the ComponentSession named stream; session teardown drops the map. | +| Data migration | Full d2b 3.0 reset; CID maps are transient and not imported | +| Validation | `cid_isolation.rs` verifies per-session allocation, round trip, no sharing across relays, and eviction on session end. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-security-key-013 + +| Field | Value | +| --- | --- | +| Dependency/owner | Probe/effect-port and activation owner; depends on ADR046-security-key-025 effect port and Core private bundle device table support. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-security-key/src/probe.rs`; Provider activation/Core private bundle device table population for label → `device_token` | +| Detailed design | hidraw probe: `probe.rs` — calls `SecurityKeyEffectPort::observe_inventory(&device_id, &policy_id)` with opaque types injected by Core; interprets `InventoryObservation`; never reads `/sys/class/hidraw/` directly; bundle device table population at activation time (Provider activation resolves label → `device_token` via Core; stored in private bundle) | +| Integration | Controller scheduled-observe invokes `probe.rs`; Core adapter implements `SecurityKeyEffectPort`; Nix activation emits private label-to-token bundle entries; Device status receives `DevicePresent` and phase updates. | +| Data migration | Full d2b 3.0 reset; no v2 probe state import | +| Validation | `controller_reconcile.rs` scheduled-observe tests, `descriptor_validation.rs` Debug-redaction capture, and path-safety tests prove Provider never reads sysfs and receives only opaque observations. | +| Removal proof | Supersedes provider-side or broker fallback sysfs scanning; ADR046-security-key-004/ADR046-security-key-018 removal proof verifies only bundle `device_token` lookup remains. | + +### ADR046-security-key-014 + +| Field | Value | +| --- | --- | +| Dependency/owner | ComponentSession/security descriptor contract owner; depends on ADR046-security-key-008, ADR046-security-key-005, and ADR-046-componentsession-and-bus. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-security-key/src/descriptor.rs` | +| Detailed design | Declare relay↔controller service and relay↔Binding-frontend `d2b.security-key.v3` fingerprints, Noise profiles, canonical Service/Binding subject pairing, bounded encrypted-stream records, and descriptor validation; no ambient path or raw vsock CID. | +| Integration | Provider descriptor declares services and fingerprints; LaunchTicket injects internal channel and Endpoint transport; relay/controller/frontend validate descriptors and peer authority before exchanging messages. | +| Data migration | Full d2b 3.0 reset; no v2 transport/session state import | +| Validation | `descriptor_validation.rs` covers wrong service, wrong descriptor digest, wrong SO_PEERCRED uid, unenrolled key, oversized records, no ambient path, and redacted opaque IDs. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-security-key-015 + +| Field | Value | +| --- | --- | +| Dependency/owner | Sandbox/minijail foundation owner; depends on ADR046-security-key-008, ADR046-security-key-007, and ADR-046-components-processes-and-sandbox. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `nixos-modules/minijail-profiles.nix` entries for relay and controller; provider descriptor sandbox templates for relay/controller/frontend | +| Detailed design | Minijail profiles for relay and controller only; frontend uses `Provider/system-systemd` hardening directives compiled from `SandboxSpec` (no minijail profile for frontend). Add relay and controller entries to `nixos-modules/minijail-profiles.nix`; `capabilityClasses: []`; `seccompClass: sk-relay` and `seccompClass: sk-controller` | +| Integration | Nix minijail profiles feed system-minijail Process launches for controller/relay; frontend Process template feeds system-systemd hardening; provider tests assert the split. | +| Data migration | Full d2b 3.0 reset; no sandbox state import | +| Validation | `minijail_sk_frontend` successor tests, sandbox template tests, and zero-capability/seccomp assertions cover relay/controller minijail profiles and no frontend minijail profile. | +| Removal proof | Supersedes `ProcessRole::SecurityKeyFrontend`-centric minijail test ownership removed by ADR046-security-key-033/ADR046-security-key-034 after Process-resource coverage passes. | + +### ADR046-security-key-016 + +| Field | Value | +| --- | --- | +| Dependency/owner | Provider process/Endpoint-template owner; depends on ADR046-security-key-008, ADR046-security-key-015, and ADR046-security-key-026 Service/Binding ownership contracts. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | Provider descriptor Process templates and owned CTAPHID `Endpoint` template for `Provider/device-security-key` | +| Detailed design | Templates: Provider controller; authority Service-owned relay Process/relay Endpoint; Binding-owned frontend Process/private Endpoint; projection Service local Endpoint. Frontend requires Guest/User and the system-core UHID DeviceGrant; no virtual Device template exists. | +| Integration | Core creates controller; Provider controller realizes authority Services and Bindings plus each projection Service's ordinary local import-route Endpoint; Process Providers launch children and preserve ownerRef boundaries. | +| Data migration | Full d2b 3.0 reset; no v2 processes.json import | +| Validation | `controller_reconcile.rs`, Process template golden tests, Endpoint resource tests, and frontend `userRef` admission tests prove templates and Endpoint shape. | +| Removal proof | Supersedes the legacy readiness-only `ProcessRole::SecurityKeyFrontend` tracking node removed by ADR046-security-key-034 after v3 Process resources are live. | + +### ADR046-security-key-017 + +| Field | Value | +| --- | --- | +| Dependency/owner | Provider package descriptor owner; depends on ADR046-security-key-008, ADR046-security-key-005, ADR046-security-key-014, ADR046-security-key-016, ADR046-security-key-026, ADR046-security-key-027, and ADR-046-provider-model-and-packaging. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | Signed Provider descriptor JSON for `Provider/device-security-key` in the provider package | +| Detailed design | Signed descriptor: config; physical Device integration; implementation claim for the provider-neutral `security-key.d2bus.org` Service/Binding base schemas/fingerprints; strict `device-security-key.d2bus.org` spec/status extensions; authority/projection union and D097 descriptor; a D096 projection factory with exact `serviceType`, `projectionSchemaFingerprint`, and semantic `factoryFingerprint`; controller/relay/frontend/Endpoint templates; export/import adapter capability; ComponentSession services; empty ProviderStateSet; permission claims. Provider/adapter identity is signed separately and Service-owned Endpoints are not factory or Export fields. | +| Integration | Core ProviderDeployment verifies the signed descriptor, installs ResourceApiBinding and component descriptors, exposes service fingerprints to ComponentSession validation, and supplies permission claims/RBAC bindings. | +| Data migration | Full d2b 3.0 reset; no provider descriptor import | +| Validation | Descriptor schema validation, semantic-base versus Provider-extension fingerprints, exact projection-schema/factory fingerprint derivation and stability under Provider/adapter identity changes, exact type/no-alias tests, service inventory tests, permission claim tests, empty ProviderStateSet tests, and README/provider package conformance checks. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-security-key-018 + +| Field | Value | +| --- | --- | +| Dependency/owner | Core LaunchTicket/broker owner; depends on ADR046-security-key-004, ADR046-security-key-005, ADR046-security-key-013, and ADR-046-resources-device. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | v3 `SecurityKeyOpenDevice` broker op and Core LaunchTicket DeviceGrant resolution path | +| Detailed design | v3 `SecurityKeyOpenDevice` broker op update: add `zone` field; implement bundle device table `device_token` lookup as sole open path; remove iterative sysfs scan from broker; add post-open revalidation steps (fstat, HIDIOCGRAWINFO, HIDIOCGRDESC). This is an internal Core operation called by LaunchTicket; the Provider controller does not call it. | +| Integration | Authority Service controller derives relay `deviceUsage` from `spec.provider.settings.deviceRef`; Core admits authority then resolves DeviceGrant through the private bundle table; broker returns an fd to Core; projection Services never enter this path. | +| Data migration | Full d2b 3.0 reset; no v2 broker state import | +| Validation | Broker unit tests for zone field and token lookup, path-rejection tests, post-open revalidation tests, and provider tests proving no Provider broker call or sysfs path. | +| Removal proof | Superseded broker iterative sysfs scan behavior is removed; tests prove only bundle `device_token` lookup is accepted for `SecurityKeyOpenDevice`. | + +### ADR046-security-key-027 + +| Field | Value | +| --- | --- | +| Dependency/owner | Provider state/status contract owner; depends on ADR046-security-key-008, ADR046-security-key-026, and ADR-046-provider-state. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | Provider descriptor state declaration, controller/status logic, Process templates, and Nix principal provisioning for `Provider/device-security-key` | +| Detailed design | Empty ProviderStateSet and strict bounded status schemas: physical presence in Device `status.resource`; semantic authority/import aggregates in Service `status.resource`; attachment aggregates in Binding `status.resource`; initial physical-backing claim, relay, Endpoint, queue, and ceremony observations only in `status.provider`. No semantic field appears directly under `status`, and Core projections contain no `spec.provider`. Ceremony rows remain high-churn non-Resource session records; CTAP/fd/LeaseId/CID data stays transient. No Process has `/state`. | +| Integration | ADR046-security-key-017 signs schemas; ADR046-security-key-009 writes resource-local status; Core Operation/session/audit surfaces own bounded records; Volume controllers see no request. | +| Data migration | Full d2b 3.0 reset; no v2 state/config import | +| Validation | `status_binding.rs` proves empty ProviderStateSet, no `/state` mounts, no Volume API calls, authority/import/attachment fields only under `status.resource`, implementation fields only under `status.provider`, no projection `spec.provider`, and no CTAP/fd/session secrets in status/log/audit/metrics. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-security-key-019 + +| Field | Value | +| --- | --- | +| Dependency/owner | Nix resource compiler owner; depends on ADR046-security-key-017, ADR046-security-key-026, ADR046-zone-control-024, and ADR-046-nix-configuration. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `nixos-modules/` resource compiler/eval assertions for physical Device, authority Service, ResourceExport/Import, and consumer Binding | +| Detailed design | Compile the owner Device→Service→export and consumer import→projection-Service→Binding shape. Emit Export `resourceRef`, `serviceType`, `projectionSchemaFingerprint`, and `factoryFingerprint`, and matching Import `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`; the Import `exportKey` identifies the ResourceExport. Reject Export Endpoint/custom-key fields, authored projections, projection `spec.provider`, Device export/projection, cross-Zone refs, duplicate authorities/Bindings, paths, and any security-key/USB configuration that does not collide through the exact Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` tuple after trusted identity resolution. | +| Integration | Nix emits Device/authority Service/export/import/Binding with canonical D096 fields; Core alone creates projection Service; the Service controller alone owns relay/import-route Endpoints; bundle feeds Provider and authority-index admission. | +| Data migration | Full d2b 3.0 reset; current Nix options migrate to v3 Zone resources without state import | +| Validation | Nix eval tests for label resolution, `busClass=hidraw`, exclusive arbitration, exact canonical Export/Import field emission and fingerprint matching, rejection of obsolete Export `endpointRef`/`exportedType`/`baseSchemaFingerprint`/`exportKey` and Import `expectedType`/`expectedBaseSchemaFingerprint`/`projectionType`, Core-only projection without `spec.provider`, byte-identical USB/security-key physical backing tuple collision, Provider-private-class bypass rejection, prohibited fields, and providerRef resolution. | +| Removal proof | Supersedes current option shape only after v3 Zone resource option parity; legacy security-key/USBIP mutual-exclusion assertion is replaced by v3 resource assertion coverage. | + +### ADR046-security-key-020 + +| Field | Value | +| --- | --- | +| Dependency/owner | Guest Nix migration owner; depends on ADR046-security-key-003, ADR046-security-key-016, and ADR046-security-key-026 Binding-owned frontend contract. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `nixos-modules/components/security-key-guest.nix` migration gate `d2b.securityKey._legacySystemdUnit` | +| Detailed design | Guest Nix module migration gate: `d2b.securityKey._legacySystemdUnit` option, defaulting to false when Provider is installed; remove `d2b-sk-frontend.service` unit | +| Integration | Guest Nix keeps `uhid` and the static frontend binary; Binding controller owns frontend lifecycle and system-core supplies the UHID DeviceGrant; no Device row or udev rule is emitted. | +| Data migration | Full d2b 3.0 reset; no legacy frontend unit state import | +| Validation | Nix eval tests show the legacy unit is absent by default with Provider installed, can be gated only during transition if required, and `uhid` module/binary wiring remains present. | +| Removal proof | ADR046-security-key-032 deletes the superseded `nixos-modules/components/security-key-guest.nix` `d2b-sk-frontend.service` declaration after the gate defaults to false. | + +### ADR046-security-key-021 + +| Field | Value | +| --- | --- | +| Dependency/owner | Audit owner for Core device-grant and Service/Binding lifecycle; depends on ADR046-security-key-009, ADR046-security-key-018, and ADR-046-telemetry-audit-and-support. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | Core `device-grant` audit and Provider controller Service/Binding ceremony lifecycle audit | +| Detailed design | Path-free authority-grant records from Core and bounded Service/Binding/session digests/outcomes from controller; no path, raw target identity, LeaseId, session content, or CTAP bytes. `resource_name_digest` is admitted only in the Core authority-grant audit after DeviceGrant authorization and is never copied to OTEL. | +| Integration | Core emits grant audit; controller emits Service/Binding lifecycle audit; Zone stream stores bounded records; CLI/support consumes digests/outcomes. | +| Data migration | Full d2b 3.0 reset; no v2 audit import | +| Validation | Audit tests assert path-free fields, bounded digests, no guest name/session content/CTAP bytes, grant emitted by Core not Provider controller, and lifecycle emitted by controller. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-security-key-022 + +| Field | Value | +| --- | --- | +| Dependency/owner | Observability owner; depends on ADR046-security-key-010 relay, ADR046-security-key-009 controller, and ADR-046-telemetry-audit-and-support. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | Provider/controller bounded telemetry emitter and observability-otel handoff for security-key metrics | +| Detailed design | OTEL metrics: `d2b_device_sk_session_total`, `d2b_device_sk_ceremony_duration_seconds`, `d2b_device_sk_relay_restarts_total` via bounded emitter ring; descriptors use only closed semantic labels and never Zone/resource-name-derived identity. Provider spans use only fixed operation/phase/outcome/error-class attributes. Neither metrics nor spans admit a resource name, UID, ref, digest (including `resource_name_digest`), session ID, or derived identity token, while `d2b.zone` and `d2b.provider` remain OTEL Resource attributes. | +| Integration | Relay/controller write metric events to the bounded ring; observability-otel Provider drains and exports; dashboards/CLI consume closed labels and bounded histograms. | +| Data migration | Full d2b 3.0 reset; no v2 telemetry import | +| Validation | `telemetry_identity_canaries.rs` and metric inventory tests structurally assert closed label/span-attribute sets; exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, resource-name-derived keys, and `resource_name_digest`; Device/Service/Binding/Guest/Zone name, UID, ref, and digest canary absence from metrics and spans; retained `d2b.zone` Resource attributes; bounded ring behavior; and correct session/ceremony/restart counters. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-security-key-023 + +| Field | Value | +| --- | --- | +| Dependency/owner | Provider documentation owner; depends on ADR046-security-key-008 through ADR046-security-key-022 and ADR046-security-key-024 through ADR046-security-key-029 for complete behavior. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-security-key/README.md` | +| Detailed design | README: initial Provider identity, provider-neutral Service/Binding catalog, strict Provider-extension fields, physical Device, owner/export/import/projection/Binding chain, process ownership, RBAC, invariants, status/telemetry, no-alias rule, and commands | +| Integration | Workspace/package policy and provider crate acceptance use the README as the human entry point; docs link to it for provider-local build/test/integration commands. | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | README presence check from provider crate standard layout; documentation review verifies every listed section and command is present and matches the crate/package behavior. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-security-key-024 + +| Field | Value | +| --- | --- | +| Dependency/owner | Endpoint/ComponentSession integration owner; depends on ADR046-security-key-003, ADR046-security-key-010, ADR046-security-key-014, ADR046-security-key-016, ADR046-security-key-026, and ADR-046-componentsession-and-bus. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | Authority/projection Service Endpoint and Binding private Endpoint resolution, including transport-vsock and ZoneLink encrypted streams | +| Detailed design | Resolve each Binding only through its same-Zone Service Endpoint; enroll Noise KK for Service/Binding frontend; authority uses transport-vsock locally, projection uses per-import bounded encrypted stream with credits/backpressure/generation/deadline/cancel. | +| Integration | Service/Binding-owned Endpoints produce opaque LaunchTicket attachments; the import adapter binds the projection Service's ordinary local import-route Endpoint; no remote Ref, FD, or raw locator is exposed. | +| Data migration | Full d2b 3.0 reset; no v2 transport state import | +| Validation | `host_relay_guest_frontend/` and `descriptor_validation.rs` verify Endpoint resolution, Noise KK enrollment, attachment opacity, and no raw vsock CID/port in status/spec. | +| Removal proof | Supersedes baseline `vsock.sock_14320` raw port usage; tests prove no `vsockPort` or raw AF_VSOCK framing remains for security-key transport. | + +### ADR046-security-key-025 + +| Field | Value | +| --- | --- | +| Dependency/owner | `d2b-contracts` neutral effect-port foundation owner; depends on ADR046-security-key-008 and ADR-046-resources-device. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `d2b-contracts` neutral `SecurityKeyEffectPort` trait/types; `packages/d2b-provider-device-security-key/src/effect_port.rs` re-export; Core adapter implementation in `d2b-provider` or `d2b-provider-toolkit` | +| Detailed design | Define/re-export the opaque redacting `SecurityKeyEffectPort` types in the neutral contract crate and implement the Core adapter; inject per physical Device into the Provider controller; relay and projection Service do not receive the port. | +| Integration | Core resolves Zone/label to opaque IDs and injects the port into the controller; controller scheduled-observe calls the trait; Provider crate depends only on the neutral contract/re-export; relay path is unaffected. | +| Data migration | Full d2b 3.0 reset; no v2 effect-port state import | +| Validation | Unit tests assert Debug redaction, controller calls `observe_inventory` with injected IDs, relay has no port dependency, and fake Core adapter returns bounded `InventoryObservation`. | +| Removal proof | None — net-new; no prior owner to remove | + +### ADR046-security-key-026 + +| Field | Value | +| --- | --- | +| Dependency/owner | Device-security-key Service/Binding implementation owner; depends on ADR046-provider-004, ADR046-security-key-008, ADR046-security-key-005, resource object/Device/D096/D097 contracts. | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | `packages/d2b-provider-device-security-key/src/{resource_type,provider_extension,admission}.rs`; controller contracts; system-core Guest UHID authority-subresource DeviceGrant (common base lives under ADR046-provider-004) | +| Detailed design | Bind the shared semantic authority/projection Service and Binding base versions/fingerprints from ADR046-provider-004, then define only the initial strict Provider extension and admission. The owner/Binding extension references the local physical Device/relay Endpoint and owns CTAPHID/fairness/frontend settings and observations. Projection is Core-owned by ResourceImport with `providerRef` plus semantic base/import fields, no `spec.provider`, and no Device/open; routing derives from the signed local descriptor, `providerRef`, and import record. Export admission binds the authority Service's `resourceRef` and `serviceType` to the signed projection-schema and factory fingerprints, never to its Endpoint. Binding is operator intent and the initial extension realizes its frontend Process/private Endpoint. Standard Device remains physical only; provider-named ResourceType aliases are rejected. | +| Integration | ResourceExport targets Service with canonical type/fingerprint fields; ResourceImport supplies matching expected fields and creates projection Service; Binding references same-Zone Service; Service controllers retain Endpoint ownership; Core injects Guest UHID without a Device row. | +| Data migration | Full d2b 3.0 reset; no legacy Device/claim projection import | +| Validation | Fast schema/lifecycle conformance consumes the ADR046-provider-004 fixtures, accepts canonical minimal base without `spec.provider`, includes a fake alternate security-key Provider, and proves Device→provider-neutral Service→export→import→projection Service→provider-neutral Binding→frontend, exact canonical Export/Import fields, no Endpoint export, projection `spec.provider` rejection, D088 status layering, strict base/Provider-extension separation, exact types with no aliases, strict ownership/finalizers, no Device projection, and no local hidraw open in consumer Zone. | +| Removal proof | Supersedes legacy frontend/import Device modeling; ADR046-security-key-035 removes udev mutation and ADR046-security-key-032 removes the legacy unit once Binding-owned realization is live. | + +### Removal items + +### ADR046-security-key-030 + +| Field | Value | +| --- | --- | +| Dependency/owner | Provider-device-security-key removal owner; depends on ADR046-security-key-001, ADR046-security-key-002, ADR046-security-key-010, ADR046-security-key-011, and ADR046-security-key-012 successor relay/session coverage. | +| Current source | `packages/d2bd/src/security_key.rs` — `start_sk_accept_loop`, `SecurityKeyState`, `LeaseState`, `SkRegistry` | +| Reuse action | delete-after-cutover | +| Destination | Removed from daemon; successor behavior lives in `packages/d2b-provider-device-security-key/src/relay.rs`, `session.rs`, and `cid.rs` | +| Detailed design | Remove target `packages/d2bd/src/security_key.rs` — `start_sk_accept_loop`, `SecurityKeyState`, `LeaseState`, `SkRegistry` after v3 relay Process is live and stable; keep behind feature gate only if needed during transition. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete. | +| Integration | d2bd no longer owns security-key accept/session state; Provider controller, authority Service relay, and Binding frontends own lifecycle; Core LaunchTicket owns hidraw/UHID grants. | +| Data migration | Full d2b 3.0 reset; no daemon session state migration | +| Validation | Provider relay/session tests pass; daemon build has no references to removed symbols; no legacy security-key accept loop starts under d2bd. | +| Removal proof | Concrete removed path/behavior: `packages/d2bd/src/security_key.rs` `start_sk_accept_loop`, `SecurityKeyState`, `LeaseState`, and `SkRegistry` daemon-internal accept/session ownership are absent. | + +### ADR046-security-key-031 + +| Field | Value | +| --- | --- | +| Dependency/owner | d2bd integration removal owner; depends on ADR046-security-key-030. | +| Current source | `packages/d2bd/src/lib.rs` — `start_sk_accept_loop` call site and daemon-internal Unix socket proxy bind | +| Reuse action | delete-after-cutover | +| Destination | Removed from daemon startup; successor launch path is ProviderDeployment/controller-created relay Process plus Endpoint/ComponentSession transport | +| Detailed design | Remove target `packages/d2bd/src/lib.rs` — `start_sk_accept_loop` call site and daemon-internal Unix socket proxy bind after ADR046-security-key-030. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete. | +| Integration | d2bd startup no longer binds a security-key Unix socket proxy; Core/ProviderDeployment starts provider controller and relay Process resources; transport-vsock Endpoint supplies frontend connectivity. | +| Data migration | Full d2b 3.0 reset; no daemon socket state migration | +| Validation | d2bd startup tests/build prove no `start_sk_accept_loop` call or security-key proxy bind remains; provider integration test proves CTAPHID flow through Endpoint/ComponentSession. | +| Removal proof | Concrete removed path/behavior: `packages/d2bd/src/lib.rs` no longer calls `start_sk_accept_loop` and no longer binds the daemon-internal security-key Unix socket proxy. | + +### ADR046-security-key-032 + +| Field | Value | +| --- | --- | +| Dependency/owner | Guest Nix module removal owner; depends on ADR046-security-key-020, ADR046-security-key-003, and ADR046-security-key-026 Binding frontend/UHID contract. | +| Current source | `nixos-modules/components/security-key-guest.nix` — `d2b-sk-frontend.service` systemd unit declaration | +| Reuse action | delete-after-cutover | +| Destination | Removed from guest Nix module; successor is Binding-owned `Process/binding--sk-frontend` | +| Detailed design | Remove target `nixos-modules/components/security-key-guest.nix` — `d2b-sk-frontend.service` systemd unit declaration after ADR046-security-key-020 migration gate defaults to false. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete. | +| Integration | Guest Nix keeps `uhid` and frontend binary only; Provider controller creates the Binding-owned frontend; system-systemd manages it. | +| Data migration | Full d2b 3.0 reset; no legacy unit state migration | +| Validation | Nix eval tests prove no static `d2b-sk-frontend.service` is emitted with Provider installed; frontend Process integration proves replacement lifecycle. | +| Removal proof | Concrete removed path/behavior: `nixos-modules/components/security-key-guest.nix` no longer declares the static `d2b-sk-frontend.service` unit. | + +### ADR046-security-key-033 + +| Field | Value | +| --- | --- | +| Dependency/owner | Test-suite migration/removal owner; depends on ADR046-security-key-006 and ADR046-security-key-007 provider-crate successor tests. | +| Current source | `packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` and `packages/d2b-contract-tests/tests/usb_sk_contract.rs` | +| Reuse action | delete-after-cutover | +| Destination | Removed from `packages/d2b-contract-tests/tests/`; successor tests live in `packages/d2b-provider-device-security-key/tests/` | +| Detailed design | Remove target `packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` and `packages/d2b-contract-tests/tests/usb_sk_contract.rs` after ADR046-security-key-006/ADR046-security-key-007 tests are in Provider crate and cover all prior assertions. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete after move/adapt. | +| Integration | D094 disposition updates closed gate manifests, layer1 jobs, pins, ledgers, and CI shards so only the provider-crate successor suite remains. | +| Data migration | None — test-only move/delete; no runtime state | +| Validation | Provider-crate tests pass with retained assertions; old contract-test paths are absent from manifests/CI; no duplicate old/new suite runs indefinitely. | +| Removal proof | Concrete removed paths: `packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` and `packages/d2b-contract-tests/tests/usb_sk_contract.rs` are deleted after provider-crate successor coverage passes. | + +### ADR046-security-key-034 + +| Field | Value | +| --- | --- | +| Dependency/owner | Core ProcessRole removal owner; depends on ADR046-security-key-016 Process resources, ADR046-security-key-015 sandbox templates, and system-minijail/system-systemd conformance. | +| Current source | `ProcessRole::SecurityKeyFrontend` in `d2b-core/src/processes.rs` | +| Reuse action | delete-after-cutover | +| Destination | Removed from `d2b-core/src/processes.rs`; successor frontend is a v3 Process resource owned by `Provider/device-security-key` | +| Detailed design | Remove target `ProcessRole::SecurityKeyFrontend` in `d2b-core/src/processes.rs` after relay and frontend are v3 Process resources; no other code reference expected. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete. | +| Integration | ProcessRole disposition table confirms all security-key frontend lifecycle, sandbox, readiness, and DeviceGrant semantics are represented by Resource Process templates and Process Providers before enum removal. | +| Data migration | Full d2b 3.0 reset; no processes.json role migration | +| Validation | Workspace build proves no `ProcessRole::SecurityKeyFrontend` references; provider Process template tests prove the v3 replacement; process conformance passes. | +| Removal proof | Concrete removed path/behavior: `d2b-core/src/processes.rs` no longer contains `ProcessRole::SecurityKeyFrontend` or a security-key frontend role in the legacy ProcessRole/VmProcessDag model. | + +### ADR046-security-key-035 + +| Field | Value | +| --- | --- | +| Dependency/owner | Broker/contracts/Nix removal owner; depends on ADR046-security-key-005, ADR046-security-key-018, ADR046-security-key-020, and ADR046-security-key-026 Guest-substrate UHID replacement. | +| Current source | `SecurityKeyApplyUdevRules` broker op, `SecurityKeyApplyUdevRulesRequest` DTO in `packages/d2b-contracts/src/security_key.rs`, and all related broker code | +| Reuse action | delete-after-cutover | +| Destination | Removed from contracts and broker; successor access is static guest Nix `uhid` module plus Core pre-opened `/dev/uhid` DeviceGrant for the frontend Process | +| Detailed design | Remove `SecurityKeyApplyUdevRules`, its DTO, and related broker code after the Binding-owned frontend and system-core UHID DeviceGrant are live. Guest Nix loads `uhid` but emits no security-key udev rule. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete. | +| Integration | Guest Nix/Process DeviceGrant path provides UHID access; contracts no longer expose the op/request; broker capability set drops the udev mutation; provider/contract tests assert absence. | +| Data migration | Full d2b 3.0 reset; no udev rule state migration | +| Validation | DTO unknown-field/capability tests prove `SecurityKeyApplyUdevRulesRequest` and op are absent; `device_grant_no_path.rs` proves frontend has UHID fd without udev/plugdev; broker build has no related code. | +| Removal proof | Concrete removed path/behavior: `SecurityKeyApplyUdevRules` broker operation, `SecurityKeyApplyUdevRulesRequest` in `packages/d2b-contracts/src/security_key.rs`, and related broker code are absent. | + +### ADR046-security-key-028 + +| Field | Value | +| --- | --- | +| Dependency/owner | Cross-Zone adapter owner; depends on ADR046-security-key-024, ADR046-security-key-026, ADR046-security-key-029, ADR046-zone-control-019, and ADR046-zone-control-020. | +| Current source | None — net-new ADR 0046 cross-Zone sharing (D096) | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-device-security-key/src/share_adapter.rs` | +| Detailed design | Signed adapters admit ResourceExport only when `resourceRef` names an authority SecurityKeyService, `serviceType` is `security-key.d2bus.org.SecurityKeyService`, and `projectionSchemaFingerprint` plus `factoryFingerprint` match the signed semantic factory. ResourceImport must supply the corresponding `expectedServiceType`, `expectedProjectionSchemaFingerprint`, and `expectedFactoryFingerprint`; its `exportKey` identifies the ResourceExport. The Service's relay Endpoint stays a Service-owned implementation child and is never an Export field. Core invokes the factory to create one projection SecurityKeyService with `ownerRef: ResourceImport/`, `providerRef`, semantic base/import fields, and no `spec.provider`; route selection comes from the signed local descriptor and ResourceImport record. The semantic factory fingerprint binds factory metadata plus projection-protocol version only, while adapter identity is authenticated separately by the signed Provider descriptor. They never project Device or auto-create Binding. Route Binding ceremonies over bounded encrypted named streams to the single authority fair queue; no FD/USBIP/hidraw/ref crosses Zones. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new (implement the signed security-key export/import adapter). | +| Integration | Core export/import routing/projection lifecycle; ADR046-security-key-029 authority; ADR046-security-key-024 Endpoint streams; Nix/operator-authored Binding consumes the same-Zone projection. | +| Data migration | Full d2b 3.0 reset; no cross-Zone sharing state | +| Validation | Fast fake-stream conformance proves owner Service→export→import→projection Service→Binding→frontend; exact canonical Export/Import type and fingerprint fields; rejection of Export `endpointRef`, `exportedType`, `baseSchemaFingerprint`, and `exportKey` plus Import `expectedType`, `expectedBaseSchemaFingerprint`, and `projectionType`; rejection of projection `spec.provider`; semantic factory-fingerprint stability when signed adapter identity changes; separate signed-descriptor identity authentication; one fair LeaseId-guarded ceremony; ciphertext to intermediaries; no Device projection/local hidraw/FD/USBIP; revocation degradation; and audit metadata only. | +| Removal proof | Not applicable (new surface) | + +### ADR046-security-key-029 + +| Field | Value | +| --- | --- | +| Dependency/owner | D097 authority foundation owner; depends on ADR046-security-key-001, ADR046-security-key-002, ADR046-security-key-003, ADR046-security-key-004, ADR046-security-key-018, ADR046-security-key-026, ADR046-zone-control-024, and the D097 authority contract. | +| Current source | `packages/d2bd/src/security_key.rs` (`CidTranslator`, `SecurityKeyState`, `LeaseId`/`LeaseState`, `CEREMONY_TIMEOUT` 120 s, `QUEUE_WAIT_TIMEOUT` 15 s, `parse_ctaphid_report`/`build_cancel_packet`); `packages/d2b-priv-broker/src/ops/security_key.rs` (`live_open_hidraw_security_key`, double `fstat` + FIDO usage-page 0xF1D0 + HID raw-info revalidation, `O_RDWR\|O_NONBLOCK\|O_NOFOLLOW`); `packages/d2b-sk-frontend/src/{main,uhid,vsock,framing}.rs` (UHID FIDO2 CTAPHID frontend, 64-byte report relay) | +| Reuse source | Same baseline daemon/broker/frontend symbols | +| Reuse action | adapt | +| Destination | `packages/d2b-provider-device-security-key/src/{authority,relay,streams}.rs`; D097 `AuthorityDescriptor` on authority SecurityKeyService | +| Detailed design | The provider-neutral authority Service, not Device/Endpoint/Process, is the stable D097 owner and carries the semantic opaque Host-scoped zero-or-one descriptor. The initial Provider extension references the local physical Device and relay Endpoint and supplies service-specific physical-key derivation, Service+relay ownerProof, and bounded-fairness details. After trusted USB identity resolution, Core additionally derives `physical-usb-backing/v1` and atomically claims the exact `(Host, physical-usb-backing, opaqueKeyDigest)` tuple used by every USB Provider before any open, withhold, bind, module, relay, or attachment effect; Provider-private claims cannot replace it. Preserve sole Core open with double-fstat/FIDO/HID validation, async fd I/O, per-session CidTranslator, LeaseId stale-release guard, cancel-all-CIDs, one ceremony, bounded FIFO wait, and Binding-owned UHID frontend. Ceremony rows are not Resources. Primary reuse disposition: `adapt`. Preserved source-plan detail: `adapt` — relay becomes the D097 hidraw authority; transport moves to Endpoint/named-stream. | +| Integration | Authority Service owns relay/Endpoint; Core index admits it and LaunchTicket supplies physical DeviceGrant; ADR046-security-key-028 exports/imports Service; Binding owns frontend/private Endpoint; USBIP conflict remains Host-wide. | +| Data migration | Full d2b 3.0 reset; no per-session/lease state persisted | +| Validation | Fast hermetic tests adapt the existing `CidTranslator`/lease/cancel/UHID/broker-revalidation suites: CID alloc/translate/release, `LeaseId` stale-release, cancel-all-CIDs on disconnect, one-ceremony + 120 s timeout, 15 s fair-wait `ERR_CHANNEL_BUSY`, UHID frame round-trip, broker double-`fstat`+FIDO+HID revalidation, byte-identical USB/security-key backing tuple derivation for one fake token, and `physical-usb-backing-conflict` before effects under alternate labels/private authority classes — all with fakes/`FakeEffectPort`, no real hidraw. Integration proves cross-Zone CTAP ceremony **serialization** over the encrypted named stream and the shared physical USB collision. | +| Removal proof | The legacy daemon accept loop, raw CTAPHID framing, fixed `SK_VSOCK_PORT`, and broker sysfs `/sys/class/hidraw/` scan fallback are deleted only after the relay `Endpoint`/named-stream successor and the `device_token`-only broker open are green (coordinated with ADR046-security-key-034 `ProcessRole` removal and ADR046-security-key-004 broker-op revalidation). | + +Per D094, each replaced current-code test is retired with an explicit +keep/adapt/move/delete disposition and a removal gate: the minimum reusable +semantic assertions migrate into this crate's hermetic `tests/`, and the old +duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest +entries are deleted once successor coverage and the removal proof pass — +updating `tests/layer1-jobs.json`, the closed gate manifests, the +flake/matrix/Nix-unit pins, the generated ledgers, and the CI workflow shards. +Old and new suites never run in parallel indefinitely. + +## Tests + +### Fast hermetic execution and test placement (D094) + +Per D094 and `ADR-046-validation-and-delivery` §10.16, this Provider's `src/` +unit tests and `tests/*.rs` hermetic suite are fast, in-process, deterministic, +and parallel-safe: an individual normal test has p95 ≤50 ms with no wall-clock +sleep, and `cargo test -p d2b-provider-device-security-key --lib --tests` +completes in ≤2 s warm-cache execution time (compilation excluded). They use a +deterministic fake clock/RNG and the toolkit fakes/FakeEffectPort only — no +process spawn, container, network, DBus, systemd, broker daemon, Nix eval/build, +KVM, USB/GPU/TPM hardware, or live cloud, and no filesystem tree beyond tiny +temp fixtures. Any scenario needing those lives only in `integration/`, which +keeps a lane timeout/budget, parallel isolation, and fake external services by +default; such a need is re-placed into `integration/`, never given a sleep, +larger timeout, or `#[ignore]`. Bounded crypto/property tests are the only +classified exception, each named with a capped case count and a declared higher +per-test budget. + +### Hermetic (in `tests/`) + +| Test file | What is tested | +| --- | --- | +| `resource_chain_conformance.rs` | Fast in-process owner chain: physical Device → authority `security-key.d2bus.org.SecurityKeyService`/Provider relay Endpoint → ResourceExport → ResourceImport → Core-owned projection Service (`ownerRef: ResourceImport/`, `providerRef`, semantic base/import fields, no `spec.provider`, Device, or hidraw) → Nix/operator `security-key.d2bus.org.SecurityKeyBinding` → Provider-realized Binding frontend Process/private Endpoint. Verifies canonical Export `resourceRef`/`serviceType`/`projectionSchemaFingerprint`/`factoryFingerprint`, matching Import `expectedServiceType`/`expectedProjectionSchemaFingerprint`/`expectedFactoryFingerprint`, rejection of obsolete Export and Import fields, Service-owned Endpoint isolation, local refs, ownership, finalizer order, semantic factory fingerprint independence from Provider/adapter identity, separately authenticated signed descriptor identity, and no Device projection. | +| `provider_neutral_type_identity.rs` | Admits only the two exact `security-key.d2bus.org` ResourceTypes; rejects provider-named aliases and unknown variants; verifies `providerRef=Provider/device-security-key`, canonical minimal bases, strict authored `spec.provider`/`status.provider` schemas, projection `spec.provider` rejection, D088 status layering, and that physical Device, CTAPHID, hidraw, relay, UHID, queue, and ceremony fields cannot enter either semantic base schema. | +| `ceremony_record_separation.rs` | Resource catalog/store contains Device, Service, Binding, Process, Endpoint, Export, and Import only; ceremony/session/LeaseId/CID/queue/cancel rows remain bounded high-churn records and cannot be admitted as Resource objects or ownerRef children. | +| `controller_reconcile.rs` | Device observation creates no children; authority Service creates relay Process/Endpoint and physical DeviceGrant; projection Service is Core/import-only and creates no hidraw grant; Binding creates frontend Process/private Endpoint and system-core UHID grant; child-first deletion and no Volume calls. | +| `session_ring.rs` | Per-Binding bounded ring evicts oldest at `sessionRingSize`; rows are non-secret non-Resource records | +| `session_state_machine.rs` | Idle → Queued → Active → Completed/TimedOut; FIFO handoff; matching LeaseId cancel; stale release denied; physical DeviceGrant persists until relay exit | +| `fair_queue.rs` | Multiple local/imported Bindings serialize one ceremony; FIFO order, bounded queue, 15 s fake-clock deadline → `ERR_CHANNEL_BUSY`; active Binding unaffected | +| `duplicate_binding_conflict.rs` | Duplicate Service/Guest/User/policy Binding rejected; distinct explicit Bindings admitted without a second hidraw open | +| `device_grant_no_path.rs` | Authority relay receives the sole Core-opened physical DeviceGrant and no hidraw path. Projection Service receives no physical grant/open. Binding frontend receives system-core Guest UHID grant and no `/dev/uhid` path; no virtual/projected Device, udev rule, or plugdev group. | +| `mutual_exclusion.rs` | USB and security-key Providers resolving one fake token through same or different labels submit a byte-identical Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` tuple; the second fails before effects with `BackingAuthorityReady=False`, `AuthorityConflict=True`, and `physical-usb-backing-conflict`; Provider-private authority classes/digests cannot bypass it; unrelated tokens pass | +| `cancel_propagation.rs` | Matching `{sessionId, LeaseId}` cancel invokes cancel-all-active-CIDs and completes only that Binding; stale LeaseId cannot cancel a later ceremony; DeviceGrant persists until authority relay exit | +| `session_timeout.rs` | CEREMONY_TIMEOUT elapsed → Active → TimedOut → Idle; audit `device-session-timeout`; relay restartable after timeout | +| `cid_isolation.rs` | Different Bindings/ceremonies do not share CID maps; round trip and cancel-all-CIDs; canonical Service/Binding subject only | +| `descriptor_validation.rs` | Relay-control and Service/Binding Noise identities, fingerprints, encrypted stream bounds, SO_PEERCRED, no ambient path/raw CID, and opaque-ID Debug redaction | +| `status_binding.rs` | Empty ProviderStateSet; authority/import/attachment observations occur only in `status.resource`, while shared-backing implementation state and relay/frontend/queue/ceremony observations remain in `status.provider`; no semantic field appears directly under `status`; ceremony rows are not resources/status history; CTAP, fd, LeaseId, CID, and session keys absent | +| `telemetry_identity_canaries.rs` | Exact semantic metric/span allowlists; `resource_name_digest` and Device/Service/Binding/Guest/Zone name, UID, ref, and digest canaries never enter metric labels or span attributes; allow-listed OTEL Resource identity remains | + +### Integration (in `integration/`) + +| Fixture | What is tested | +| --- | --- | +| `host_relay_guest_frontend/` | Authority Service relay receives fake hidraw DeviceGrant; Binding frontend receives system-core fake UHID grant; owned Endpoints establish Noise KK; 64-byte CTAPHID exchange translates/reverses CID with no Device projection | +| `cross_zone_service_binding/` | Real processes exercise canonical type/fingerprint Export/Import fields, projection Service, and Binding frontend over an encrypted named stream; the relay/import-route Endpoints remain Service-owned and absent from Export/Import; no FD/USBIP/hidraw open occurs in the consumer Zone; revocation degrades Binding | +| `fair_queue/` | Multiple Binding frontends serialize one authority ceremony; bounded wait returns `ERR_CHANNEL_BUSY` without disturbing active holder | +| `usbip_mutual_exclusion/` | Host-wide trusted identity resolution produces the same `physical-usb-backing` tuple for USBIP and security-key; the second claim fails before either duplicate effect | + +### Existing contract tests (reuse/update) + +| Existing test | Action | +| --- | --- | +| `packages/d2b-contract-tests/tests/usb_sk_contract.rs` | Move to `packages/d2b-provider-device-security-key/tests/` as part of ADR046-security-key-006; update v3 type imports; retain all existing assertions | +| `packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` | Move to `packages/d2b-provider-device-security-key/tests/` as part of ADR046-security-key-007; update for the v3 Process resource sandbox; retain zero-`capabilityClasses` and `seccompClass` assertions | +| `packages/d2b-priv-broker/tests/security_key_broker.rs` | Retain in broker crate; update for v3 bundle table lookup path; add zone-field round-trip test | + +## Nix option migration + +| Current option | v3 successor | +| --- | --- | +| `d2b.host.usb.securityKey.enable = true` | Install `Provider/device-security-key` in Zone; configure `spec.config.devices` | +| `d2b.host.usb.securityKey.devices[].label` | `spec.config.devices[].label` in Provider resource | +| `d2b.host.usb.securityKey.devices[].vendorId` | `spec.config.devices[].vendorId` in Provider resource | +| `d2b.host.usb.securityKey.devices[].productId` | `spec.config.devices[].productId` in Provider resource | +| `d2b.vms..securityKey.enable = true` | Declare a `SecurityKeyBinding` for the Guest/User referencing a same-Zone authority or projection `SecurityKeyService` | +| (none — was not configurable) | `sessionRingSize`, `leaseTimeoutSecs`, `queueWaitTimeoutSecs` in Provider root config | + +The current USBIP/security-key mutual-exclusion assertion is preserved as a +Host-wide authority-index/eval assertion. Nix preflights known-equivalent +selectors across all Zones, while runtime Core resolves trusted physical USB +identity and requires both implementations to claim the byte-identical +`(Host, physical-usb-backing, opaqueKeyDigest)` tuple. The second claim receives +`physical-usb-backing-conflict` before either effect; a Provider-private +authority cannot bypass the shared tuple. + +## References + +- `packages/d2bd/src/security_key.rs` — baseline relay implementation (implemented-and-reachable) +- `packages/d2b-sk-frontend/src/` — baseline guest frontend binary (implemented-and-reachable) +- `packages/d2b-priv-broker/src/ops/security_key.rs` — broker hidraw open op (implemented-and-reachable) +- `packages/d2b-contracts/src/security_key.rs` — public and broker wire DTOs (implemented-and-reachable) +- `packages/d2b-contract-tests/tests/usb_sk_contract.rs` — existing contract tests (implemented-and-reachable) +- `packages/d2b-contract-tests/tests/minijail_sk_frontend.rs` — existing minijail contract tests (implemented-and-reachable) +- `packages/d2b-priv-broker/tests/security_key_broker.rs` — existing broker tests (implemented-and-reachable) +- `docs/specs/ADR-046-resources-device.md` §Provider: device-security-key — Device ResourceType contract and key invariants +- `docs/specs/ADR-046-componentsession-and-bus.md` — ComponentSession Noise profiles, descriptor validation, attachments +- `docs/specs/ADR-046-provider-model-and-packaging.md` — Provider crate boundary, component descriptors +- `docs/specs/ADR-046-components-processes-and-sandbox.md` — Process model, ProviderSupervisor, minijail +- `docs/specs/ADR-046-resource-reconciliation.md` — standard async reconcile interface +- `docs/specs/ADR-046-telemetry-audit-and-support.md` — OTEL label constraints, audit stream +- `docs/specs/ADR-046-nix-configuration.md` — Nix resource compilation, prohibited fields, eval invariants +- `nixos-modules/components/security-key-guest.nix` — current guest Nix module (eval-contract) +- `nixos-modules/assertions.nix` — current mutual-exclusion assertion (eval-contract) diff --git a/docs/specs/providers/ADR-046-provider-device-tpm.md b/docs/specs/providers/ADR-046-provider-device-tpm.md new file mode 100644 index 000000000..8c7202ece --- /dev/null +++ b/docs/specs/providers/ADR-046-provider-device-tpm.md @@ -0,0 +1,1598 @@ +# Provider: device-tpm + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-device-tpm` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 3 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-provider-device-tpm` crate | +| Depends on | `ADR-046-resources-device`, `ADR-046-resources-volume`, `ADR-046-resources-host-guest-process-user`, `ADR-046-provider-model-and-packaging`, `ADR-046-provider-state`, `ADR-046-primitive-resource-composition`, `ADR-046-resource-object-model`, `ADR-046-resource-reconciliation` | +| Supersedes | Current `nixos-modules/components/tpm.nix` swtpm provisioning and `d2b-priv-broker` swtpm-dir hardening path | +| Current code fit | Partial; v3 requires privilege-boundary inversion (controller → TpmEffectPort), controller Process resource, controller-created Device-owned Volume with exact canonical base fields, `userNamespace.mappingClass`, Device-owned TPM `Endpoint` resource, and crate split. | + +--- + +## 1. Purpose + +`Provider/device-tpm` manages one emulated per-Guest virtual TPM 2.0 device +backed by `swtpm`. It reconciles a `Device` resource whose claim holder +is a `Guest`. The Provider: + +- discovers emulated devices (always present for `deviceClass: emulated`, + verified against the Host's observed `tpm2` capability); +- creates and supervises the per-Device persistent TPM state `Volume` + (controller-created, `managedBy: controller`, owned by the `Device`); +- creates and supervises the long-lived `swtpm socket` worker `Process`; +- creates and supervises a mandatory pre-start flush `EphemeralProcess` + before each swtpm activation cycle to prevent stale session handles + (`TPM_RC_SESSION_HANDLES`) — this flush is a **load-bearing invariant** + and has no configurable skip path; +- publishes a typed TPM `EndpointRef` in Device status for the Guest runtime + Provider to obtain the socket file descriptor via a sealed LaunchTicket; +- preserves TPM identity state unconditionally: a `repairPolicy: fail-closed` + + `createPolicy: create-if-never-provisioned` Volume entry ensures a + missing or replaced swtpm directory after first provision is a hard + failure; the controller never silently re-provisions. + +The controller communicates with privileged infrastructure **only through an +injected async `TpmEffectPort`** over opaque resource IDs. It never calls +broker operations directly, never receives socket paths, UIDs, GIDs, pidfds, +or broker wire types. `volume-local` and `system-minijail` Providers translate +resource API operations into the actual broker effects; the broker remains the +sole executor and audit owner of all privileged filesystem and process-spawn +operations. + +--- + +## 2. Crate boundary + +```text +packages/d2b-provider-device-tpm/ + src/ + tests/ + integration/ + README.md +``` + +Workspace policy rejects this crate if any of `src/`, `tests/`, +`integration/`, or `README.md` is absent. + +Canonical internal layout (informative; workspace policy checks root-level +paths only): + +```text +src/ + controller.rs — Device reconcile loop; TpmEffectPort consumer + effect_port.rs — TpmEffectPort trait; EndpointRef handoff + effect_impl.rs — ResourceClient-backed TpmEffectPort implementation + status.rs — Device status builder (tpmEndpointRef, markerStatus) + resources.rs — Volume/Process/EphemeralProcess spec builders + errors.rs — TpmProviderError, TpmEffectError + telemetry.rs — typed OTEL span/metric helpers + lib.rs +tests/ + controller_fsm.rs — Device state-machine: all phase transitions + effect_fake.rs — FakeTpmEffectPort; no broker import + volume_create.rs — controller-created Volume canonical fields + flush_mandatory.rs — flush always issued; no skip path + endpoint_ref.rs — tpmEndpointRef is a ResourceRef; no path string + marker_fail_closed.rs — fail-closed marker → Device Failed + finalizer.rs — finalizer: Process deleted; Volume retained + redaction.rs — no path/UID/socket/pidfd in status/audit + schema.rs — Device spec admission round-trip + nix_roundtrip.rs — Nix form emits no Volume/Process/EphemeralProcess +integration/ + README.md + basic_tpm_start.rs + marker_tamper.rs + guest_endpoint.rs + lifecycle_restart.rs +``` + +--- + +## 3. Provider resource + +### 3.1 Canonical Provider spec + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Provider +metadata: + name: device-tpm + zone: dev +spec: + artifactId: d2b-provider-device-tpm + config: + controllerExecutionRef: "Host/host-system" # required; Host for controller Process + logLevel: 20 # swtpm --log level; 1–20; default 20 + # startupClear: REJECTED — flush always mandatory (load-bearing invariant) + # stateDirPath: REJECTED — path is policy-derived by volume-local; never configurable +``` + +The Provider ResourceSpec is exactly `{ artifactId; config }` (D075). The +exported ResourceTypes and permission claims are resolved from the signed +manifest/catalog entry `artifactId` selects; they are read-only derived data, +never authored Provider spec fields. For `device-tpm` the manifest declares: + +- exported ResourceType: `Device`; +- permission claims: `Device`, `Volume`, `Process`, `EphemeralProcess` + (`get`, `list`, `watch`, `create`, `update-spec`, `update-status`, `delete`, + `update-metadata`) and `Host`, `User` (`get`, `list`, `watch`). + +### 3.2 config field reference + +| Field | Type | Required | Default | Bound | Description | +| --- | --- | --- | --- | --- | --- | +| `controllerExecutionRef` | ResourceRef | yes | — | `Host/` in same Zone | Host on which the controller Process is placed. Must resolve to a Ready Host with `system` in `allowedDomains`. | +| `logLevel` | uint | no | `20` | `1..20` | swtpm `--log level`; compiled into the `swtpm-socket` template by the signed component descriptor. | + +`startupClear` is rejected at spec admission. The pre-start flush +(`swtpm_ioctl -i`) is compiled unconditionally into the `swtpm-init-flush` +worker template; there is no knob to disable it. + +`stateDirPath` is rejected. volume-local derives the TPM state path from its +own policy, given the Volume's `source.executionRef` and `sourceId`. + +Binary paths for swtpm and swtpm_ioctl are embedded in the signed component +descriptor inside the Provider's package closure; they are not configurable. + +--- + +## 4. Controller Process + +The Provider framework creates one controller Process from the Provider's +signed controller component descriptor and `config.controllerExecutionRef`. + +### 4.1 Canonical controller Process spec + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: device-tpm-controller + zone: dev + ownerRef: Provider/device-tpm +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system # from Provider config.controllerExecutionRef + domain: system + processClass: controller + template: controller + mounts: [] # no Provider state Volume; operational state is in status/core ledger (D087) + sandbox: + namespaceClasses: [pid, mount] + capabilityClasses: [] + seccompClass: controller-default + noNewPrivileges: true + startRoot: false + readOnlyRoot: true + environmentClass: minimal + umask: "0022" + oomScoreAdj: 0 + budget: + cpu: + request: "100m" + limit: "500m" + memory: + request: "32Mi" + limit: "128Mi" + pids: + limit: 64 + fds: + limit: 128 + telemetry: + metricsEnabled: true + tracingEnabled: true + logLevel: info + sensitiveLabels: false + readiness: + initialDelay: "0s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined + healthCheck: + enabled: true + interval: "30s" + timeout: "5s" + failureThreshold: 3 + class: provider-defined + restartPolicy: + class: on-failure + backoffBase: "2s" + backoffMax: "120s" + backoffMultiplier: 2.0 + maxRestarts: null + resetAfter: "3600s" + adoptionPolicy: adopt-on-restart + drainTimeout: "30s" + desiredLifecycle: running +``` + +### 4.2 ProviderStateSet + +The **ProviderStateSet** for `Provider/device-tpm` is the optional, query-time +set `{ v : Volume | v.metadata.zone == zone && v.metadata.ownerRef == "Provider/device-tpm" }`. +It is a logical grouping concept, not a ResourceType or stored artifact, and is +empty of controller state Volumes. + +`Provider/device-tpm`'s controller declares **no** Provider state Volume. The +controller reconstructs all Device reconcile state from the Zone resource store +on restart, and its bounded non-secret operational state — reconcile stage, +per-Device attach/provision observations, marker-status observations, bounded +counters, and closed-enum error detail — lives in the owning resource's +`status` subresource and the core Operation ledger (D087). Because that +operational state is fully derivable from spec, `status`, the core Operation +ledger, and independent external observation, the controller-scratch payload +fails the storage-need test: there is no controller `scratch` state Volume, no +`controller-scratch` state namespace, no `scratch` mount, and no dedicated +`User/device-tpm-controller-system` state-layout principal. There is no empty +identity-only Volume. + +The **TPM data Volume** described in §7 is a separate matter: it is the +per-Device swtpm NVRAM/EK-seed payload (a genuine large, secret, private +Device payload), created by the controller as a Device-owned Volume. It is +`ownerRef: Device/` (not `Provider/device-tpm`), so it is not part of the +ProviderStateSet, and it is retained unchanged — it easily passes the +storage-need test as secret/large private Device state that must never enter +status. + +--- + +## 5. TpmEffectPort — controller/privilege boundary + +### 5.1 Rationale + +The device-tpm controller creates Device child resources (Volume, Process, +EphemeralProcess) via a `TpmEffectPort` abstraction over the `ResourceClient`. +The downstream Providers (`volume-local`, `system-minijail`) drive their own +reconcile loops and call the broker for privileged effects. This means: + +- No socket path, filesystem path, UID integer, GID integer, pidfd, or broker + wire type ever crosses the controller/port boundary. +- `PrepareSwtpmDir` and `SpawnRunner` are invoked exclusively by `volume-local` + and `system-minijail` respectively — never by the device-tpm controller. +- The broker remains the sole audited executor of all privileged effects. +- The controller can be tested against `FakeTpmEffectPort` without any store, + broker, or host. + +Any `use d2b_priv_broker::` in a non-test file in this crate is a workspace +policy violation. + +### 5.2 TpmEffectPort trait + +```rust +/// Injected async effect abstraction for the device-tpm Device controller. +/// All parameters and return values are ResourceRefs or controller-internal opaque IDs. +/// No path, UID, GID, pidfd, socket name, or broker wire type is accepted +/// or returned. +pub trait TpmEffectPort: Send + Sync + 'static { + /// Create or verify the persistent TPM state Volume for this Device. + /// Returns the opaque VolumeId once the Volume is Ready. + async fn ensure_state_volume( + &self, + device_uid: &DeviceUid, + execution_ref: &ResourceRef, + ) -> Result; + + /// Request creation of the swtpm worker Process. + /// Returns the opaque ProcessId once the Process is Running (pre-Ready). + async fn request_swtpm_process( + &self, + device_uid: &DeviceUid, + volume_id: &VolumeId, + execution_ref: &ResourceRef, + ) -> Result; + + /// Request the mandatory pre-start flush EphemeralProcess. + /// The flush is always issued; there is no skip parameter. + /// The ctrl socket fd is passed as a local LaunchTicket attachment; + /// no socket path crosses this boundary. + async fn request_flush_process( + &self, + device_uid: &DeviceUid, + swtpm_process_id: &ProcessId, + execution_ref: &ResourceRef, + ) -> Result; + + /// Stop the swtpm Process. Idempotent. + async fn stop_swtpm_process( + &self, + process_id: &ProcessId, + ) -> Result<(), TpmEffectError>; + + /// Delete the flush EphemeralProcess if present and non-terminal. Idempotent. + async fn delete_flush_process( + &self, + process_id: &EphemeralProcessId, + ) -> Result<(), TpmEffectError>; + + /// Watch the swtpm Process's TPM Endpoint resource. + /// Returns `Endpoint/` once the Process is Ready. + /// Never returns a socket path, raw endpoint ID, or file descriptor number. + async fn watch_tpm_endpoint( + &self, + process_id: &ProcessId, + ) -> Result; +} +``` + +All opaque/internal IDs (`DeviceUid`, `VolumeId`, `ProcessId`, +`EphemeralProcessId`) and typed refs (`ResourceRef`, `EndpointRef`) are bounded +newtypes in `d2b-contracts`. They carry no path, UID integer, or network +address. + +### 5.3 Port implementation + +`TpmEffectPortImpl` wraps a `ResourceClient` and `WatchStream`. For each +port method it constructs the canonical resource spec (§7, §8, §9) using +only opaque IDs, issues a `Create` or `Get` call, and watches the resource +status until the relevant condition is True. The broker is never imported. + +### 5.4 FakeTpmEffectPort + +`FakeTpmEffectPort` records all calls in an in-memory log and returns +configurable success/failure. Controller state-machine tests run against +it without daemon, store, or broker. + +--- + +## 6. Device resource + +### 6.1 Canonical Device spec + +Normative D089 spec layering: Device base fields are ResourceType base +`spec.*` fields, including `spec.providerRef`, `deviceClass`, +`inventory.selector`, attachments, and arbitration. This Provider's +desired-only extension is the canonical `spec.provider = { schemaId: +"device-tpm.d2bus.org/Device/spec", schemaVersion, settings }` envelope; it +is manifest-registered/signed, strict deny-unknown, bounded, versioned and +digested, +validated against `spec.providerRef` at Nix build and API admission, +implementation-only, and may not shadow base fields. Shared fields are promoted +to the Device base. The Provider implements the exact base Device spec/status +version/fingerprint, accepts the canonical minimal valid base Spec, and rejects +unsupported optional base capabilities only through its signed capability matrix +and provider-neutral `unsupported-capability`. `spec.provider` aligns with +`status.provider`; generic CLI/controllers operate on the base spec and base +status only. No secret bytes are allowed in any spec layer, and no +credential material is allowed in `spec.provider.settings`. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Device +metadata: + name: corp-vm-tpm + zone: dev + ownerRef: Guest/corp-vm +spec: + providerRef: Provider/device-tpm + deviceClass: emulated + arbitration: exclusive + maxConcurrentClaims: 1 + inventory: + selector: {} + provider: + schemaId: "device-tpm.d2bus.org/Device/spec" + schemaVersion: "1.0.0" + settings: + logLevel: 20 + executionRef: "Host/host-system" + # startupClear: REJECTED — flush is always mandatory +``` + +### 6.2 Device provider settings schema + +| Field | Type | Required | Default | Bound | Description | +| --- | --- | --- | --- | --- | --- | +| `logLevel` | uint | no | Provider config.logLevel | `1..20` | Per-Device swtpm log level override. | +| `executionRef` | string | no | Provider config.controllerExecutionRef | `Host/` | Which Host to run the swtpm Process on. Validated as a ResourceRef resolving to a Ready Host at admission. | + +No binary path, UID, GID, socket path, broker wire type, secret bytes, or +credential material is accepted in `spec.provider.settings`. + +--- + +## 7. TPM state Volume (controller-created) + +The Device controller creates this Volume once per Device via +`TpmEffectPort.ensure_state_volume`. It is never declared in Nix. +It is separate from the ProviderStateSet (§4.2). + +### 7.1 Canonical Volume spec + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Volume +metadata: + name: device--tpm-state # uid-short = first 12 hex chars of Device UID + zone: dev + ownerRef: Device/corp-vm-tpm + managedBy: controller +spec: + providerRef: Provider/volume-local + source: + sourceId: "d2b/tpm-state" # opaque policy class; volume-local resolves path + executionRef: Host/host-system # resolved from Device settings.executionRef + kind: state + layout: + - path: "" + type: directory + ownerRef: User/device--swtpm-system + groupRef: User/device--swtpm-system + mode: "0700" + sensitivity: secret-adjacent + createPolicy: create-if-never-provisioned + repairPolicy: fail-closed + cleanupPolicy: never + adoptionPolicy: quarantine-on-ambiguity + restartPolicy: preserve-across-controller-restart + leaseClass: none + noFollow: true + invariants: + - no-symlink + - broker-opaque-id-only + - scope-authorization-required + views: + swtpm-process: + path: "" + rights: [read, write, create] + controller: + path: "" + rights: [read, write, create, delete, traverse] + attachments: [] + quota: null +``` + +### 7.2 Volume naming + +`uid-short` = first 12 hexadecimal characters of the Device resource's +store-assigned UID. Stable across restarts and renames. Guest human names +never appear in the Volume name. + +### 7.3 Identity marker and fail-closed detection + +The identity marker is maintained by the broker (via volume-local's +`PrepareSwtpmDir` operation) outside the Volume tree — the broker-opaque-id-only +and scope-authorization-required invariants on the `""` entry enforce that +no caller below the broker can substitute or replace the swtpm directory. + +The `createPolicy: create-if-never-provisioned` + `repairPolicy: fail-closed` +combination is the v3 canonical encoding of the current +`previously-provisioned-swtpm-state-missing` invariant: if the swtpm +directory is absent or its `st_ino` has changed after prior provision, the +volume-local controller sets the Volume to `Failed` with a typed condition +rather than silently re-provisioning. + +| Condition | Volume phase | Device controller response | +| --- | --- | --- | +| `markerStatus: missing` after prior provision | `Failed` | Device → Failed; no auto-recovery | +| `markerStatus: replaced` (st_ino mismatch) | `Failed` | Device → Failed; no auto-recovery | +| Volume root absent, marker present | `Failed` | Device → Failed | + +None of these cases auto-recover. The controller watches Volume status and +propagates `Failed` to the Device phase. The controller never issues a second +`ensure_state_volume` call after `markerStatus: replaced` or `missing` — it +transitions to Device `Failed` with condition `TpmStateCompromised` and +requires operator intervention followed by explicit Device deletion and +re-creation. + +### 7.4 Key constraints + +- `cleanupPolicy: never` — the entry is never removed by the Volume + controller, even on Device or Guest deletion. +- `repairPolicy: fail-closed` — any owner/mode drift is treated as a fatal + condition; no automatic chown is performed. +- `createPolicy: create-if-never-provisioned` — existing content is preserved + on first bind; the broker never overwrites NVRAM. +- `adoptionPolicy: quarantine-on-ambiguity` — if the existing directory's + identity proof is ambiguous on controller restart, the Volume is quarantined + rather than adopted or destroyed. +- `sensitivity: secret-adjacent` — the swtpm state path must not appear in + public status, audit records, or log output. +- `source.sourceId` is opaque; volume-local resolves the path from its + internal policy. The device-tpm controller never provides a `hostPath`. +- No `identityMarker`, `persistenceClass`, `quotaBytes`, `stateSchema`, + `snapshotPolicy`, or `retentionPolicy` top-level fields — those are + ProviderStateSet extensions that do not apply to a Device-owned Volume. +- No automatic snapshots. Restoring a TPM state snapshot appears as device + tampering to any Identity Provider and forces re-enrollment. + +--- + +## 8. swtpm Process (controller-created) + +Created via `TpmEffectPort.request_swtpm_process` once the TPM state +Volume reaches `Ready` phase. + +### 8.1 Canonical Process spec + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: device--swtpm + zone: dev + ownerRef: Device/corp-vm-tpm + managedBy: controller +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system # from Device settings.executionRef + domain: system + processClass: worker + template: swtpm-socket + mounts: + - volumeRef: Volume/device--tpm-state + view: swtpm-process + mountPath: /state + access: read-write + required: true + sandbox: + namespaceClasses: [pid, mount, user] + capabilityClasses: [] + seccompClass: w1-swtpm + noNewPrivileges: true + startRoot: false + readOnlyRoot: true + userNamespace: + mappingClass: process-principal-root + environmentClass: minimal + umask: "0022" + oomScoreAdj: 500 + budget: + cpu: + request: "50m" + limit: "500m" + memory: + request: "32Mi" + limit: "128Mi" + pids: + limit: 32 + fds: + limit: 64 + telemetry: + metricsEnabled: true + tracingEnabled: true + logLevel: info + sensitiveLabels: true + readiness: + initialDelay: "0s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined + healthCheck: + enabled: false + restartPolicy: + class: on-failure + backoffBase: "2s" + backoffMax: "60s" + backoffMultiplier: 2.0 + maxRestarts: 10 + resetAfter: "3600s" + adoptionPolicy: adopt-on-restart + drainTimeout: "10s" + desiredLifecycle: running +``` + +### 8.2 `userNamespace.mappingClass: process-principal-root` + +`mappingClass: process-principal-root` declares that the broker shall +pre-establish a single-entry user namespace mapping the process's principal +UID/GID (resolved by core from `User/device--swtpm-system`) to +in-namespace UID/GID 0. Core resolves the numeric UID/GID privately; no +numeric value appears in the Process spec or status. The broker pre-establishes +the user namespace via `clone3(CLONE_NEWUSER)` + pipe sync + uid_map/gid_map +writes before swtpm's first instruction, per ADR 0021. + +`namespaceClasses: [pid, mount, user]` is the matching class set; `user` is +required when `userNamespace` is set. The resulting swtpm process has zero +host capabilities (proven by `minijail_swtpm_video.rs`) and is confined to +read-only rootfs + the mounted Volume view at `/state`. + +### 8.3 `readOnlyRoot: true` and Volume mount + +The rootfs is mounted read-only. The only writable path is `/state`, provided +by the Volume mount (view `swtpm-process`, `path: ""`). swtpm writes NVRAM +content and its working files directly under `/state`. + +### 8.4 Endpoint resources (D092) + +`Provider/device-tpm` declares conformance to the standard `Endpoint` base +schema. Two private Endpoint resources are controller-created and owned by the +Device; neither socket path, address, fd number, nor credential appears in +Device spec, Device status, audit, metrics, or CLI output. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: device--tpm + zone: dev + ownerRef: Device/corp-vm-tpm +spec: + providerRef: Provider/device-tpm + producerRef: Device/corp-vm-tpm + endpointClass: device + transport: fd-attachment + purpose: swtpm-tpm-socket + serviceFingerprint: device-tpm.d2bus.org/tpm/v1 + locality: host-local + visibility: provider + attachmentPolicy: launch-ticket-only + consumerPolicy: + allowedSubjects: [Provider/runtime-cloud-hypervisor] + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +--- +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: device--tpm-ctrl + zone: dev + ownerRef: Device/corp-vm-tpm +spec: + providerRef: Provider/device-tpm + producerRef: Process/device--swtpm + endpointClass: control + transport: fd-attachment + purpose: swtpm-control-socket + serviceFingerprint: device-tpm.d2bus.org/control/v1 + locality: host-local + visibility: provider + attachmentPolicy: launch-ticket-only + consumerPolicy: + allowedSubjects: [Provider/device-tpm] + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +``` + +The Device base status exposes `status.resource.endpointRefs.tpmEndpointRef: Endpoint/device--tpm`. +Consumers use EndpointRefs; Core/ProviderSupervisor resolves private fds only +through authorized EffectPort/LaunchTicket flows. Unauthorized resolve fails +`endpoint-resolve-denied`, and a swtpm restart bumps `endpointGeneration`, +triggering dependent runtimes with `dependency-changed`. + +### 8.5 Template and argv + +The exact swtpm argv is compiled by `system-minijail` from the `swtpm-socket` +template in the signed component descriptor. Template parameters are resolved +from the Volume mount path (`/state`), the EndpointRefs resolved through the LaunchTicket, the `logLevel` from Device settings, and `--flags startup-clear` +(always compiled in; not controlled by any config field). No path, UID, GID, or +binary path from spec or controller code reaches swtpm argv outside the +template compilation step. + +### 8.6 Retained opaque handles + +Retained opaque values are limited to `DeviceUid`, `VolumeId`, `ProcessId`, +`EphemeralProcessId`, pidfd observations, LaunchTicket fd indexes, per-session +control-socket attachments, and operation IDs. They are controller-internal, +high-churn, or lack independent lifecycle; stable TPM and control socket +identities are promoted to `Endpoint` resources by D092. + +--- + +## 9. Pre-start flush EphemeralProcess (mandatory; load-bearing invariant) + +Before each swtpm activation cycle the controller issues a mandatory +`swtpm_ioctl -i` flush. This is a **load-bearing invariant**: without it, +an unclean prior shutdown leaves stale TPM session handles causing +`TPM_RC_SESSION_HANDLES`. There is no `startupClear` knob or any other +mechanism to disable the flush. A Device spec containing `settings.startupClear` +is rejected at admission. + +### 9.1 Canonical EphemeralProcess spec + +```yaml +apiVersion: resources.d2bus.org/v3 +type: EphemeralProcess +metadata: + name: device--flush + zone: dev + ownerRef: Device/corp-vm-tpm + managedBy: controller +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system # same Host as swtpm Process + domain: system + processClass: worker + template: swtpm-init-flush # always compiles --flags startup-clear + mounts: [] + sandbox: + namespaceClasses: [pid, mount] + capabilityClasses: [] + seccompClass: w1-swtpm + noNewPrivileges: true + startRoot: false + readOnlyRoot: true + environmentClass: minimal + umask: "0022" + oomScoreAdj: 0 + budget: + cpu: + request: "10m" + limit: "100m" + memory: + request: "8Mi" + limit: "16Mi" + pids: + limit: 4 + fds: + limit: 16 + telemetry: + metricsEnabled: false + tracingEnabled: true + logLevel: info + sensitiveLabels: true + startDeadline: "30s" + runtimeDeadline: "60s" + successfulTtl: "1h" + failedTtl: "24h" + incidentHold: false +``` + +No `userNamespace` is set for the flush process; it runs as the system +principal directly. The contract test `minijail_swtpm_video.rs` proves +"user-NS long-lived only" — the user namespace applies exclusively to the +long-lived swtpm Process, not to the one-shot flush. + +### 9.2 Control socket handoff (local fd attachment) + +The flush EphemeralProcess receives the swtpm `ctrl` endpoint fd as an +**inherited fd attachment** in its LaunchTicket — not as a path string or +endpoint name. The Provider supervisor: + +1. Reads the swtpm Process's `ctrl` endpoint opaque ID after the Process is + Running (pre-Ready). +2. Resolves `Endpoint/device--tpm-ctrl` and opens the ctrl socket fd as a local LaunchTicket attachment (a validated, + CLOEXEC-cleared fd in the inherited fd table). +3. The `swtpm-init-flush` template uses the inherited fd directly for the + `swtpm_ioctl -i` call; no path traversal occurs. + +No path string, socket name, or ctrl socket address appears in the +EphemeralProcess spec, its status, or any audit record. + +### 9.3 Deadlines and TTL + +| Field | Value | +| --- | --- | +| `startDeadline` | `"30s"` — max time from spec commit to process start | +| `runtimeDeadline` | `"60s"` — max wall-clock runtime for `swtpm_ioctl -i` | +| `successfulTtl` | `"1h"` — retention after Succeeded | +| `failedTtl` | `"24h"` — retention after Failed | + +### 9.4 Flush ordering in the start cycle + +``` +Volume Ready (layoutPhase: Ready, markerStatus: verified) + → create flush EphemeralProcess via TpmEffectPort.request_flush_process + → wait flush.status.phase = Succeeded + → set swtpm Process desiredLifecycle = running + → wait swtpm Process'status.phase = Ready + → TpmEffectPort.watch_tpm_endpoint → EndpointRef + → Device status.resource.endpointRefs.tpmEndpointRef = Endpoint/device--tpm; phase = Ready +``` + +If the flush reaches `Failed`, Device transitions to `Failed` with condition +`TpmFlushFailed`. The swtpm Process is not started. The controller does not +automatically retry; it waits for the flush EphemeralProcess to be cleaned up +(after `failedTtl` expires) or for an operator-initiated Device spec update. + +--- + +## 10. Device status + +Per D088, ResourceType-common Device observation lives in +`status.resource`: the provider-neutral claim/arbitration/presence base that is +identical across Device implementations. TPM-specific observation lives only in +`status.provider` with `providerRef`, qualified `schemaId` +`device-tpm.d2bus.org/Device/status`, `schemaVersion`, +`observedProviderGeneration`, and strict bounded redacted `details` +(≤32 KiB, unknown-field-denied). The controller writes all present layers +atomically in one status mutation; shared +fields are never duplicated into `status.provider`, and the extension schema is +registered and signed in the Provider manifest. + +### 10.1 Currency and expedited reconcile (D091/D090) + +D091 currency is universal status, not TPM provider detail. The controller +implements `assess_update`, `plan_upgrade`, and `execute_upgrade`, populates +universal `status.update`, and keeps shared currency fields out of +`status.provider`; TPM-specific observations may appear only under +`status.provider.details`. Provider generation, spec, artifact, or +security-policy changes that are disruptive MUST set `status.update.state = +UpgradeRequired`, with `reasons = [ProviderGenerationChanged]`, `[SpecChanged]`, +`[ArtifactChanged]`, or `[SecurityPolicyChanged]`, `disruption = Recycle`, and +`preserveState = true` rather than applying disruption in place. Non-disruptive +changes reconcile normally. `execute_upgrade` preserves TPM identity and the +per-Device TPM data Volume; disruption is at most `Recycle`, and the controller +MUST never wipe or re-provision TPM state as part of an upgrade. + +D090 expedited `waitForReconcile` on `Create`/`UpdateSpec`/`Delete` performs no +external effect, finalizer change, or status mutation until Core supplies +`CommittedRevisionProof {resourceUid,generation,revision,operationId}`. The +one-pass response returns the committed object, projected layered status, +disposition `Converged|Progressing|Blocked|UpgradeRequired|Failed`, and +`statusPersistence = pending|committed`; the durable commit is never rolled back +after a reconcile timeout. Effect idempotency keys derive from +`(UID,generation,revision,operationId)`, and the expedited pass uses the bounded +priority lane inside the same per-resource single-flight. + +### 10.2 Common phase model + +`Provider/device-tpm` uses the standard Device phase model without +Device-specific phases. + +| Phase | Meaning | +| --- | --- | +| `Pending` | Controller initializing; Volume not Ready or flush not Succeeded | +| `Ready` | swtpm Process Ready; `tpmEndpointRef` published; claim holder may proceed | +| `Degraded` | swtpm Process in restart backoff or health check failing | +| `Failed` | Volume marker fail-closed, flush Failed, or swtpm maxRestarts exceeded | +| `Deleted` | Terminal event only; core emits and removes the row | + +### 10.3 Canonical Device status + +```yaml +status: + observedGeneration: 1 + phase: Ready + conditions: [] + lastReconciledAt: "2026-07-22T00:00:10Z" + startedAt: "2026-07-22T00:00:01Z" + completedAt: null + outcome: null + resource: + present: true + health: healthy + holderRefs: + - Guest/corp-vm + claims: [] + provisionedAt: "2026-07-22T00:00:05Z" + endpointRefs: + tpmEndpointRef: "Endpoint/device-7f3a9e12b4c6-tpm" # EndpointRef; NOT a path + provider: + providerRef: Provider/device-tpm + schemaId: "device-tpm.d2bus.org/Device/status" + schemaVersion: "1.0.0" + observedProviderGeneration: 1 + details: + tpm: + stateVolumeRef: "Volume/device-7f3a9e12b4c6-tpm-state" # ResourceRef; opaque to path + swtpmProcessRef: "Process/device-7f3a9e12b4c6-swtpm" # ResourceRef; for diagnostics + markerStatus: "verified" # verified|missing|replaced|unknown + lastFlushRef: "EphemeralProcess/device-7f3a9e12b4c6-flush" + lastFlushPhase: "Succeeded" + lastFlushAt: "2026-07-22T00:00:09Z" +``` + +### 10.4 Typed TPM provider details fields + +The fields below are TPM-specific provider details and therefore live under +`status.provider.details.tpm`; the Device claim/arbitration/presence base and +typed `endpointRefs.tpmEndpointRef` stay promoted to `status.resource`. + +| Field | Type | Description | +| --- | --- | --- | +| `stateVolumeRef` | ResourceRef string | `Volume/` in same Zone. Opaque to filesystem path; never a path. | +| `swtpmProcessRef` | ResourceRef string | `Process/` in same Zone. Stable for diagnostics; not a PID. | +| `markerStatus` | string | Volume marker status reflected from `Volume.status.markerStatus`: `verified`, `missing`, `replaced`, `unknown`. | +| `lastFlushRef` | ResourceRef string | Most recent flush EphemeralProcess. | +| `lastFlushPhase` | string | Phase of the most recent flush at last observation. | +| `lastFlushAt` | string? | RFC 3339 UTC timestamp of last flush completion. | + +`status.resource.endpointRefs.tpmEndpointRef` is a canonical ResourceRef +pointing at a typed `Endpoint` resource in the same Zone. There is no +compatibility alias or opaque TPM endpoint ID in d2b 3.0. + +### 10.5 Guest runtime endpoint handoff + +When the Guest runtime Provider needs the TPM socket for Cloud Hypervisor: + +1. Reads `Device.status.resource.endpointRefs.tpmEndpointRef` (`Endpoint/`). +2. Presents the EndpointRef to the authorized Zone runtime endpoint resolver. +3. Zone runtime returns an inherited fd attachment in the Guest's LaunchTicket. +4. Cloud Hypervisor receives the socket fd (not a path) via the inherited fd table. + +The `--tpm socket=` form is an internal detail of the Cloud Hypervisor +runtime; the path is resolved from the fd inside the LaunchTicket handling and +never appears in Device spec, Device status, or Guest spec. + +--- + +## 11. Controller reconcile algorithm + +### 11.1 Device reconcile loop + +``` +trigger: spec-generation-changed | dependency-changed | startup-relist | scheduled-observe + + 1. Read fresh Device spec snapshot. + 2. Validate spec invariants: + - providerRef = Provider/device-tpm; deviceClass = emulated + - settings.executionRef resolves to Ready Host with system domain + - settings.startupClear absent (reject if present) + 3. Resolve: executionRef = settings.executionRef ?? config.controllerExecutionRef + 4. Probe Host.status.capabilities for tpm2. + - Absent → Device Degraded; condition TpmCapabilityAbsent; requeue 60s. + 5. TpmEffectPort.ensure_state_volume(device_uid, executionRef) + - Volume not Ready → Device Pending; condition VolumeNotReady; return pending. + - Volume.status.markerStatus ∈ {missing, replaced} + → Device Failed; condition TpmStateCompromised; stop swtpm; return. + 6. Check flush EphemeralProcess: + - Succeeded: proceed to step 8. + - Failed: Device Failed; condition TpmFlushFailed; return. + - Pending/Running: Device Pending; return pending (watch fires on completion). + 7. No prior flush (or prior flush cleaned up after TTL): + a. Set swtpm Process desiredLifecycle = stopped (if running). + b. Wait for swtpm stopped (bounded: drainTimeout + 30s). + c. TpmEffectPort.request_flush_process(device_uid, swtpm_process_id_or_none, executionRef) + d. Watch flush EphemeralProcess status. + e. Succeeded → proceed to step 8. Failed → Device Failed; condition TpmFlushFailed. + 8. TpmEffectPort.request_swtpm_process(device_uid, volume_id, executionRef) + - Set desiredLifecycle = running. + - Pending/Launching → Device Pending; return pending. + - Failed → Device Failed; condition TpmProcessFailed. + 9. TpmEffectPort.watch_tpm_endpoint(swtpm_process_id) → EndpointRef +10. UpdateStatus (expected revision): + - phase = Ready + - status.resource.present = true; status.resource.health = healthy + - status.resource.endpointRefs.tpmEndpointRef = EndpointRef (no path or raw locator) + - status.provider.details.tpm.markerStatus from Volume.status + - status.provider.details.tpm.stateVolumeRef / swtpmProcessRef / lastFlushRef (ResourceRefs) +11. Emit Claim for holderRef from Device ownerRef or claim request. +12. Return converged. +``` + +Non-blocking steps complete in ≤ 10 s. Long-running waits (Volume Ready, +flush completion, Process Ready) are async task-gated: the handler writes +a `Pending` condition and returns `pending`; the async watch task writes a +reconcile trigger when the watched condition fires. + +### 11.2 Device finalizer + +Registered as `device-tpm/cleanup`. + +On `deletion-requested`: + +1. Set swtpm Process `desiredLifecycle = stopped`. +2. Wait for swtpm Process terminal phase (bounded: `drainTimeout + 30s`). +3. Delete swtpm Process resource via resource API; wait deletion confirmed (60s). +4. Delete flush EphemeralProcess if non-terminal. +5. **Do not delete the TPM state Volume** (`cleanupPolicy: never`). Volume + persists. Controller releases its references to the Volume. +6. Clear the `device-tpm/cleanup` finalizer. +7. Return `finalized`. + +Core emits `phase=Deleted` event and removes the Device row after all +finalizers clear. The controller never manages the Deleted phase directly. + +If any child resource is in ambiguous state, the finalizer returns `blocked` +with condition `device-tpm/cleanup: ambiguous-child-state`; max 5 retries +at 300 s before `failed-terminal` with explicit audit record. + +--- + +## 12. Error classes and conditions + +### 12.1 Device conditions + +| Condition type | reason codes | +| --- | --- | +| `TpmCapabilityVerified` | `tpm-capability-absent`, `host-not-ready` | +| `StateVolumeReady` | `volume-not-ready`, `volume-failed`, `volume-pending` | +| `MarkerVerified` | `marker-missing`, `marker-replaced`, `marker-unknown` | +| `FlushSucceeded` | `flush-process-failed`, `flush-process-pending`, `flush-start-deadline-exceeded` | +| `SwtpmReady` | `swtpm-process-failed`, `swtpm-process-pending`, `swtpm-process-crashed` | +| `EndpointReady` | `endpoint-not-ready`, `endpoint-resolve-denied`, `endpoint-resolve-failed` | +| `ClaimAdmitted` | `device-claim-already-held`, `device-claim-invalid` | + +### 12.2 Error codes + +| Code | Retryable | Description | +| --- | --- | --- | +| `tpm-capability-absent` | yes | Host capability `tpm2` not observed. | +| `volume-marker-missing` | no | Marker absent after prior provision. | +| `volume-marker-replaced` | no | st_ino mismatch: swtpm directory replaced. | +| `flush-process-failed` | no | `swtpm_ioctl -i` exited non-zero or timed out. | +| `swtpm-process-crashed` | yes | swtpm exited with signal before Ready. | +| `swtpm-max-restarts-exceeded` | no | swtpm restart count > `maxRestarts`. | +| `endpoint-resolve-failed` | yes | TPM EndpointRef could not be resolved. | +| `device-claim-already-held` | no | Exclusive claim already active. | +| `host-not-ready` | yes | executionRef Host not in Ready phase. | +| `effect-port-error` | yes | TpmEffectPort returned a transient error. | + +Non-retryable errors transition Device to `Failed`. `volume-marker-missing`, +`volume-marker-replaced`, and `swtpm-max-restarts-exceeded` require operator +action; the controller does not auto-recover. + +--- + +## 13. Audit events + +All payloads carry `sensitivity: private`. No path, socket name, UID, GID, +PID, argv fragment, TPM NVRAM content, or raw binary data appears in any +audit payload. + +| Event | Stable kind | Payload fields | +| --- | --- | --- | +| Device first provision | `device-tpm/state-volume-created` | `zone`, `device_uid` (opaque), `volume_uid` (opaque) | +| Marker verified on start | `device-tpm/marker-verified` | `zone`, `device_uid`, `volume_uid` | +| Marker fail-closed | `device-tpm/marker-fail-closed` | `zone`, `device_uid`, `volume_uid`, `marker_status` | +| Flush succeeded | `device-tpm/flush-succeeded` | `zone`, `device_uid`, `process_uid` (opaque) | +| Flush failed | `device-tpm/flush-failed` | `zone`, `device_uid`, `process_uid`, `exit_class` | +| swtpm started | `device-tpm/swtpm-started` | `zone`, `device_uid`, `process_uid`, `adoption_state` | +| swtpm crashed | `device-tpm/swtpm-crashed` | `zone`, `device_uid`, `process_uid`, `exit_class`, `restart_count` | +| swtpm max restarts | `device-tpm/swtpm-max-restarts-exceeded` | `zone`, `device_uid`, `process_uid` | +| Claim granted | `device-tpm/claim-granted` | `zone`, `device_uid`, `holder_ref` (opaque ResourceRef) | +| Claim released | `device-tpm/claim-released` | `zone`, `device_uid`, `holder_ref` | +| Finalizer cleared | `device-tpm/finalized` | `zone`, `device_uid`, `volume_retained: true` | +| Security violation | `device-tpm/security-violation` | `zone`, `device_uid`, `violation_class` | + +`device_uid`, `volume_uid`, `process_uid`, `holder_ref` are opaque +store-assigned UIDs — not human names, filesystem paths, or socket addresses. + +--- + +## 14. Observability (OTEL) + +### 14.1 Metrics + +| Metric | Type | Labels | Description | +| --- | --- | --- | --- | +| `d2b_device_tpm_phase` | gauge | `phase` | Device count per phase | +| `d2b_device_tpm_flush_duration_seconds` | histogram | `outcome` | Pre-start flush wall-clock duration | +| `d2b_device_tpm_swtpm_restart_count` | counter | (none) | swtpm restart events | +| `d2b_device_tpm_marker_status` | gauge | `status` | TPM state Volume marker status | + +No label carries a Zone name/UID, device name, VM name, path, process name, +UID, GID, PID, or socket address. Zone identity remains in the bounded +`d2b.zone` OTEL resource attribute. + +### 14.2 Traces + +| Span | Description | +| --- | --- | +| `device-tpm.reconcile` | Full Device reconcile cycle | +| `device-tpm.ensure-state-volume` | `ensure_state_volume` call | +| `device-tpm.flush` | EphemeralProcess flush lifecycle | +| `device-tpm.swtpm-start` | swtpm Process start or adopt | +| `device-tpm.endpoint-ready` | TPM EndpointRef resolution | +| `device-tpm.finalize` | Finalizer execution | + +Span attributes contain only `outcome` (bounded stable value). Zone and Device +identity remain in bounded OTEL resource attributes and permitted audit fields. +No path, argv, socket, UID, GID, or PID appears in a span attribute. + +--- + +## 15. RBAC + +### 15.1 Provider-declared Roles + +#### `device-tpm-operator` + +```json +{ + "rules": [ + { + "resourceTypes": ["Device"], + "verbs": ["get", "list", "watch", "create", "update-spec", "delete"], + "providerConstraint": "device-tpm" + }, + { + "resourceTypes": ["Guest"], + "verbs": ["get", "list"] + } + ] +} +``` + +#### `device-tpm-viewer` + +```json +{ + "rules": [ + { + "resourceTypes": ["Device"], + "verbs": ["get", "list", "watch"], + "providerConstraint": "device-tpm" + } + ] +} +``` + +### 15.2 Controller-internal permissions + +Declared in the Provider descriptor; granted by the Provider framework: + +| ResourceType | Verbs | Scope | +| --- | --- | --- | +| `Device` | `get`, `list`, `watch`, `update-status`, `update-metadata` | `providerConstraint: device-tpm` | +| `Volume` | `get`, `list`, `watch`, `create`, `update-status`, `update-metadata` | `ownerConstraint: Device/` | +| `Process` | `get`, `list`, `watch`, `create`, `update-spec`, `delete` | `ownerConstraint: Device/` | +| `EphemeralProcess` | `get`, `list`, `watch`, `create`, `delete` | `ownerConstraint: Device/` | +| `Host` | `get`, `list`, `watch` | read-only; for capability check | +| `User` | `get`, `list`, `watch` | read-only; for principal resolution | + +The controller holds **no** permissions to call broker operations. Broker +permissions belong exclusively to `Provider/volume-local` and +`Provider/system-minijail`. + +--- + +## 16. Async reconcile patterns + +### 16.1 Watch-gated async tasks + +Long-running waits (Volume Ready, flush Succeeded, Process Ready) do not +block the controller's reconcile queue. The reconcile handler starts an +async watch task, writes a `Pending` condition, and returns `pending`. When +the watch fires, the task writes a reconcile trigger. The reconcile loop +re-enters and short-circuits the wait. + +### 16.2 Bounded concurrency + +At most 32 concurrent Device reconcile tasks (semaphore-guarded). Per-Device +TpmEffectPort calls are sequential; no concurrent calls for the same Device UID. + +### 16.3 Restart adoption + +After controller restart: +1. Re-list all Device resources. +2. For each Ready/Degraded Device: `ensure_state_volume` and + `request_swtpm_process` use `Get` semantics (resources already exist). +3. If swtpm Process is already Ready: re-fetch the TPM EndpointRef and + publish `tpmEndpointRef`; no new flush. +4. New flush issued only if swtpm Process is not running or has exited. + +### 16.4 Idempotency + +All controller effects are idempotent: +- `ensure_state_volume`: `Create` is a no-op if Volume exists with the same + name and owner. Port returns existing `VolumeId`. +- `request_swtpm_process`: `Create` is a no-op if Process already exists. +- `request_flush_process`: controller deletes prior EphemeralProcess before + issuing a new `Create`; name `device--flush` is stable. +- `stop_swtpm_process`: no-op if already stopped or Process absent. + +--- + +## 17. Nix authoring form + +### 17.1 What operators declare + +Operators declare **only** the Device resource. The TPM state Volume, swtpm +Process, and flush EphemeralProcess are **never** declared in Nix. They are +`managedBy: controller` resources; the Nix emitter does not emit them. + +```nix +d2b.zones.dev.resources."corp-vm-tpm" = { + type = "Device"; + metadata.ownerRef = "Guest/corp-vm"; + spec = { + providerRef = "Provider/device-tpm"; + deviceClass = "emulated"; + arbitration = "exclusive"; + maxConcurrentClaims = 1; + inventory.selector = {}; + provider = { + schemaId = "device-tpm.d2bus.org/Device/spec"; + schemaVersion = "1.0.0"; + settings = { + logLevel = 20; + # executionRef may be omitted to inherit Provider config.controllerExecutionRef + }; + }; + }; +}; +``` + +The Guest Provider declares the Device attachment: + +```nix +d2b.zones.dev.resources."corp-vm" = { + type = "Guest"; + spec = { + providerRef = "Provider/runtime-cloud-hypervisor"; + deviceAttachments = [ + { deviceRef = "Device/corp-vm-tpm"; exclusive = true; } + ]; + }; +}; +``` + +### 17.2 Provider config in Nix + +```nix +d2b.zones.dev.resources."device-tpm" = { + type = "Provider"; + spec = { + artifactId = "d2b-provider-device-tpm"; + config = { + controllerExecutionRef = "Host/host-system"; + logLevel = 20; + }; + }; +}; +``` + +### 17.3 Migration from current form + +Current: `d2b.vms.corp-vm.tpm.enable = true` (in `nixos-modules/components/tpm.nix`). + +v3: the Nix Device declaration in §17.1 replaces this option. Migration steps: +- Remove `d2b.vms..tpm.enable`. +- Declare Device resource per §17.1. +- The existing `/var/lib/d2b/vms//swtpm/` directory is migrated to the + controller-created Volume path via a one-time migration EphemeralProcess. +- The existing provisioning marker in `swtpm-markers/` must be preserved + (re-keyed by volume-local from the old basename to the new `device_uid`-based + name). A missing or dropped marker fails the Volume provision fail-closed — + no silent re-creation. + +--- + +## 18. Current-code fit + +| Current artifact | Location | v3 disposition | +| --- | --- | --- | +| `SwtpmArgvInput`, `SwtpmIoctlFlushInput` | `packages/d2b-host/src/swtpm_argv.rs` | Extract into `d2b-provider-device-tpm/src/`; remove caller-supplied binary path fields | +| `PrepareSwtpmDir` | `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` | Retained; invoked only by `volume-local`; device-tpm controller never calls it | +| `SpawnRunner { role: Swtpm }` | `packages/d2b-priv-broker/src/ops/spawn_runner.rs` | Retained; invoked only by `system-minijail` | +| `ProcessRole::Swtpm`, `::SwtpmPreStartFlush` | `packages/d2b-core/src/processes.rs` | Retire after Provider parity | +| `minijail_swtpm_video.rs` | `packages/d2b-contract-tests/tests/` | Preserved; proves zero caps, `w1-swtpm`, user-NS long-lived only | +| `policy_swtpm_readiness.rs` | `packages/d2b-contract-tests/tests/` | Preserved; socket-ready predicate | +| `nixos-modules/components/tpm.nix` | `nixos-modules/components/tpm.nix` | Replaced by Device Nix declaration (§17.1) | +| Direct broker/swtpm call sites in daemon | `packages/d2bd/src/*` | Remove; device-tpm controller uses TpmEffectPort | + +--- + +## 19. Work items + +### ADR046-device-tpm-001 — Crate scaffold + +| Field | Value | +| --- | --- | +| Dependency/owner | P0; unblocked; owner: `d2b-provider-device-tpm` crate | +| Current source | No existing provider crate; baseline TPM behavior is in `nixos-modules/components/tpm.nix`, `packages/d2b-host/src/swtpm_argv.rs`, and broker/daemon swtpm paths listed in §18 | +| Reuse action | adapt | +| Destination | packages/d2b-provider-device-tpm/{src/,tests/,integration/README.md,README.md}; Cargo workspace membership | +| Detailed design | Crate scaffold: create `packages/d2b-provider-device-tpm/` with `src/`, `tests/`, `integration/README.md`, and `README.md`; add it to the Cargo workspace; workspace policy test must pass. Primary reuse disposition: `adapt`. Preserved source-plan detail: net-new crate scaffold; later items adapt baseline TPM behavior into the new Provider boundary. | +| Integration | Workspace policy and Cargo consume the new crate; all controller, effect-port, resource-builder, status, Nix, and integration work lands under this scaffold. | +| Data migration | None — scaffold only; TPM state migration is covered by later Volume/Nix work per §17.3 | +| Validation | Workspace policy test for required crate paths and Cargo workspace membership | +| Removal proof | None — net-new crate scaffold; legacy TPM owners are removed only by later parity/removal items | + +Create `packages/d2b-provider-device-tpm/` with `src/`, `tests/`, +`integration/README.md`, `README.md`. Add to Cargo workspace. Workspace +policy test must pass. + +### ADR046-device-tpm-002 — TpmEffectPort and FakeTpmEffectPort + +| Field | Value | +| --- | --- | +| Dependency/owner | P0; blocked by ADR046-device-tpm-001; owner: device-tpm effect boundary | +| Current source | `PrepareSwtpmDir` in `packages/d2b-priv-broker/src/ops/swtpm_dir.rs` and `SpawnRunner { role: Swtpm }` in `packages/d2b-priv-broker/src/ops/spawn_runner.rs` remain privileged executors, but the controller must not import broker crates | +| Reuse action | wrap | +| Destination | packages/d2b-provider-device-tpm/src/{effect_port.rs,effect_impl.rs}; packages/d2b-provider-device-tpm/tests/effect_fake.rs | +| Detailed design | TpmEffectPort and FakeTpmEffectPort: implement the effect trait, typed TPM EndpointRef handoff, and fake test port. Prove non-test files contain no `use d2b_priv_broker::` and the controller sees only opaque resource IDs and EndpointRefs. Primary reuse disposition: `wrap`. Preserved source-plan detail: wrap privileged effects behind an injected async `TpmEffectPort`; keep broker operations only behind `volume-local` and `system-minijail`. | +| Integration | Device controller calls `TpmEffectPort`; ResourceClient-backed implementation talks to ResourceAPI/ComponentSession; `volume-local` and `system-minijail` translate resource operations into broker effects. | +| Data migration | Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state. | +| Validation | `tests/effect_fake.rs`; static proof that non-test files do not import `d2b_priv_broker` | +| Removal proof | Direct broker references in controller/daemon TPM paths are superseded by the effect-port/resource-provider boundary; final deletion is ADR046-device-tpm-013 | + +Implement `TpmEffectPort` trait, typed TPM EndpointRef handoff, and +`FakeTpmEffectPort`. Prove: no `use d2b_priv_broker::` in non-test files. + +### ADR046-device-tpm-003 — Controller reconcile state machine + +| Field | Value | +| --- | --- | +| Dependency/owner | P0; blocked by ADR046-device-tpm-002; owner: device-tpm controller FSM | +| Current source | Current direct daemon/broker swtpm lifecycle call sites in `packages/d2bd/src/*` are superseded; controller algorithm is specified in §11.1 | +| Reuse action | replace | +| Destination | packages/d2b-provider-device-tpm/src/controller.rs; packages/d2b-provider-device-tpm/tests/controller_fsm.rs | +| Detailed design | Controller reconcile state machine: implement the Device reconcile algorithm from §11.1 against `FakeTpmEffectPort`, covering happy path, Volume not-ready, marker fail-closed, flush failure, swtpm maxRestarts, and finalizer behavior where Process is deleted and Volume retained; emit §14 metrics with only closed `phase`/`outcome`/`status` semantics and no Zone/resource-name-derived label. Primary reuse disposition: `replace`. Preserved source-plan detail: replace direct daemon lifecycle with Provider reconcile against `FakeTpmEffectPort` and resource status. | +| Integration | Resource watches drive the controller; controller creates/observes Volume, Process, EphemeralProcess, and Endpoint resources through `TpmEffectPort`; Device status/finalizers expose outcomes to the ResourceAPI. | +| Data migration | Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state. | +| Validation | `tests/controller_fsm.rs` covering happy path, Volume not-ready, marker fail-closed, flush failed, swtpm maxRestarts, and finalizer behavior; `tests/metrics_labels.rs` structurally asserts exact absence of `vm`, `zone`, `zone_id`, `zone_uid`, and resource-name-derived keys and Device/Zone-name canary absence | +| Removal proof | Direct daemon swtpm lifecycle logic is removable after this Provider reconcile FSM reaches parity and ADR046-device-tpm-013 removes the old call sites | + +Implement Device reconcile algorithm (§11.1) against `FakeTpmEffectPort`. +Tests cover: happy path, Volume not-ready, marker fail-closed, flush failed, +swtpm maxRestarts, finalizer (Process deleted; Volume retained). + +### ADR046-device-tpm-004 — Controller-created Volume spec + +| Field | Value | +| --- | --- | +| Dependency/owner | P0; blocked by ADR046-device-tpm-001; owner: device-tpm resource builders | +| Current source | `nixos-modules/components/tpm.nix` declares TPM enablement today; §17.3 defines migration from the existing swtpm directory and marker | +| Reuse action | replace | +| Destination | packages/d2b-provider-device-tpm/src/resources.rs; packages/d2b-provider-device-tpm/tests/volume_create.rs | +| Detailed design | Controller-created Volume spec: implement `build_tpm_state_volume_spec` with `cleanupPolicy: never`, `repairPolicy: fail-closed`, `adoptionPolicy: quarantine-on-ambiguity`, `sensitivity: secret-adjacent`, required invariants, `source.sourceId`, no `hostPath`, no top-level identityMarker/persistenceClass/quotaBytes/stateSchema, `ownerRef: Device/`, `managedBy: controller`, empty attachments, and `quota: null`. Primary reuse disposition: `replace`. Preserved source-plan detail: replace VM-level TPM option/state path with controller-created Device-owned Volume spec. | +| Integration | Device controller creates the TPM data Volume; `volume-local` materializes/protects state and marker; swtpm Process mounts the Volume; Guest runtime receives only EndpointRefs. | +| Data migration | Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state. | +| Validation | `tests/volume_create.rs` proving every canonical Volume field and forbidden field listed in this item | +| Removal proof | `nixos-modules/components/tpm.nix` state-path ownership is superseded by the Device-owned Volume once migration and Nix roundtrip are complete | + +Implement `build_tpm_state_volume_spec` in `resources.rs`. Tests prove: +- `cleanupPolicy: never`; `repairPolicy: fail-closed`; + `adoptionPolicy: quarantine-on-ambiguity`; `sensitivity: secret-adjacent`. +- `invariants` includes `no-symlink`, `broker-opaque-id-only`, + `scope-authorization-required`. +- `source.sourceId` present; no `hostPath` field. +- No `identityMarker`/`persistenceClass`/`quotaBytes`/`stateSchema` top-level fields. +- `ownerRef: Device/`; `managedBy: controller`. +- `attachments: []`; `quota: null`. + +### ADR046-device-tpm-005 — Canonical swtpm Process spec + +| Field | Value | +| --- | --- | +| Dependency/owner | P0; blocked by ADR046-device-tpm-004; owner: device-tpm Process resource builder | +| Current source | `SwtpmArgvInput` in `packages/d2b-host/src/swtpm_argv.rs`; `ProcessRole::Swtpm` in `packages/d2b-core/src/processes.rs`; `minijail_swtpm_video.rs` contract tests | +| Reuse action | adapt | +| Destination | packages/d2b-provider-device-tpm/src/resources.rs; Process spec tests under packages/d2b-provider-device-tpm/tests/ | +| Detailed design | Canonical swtpm Process spec: implement `build_swtpm_process_spec` with `readOnlyRoot: true`, `userNamespace.mappingClass: process-principal-root`, namespace classes `[pid, mount, user]`, empty capability classes, `seccompClass: w1-swtpm`, two Device-owned Endpoint resources (`tpm` and `ctrl`), `mounts[0].required: true`, and no socket path, binary path, UID integer, or GID integer in any spec field. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract swtpm argv/sandbox intent into canonical Process resources; remove caller-supplied binary path, UID, GID, and socket path fields. | +| Integration | Controller emits the Process spec; `system-minijail` consumes it and invokes broker `SpawnRunner`; Endpoint resources publish TPM and control sockets for downstream consumers. | +| Data migration | Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state. | +| Validation | Process spec golden tests proving all required and forbidden fields; preserved `minijail_swtpm_video.rs` contract tests | +| Removal proof | `ProcessRole::Swtpm` and swtpm argv builder call sites can be retired after the canonical Process resource covers runner launch | + +Implement `build_swtpm_process_spec` in `resources.rs`. Tests prove: +- `readOnlyRoot: true`. +- `userNamespace: {mappingClass: process-principal-root}` (no numeric UID/GID fields). +- `namespaceClasses: [pid, mount, user]`; `capabilityClasses: []`; + `seccompClass: w1-swtpm`. +- Two Device-owned `Endpoint` resources: `tpm` and `ctrl`; no path. +- `mounts[0].required: true` (canonical MountSpec field). +- No socket path, binary path, UID integer, or GID integer in any spec field. + +### ADR046-device-tpm-006 — Mandatory flush EphemeralProcess spec + +| Field | Value | +| --- | --- | +| Dependency/owner | P0; blocked by ADR046-device-tpm-003; owner: device-tpm EphemeralProcess resource builder | +| Current source | `SwtpmIoctlFlushInput` in `packages/d2b-host/src/swtpm_argv.rs`; `ProcessRole::SwtpmPreStartFlush` in `packages/d2b-core/src/processes.rs` | +| Reuse action | adapt | +| Destination | packages/d2b-provider-device-tpm/src/resources.rs; packages/d2b-provider-device-tpm/tests/flush_mandatory.rs | +| Detailed design | Mandatory flush EphemeralProcess spec: implement `build_flush_ephemeral_process_spec`; no `startupClear` field exists; flush is always created before swtpm Process start with no skip path; TTLs are `successfulTtl: "1h"` and `failedTtl: "24h"`; no userNamespace on flush Process; deadlines are `startDeadline: "30s"` and `runtimeDeadline: "60s"`. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract flush intent into mandatory EphemeralProcess resource; remove configurable `startupClear` path and caller-supplied binary path fields. | +| Integration | Controller inserts the EphemeralProcess before every swtpm activation cycle; Process provider runs the flush against the control Endpoint fd before the long-lived swtpm Process becomes Ready. | +| Data migration | Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state. | +| Validation | `tests/flush_mandatory.rs` plus contract proof that user-NS is long-lived only | +| Removal proof | `ProcessRole::SwtpmPreStartFlush` and any optional startup-clear wiring are retired after mandatory EphemeralProcess coverage is complete | + +Implement `build_flush_ephemeral_process_spec` in `resources.rs`. Tests prove: +- No `startupClear` field anywhere in spec or controller code. +- Flush always created before swtpm Process start; no code path skips it. +- `successfulTtl: "1h"`; `failedTtl: "24h"` exactly. +- No `userNamespace` on flush Process (user-NS long-lived only, per contract test). +- `startDeadline: "30s"`; `runtimeDeadline: "60s"`. + +### ADR046-device-tpm-007 — Device status builder + +| Field | Value | +| --- | --- | +| Dependency/owner | P1; blocked by ADR046-device-tpm-003; owner: device-tpm status builder | +| Current source | Existing status/path observations from direct daemon TPM paths are superseded; status fields are defined in §10 | +| Reuse action | create | +| Destination | packages/d2b-provider-device-tpm/src/status.rs; packages/d2b-provider-device-tpm/tests/{endpoint_ref.rs,redaction.rs} | +| Detailed design | Device status builder: implement `build_device_status`; `tpmEndpointRef` is an `Endpoint/` ResourceRef with no opaque endpoint ID compatibility alias and never a filesystem path; `stateVolumeRef` and `swtpmProcessRef` are canonical ResourceRef strings; no path, socket name, UID, GID, PID, or pidfd appears in status; `markerStatus` is one of `verified`, `missing`, `replaced`, or `unknown`. Primary reuse disposition: `create`. Preserved source-plan detail: net-new bounded Device status projection; do not reuse path/socket/UID/GID/PID observations. | +| Integration | Controller writes Device status; Guest runtime Provider reads EndpointRef from Device status; CLI/support tooling reads bounded non-secret status. | +| Data migration | None — status is re-derived during v3 reconcile; TPM state migration remains the Volume/marker migration in §17.3 | +| Validation | `tests/endpoint_ref.rs`; `tests/redaction.rs`; status builder tests for allowed `markerStatus` values | +| Removal proof | Legacy path/socket status compatibility aliases are absent; removal is proven by redaction/status tests | + +Implement `build_device_status` in `status.rs`. Tests prove: +- `tpmEndpointRef` is an `Endpoint/` ResourceRef; no opaque endpoint ID compatibility alias; + never a filesystem path. +- `stateVolumeRef` and `swtpmProcessRef` are canonical `ResourceType/` strings. +- No path, socket name, UID, GID, PID, or pidfd in any status field. +- `markerStatus` carries only: `verified`, `missing`, `replaced`, `unknown`. + +### ADR046-device-tpm-008 — EndpointRef handoff + +| Field | Value | +| --- | --- | +| Dependency/owner | P1; blocked by ADR046-device-tpm-007; owner: device-tpm endpoint handoff integration | +| Current source | Baseline guest wiring consumed socket paths from TPM sidecar state; v3 handoff is the Endpoint resource contract in §8.4 and §10.5 | +| Reuse action | replace | +| Destination | packages/d2b-provider-device-tpm/src/{effect_port.rs,status.rs}; packages/d2b-provider-device-tpm/integration/guest_endpoint.rs | +| Detailed design | EndpointRef handoff: hermetic tests prove `tpmEndpointRef` is an EndpointRef and never a path; integration proves Guest runtime Provider reads `tpmEndpointRef` and obtains the socket fd from the Zone runtime endpoint resolver with no path string in Guest spec or LaunchTicket API surface. Primary reuse disposition: `replace`. Preserved source-plan detail: replace path handoff with EndpointRef and Zone runtime endpoint resolver fd acquisition. | +| Integration | Device status publishes EndpointRef; Guest runtime Provider resolves the Endpoint through the Zone endpoint resolver; LaunchTicket receives an fd, not a socket path. | +| Data migration | None — endpoint handoff has no state migration; TPM data migration remains §17.3 | +| Validation | `tests/endpoint_ref.rs`; `integration/guest_endpoint.rs` | +| Removal proof | Socket-path handoff is superseded when all Guest runtime TPM attachment uses EndpointRef resolution | + +Hermetic test: `tpmEndpointRef` is an EndpointRef; no path. Integration test: +Guest runtime Provider reads `tpmEndpointRef` and obtains socket fd from Zone +runtime endpoint resolver; no path string in Guest spec or LaunchTicket API +surface. + +### ADR046-device-tpm-009 — Marker fail-closed test + +| Field | Value | +| --- | --- | +| Dependency/owner | P0; blocked by ADR046-device-tpm-004; owner: device-tpm marker/fail-closed tests | +| Current source | Existing provisioning marker behavior in `/var/lib/d2b/swtpm-markers/` is preserved and re-keyed by volume-local per §17.3 | +| Reuse action | adapt | +| Destination | packages/d2b-provider-device-tpm/tests/marker_fail_closed.rs; packages/d2b-provider-device-tpm/integration/marker_tamper.rs | +| Detailed design | Marker fail-closed test: FakeTpmEffectPort returning `markerStatus: replaced` makes Device Failed, prevents a second `ensure_state_volume` call, and prevents swtpm Process creation. Integration physically replaces `swtpm/`; volume-local sets Volume Failed; Device fails with no auto-recovery. Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve fail-closed marker semantics while moving ownership to Volume/Device resources. | +| Integration | volume-local observes marker state and reports Volume/marker status; controller maps that to Device failure and blocks Process creation; integration exercises the actual filesystem marker path through volume-local. | +| Data migration | Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state. | +| Validation | `tests/marker_fail_closed.rs`; `integration/marker_tamper.rs` | +| Removal proof | Any silent marker recreation path is removed/proven absent by the fail-closed tests | + +Hermetic: FakeTpmEffectPort returns `markerStatus: replaced` → Device Failed; +no second `ensure_state_volume` call; swtpm Process not created. + +Integration: physically replace swtpm/ dir → volume-local sets Volume Failed → +Device Failed; no auto-recovery. + +### ADR046-device-tpm-010 — Controller Process (status-first; no Provider state Volume) + +| Field | Value | +| --- | --- | +| Dependency/owner | P1; blocked by ADR046-device-tpm-001; owner: device-tpm controller Process descriptor | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | packages/d2b-provider-device-tpm/src/resources.rs; packages/d2b-provider-device-tpm/tests/controller_process.rs | +| Detailed design | Controller Process (status-first; no Provider state Volume): implement the controller Process spec from §4.1 with `processClass: controller`, `readOnlyRoot: true`, empty mounts, no controller-scratch namespace, no scratch mount, no `User/device-tpm-controller-system` state-layout principal, no permission to create Provider-owned Volumes, and restart re-derivation from resource store plus external marker/process observations while treating status as observation. Primary reuse disposition: `create`. Preserved source-plan detail: net-new status-first controller Process spec; no Provider state Volume is reused or created. | +| Integration | ProviderDeployment creates the controller Process from the descriptor; controller status and Operation ledger carry bounded non-secret observations; Device-owned TPM data Volume remains separate from ProviderStateSet. | +| Data migration | None — controller has no Provider state Volume to migrate; Device data migration remains §17.3 | +| Validation | Controller Process spec tests proving the bullets in this item | +| Removal proof | None — this item prevents introduction of a Provider state Volume and has no prior state owner to remove | + +Implement controller Process spec (§4.1). Tests prove: +- `processClass: controller`; `readOnlyRoot: true`. +- `mounts` is empty: the controller declares no Provider state Volume; its + bounded non-secret operational state lives in the owning resource's `status` + subresource and the core Operation ledger (D087). No `controller-scratch` + namespace, no `scratch` mount, and no `User/device-tpm-controller-system` + state-layout principal exist. +- The device-tpm controller holds no `create` permission for Volumes with + `ownerRef: Provider/`; it creates only the per-Device TPM data Volume + with `ownerRef: Device/` (see §7 and §15.2), which is not part of the + ProviderStateSet. +- On restart the controller re-derives Device reconcile state from the Zone + resource store and reverifies against external reality (marker checks, running + swtpm processes), treating `status` as observation, never authority. + +### ADR046-device-tpm-011 — Nix roundtrip test + +| Field | Value | +| --- | --- | +| Dependency/owner | P1; blocked by ADR046-device-tpm-001; owner: Nix Resource compiler for Device declarations | +| Current source | `nixos-modules/components/tpm.nix` current `d2b.vms..tpm.enable` option is replaced by the Device declaration in §17.1 | +| Reuse action | replace | +| Destination | nixos-modules/options-resources.nix and Nix eval/golden tests for §17.1 Device JSON | +| Detailed design | Nix roundtrip test: Device Nix spec from §17.1 round-trips through the Nix emitter to expected resource JSON; emitted bundle contains no Volume, Process, or EphemeralProcess resources because controller-managed resources are not in the Nix bundle. Primary reuse disposition: `replace`. Preserved source-plan detail: replace VM-level TPM enable option with v3 Device resource emission and assertions. | +| Integration | Nix authoring emits only the Device and Provider resources; ResourceAPI admission hands the Device to the controller; controller creates managed resources at runtime. | +| Data migration | Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state. | +| Validation | Nix roundtrip/golden test for §17.1 and emitted-bundle absence of controller-managed resources | +| Removal proof | `d2b.vms..tpm.enable` in `nixos-modules/components/tpm.nix` is superseded after Device Nix declaration roundtrip and migration are in place | + +Device Nix spec (§17.1) round-trips through the Nix emitter to expected +resource JSON. Emitted bundle contains no Volume, Process, or EphemeralProcess +resources (`managedBy: controller` resources are not in the Nix bundle). + +### ADR046-device-tpm-012 — Finalizer: Volume retained on Device deletion + +| Field | Value | +| --- | --- | +| Dependency/owner | P0; blocked by ADR046-device-tpm-003; owner: device-tpm finalizer lifecycle | +| Current source | Current TPM state retention behavior is tied to the swtpm directory and marker; v3 retention is `cleanupPolicy: never` on the Device-owned Volume | +| Reuse action | adapt | +| Destination | packages/d2b-provider-device-tpm/src/controller.rs; packages/d2b-provider-device-tpm/tests/finalizer.rs | +| Detailed design | Finalizer: Volume retained on Device deletion. Hermetic tests cover Device deletion finalizer leading to swtpm Process deletion, TPM state Volume not deleted because `cleanupPolicy: never`, Volume persists, Core emits `phase=Deleted` for Device after finalizer clears, and audit carries no path/UID. Primary reuse disposition: `adapt`. Preserved source-plan detail: preserve TPM identity retention while moving deletion sequencing to Resource finalizers. | +| Integration | Resource deletion sets finalizer; controller deletes Process and retains Volume; core completes Deleted revision after finalizer clears; audit subsystem records redacted deletion outcome. | +| Data migration | Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state. | +| Validation | `tests/finalizer.rs` proving Process deletion, retained Volume, core Deleted phase, and redacted audit | +| Removal proof | Any cleanup path that deletes TPM state on Device deletion is removed/proven absent by the finalizer test | + +Hermetic: Device deletion finalizer → swtpm Process deleted → TPM state +Volume NOT deleted (`cleanupPolicy: never`) → Volume persists. Core emits +`phase=Deleted` for Device after finalizer clears. No path/UID in audit event. + +### ADR046-device-tpm-013 — Remove direct broker references + +| Field | Value | +| --- | --- | +| Dependency/owner | P0; blocked by ADR046-device-tpm-002; owner: device-tpm migration/removal cleanup | +| Current source | `packages/d2bd/src/*` direct broker/swtpm call sites; `packages/d2b-core/src/processes.rs` `ProcessRole::Swtpm` and `ProcessRole::SwtpmPreStartFlush`; `packages/d2b-host/src/swtpm_argv.rs` argv builders | +| Reuse action | delete-after-cutover | +| Destination | packages/d2bd/src/*; packages/d2b-core/src/processes.rs; packages/d2b-provider-device-tpm/src/; packages/d2b-host/src/swtpm_argv.rs | +| Detailed design | Remove direct broker references: remove pre-ADR-0046 daemon swtpm broker call sites, retire `ProcessRole::Swtpm` and `ProcessRole::SwtpmPreStartFlush`, move argv builders from `d2b-host/src/swtpm_argv.rs` to `d2b-provider-device-tpm/src/` with binary path fields removed, while retaining `d2b-priv-broker/src/ops/swtpm_dir.rs` for `volume-local` and `spawn_runner.rs` for `system-minijail`. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: remove direct broker references from daemon and move argv builders into the Provider with binary path fields removed; retain broker ops only behind resource providers. | +| Integration | After controller/effect-port parity, daemon no longer calls TPM broker ops; Resource providers invoke broker effects from Volume and Process reconciliation; contract tests ensure swtpm sandbox/readiness still hold. | +| Data migration | Existing TPM state migration follows §17.3: the old `/var/lib/d2b/vms//swtpm/` directory moves to the controller-created Volume path with the provisioning marker preserved and re-keyed; this item must not silently recreate missing state. | +| Validation | Static search/proof for no direct broker swtpm references in daemon/controller plus preserved swtpm contract tests | +| Removal proof | Direct daemon swtpm call sites removed; `ProcessRole::Swtpm` and `ProcessRole::SwtpmPreStartFlush` retired; `d2b-host/src/swtpm_argv.rs` no longer owns Provider argv builders | + +Remove direct broker call sites for swtpm from pre-ADR-0046 daemon path. +Retire `ProcessRole::Swtpm` and `::SwtpmPreStartFlush` from `d2b-core`. +Move argv builders from `d2b-host/src/swtpm_argv.rs` to +`d2b-provider-device-tpm/src/` with binary path fields removed. +`d2b-priv-broker/src/ops/swtpm_dir.rs` and `spawn_runner.rs` retained +(used by `volume-local` and `system-minijail` respectively). +## 20. Tests + +### Fast hermetic execution and test placement (D094) + +Per D094 and `ADR-046-validation-and-delivery` §10.16, this Provider's `src/` +unit tests and `tests/*.rs` hermetic suite are fast, in-process, deterministic, +and parallel-safe: an individual normal test has p95 ≤50 ms with no wall-clock +sleep, and `cargo test -p d2b-provider-device-tpm --lib --tests` completes in +≤2 s warm-cache execution time (compilation excluded). They use a deterministic +fake clock/RNG and the toolkit fakes/FakeEffectPort only — no process spawn, +container, network, DBus, systemd, broker daemon, Nix eval/build, KVM, +USB/GPU/TPM hardware, or live cloud, and no filesystem tree beyond tiny temp +fixtures. Any scenario needing those lives only in `integration/`, which keeps a +lane timeout/budget, parallel isolation, and fake external services by default; +such a need is re-placed into `integration/`, never given a sleep, larger +timeout, or `#[ignore]`. Bounded crypto/property tests are the only classified +exception, each named with a capped case count and a declared higher per-test +budget. + +### 20.1 Hermetic (Cargo) — `tests/` + +| Test file | What it proves | +| --- | --- | +| `controller_fsm.rs` | All Device reconcile state transitions; FakeTpmEffectPort | +| `effect_fake.rs` | FakeTpmEffectPort records all calls; no broker import in crate | +| `volume_create.rs` | Volume canonical fields: `cleanupPolicy: never`, `repairPolicy: fail-closed`, `sensitivity: secret-adjacent`, correct invariants, no ProviderStateSet extensions | +| `flush_mandatory.rs` | Flush always issued; no skip; no `startupClear`; correct TTLs; no userNamespace | +| `endpoint_ref.rs` | `tpmEndpointRef` is an EndpointRef; no path or raw locator | +| `marker_fail_closed.rs` | Marker replaced/missing → Device Failed; no auto-recovery; no second `ensure_state_volume` | +| `finalizer.rs` | Process deleted; Volume retained; Deleted emitted by core | +| `redaction.rs` | No path/UID/socket/pidfd in status, audit span attrs, or log records | +| `metrics_labels.rs` | Closed semantic metric descriptors; exact identity-key absence; Device/Zone-name canaries absent from label values; `d2b.zone` resource attribute retained | +| `schema.rs` | Device spec admission round-trip through JSON schema | +| `nix_roundtrip.rs` | Nix form emits no Volume/Process/EphemeralProcess resources | + +### 20.2 Integration — `integration/` + +| Test file | What it proves | +| --- | --- | +| `basic_tpm_start.rs` | Host fixture: Device → Volume Ready → flush Succeeded → swtpm Ready | +| `marker_tamper.rs` | Replace swtpm/ dir → Volume Failed → Device Failed; no auto-recovery | +| `guest_endpoint.rs` | Guest reads `tpmEndpointRef`; receives socket fd; no path in LaunchTicket | +| `lifecycle_restart.rs` | Controller restart: adopts swtpm Process; Volume retained; no double-flush | + +### 20.3 Existing contract tests (preserved) + +| Test file | Location | Invariants proved | +| --- | --- | --- | +| `minijail_swtpm_video.rs` | `d2b-contract-tests/tests/` | Zero host caps; `w1-swtpm` seccomp; user-NS long-lived only (flush has no userNamespace) | +| `policy_swtpm_readiness.rs` | `d2b-contract-tests/tests/` | Unix socket readiness predicate | +| `swtpm_dir.rs` unit tests | `d2b-priv-broker/src/ops/` | Tamper marker; fresh/existing dir; symlink/mismatch fail-closed | + +--- + +## 21. Removal plan + +When this Provider reaches `Evidence class: implemented-and-reachable`: + +1. Remove `d2b.vms..tpm.enable` from `nixos-modules/components/tpm.nix`. +2. Retire `ProcessRole::Swtpm` and `::SwtpmPreStartFlush` from `d2b-core`. +3. Move `SwtpmArgvInput`, `SwtpmIoctlFlushInput` from `d2b-host/src/swtpm_argv.rs` + to `d2b-provider-device-tpm/src/`; remove binary path fields. +4. Remove direct swtpm call sites from `packages/d2bd/src/`. +5. Update `CHANGELOG.md` under `## [Unreleased]`. +6. Emit migration guide entry in `docs/how-to/migrate-d2b-v2-to-v3.md`. +7. Update `docs/specs/ADR-046-resources-device.md` device-tpm section to + reference this dossier as canonical source. + +Per D094, each replaced current-code test is retired with an explicit +keep/adapt/move/delete disposition and a removal gate: the minimum reusable +semantic assertions migrate into this crate's hermetic `tests/`, and the old +duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest +entries are deleted once successor coverage and the removal proof pass — +updating `tests/layer1-jobs.json`, the closed gate manifests, the +flake/matrix/Nix-unit pins, the generated ledgers, and the CI workflow shards. +Old and new suites never run in parallel indefinitely. diff --git a/docs/specs/providers/ADR-046-provider-device-usbip.md b/docs/specs/providers/ADR-046-provider-device-usbip.md new file mode 100644 index 000000000..7835046f9 --- /dev/null +++ b/docs/specs/providers/ADR-046-provider-device-usbip.md @@ -0,0 +1,2039 @@ +# ADR 0046 Provider dossier: device-usbip + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-device-usbip` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 7 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | `d2b-provider-device-usbip` crate, USB Service/Binding controller contracts, Nix USB resource emitter | +| Depends on | `ADR-046-resources-device`, `ADR-046-resources-network`, `ADR-046-resources-zone-control`, `ADR-046-zone-routing`, `ADR-046-components-processes-and-sandbox`, `ADR-046-provider-model-and-packaging`, `ADR-046-resource-reconciliation`, `ADR-046-telemetry-audit-and-support`, `ADR-046-componentsession-and-bus`, `ADR-046-nix-configuration`, `ADR-046-resource-api-and-authorization`, `ADR-046-provider-state` | +| Supersedes | `nixos-modules/components/usbip.nix` (host-side), per-env usbipd systemd units in `nixos-modules/network.nix`, `ProcessRole::Usbip` / `RunnerRole::Usbip` in current v3 baseline | + +--- + +## Purpose + +This dossier specifies `Provider/device-usbip`, the initial implementation of +the generic D098 USB Service/Binding family. This Provider owns USB/IP (USBIP) +device inventory, arbitration, busid probe/claim, host-side kernel bind, +singleton backend and per-Network relay lifecycle, firewall carve-out, +per-Guest attachment state, policy-gated cross-Zone service propagation, and +operator CLI for the `d2b device usb` surface. Future Providers may implement +the same `usb.d2bus.org.UsbService` and `usb.d2bus.org.UsbBinding` base +contracts without USBIP. + +`Provider/device-usbip` is one of the four frozen Device Providers in the +`ADR-046-resources-device` catalog. It replaces: + +- the broker-spawned `RunnerRole::Usbip` backend and proxy runners managed by + `packages/d2bd/src/usbipd_perenv_autostart.rs`; +- the typed step machine in `packages/d2bd/src/usbip_state_machine.rs`; +- the reconcile state model in `packages/d2bd/src/usbip_reconcile_state.rs`; +- the per-env firewall carve-out in `nixos-modules/network.nix` lines 444–461; +- the guest-side module wiring in `nixos-modules/components/usbip.nix`. + +--- + +## Provider identity + +```text +Provider/device-usbip +``` + +Crate: `packages/d2b-provider-device-usbip/` + +The Provider is installed in a Zone via the standard artifact catalog mechanism. +The Provider resource `spec.artifactId` references the catalog entry named +`provider-device-usbip`. The Zone controller creates the controller Process +resource at install time using the `controllerExecutionRef` from +`Provider.spec.config`. + +--- + +## Crate layout and dependency rules + +Required root layout (per `ADR-046-provider-model-and-packaging`): + +``` +d2b-provider-device-usbip/ + src/ + tests/ + integration/ + README.md +``` + +Workspace policy rejects the crate if any of these four paths is absent. +Internal module organisation within `src/` and `tests/` is implementation +detail; notable files are called out in work items and test coverage sections +below. + +### Dependency rules + +The `d2b-provider-device-usbip` crate depends **only** on: + +| Crate | Purpose | +| --- | --- | +| `d2b-contracts` | busid validation/redaction, EffectPort types, signed D096 export/import adapter contracts, D097 authority descriptors | +| `d2b-provider-toolkit` | `ReconcileContext`, `ResourceClient`, `ResourceMutationBatch`, Service/Binding schema helpers, `phase_*` helpers, generic conformance | + +The Provider crate **must not** import `d2b-priv-broker`, `d2bd`, `d2b-host`, +`d2b-realm-core`, Zone-store internals, or another Provider's implementation. +No raw broker op DTOs, lock file paths, nftables rule bodies, or busid strings +appear in the Provider's public types or internal reconcile logic. + +--- + +## Implements + +| ResourceType | Role | Exportability | Arbitration | +| --- | --- | --- | --- | +| `Device` | Owner-Zone physical USB inventory and busid identity | forbidden | `exclusive` physical claim | +| `usb.d2bus.org.UsbService` | Provider-neutral whole-USB-device authority or imported local projection | `explicit-export` for authority mode only | shared Host-global backing authority is `exclusive` | +| `usb.d2bus.org.UsbBinding` | Provider-neutral per-Guest access and attachment intent; observation lives in resource `status` | forbidden | one active Binding per Service | + +--- + +## Provider.spec.config + +```yaml +spec: + artifactId: provider-device-usbip + config: + controllerExecutionRef: Host/host-system # required; may reference any Zone Host +``` + +| Field | Type | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | +| `controllerExecutionRef` | ResourceRef | — | required; `Host/` only | Host on which the framework creates the controller Process. | + +`controllerExecutionRef` is the only operator-configurable field. +`usbipHostKernelModule`, `vhciHcdKernelModule`, the private backend transport, +and standard USBIP TCP port 3240 are signed manifest/effect policy constants +embedded in the Provider package descriptor. They are never +operator-configurable. Executable paths for usbip, usbipd, and relay/proxy +binaries are resolved exclusively from the signed component descriptor inside +the Provider package closure. + +--- + +## Device spec + +Normative D089 spec layering: Device base fields are ResourceType base +`spec.*` fields, including `spec.providerRef`, `deviceClass`, +`inventory.selector`, attachments, and arbitration. This Provider's +desired-only extension is the canonical `spec.provider = { schemaId: +"device-usbip.d2bus.org/Device/spec", schemaVersion, settings }` envelope; it +is manifest-registered/signed, strict deny-unknown, bounded, versioned and +digested, +validated against `spec.providerRef` at Nix build and API admission, +implementation-only, and may not shadow base fields. Shared fields are promoted +to the Device base. The Provider implements the exact base Device spec/status +version/fingerprint, accepts the canonical minimal valid base Spec, and rejects +unsupported optional base capabilities only through its signed capability matrix +and provider-neutral `unsupported-capability`. `spec.provider` aligns with +`status.provider`; generic CLI/controllers operate on the base spec and base +status only. No secret bytes are allowed in any spec layer, and no +credential material is allowed in `spec.provider.settings`. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Device +metadata: + name: corp-vm-usb + zone: work + ownerRef: null # operator-declared; no automated owner +spec: + providerRef: Provider/device-usbip + deviceClass: physical + arbitration: exclusive + maxConcurrentClaims: 1 + inventory: + selector: + busClass: usb + label: yubikey-work # logical label; never a raw busid + vendorId: "1050" # exactly 4 ASCII hex digits, lowercased + productId: "0407" + serial: null # optional; null = match any serial for this vid/pid + provider: + schemaId: "device-usbip.d2bus.org/Device/spec" + schemaVersion: "1.0.0" + settings: {} +``` + +### Provider settings fields + +The provider settings object is empty. The physical `Device` owns only +inventory, presence, and anti-spoof state; its common claim observation reflects +the Service authority holder. It carries no Guest, Network, relay, export, or +attachment policy. The provider-neutral Service/Binding pair owns only generic +whole-device backing, access, and attachment semantics; USBIP Network and relay +metadata lives in those resources' strict Provider extensions. + +Bus-class `usb` is the only accepted value for `busClass` in a `device-usbip` +Device. Any other value fails spec admission with `unsupported-bus-class`. + +--- + +## USB Service/Binding resource split (D096/D097) + +`usb.d2bus.org.UsbService` and `usb.d2bus.org.UsbBinding` are +provider-neutral whole-USB-device contracts. `Provider/device-usbip` is one +implementation Provider; it is not the ResourceType namespace. A future +direct-local Provider or another USB transport Provider may implement the same +base types without exposing USBIP concepts in their base spec or status. No +USBIP-qualified ResourceType or compatibility alias exists. + +The Service is the stable backing authority and the only USB resource that may +be exported. The Binding is one consuming Guest's access and attachment intent; +every observation belongs in resource `status`. A physical `Device`, an +`Endpoint`, or a `UsbBinding` is never an export target. The frozen D096 catalog +classifies this USB capability as policy-gated `explicit-export`; exclusive +backing authority, typed transport, and encrypted stream boundaries make the +USBIP implementation exportable. + +### Owner authority `UsbService` + +The owner Zone declares one authority-mode `usb.d2bus.org.UsbService` for each +whole physical USB device it may offer: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: usb.d2bus.org.UsbService +metadata: + name: work-token + zone: work + ownerRef: null +spec: + providerRef: Provider/device-usbip + mode: authority + backingDeviceRef: Device/work-token + accessPolicy: + mode: exclusive + maxActiveBindings: 1 + queue: + discipline: fair + maxDepth: 16 + acquireDeadline: "15s" + backingAuthority: + authorityScope: physical-device + authorityClass: physical-usb-backing + keyDerivation: core-trusted-usb-v1 + cardinality: zero-or-one + arbitration: exclusive + authorityRef: usb.d2bus.org.UsbService/work-token + duplicateConflict: physical-usb-backing-conflict + exportability: explicit-export + provider: + schemaId: "device-usbip.d2bus.org/UsbService/spec" + schemaVersion: "1.0.0" + settings: + relayEndpointRef: Endpoint/work-net-usbip-relay +``` + +The base spec contains only generic whole-device semantics: +`backingDeviceRef`, generic access policy, and the shared Host-global physical +backing authority. `backingDeviceRef` resolves in the Service's Zone to a +physical standard `Device` implemented by `Provider/device-usbip`; it is the +sole source of inventory and anti-spoof identity. Core derives the backing +authority's opaque Host-global key from the trusted selector. USB services and +security-key resources contend on this same shared backing-authority index, so +neither Provider can bypass mutual exclusion by using a private authority +namespace. After trusted physical-USB identity resolution, Core derives the +`physical-usb-backing/v1` opaque digest and indexes the exact tuple +`(Host, physical-usb-backing, opaqueKeyDigest)`. Every USB or security-key +implementation backed by the same token submits that byte-identical tuple +before any open, withhold, bind, module, relay, or attachment effect. The digest +is never caller supplied or exposed in public spec/status. + +The strict `spec.provider` extension is selected by `providerRef`. For the +USBIP implementation it may carry only desired USBIP implementation metadata, +including the local relay `Endpoint` reference. Network selection, TCP 3240 +topology, host module/backend policy, proxy/server/client behavior, firewall +policy, busid handling, and transport tuning are Provider extension, Provider +configuration, or `status.provider` concerns; they never enter the base +`UsbService` or `UsbBinding` contract. No raw busid, sysfs path, interface, +address, port, fd, or credential appears in either base spec. + +The USBIP implementation registers additional signed D097 authorities: + +| Authority class | Owning Resource | Scope/cardinality | Arbitration | Conflict | +| --- | --- | --- | --- | --- | +| physical USB backing | authority `UsbService` referencing local `Device` | Host-global, `zero-or-one` per Core-derived trusted physical-USB digest under exact class `physical-usb-backing` | `exclusive`, shared with every security-key Provider | `physical-usb-backing-conflict` | +| `usbip-host` module/backend | Provider-owned Host authority derived from backing placement | `host`, `exactly-one` | shared by that Host's USBIP Services | `usbip-host-module-authority-conflict` | +| TCP 3240 relay/firewall | Provider-owned relay `Endpoint` | Host-global `host`, `exactly-one` per Core-derived Network UID/signed-policy-port opaque digest | `multiplexed` | `usbip-network-relay-authority-conflict` | + +Only the first row is part of provider-neutral USB backing semantics. The Host +module/backend and relay rows are USBIP Provider metadata. Core indexes all +three before module load, busid bind, listener open, or firewall effect. +Restart adopts exact owner proofs; ambiguity quarantines instead of opening a +second backing. + +There is exactly one `usbip-host` module/backend authority per Host and one +typed relay `Endpoint` per Network. The Provider's relay binds that Network on +TCP 3240 and multiplexes every admitted USBIP Service. A new Service registers +another exclusively claimed busid with the existing multiplexer; it does not +create another listener or module authority. + +### Imported `UsbService` projection + +The signed import adapter preserves the same provider-neutral ResourceType: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: usb.d2bus.org.UsbService +metadata: + name: work-token + zone: dev + ownerRef: ResourceImport/work-token +spec: + providerRef: Provider/device-usbip + mode: projection + accessPolicy: + mode: exclusive + maxActiveBindings: 1 + sourceSchemaFingerprint: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +``` + +Projection mode forbids `backingDeviceRef`, backing-authority ownership, and +any local physical effect. It permits only `providerRef`, semantic base/import +fields, and ResourceImport ownership; `spec.provider` is forbidden. Core +creates only this same-type Service projection, with `ownerRef` exactly equal +to the creating `ResourceImport`; it never projects a Device, Endpoint, or +Binding. Routing derives from the signed local Provider descriptor, +`providerRef`, and ResourceImport record; USBIP route observations may appear +only in `status.provider`. +The projection never discovers, opens, binds, withholds, or loads a module for +local physical USB. Import revocation degrades every dependent `UsbBinding`. + +### Signed D096 projection factory + +The Provider descriptor carries exactly one signed factory: + +| Field | USBIP value | +| --- | --- | +| `serviceType` | `usb.d2bus.org.UsbService` | +| `bindingType` | `usb.d2bus.org.UsbBinding` | +| `allowedBackingRefTypes` | `Device` | +| `allowedBindingTargetRefTypes` | `Guest` | +| `projectionSchema` | strict same-type projection schema with `providerRef` and semantic base/import fields; no `spec.provider`, backing ref, or raw locator/path/credential/fd/bytes | +| `projectionSchemaFingerprint` | SHA-256 of canonical committed projection schema | +| `factoryFingerprint` | SHA-256 binding the semantic factory fields plus the projection-protocol version; never Provider or ExportAdapter/ImportAdapter identity/version | + +Provider install, Nix admission, API admission, export, and import fail closed +if the factory/signature/type/schema/fingerprint differs. Provider adapters +are authenticated separately by the signed Provider descriptor and therefore +cannot affect the semantic fingerprint; they validate implementation +extensions. Core owns provider-neutral base lifecycle, +Export/Import routing, projection ownership, and layered status. + +### Per-Guest `UsbBinding` + +Each consuming Guest has one `usb.d2bus.org.UsbBinding`: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: usb.d2bus.org.UsbBinding +metadata: + name: corp-vm-work-token + zone: dev + ownerRef: Guest/corp-vm +spec: + providerRef: Provider/device-usbip + serviceRef: usb.d2bus.org.UsbService/work-token + guestRef: Guest/corp-vm + accessPolicy: + mode: exclusive + priority: 100 + attachmentPolicy: + desired: attached + activation: declared + provider: + schemaId: "device-usbip.d2bus.org/UsbBinding/spec" + schemaVersion: "1.0.0" + settings: + networkRef: Network/dev-net +``` + +The base Binding contains only `serviceRef`, `guestRef`, and generic access and +attachment policy. `serviceRef` and `guestRef` are same-Zone; `ownerRef` must +equal `guestRef`. The Service may be an owner authority or imported projection. +The USBIP-only `networkRef` is validated as a same-Zone reference inside the +strict Provider extension. `attachmentPolicy.desired` is +`attached|detached`; `activation` is `declared|explicit`; priority never +overrides fair exclusive arbitration. + +The USBIP Provider realization owns the Guest proxy Process, its private +exact-Guest Endpoint, `vhci_hcd` attach/detach progress, and finalizer. Those +are Provider implementation children, not base Binding fields. The proxy uses +Guest-private loopback TCP 3240 and resolves the relay through a LaunchTicket +or encrypted import stream; it never opens the physical Device. Attach/detach +commands remain internal EffectPort operations. + +Only one Binding may hold the Service's shared Host-global backing claim. A +second Binding waits fairly and never causes a second busid bind, module load, +listener, or physical open. Binding deletion drains Provider-owned children, +detaches the Guest, releases the Service lease, and clears the finalizer. + +### ResourceExport/ResourceImport contract + +The owner Zone exports the provider-neutral Service: + +```yaml +type: ResourceExport +spec: + providerRef: Provider/device-usbip + resourceRef: usb.d2bus.org.UsbService/work-token + serviceType: usb.d2bus.org.UsbService + projectionSchemaFingerprint: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + factoryFingerprint: sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + operations: [usbip-control, usbip-data] + arbitration: exclusive + quota: + maxConsumers: 1 + fairness: fifo + leaseDeadlineMs: 15000 + consumerZonePolicy: + relation: child-zones + capabilityCeiling: [usbip-control, usbip-data] + visibility: child-zones +``` + +`resourceRef` MUST target an authority-mode `UsbService`. Import materializes a +same-qualified-type `usb.d2bus.org.UsbService` projection; it never substitutes +a Provider-qualified type. Admission rejects Device, Endpoint, `UsbBinding`, +projection Service, and internal session/transfer handles as export targets. +The Provider capability manifest advertises only `UsbService` as +`explicit-export`. + +Across a ZoneLink, USBIP control and data use bounded, credit-controlled, +cancellable ComponentSession named streams under enrolled Noise_KK. Transport +Endpoint, server/client, proxy, port, session, and frame details remain +Provider-internal. No fd, DeviceGrant, busid/sysfs path, address, socket path, +or credential crosses a Zone. Session, lease, transfer, and stream handles are +never ResourceTypes. Revocation cancels the stream, detaches the Guest, and +degrades the same-type local Service projection and Binding. + +--- + +## Controller Process resource + +The framework creates the controller state Volume **before** starting the +controller Process. This is done by core **ProviderDeployment**, not by the +semantic `device-usbip` controller. The `device-usbip` controller does not own +Volumes, does not add Volume to its exported ResourceTypes, and does not create +its own prerequisite. The controller Process spec carries a mandatory `mounts` +entry that references that Volume; the runtime rejects the Process launch if +the Volume is not Ready. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: device-usbip--controller + zone: work + ownerRef: Provider/device-usbip +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system # from Provider.spec.config.controllerExecutionRef + domain: system + processClass: controller + template: controller-main # plain ID from signed Provider component descriptor + sandbox: + namespaceClasses: [mount] + capabilityClasses: [] + seccompClass: strict + noNewPrivileges: true + startRoot: false + environmentClass: minimal + readOnlyRoot: true + budget: + cpu: + request: "100m" + limit: "1000m" + memory: + request: "32Mi" + limit: "128Mi" + pids: + limit: 128 + fds: + limit: 256 + networkUsage: null + readiness: + class: ready-condition + initialDelay: "0s" + timeout: "10s" + failureThreshold: 3 + restartPolicy: + class: on-failure + backoffBase: "2s" + backoffMax: "60s" + backoffMultiplier: 2.0 + maxRestarts: null + resetAfter: "300s" + adoptionPolicy: adopt-on-restart + drainTimeout: "10s" + mounts: + - volumeRef: Volume/device-usbip--controller--state--host-system + view: state + mountPath: /state + access: read-write + required: true +``` + +The `template: controller-main` field binds to the signed component descriptor +inside the Provider package closure. No caller-controlled executable path, +argv, UID/GID, host path, capability, or broker op escapes this schema. + +--- + +## Worker Process resources + +Only **real long-lived processes** become Process resources. Ephemeral +operations (kernel module load, device withhold, busid bind/unbind, lock +acquisition) are **semantic EffectPort steps** executed through the injected +`UsbipEffectPort`; they are NOT EphemeralProcess resources. + +The controller reconciles three closed worker classes: + +| Worker | Cardinality/owner | Endpoint | Network behavior | +| --- | --- | --- | --- | +| `usbip-host--backend` | exactly one per Host; Provider-owned Host authority | one host-local Endpoint with `visibility: provider` and a controller-only `consumerPolicy` | no externally reachable listener; all busids register with this backend | +| `usbip-net--relay` | exactly one per Network; Provider-owned relay authority | `Endpoint/-usbip-relay` | binds only that Network's uplink on TCP 3240 and multiplexes all admitted Services | +| `usbip-binding--proxy` | one per attached `UsbBinding`; Binding-owned | `Endpoint/-usbip-private` | runs for the exact Guest and exposes only Guest-private loopback TCP 3240 | + +Every Process uses `Provider/system-minijail`, a signed template +(`usbip-daemon`, `usbip-relay`, or `usbip-guest-proxy`), semantic sandbox +classes, bounded CPU/memory/pid/fd budgets, provider-defined readiness, +`adopt-on-restart`, and bounded drain. No Process spec contains argv, an +executable or socket path, a busid, an IP address, an interface name, or an fd. +The Host backend and per-Network relay are shared authorities, not per-Device +workers. The Binding proxy is the only per-consumer long-lived worker. + +The Network relay Endpoint has: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: work-net-usbip-relay + zone: work + ownerRef: Provider/device-usbip +spec: + providerRef: Provider/device-usbip + producerRef: Process/usbip-net-a09c3f218ee1-relay + endpointClass: service + transport: tcp + purpose: device-usbip.d2bus.org/relay + serviceFingerprint: device-usbip.d2bus.org/UsbipRelay.v4 + locality: cross-domain + visibility: zone + attachmentPolicy: launch-ticket-only + consumerPolicy: + allowedProviderComponents: + - device-usbip.d2bus.org/service-controller + - device-usbip.d2bus.org/binding-proxy + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer + authority: + authorityScope: host + authorityKey: usbip-network-relay/v1 + cardinality: exactly-one + arbitration: multiplexed + authorityRef: Endpoint/work-net-usbip-relay + duplicateConflict: usbip-network-relay-authority-conflict + ownerProof: endpoint-and-relay-process-identity + updateStrategy: drain-recycle + exportability: forbidden + quota: + maxServices: 16 + fairness: fifo +``` + +Core derives the relay key from the Network UID and signed standard-port policy; +neither the Provider nor an operator supplies the digest or port. This +`Endpoint` is the single D097 relay/listener/firewall authority for that Network. +Every admitted Service is a holder of this multiplexer, never a second listener +or firewall owner. + +The per-Binding private Endpoint has `ownerRef: +usb.d2bus.org.UsbBinding/`, names the Binding-owned Guest proxy as +`producerRef`, uses purpose `device-usbip.d2bus.org/guest-attachment`, is +`guest-local`, and permits only the exact `guestRef`. Neither Endpoint is an +export target. Producer restart increments `endpointGeneration`; dependent +Services or Bindings reconnect through fresh authorized LaunchTickets. + +## Endpoint resources (D092) + +`Provider/device-usbip` conforms to the standard `Endpoint` base schema. Stable +USBIP attach/relay identities that can be independently consumed are owned +`Endpoint` resources with `producerRef`; consumers use `Endpoint/`. +Endpoint spec/status never carries raw bind addresses, bus IDs, CIDs, ports, +paths, fds, or credentials. Resolution occurs only through an authorized +EffectPort/LaunchTicket; unauthorized resolution returns `endpoint-resolve-denied`. +Producer restart bumps `Endpoint.status.endpointGeneration`, causing consumers to +observe `dependency-changed` and reconnect through a fresh authorized ticket. +The relay Endpoint may be resolved only by the device-usbip Core adapter and an +exact Binding-owned proxy with an active Service lease. Network controllers, +generic Network readers, unrelated Services, and arbitrary same-Zone Processes +have no resolve permission. Binding proxies receive an already-authorized +connected stream through their LaunchTicket, so network-local needs no generic +TCP/3240 host or net-VM allow. + +## Retained opaque handles (D092 promotion test) + +- pidfds for backend/relay/Binding-proxy supervision stay process-local because they are + restart-time identity handles, not stable managed endpoint identities. +- LaunchTicket fd indexes and inherited listener fds stay opaque; they are + per-launch attachment slots resolved under authorization. +- Per-busid attach connection handles, `LeaseToken`, `FirewallToken`, and OFD + lock fds stay opaque because they are high-churn effect-port capabilities tied + to one operation or lease. +- `operationId` and committed-revision proofs stay opaque correlation/idempotency + handles in the core Operation ledger. +- `OwnedTransport`/ComponentSession transport handles stay in-memory transport + capabilities behind Endpoint resolution, not addressable resources. + +### Guest-side attachment effects + +Guest-local USBIP effects (`vhci_hcd` module load, `usbip attach`, `usbip +detach`) go through a **guest-side `UsbipGuestEffectPort`** adapter injected +into the controller when the Binding's `guestRef` supervisor is addressed. The +Guest supervisor (e.g., `Provider/runtime-cloud-hypervisor`) owns this +adapter and exposes it to the controller through the reconcile framework's +per-Guest effect channel. + +The controller calls `guest_effect.attach(binding_uid)` or +`guest_effect.detach(binding_uid)` semantically. The Guest supervisor +adapter privately: + +1. Locates the Guest-side `usbip` binary from the signed bundle. +2. Resolves the Binding-owned private Endpoint and issues the attach/detach + command via its privileged guest-control channel. +3. Returns a typed `UsbipGuestEffectError` to the controller. + +A one-shot guest-side command is neither a long-lived worker nor an +EphemeralProcess resource. The Binding-owned Guest proxy is a normal long-lived +Process; the attach/detach command itself is not a second Process resource. +The guest-side `vhci_hcd` kernel module is wired at Guest build time via +`nixos-modules/components/usbip.nix`. + +### What is NOT a Process resource + +The following operations are **not** EphemeralProcesses and not long-lived +Process resources. They are semantic steps executed through injected EffectPorts: + +- `modprobe usbip-host` — `EnsureKernelModule` host EffectPort step +- Device withhold (sysfs `authorized = 0`) — `WithholdDevice` host EffectPort step +- `usbip bind --busid ` — `BindBusid` host EffectPort step +- `usbip unbind --busid ` — `UnbindBusid` host EffectPort step +- Per-busid OFD lock acquisition / release — `AcquireLease` / `ReleaseLease` host EffectPort steps +- nftables firewall carve-out — `apply_firewall` / + `UsbipBindFirewallRule { action: Ensure, ... }` and `release_firewall` / + `UsbipBindFirewallRule { action: Remove, ... }` +- Guest-side `usbip attach` / `usbip detach` — `UsbipGuestEffectPort` steps + +The host adapter and guest supervisor adapter execute these internally. +No EphemeralProcess resource is created for any of these operations. + +--- + +## UsbipEffectPort — injected semantic port + +The `UsbipEffectPort` trait is defined in `d2b-contracts` (or +`d2b-provider-toolkit`). The Provider controller receives an +`Arc` through its reconcile context at construction time. +The controller calls semantic methods with validated opaque IDs only. + +```rust +use std::fmt; + +/// Opaque validated identifiers that cross the EffectPort boundary. +/// Custom Debug impls redact bytes; `derive(Debug)` is intentionally absent +/// to prevent byte arrays from leaking into logs or error output. +pub struct DeviceUid([u8; 32]); +pub struct NetworkUid([u8; 32]); +pub struct UsbBindingUid([u8; 32]); +/// Zeroized on drop; Debug is redacted. +pub struct LeaseToken([u8; 32]); +pub struct FirewallToken([u8; 16]); +pub struct FirewallGenerationFence { + pub expected_network_generation: u64, + pub expected_service_generation: u64, +} +pub struct FirewallObservation { + pub matches_expected: bool, + pub digest: [u8; 32], // opaque ownership-scoped digest + pub observed_generation: u64, +} + +impl fmt::Debug for DeviceUid { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "DeviceUid()") } } +impl fmt::Debug for NetworkUid { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "NetworkUid()") } } +impl fmt::Debug for UsbBindingUid{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "UsbBindingUid()") } } +impl fmt::Debug for LeaseToken { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "LeaseToken()") } } +impl fmt::Debug for FirewallToken{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "FirewallToken()") } } +impl fmt::Debug for FirewallGenerationFence { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "FirewallGenerationFence()") } } + +// Opaque UID/token Clone impls are manual (not derived) to avoid +// clippy::expl_impl_clone_on_copy on the non-Copy structs; this also prevents +// accidental derive(Debug) via derive macro chains. + +/// Kernel module class (non-exhaustive; additions are non-breaking). +#[non_exhaustive] +#[derive(Debug, Clone)] +pub enum KernelModuleClass { + UsbipHost, +} + +/// Device probe result. Fields are opaque: `label` is a logical label digest, +/// not a raw busid; `present`/`anti_spoof` are boolean outcomes only. +#[derive(Debug, Clone)] +pub struct DeviceProbeResult { + pub present: bool, + pub anti_spoof: bool, + pub label: String, // opaque logical label; never raw busid or path +} + +/// Closed-set error enum. No String payload that could carry raw paths, busids, +/// or broker wire details. Retriability is inferred from the variant. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UsbipEffectError { + KernelModuleLoadFailed, + DeviceNotPresent, + AntiSpoofFailed, + LeaseDenied, + LeaseNotHeld, + FirewallDenied, + FirewallGenerationMismatch, + FirewallForeignConflict, + WrongZone, + BackendNotReady, + BindFailed, + UnbindFailed, + WithholdFailed, + ReleaseWithholdFailed, + GuestEffectUnavailable, + /// Transient adapter-internal error. The bounded detail string is + /// redacted in Display/Debug to prevent log leakage; use the + /// structured span attribute `error_class` for observability. + Transient(TransientDetail), +} + +/// Bounded redacted detail for transient errors. Stored in the adapter's +/// internal log only; redacted in all Debug/Display impls so it cannot leak +/// into spans, error messages, or audit fields. +#[derive(Clone, PartialEq, Eq)] +pub struct TransientDetail(Box); // bounded; no raw paths +impl fmt::Debug for TransientDetail { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "") } } +impl fmt::Display for TransientDetail { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "") } } + +#[async_trait] +pub trait UsbipEffectPort: Send + Sync { + /// Ensure `usbip-host` kernel module is loaded. + async fn ensure_kernel_module( + &self, + device_uid: &DeviceUid, + module_class: KernelModuleClass, + ) -> Result<(), UsbipEffectError>; + + /// Probe physical device presence and return presence/label/anti-spoof status. + /// The adapter reads sysfs using its own bundle-validated busid; the controller + /// never receives the raw busid. + async fn probe_device( + &self, + device_uid: &DeviceUid, + ) -> Result; + + /// Acquire an exclusive per-device lease. Returns an opaque token. + /// The adapter acquires the OFD lock internally; the lock fd or path + /// never leaves the adapter. + async fn acquire_lease( + &self, + device_uid: &DeviceUid, + ) -> Result; + + /// Release a previously acquired lease. + async fn release_lease( + &self, + device_uid: &DeviceUid, + token: LeaseToken, + ) -> Result<(), UsbipEffectError>; + + /// Withhold the physical device from OS auto-claim (sysfs authorized=0). + /// Requires a valid lease token. + async fn withhold_device( + &self, + device_uid: &DeviceUid, + lease_token: &LeaseToken, + ) -> Result<(), UsbipEffectError>; + + /// Re-allow OS auto-claim (sysfs authorized=1). + async fn release_device_withhold( + &self, + device_uid: &DeviceUid, + lease_token: &LeaseToken, + ) -> Result<(), UsbipEffectError>; + + /// Acquire a reference to the Network relay's nftables carve-out through + /// UsbipBindFirewallRule with action Ensure. + /// The adapter validates that device_uid and network_uid are in the same Zone + /// before dispatching UsbipBindFirewallRule through the privileged broker. + /// Wrong-Zone returns WrongZone immediately without any host mutation. + async fn apply_firewall( + &self, + device_uid: &DeviceUid, + network_uid: &NetworkUid, + lease_token: &LeaseToken, + expected: &FirewallGenerationFence, + ) -> Result; + + /// Observe the exact per-Network/per-busid USBIP ownership projection. + /// Network-local rules are excluded. The digest is opaque and may be + /// written only to the owning Service's strict provider status. + async fn observe_firewall( + &self, + device_uid: &DeviceUid, + network_uid: &NetworkUid, + firewall_token: &FirewallToken, + ) -> Result; + + /// Remove the exact nftables carve-out through the same + /// UsbipBindFirewallRule request with action Remove. Validated absence is + /// success. The caller retains status/authority until this returns success. + async fn release_firewall( + &self, + device_uid: &DeviceUid, + network_uid: &NetworkUid, + firewall_token: &FirewallToken, + expected: &FirewallGenerationFence, + ) -> Result<(), UsbipEffectError>; + + /// Bind the physical device to usbip-host (kernel-level). + /// Requires the shared Network firewall/relay and Host backend Ready. + async fn bind_busid( + &self, + device_uid: &DeviceUid, + lease_token: &LeaseToken, + ) -> Result<(), UsbipEffectError>; + + /// Unbind the physical device from usbip-host. + async fn unbind_busid( + &self, + device_uid: &DeviceUid, + lease_token: &LeaseToken, + ) -> Result<(), UsbipEffectError>; +} + +/// Guest-side effect port injected by the Guest supervisor when a UsbBinding +/// is associated with a specific Guest. The controller calls semantic +/// methods; the adapter issues commands through the Guest supervisor's +/// privileged guest-control channel. +#[async_trait] +pub trait UsbipGuestEffectPort: Send + Sync { + /// Resolve the private Endpoint and trigger `usbip attach` in the Binding's Guest. + async fn attach(&self, binding_uid: &UsbBindingUid) -> Result<(), UsbipEffectError>; + /// Trigger `usbip detach` inside the Binding's Guest. + async fn detach(&self, binding_uid: &UsbBindingUid) -> Result<(), UsbipEffectError>; +} +``` + +The framework-internal broker DTO is one closed request, not separate apply and +release operations: + +```rust +#[derive(Serialize, Deserialize)] +pub enum UsbipBindFirewallRuleAction { + Ensure, + Remove, +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct UsbipBindFirewallRuleRequest { + pub action: UsbipBindFirewallRuleAction, + pub scope: OpaqueUsbipFirewallScope, + pub expected_network_generation: u64, + pub expected_service_generation: u64, +} +``` + +The closed action values are exactly `Ensure|Remove`; unknown values fail +decoding. Both actions bind the expected Network and authority Service +generations. `apply_firewall` maps only to `Ensure`; `release_firewall` maps only +to `Remove`. `OpaqueUsbipFirewallScope` is resolved by Core from the validated +Device/Network identities and carries the exact per-Network/per-busid ownership +projection without exposing its bytes to the Provider. The request accepts no +Provider-supplied IfName, address, path, busid, ownership marker, or rule body. +There is no release variant or second broker op. + +### Adapter responsibilities (framework-internal, NOT in Provider crate) + +The core adapter that implements `UsbipEffectPort` is owned by the framework +(not the Provider crate). It: + +1. Looks up the raw busid from the Device UID through the trusted signed bundle + (never from the Device spec payload; the spec contains only logical labels). +2. Validates that the Device and Network UIDs belong to the same Zone + (`device_uid.zone() == network_uid.zone()`); returns `WrongZone` immediately + if they differ. +3. Acquires / releases the per-busid OFD lock at + `/run/d2b/locks/usbip/` — adapter-internal; no controller visibility. +4. Dispatches the existing closed `UsbipBindFirewallRule` request through the + privileged broker (the sole privileged executor), using action `Ensure` for + apply and `Remove` for release, for the complete per-Network/per-busid + TCP/3240 exposure. No generic network-local allow is a prerequisite and no + separate release op exists. +5. Records the broker/core audit record **post-effect** (after the host mutation + completes); audit is not atomic with resource store commits. +6. Reads the same ownership projection for drift through `observe_firewall`; + Network-local ownership is excluded. +7. Returns typed `UsbipEffectError` variants to the controller; no broker wire + types, lock paths, audit structs, or nftables details leak. + +For `Remove`, the adapter supplies the opaque Network/busid ownership identity, +the token, and expected Network/Service generations. The broker validates the +generation fence and exact ownership marker before mutation. It removes only +that ownership projection; validated absence is idempotent success, while a +foreign marker fails closed. A transient error is retryable with the retained +token/fence. `FirewallGenerationMismatch` causes a fresh dependency/status read +before requeue and never deletes a newer realization. + +The audit record emitted by the adapter contains: zone, device name-digest, +network name-digest, operation tag, outcome, error_class, and correlation_id. +No raw busid, lock path, nftables body, or vendor/product ID appears in audit. + +--- + +## Bring-up sequence + +The authority Service preserves the safety ordering from +`packages/d2bd/src/usbip_state_machine.rs` while sharing Host and Network +singletons: + +``` +host-module/backend → physical-lock → withhold → network-firewall/relay + → bind-busid → service-ready +``` + +| Step | EffectPort call / resource action | Authority rule | +| --- | --- | --- | +| Host module/backend | `ensure_kernel_module` and adopt/create the Host backend Process | exactly one per Host; idempotently shared | +| Physical lock | `acquire_lease(device_uid)` | exclusive OFD claim for the Service's local Device | +| Withhold | `withhold_device(device_uid, token)` | requires the exclusive token | +| Network firewall/relay | reference-counted `apply_firewall` → `UsbipBindFirewallRule { action: Ensure, ... }`, `observe_firewall`, and adopt/create the Network relay Process/Endpoint authority | exactly one TCP 3240 multiplexer and firewall authority per Network; exact per-busid rules; expected Network/Service generations required | +| Bind busid | `bind_busid(device_uid, token)` | requires backend and relay Ready | +| Service Ready | commit Service authority status | no Guest is attached yet | + +An attached Binding then reconciles: + +| Step | Action | +| --- | --- | --- | +| Acquire Service slot | fair exclusive admission; imported Services also acquire a D096 lease | +| Create proxy/Endpoint | Binding-owned Guest proxy Process and private Endpoint become Ready | +| Attach Guest | `UsbipGuestEffectPort::attach(binding_uid)` targets the private Endpoint | +| Commit status | Binding becomes Ready with the exact observed Service/Endpoint generations | + +Binding teardown is Guest detach → private Endpoint/proxy deletion → Service slot +release. Service deletion first drains all Bindings and exports/imports, then +unbinds the busid, confirms `UsbipBindFirewallRule { action: Remove, ... }`, +then releases firewall status/token and relay authority references, releases withhold and +physical lock, and finally releases Host backend/module references. Shared Host +or Network authorities remain while another Service references them. + +During `Remove`, Service status remains `firewallState: releasing`; the +`FirewallToken`, expected Network/Service generation fence, and relay authority +reference remain held. Only confirmed broker success (including +ownership-validated already-absent success) permits the controller to clear +firewall status/token and decrement/release the relay authority. A transient +failure retries without releasing either authority; a generation mismatch +refreshes dependencies and requeues; a foreign marker blocks teardown. + +Each observe cycle calls `observe_firewall` for the Service's exact +Network/busid claim. A mismatch changes only the USB Service's strict provider +firewall state, degrades that Service, and queues device-usbip reconcile. +Network `FirewallReady` and its digest do not change. + +Every step is idempotent. Completed authority steps live in Service status; +completed attachment steps live in Binding status. Physical Device status never +stores a Guest attachment state. + +--- + +## Typed Device, Service, and Binding status + +Per D088, ResourceType-common Device observation lives in `status.resource`. +`UsbService` base status contains only generic whole-device availability and +access observations; `UsbBinding` base status contains only generic attachment +phase, queue position, and observed Service generation. A direct-local Provider +would populate the same base fields. USBIP module, backend, Network, Endpoint, +proxy, stream, server/client, port, firewall, and busid observations exist only +in strict `status.provider`. Every Provider extension is signed, bounded to +32 KiB, unknown-field-denied, and may not duplicate common status fields. + +```yaml +status: + phase: Ready + observedGeneration: 1 + conditions: + - type: BackingAuthorityReady + status: "True" + reason: physical-usb-backing-claimed + resource: + access: + mode: exclusive + available: true + activeBindings: 0 + backingAuthority: + authorityScope: physical-device + authorityClass: physical-usb-backing + claimed: true + provider: + providerRef: Provider/device-usbip + schemaId: "device-usbip.d2bus.org/UsbService/status" + schemaVersion: "1.0.0" + observedProviderGeneration: 1 + details: + mode: authority + authorityAvailable: true + completedSteps: [host-backend, physical-lock, withhold, network-relay, bind] + backendProcessRef: Process/usbip-host-b3a7f1d2c591-backend + relayEndpointRef: Endpoint/work-net-usbip-relay + hostModuleAuthority: ready + networkRelayAuthority: ready + firewallState: ready # ready|drifted|applying|releasing + firewallDigest: "" # exact USBIP ownership projection + observedFirewallGeneration: 1 + labelDigest: "a3f7..." +``` + +Projection Service `status.resource` uses the same availability/access shape +plus common import lease state and remote generation/fingerprint, but omits the +authority-only backing claim. `status.provider` reports only internal route +readiness while omitting physical and owner-Endpoint fields. Binding +`status.resource` +reports `attachmentPhase` +(`detached|waiting|attaching|attached|detaching|failed|unknown`), observed +Service generation, bounded queue position, last closed error, and +attach/detach timestamps. USBIP-only proxy subphases, relay generation, and +private Process/Endpoint refs are confined to Binding `status.provider`. No +status layer carries a busid, host path, fd, remote address, stream handle, +session identifier, or transfer bytes. + +`firewallState`, `firewallDigest`, and `observedFirewallGeneration` are owned +and written only by `Provider/device-usbip`. They never appear in Network status +or provider-neutral USB status. Drift sets `firewallState: drifted`, degrades +the affected authority Service with closed error class `firewall-drift`, and +requeues `apply_firewall`; no raw rule or identity enters status. + +A shared backing collision sets `BackingAuthorityReady=False`, reason +`physical-usb-backing-conflict`, and phase `Failed` before any effect. That +condition and the claim state are provider-neutral USB Service observations; +USBIP module/relay diagnostics remain only in `status.provider`. + +### Phase semantics + +| Phase | Meaning | +| --- | --- | +| `Pending` | Service authority/projection or Binding attachment is reconciling or fairly queued | +| `Ready` | Service is available, or Binding proxy/Endpoint and Guest attachment are Ready | +| `Degraded` | Previously Ready dependency, authority, import, stream, proxy, or attachment is unavailable | +| `Failed` | Terminal failure; manual intervention required | +| `Unknown` | Controller unreachable or crashed; last known status stale | + +**Currency and upgrade (D091).** The controller implements `assess_update`, +`plan_upgrade`, and `execute_upgrade` for Service authority/projection and +Binding attachment realizations and populates only universal `status.update` with +`state: Current|UpdateAvailable|UpgradeRequired|Upgrading|Blocked|Unknown`, +`reasons` from `CoreGenerationChanged`, `ProviderGenerationChanged`, +`ArtifactChanged`, `ImageOrSystemGenerationChanged`, `SpecChanged`, +`DependencyChanged`, or `SecurityPolicyChanged`, observed/target +generation/digest IDs, `disruption: None|Reload|Restart|Recycle|Replace`, +`preserveState`, optional `operationId`, `lastAssessedAt`, and +`owned`/`dependencies` refs. It honors base `spec.updatePolicy` (manual +disruptive default; auto non-disruptive), while the Core Operation ledger owns +upgrade operation, idempotency, and progress. Disruptive attach changes return +`UpgradeRequired` rather than applying in place; the planner recycles the attach +realization, drains/restarts dependent Processes and Guests, and preserves +Device and Service identity. A Service upgrade drains dependent Bindings before +recycling the authority; an imported source update propagates import → +projection Service → Binding. Non-disruptive changes reconcile normally. + +--- + +## Declared vs explicit attachment activation + +### `attachmentPolicy.activation: declared` + +The controller automatically acquires the Service slot, creates the +Binding-owned Guest proxy/private Endpoint, and invokes +`UsbipGuestEffectPort::attach` when `attachmentPolicy.desired: attached` and the +Service, Guest, and local Network are Ready. + +Claim source: `UsbipClaimSource::Declared { device_uid, network_uid }` — +tracked in the Binding's bounded status; never exposed to the Guest. + +### `attachmentPolicy.activation: explicit` + +The authority Service may become Ready, but the Binding pauses before Service +slot acquisition until an operator invokes `d2b device usb attach +`. `AttachBinding` then creates the Binding-owned proxy/private +Endpoint and calls `guest_effect.attach(binding_uid)`. The attach command is an +internal EffectPort operation; the proxy is the Binding-owned long-lived Process. + +Claim source: `UsbipClaimSource::Explicit` — tracked in +the Binding's bounded status. + +--- + +## Exclusivity and cardinality + +- `accessPolicy.mode: exclusive` and `maxActiveBindings: 1` are enforced by the + Service authority, including imported consumers. +- A pending second Binding waits in a bounded fair queue until the first releases + its lease; it never creates a second physical bind. +- The shared Host-global backing-authority index enforces one physical USB + owner across all USB transport and security-key Providers before an external + effect. The USBIP adapter separately enforces one busid owner. +- Nix preflights duplicate trusted selectors, but the shared Host-global + backing-authority index is authoritative across all Zones and API-created + resources. USBIP, direct-local USB, and security-key Providers must submit + the exact `(Host, physical-usb-backing, opaqueKeyDigest)` tuple derived by + Core under the `physical-usb-backing/v1` domain after trusted identity + resolution; no Provider-private label, class, or digest can satisfy or bypass + this invariant. `nixos-modules/assertions.nix` rejects a known + duplicate before activation: + ``` + assertion: uniqueHostGlobalUsbBackingAuthority allUsbAndSecurityKeyResources + message: "physical USB backing '' has multiple Host-global owners" + ``` + +--- + +## Async reconcile loop design + +The controller's async reconcile loop follows +`ADR-046-resource-reconciliation` steps 10–11: + +``` +┌─────────────────────────────────────────────────┐ +│ Dedicated watch receiver (async task) │ +│ continuously reads hints and dispatches: │ +│ hint → resource idle? → spawn per-resource │ +│ task (non-blocking) │ +└───────────────┬─────────────────────────────────┘ + │ per-resource tasks run concurrently + ▼ +┌─────────────────────────────────────────────────┐ +│ Per-resource reconcile task │ +│ 1. Read fresh Service/Binding + dependencies │ +│ 2. Compute desired step delta │ +│ 3. Execute EffectPort steps (await each) │ +│ 4. Create/delete child Process resources │ +│ 5. Commit ResourceMutationBatch + status │ +└─────────────────────────────────────────────────┘ +``` + +**The watch receiver loop MUST continue reading and dispatching during steps 3 +and 4.** Independent Services and Bindings run concurrently under bounded +semaphores; per-Service single-flight serializes authority/attachment +arbitration. The receiver never blocks waiting for an effect step. + +For `Create`, `UpdateSpec`, or `Delete` with `waitForReconcile` (D090), the +controller performs no external effect, finalizer mutation, or status mutation +until Core supplies +`CommittedRevisionProof {resourceUid,generation,revision,operationId}`. `Abort` +means no effect; a durable commit is never rolled back after a later reconcile +timeout. The response contains the committed object, one-pass projected layered +status, `disposition: Converged|Progressing|Blocked|UpgradeRequired|Failed`, +and `statusPersistence: pending|committed`; effect idempotency keys derive from +`(UID,generation,revision,operationId)` in the same per-resource single-flight +using a bounded priority lane. + +On restart, the controller: +1. Lists all authority/projection Services and Bindings it owns and reads their + referenced Device, Host, Network, Guest, import, and Endpoint dependencies. +2. Reads completed authority steps from Service status and attachment steps + from Binding status. +3. Skips completed steps idempotently. +4. Adopts exact Host backend, Network relay, and Binding proxy Processes using + their authority owner proofs (`adoptionPolicy: adopt-on-restart`). +5. Quarantines ambiguous authority, otherwise resumes from the earliest + non-completed step. + +--- + +## Network dependency handling + +The controller uses `ResourceClient` dependency watches (read-only) on the +Network identified by each authority Service's USBIP provider +`relayEndpointRef` and on each Binding's USBIP provider `networkRef`. Neither +reference is a base `UsbService`/`UsbBinding` field. The controller never +contacts the Network controller directly; it observes Network status changes +via the watch. + +The `ResourceClient` dependency watch is the **only** channel for Network +information. No broker connection for Network data; no route table access; +no direct NetworkManager/nftables query. + +Fields read from Network status by the controller (read-only, via ResourceClient watch): +- `status.phase` — gates Network relay and Binding proxy creation until Ready + +The adapter supplies the Network UID to Core's private fabric resolver when +constructing the shared relay or executing firewall effects. Core derives the +host uplink attachment and address from private Network realization state; no +raw field is read from public Network status, and the controller never reads or +holds a raw address or IfName. + +The Network controller does **not** own the USBIP firewall carve-out. +`D-NETWORK-002` in `ADR-046-resources-network` confirms: +`device-usbip` owns the firewall semantic authority. The `apply_firewall` +EffectPort step maps to `UsbipBindFirewallRule` action `Ensure`, and +`release_firewall` maps to action `Remove` on that same request. These are the +only paths that create or remove the nftables rule; no separate release op exists; +the Network controller must not create, remove, or reference USBIP firewall +rules. `observe_firewall` is the only USBIP drift reader, and strict USBIP +provider status is the only status authority. Network-local emits no generic +TCP/3240 host or net-VM allow and excludes device-usbip markers from its digest. + +--- + +## Finalizer lifecycle + +The controller adds `device-usbip.d2bus.org/service-finalizer` to an authority +Service after its first host effect and +`device-usbip.d2bus.org/binding-finalizer` to a Binding after acquiring its Service +slot. A projection Service's lifecycle is owned by its `ResourceImport`. No +finalizer is added before an effect or lease exists. + +Teardown on deletion request: +1. Controller detects `deletionTimestamp` set. +2. For a Binding, detaches the Guest, drains/deletes its private Endpoint/proxy, + and releases the Service/import lease. +3. For an authority Service, drains dependent Bindings and exports, unbinds its + busid, calls `release_firewall(device_uid, network_uid, firewall_token, + expected_fence)` which maps to `UsbipBindFirewallRule` action `Remove`, waits + for confirmed effect, and only then clears firewall status/token and releases + the relay authority reference, releases withhold and physical lease, and + finally releases the Host backend/module reference; shared authorities remain + until their last reference is gone. +4. Marks teardown progress in the deleting Service or Binding status; restart is + idempotent. +5. Clears the finalizer only after all teardown steps succeed. +6. Core commits the finalizer removal; the resource is garbage-collected. + +If a teardown step fails retryably, the controller sets `phase: Degraded` and +the typed Service/Binding `teardownBlocked: true`, emits a structured event, and +requeues under exponential backoff. A terminal foreign-marker conflict sets +`phase: Failed`, leaves the finalizer/status/authority in place, emits the +closed error class, and requires operator remediation rather than automatic +retry. + +`Remove` is idempotent only for ownership-validated absence. A transient broker +failure retains the token, fence, status, and authority and retries with bounded +backoff. `FirewallGenerationMismatch` refreshes Network/Service generations +before requeue; `FirewallForeignConflict` is terminal and never removes the +foreign rule or releases status/authority. + +--- + +## ProviderStateSet + +A **ProviderStateSet** is the optional, query-time set of the *declared* Volume +resources in a Zone whose `metadata.ownerRef` resolves to `Provider/device-usbip`. +It is a query-time grouping, not a ResourceType or stored artifact, and is empty +for a Provider that declares no state Volume: + +```text +ProviderStateSet(zone, "device-usbip") = + { v : Volume | v.metadata.zone == zone + && v.metadata.ownerRef == "Provider/device-usbip" } +``` + +`Provider/device-usbip` declares **no** Provider state Volume; its +`ProviderStateSet` is empty. The controller has no durable payload state beyond +Device/Service/Binding resource status, owned Process/Endpoint resources, and the +core Operation ledger. Bounded non-secret authority state lives in Service +status; per-Guest attach/detach state lives in `UsbBinding` status; common +Device presence/probe state stays in Device `status.resource` (D087/D088). +Sessions, transfers, leases, and stream handles remain internal and are not +persisted as resources. + +Because this Provider's operational state is fully derivable from spec, +`status`, the core Operation ledger, and independent external observation +(running attach Processes re-derived from cgroup leaves, fresh pidfds), it fails +the storage-need test and declares no state namespace, no state Volume, no +state-view mount, and no dedicated state-layout `User/` principal. There +is no empty identity-only Volume. + +The backend, relay, and Binding proxy workers likewise hold no state Volume; +stateless workers receive no dirfd. The stable identity of an adopted Process is +re-derived after restart from its declared cgroup leaf and a fresh pidfd, not +from any persisted snapshot. + +--- + +## Audit separation and OTEL telemetry + +### Audit (broker/core) + +The privileged broker emits an audit record **after** each +`UsbipBindFirewallRule` attempt reaches a completed success, failure, or denial +outcome. The core resource store emits its own audit record after each +`ResourceMutationBatch` commit. These two audit records are independent events; +the Provider controller does not own, submit, or claim ownership of either. + +**The Provider controller must never assert that its own status update is +atomic with a broker audit record.** They are separate commits on separate +subsystems. + +Each broker-emitted audit record contains: + +| Field | Value | +| --- | --- | +| `subject` | Provider Process identity digest | +| `zone` | Zone name | +| `op` | `UsbipBindFirewallRule` | +| `action` | `Ensure` or `Remove` | +| `resource_type` | `usb.d2bus.org.UsbService` | +| `resource_name_digest` | Stable hash of Service name; never raw busid or path | +| `expected_network_generation` | Generation fence supplied to the closed request | +| `expected_service_generation` | Authority Service generation fence supplied to the closed request | +| `outcome` | `success \| failure \| denied` | +| `error_class` | closed-set slug | +| `correlation_id` | operation/trace ID from reconcile context | +| `timestamp` | RFC 3339 UTC | + +No raw busid, lock path, nftables rule body, vendor/product ID, or network +interface name appears in any audit record field. + +`Remove` audit is emitted only after the broker has validated the generation +fence and ownership marker and completed the effect (including validated +already-absent success). A foreign marker emits +`action: Remove`, `outcome: denied`, and the closed +`firewall-foreign-conflict` error class without mutating the rule. There is no +release op name or release audit variant. + +### OTEL telemetry (Provider controller) + +The controller emits OTEL spans for its own reconcile operations: + +| Span name | Attributes | +| --- | --- | +| `device-usbip.service.reconcile` | `mode`, `phase`, `trigger_reason` | +| `device-usbip.binding.reconcile` | `phase`, `trigger_reason` | +| `device-usbip.effect.ensure_kernel_module` | `outcome`, `error_class` | +| `device-usbip.effect.acquire_lease` | `outcome`, `error_class` | +| `device-usbip.effect.withhold_device` | `outcome`, `error_class` | +| `device-usbip.effect.apply_firewall` | `outcome`, `error_class`, `zone_match: bool` | +| `device-usbip.effect.release_firewall` | `outcome`, `error_class` | +| `device-usbip.effect.bind_busid` | `outcome`, `error_class` | +| `device-usbip.effect.unbind_busid` | `outcome`, `error_class` | +| `device-usbip.process.backend_start` | `outcome`, `error_class` | +| `device-usbip.process.relay_start` | `outcome`, `error_class` | +| `device-usbip.process.binding_proxy_start` | `outcome`, `error_class` | + +Attributes must never carry raw busids, lock paths, nftables text, binary +paths, Endpoint addresses, import/export keys, session/transfer identifiers, +operator/user identifiers, Zone identity, or resource-name-derived identity. +Cardinality is bounded: `error_class`, `mode`, `phase`, `trigger_reason`, and +`outcome` are closed semantic enums. Zone identity is retained only as the +`d2b.zone` OTEL resource attribute and in the authorized audit record. + +--- + +## d2b-bus methods + +The controller registers these typed d2b-bus methods: + +| Method | Authority | Description | +| --- | --- | --- | +| `AttachBinding` | Admin | Trigger Service-slot acquisition, proxy creation, and Guest attach for an explicit `UsbBinding`; progress is reflected in Binding status. | +| `DetachBinding` | Admin | Detach the Guest and release only that Binding realization; returns immediately. | +| `ProbeDevice` | Admin | Re-run physical probe; update `status.provider.details.usbip.lastProbeResult`. Returns synchronously. | +| `GetServiceStatus` | Admin, StatusReader | Return the bounded authority/projection status. | +| `GetBindingStatus` | Admin, StatusReader | Return bounded attachment status. | +| `ListServices` | Admin, StatusReader | Return local Service refs and logical name digests only. | + +No bus method returns or accepts a raw busid, lock path, or broker wire type. +Sessions, transfers, stream handles, and import leases remain internal and have +no resource or general bus API. + +--- + +## RBAC + +| Role | ResourceType | Verbs | Scope | Granted to | +| --- | --- | --- | --- | --- | +| `device-reader` | Device | `get`, `list`, `watch`, `update/status` | Zone-scoped referenced physical Devices | device-usbip controller Process identity | +| `usbip-service-manager` | `usb.d2bus.org.UsbService` implemented by `Provider/device-usbip` | `get`, `list`, `watch`, `update/status`, `patch/finalizers` | Zone-scoped owned Services/projections | controller identity | +| `usbip-binding-manager` | `usb.d2bus.org.UsbBinding` implemented by `Provider/device-usbip` | all lifecycle/status/finalizer verbs | Zone-scoped owned Bindings | controller identity | +| `process-endpoint-owner` | Process, Endpoint | lifecycle verbs | exact Provider/Service/Binding-owned component identities | controller identity | +| `dependency-reader` | Host, Guest, Network, ResourceExport, ResourceImport | `get`, `list`, `watch` | exact same-Zone referenced resources | controller identity | + +There is no direct broker channel in the RBAC table. The controller communicates +with the broker exclusively through the injected `UsbipEffectPort`; the +framework adapter holds the broker connection. The controller Process identity +does not have a direct broker credential. + +No wildcard `*` over all Device resources. No cross-Zone Device access or +ResourceRef exists; the export/import adapter receives only signed D096 +capabilities. +No role grants the controller access to raw nftables operations, lock files, +or other Device providers' resources. + +--- + +## Security model + +### Provider boundary + +The `d2b-provider-device-usbip` crate has no compile-time or runtime access to: + +- the privileged broker socket or any broker op DTOs; +- the OFD lock file at `/run/d2b/locks/usbip/`; +- raw busid strings (the adapter derives them from the signed bundle); +- nftables rule bodies or ownership-marker strings; +- the Network bridge interface name or host-side route table. + +These are all adapter-internal. The Provider's security boundary is the +`UsbipEffectPort` interface; the adapter is the trust boundary between the +unprivileged Provider crate and the privileged broker. + +### Same-Zone gating (wrong-Zone denial) + +The adapter's `apply_firewall` implementation checks: + +``` +assert device_uid.zone() == network_uid.zone() +``` + +If they differ, `apply_firewall` returns `UsbipEffectError::WrongZone` +immediately with no host mutation. No nftables rule is written, no firewall +token is issued, and the controller transitions the authority Service to `Degraded` with +`error_class: wrong-zone`. + +This invariant ensures a Device bound in Zone A cannot be reached by a +different local Zone without D096 export/import: +- The shared relay in Zone A binds only to the Zone A Network uplink IP. +- The firewall carve-out allows only the Zone A bridge. +- A wrong-Zone firewall request is rejected before any effect. +- Cross-Zone consumers terminate only the encrypted import stream at their + local projection/Binding proxy; they receive no direct firewall opening. + +The wrong-Zone case in `tests/host-integration/usbip-service.nix` MUST pass in +the required host-integration gate. + +### Anti-spoofing + +The adapter's `probe_device` implementation validates the physical device's +vendor/product/serial against the signed bundle's expected values. An +anti-spoofing mismatch returns `UsbipEffectError::AntiSpoofFailed` and the +Device transitions to `Failed` with `status.provider.details.usbip.lastProbeResult.antiSpoofFailed: true`. + +The controller never uses the spec-level `vendorId`/`productId` fields for +anti-spoofing decisions; those are configuration-level filters only. The +authoritative values come from the signed bundle. + +### Firewall ownership + +`Provider/device-usbip` is the **semantic owner** of the USBIP nftables +carve-out. Only `apply_firewall` mapped to +`UsbipBindFirewallRule { action: Ensure, ... }` and `release_firewall` mapped +to `UsbipBindFirewallRule { action: Remove, ... }` may create or remove USBIP +TCP/3240 exposure; there is no separate release op. `observe_firewall` is its only +drift reader. Network-local emits no host or net-VM USBIP allow and owns no +USBIP status. The ownership marker used in the nftables comment is constructed +and verified by the adapter from opaque Network and busid identity, not the +controller; no raw identity is embedded in the marker or surfaced in status. + +The adapter enforces that: +- `apply_firewall` is reference-counted and idempotent for the same Network + relay authority; multiple Services reuse one TCP 3240 listener and its + Network-scoped base hook while retaining separate per-busid authorization + entries; +- each Service still receives exact per-busid authorization under that + per-Network relay; releasing one busid cannot retain or remove another's + exposure; +- the closed `UsbipBindFirewallRule` operation installs every required + host-ingress/coexistence hook atomically for action `Ensure`, and action + `Remove` removes only the exact ownership projection, so no generic uplink + allow can bypass the per-busid decision; +- `observe_firewall` hashes only the exact device-usbip ownership projection + and writes drift solely to the owning Service's strict provider status; +- `Ensure` and `Remove` are the only accepted action values; decoding any + other value fails closed; +- both actions are bound to expected Network and authority Service + generations; stale values return + `UsbipEffectError::FirewallGenerationMismatch` with no mutation; +- `Remove` is idempotent when the exact owned rule is already absent and the + trusted ownership state has no foreign replacement; +- a `release_firewall` with a token that does not match the installed rule + returns `UsbipEffectError::FirewallDenied`; +- a foreign rule with the same ownership marker prefix causes the adapter to + return `UsbipEffectError::FirewallForeignConflict` and emit a + `foreign-rule-conflict` audit event without mutation; +- the controller releases firewall status/token and relay authority only after + confirmed `Remove`; every failure retains them for retry or operator action. + +### Relay and private proxy bind addresses + +The shared relay binds to the per-Zone Network uplink IP only (not `0.0.0.0`). +The Binding-owned Guest proxy binds only Guest loopback. The +adapter derives this address through Core's private Network fabric resolver; +the controller never reads or passes an IP address. Endpoint resources declare +only stable semantic fields; the adapter resolves actual bind addresses +privately when Processes start. An exact Binding-owned proxy with an active +Service lease receives the connected relay stream through a LaunchTicket; +generic Network consumers cannot resolve the Endpoint. + +--- + +## Nix configuration + +### Operator declaration (Zone resource) + +```nix +d2b.zones.work.resources.corp-vm-usb = { + type = "Device"; + spec = { + providerRef = "Provider/device-usbip"; + deviceClass = "physical"; + arbitration = "exclusive"; + maxConcurrentClaims = 1; + inventory.selector = { + busClass = "usb"; + label = "yubikey-work"; + vendorId = "1050"; + productId = "0407"; + }; + provider = { + schemaId = "device-usbip.d2bus.org/Device/spec"; + schemaVersion = "1.0.0"; + settings = {}; + }; + }; +}; + +d2b.zones.work.resources.work-token = { + type = "usb.d2bus.org.UsbService"; + spec = { + providerRef = "Provider/device-usbip"; + mode = "authority"; + backingDeviceRef = "Device/corp-vm-usb"; + accessPolicy = { + mode = "exclusive"; + maxActiveBindings = 1; + queue = { + discipline = "fair"; + maxDepth = 16; + acquireDeadline = "15s"; + }; + }; + backingAuthority = { + authorityScope = "physical-device"; + authorityClass = "physical-usb-backing"; + keyDerivation = "core-trusted-usb-v1"; + cardinality = "zero-or-one"; + arbitration = "exclusive"; + authorityRef = "usb.d2bus.org.UsbService/work-token"; + duplicateConflict = "physical-usb-backing-conflict"; + exportability = "explicit-export"; + }; + provider = { + schemaId = "device-usbip.d2bus.org/UsbService/spec"; + schemaVersion = "1.0.0"; + settings.relayEndpointRef = "Endpoint/work-net-usbip-relay"; + }; + }; +}; + +d2b.zones.work.resources.corp-vm-work-token = { + type = "usb.d2bus.org.UsbBinding"; + metadata.ownerRef = "Guest/corp-vm"; + spec = { + providerRef = "Provider/device-usbip"; + serviceRef = "usb.d2bus.org.UsbService/work-token"; + guestRef = "Guest/corp-vm"; + accessPolicy = { + mode = "exclusive"; + priority = 100; + }; + attachmentPolicy = { + desired = "attached"; + activation = "declared"; + }; + provider = { + schemaId = "device-usbip.d2bus.org/UsbBinding/spec"; + schemaVersion = "1.0.0"; + settings.networkRef = "Network/work-net"; + }; + }; +}; +``` + +### Provider installation + +```nix +d2b.zones.work.providers.device-usbip = { + artifactId = "provider-device-usbip"; + config = { + controllerExecutionRef = "Host/host-system"; + }; +}; +``` + +### Cross-Zone authoring + +```nix +# Owner Zone. resourceRef is always UsbService, never Device/Endpoint/Binding. +d2b.zones.work.resources.work-token-export = { + type = "ResourceExport"; + spec = { + providerRef = "Provider/device-usbip"; + resourceRef = "usb.d2bus.org.UsbService/work-token"; + serviceType = "usb.d2bus.org.UsbService"; + projectionSchemaFingerprint = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + factoryFingerprint = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + operations = [ "usbip-control" "usbip-data" ]; + arbitration = "exclusive"; + quota = { + maxConsumers = 1; + fairness = "fifo"; + leaseDeadlineMs = 15000; + }; + consumerZonePolicy = { + relation = "child-zones"; + capabilityCeiling = [ "usbip-control" "usbip-data" ]; + }; + visibility = "child-zones"; + }; +}; + +d2b.zones.dev.resources.work-token-import = { + type = "ResourceImport"; + spec = { + providerRef = "Provider/device-usbip"; + zoneLinkRef = "ZoneLink/work"; + exportKey = "work-token"; + expectedServiceType = "usb.d2bus.org.UsbService"; + expectedProjectionSchemaFingerprint = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + expectedFactoryFingerprint = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + projectionName = "work-token"; + requestedCapabilities = [ "usbip-control" "usbip-data" ]; + requestedQuota = { maxConsumers = 1; }; + disconnectPolicy = { mode = "degrade"; }; + }; +}; + +# Core creates the same-type UsbService/work-token with +# ownerRef=ResourceImport/work-token-import. The operator authors a dev-Zone +# UsbBinding with base Guest/service policy and USBIP network settings; the +# import controller never creates that Binding. +``` + +### Guest-side module wiring (unchanged from v3 baseline) + +`nixos-modules/components/usbip.nix` remains under the guest's +`runtime-cloud-hypervisor` Nix module. It wires: +- `vhci_hcd` kernel module +- `usbip` CLI tools (from the Provider package closure via the guest bundle) +- `d2b.guestControl.usbipPath` for guest-side `usbip attach` + +The old `d2b.vms..usbip.yubikey = true` option is removed at the v3 reset +boundary. The new expression is Device + authority/projection Service + +per-Guest Binding, with explicit ResourceExport/ResourceImport only when crossing +a Zone. + +### Eval-time assertions + +```nix +# Mutual exclusion: every USB transport and security-key Provider uses the +# same Host-global backing-authority key derived from the physical selector. +assert uniqueHostGlobalUsbBackingAuthority allUsbAndSecurityKeyResources; + +# controllerExecutionRef must resolve to a Host in the same zone +assert isValidHostRef cfg.providers.device-usbip.config.controllerExecutionRef; + +# Base Service Device and Binding Service/Guest refs resolve in-Zone. +assert sameZoneRefs usbService; +assert sameZoneRefs usbBinding; +# USBIP Endpoint/Network refs are validated only in strict provider settings. +assert sameZoneUsbipProviderRefs usbService usbBinding; + +# Only an authority UsbService is exportable. +assert export.resourceRef.type == "usb.d2bus.org.UsbService"; +assert resolve(export.resourceRef).spec.mode == "authority"; +``` + +--- + +## Errors + +| Error class | Phase | Retryable | Notes | +| --- | --- | --- | --- | +| `unsupported-bus-class` | `Failed` | no | Only `usb` accepted for `busClass` | +| `invalid-vendor-id` | `Failed` | no | `vendorId` not exactly 4 ASCII hex digits | +| `invalid-product-id` | `Failed` | no | `productId` not exactly 4 ASCII hex digits | +| `invalid-selector-label` | `Failed` | no | Label violates ResourceName grammar | +| `network-ref-not-found` | `Degraded` | yes | USBIP provider `networkRef` or relay Endpoint Network does not resolve | +| `network-not-ready` | `Pending` | yes | Network dependency not yet Ready | +| `wrong-zone` | `Degraded` | no | A base or USBIP provider-extension reference resolves outside its Zone | +| `kernel-module-load-failed` | `Degraded` | yes | `ensure_kernel_module` returned error | +| `device-not-present` | `Degraded` | yes | Physical device absent from sysfs | +| `anti-spoof-failed` | `Failed` | no | Vendor/product/serial mismatch | +| `lease-denied` | `Degraded` | yes | `acquire_lease` failed; adapter contention | +| `withhold-failed` | `Degraded` | yes | sysfs write failed | +| `firewall-denied` | `Degraded` | yes | Adapter rejected `Ensure` or token-bound `Remove` | +| `firewall-generation-mismatch` | `Degraded` | yes, after refresh | Expected Network or authority Service generation is stale; no mutation; refresh dependencies before requeue | +| `firewall-foreign-conflict` | `Failed` | no | Foreign ownership marker at expected position | +| `firewall-drift` | `Degraded` | yes | Device-usbip ownership projection differs from expected Network/busid rules | +| `backend-start-failed` | `Degraded` | yes | Host backend authority failed to become Ready | +| `bind-failed` | `Degraded` | yes | `bind_busid` returned error | +| `relay-start-failed` | `Degraded` | yes | per-Network TCP 3240 relay failed to become Ready | +| `binding-proxy-start-failed` | `Degraded` | yes | Binding-owned Guest proxy/private Endpoint failed | +| `physical-usb-backing-conflict` | `Failed` | no | Another USB or security-key Service owns the Core-derived Host-global physical USB tuple; `BackingAuthorityReady=False` and no backing effect occurred | +| `usbip-host-module-authority-conflict` | `Failed` | no | A second Host module/backend authority was proposed | +| `usbip-network-relay-authority-conflict` | `Failed` | no | A second TCP 3240 listener was proposed for one Network | +| `invalid-export-target` | `Failed` | no | ResourceExport target is not an authority-mode `UsbService` | +| `import-revoked` | `Degraded` | yes | ResourceImport/ZoneLink lease was revoked | +| `stream-generation-mismatch` | `Degraded` | yes | Encrypted import stream generation/fingerprint is stale | +| `teardown-blocked` | `Degraded` | yes unless foreign-marker terminal | One or more teardown steps failed; firewall token/status/authority remain held until confirmed `Remove` | +| `claim-arbitration-conflict` | `Pending` | yes | Second Binding waits fairly for the exclusive Service slot | + +--- + +## Current-code baseline reuse + +| v3 baseline source | Reuse disposition | Notes | +| --- | --- | --- | +| `packages/d2b-contracts/src/usbip.rs` — `validate_bus_id`, `SYSFS_BUS_ID_MAX`, `UsbipClaimSource`, `sanitize_usb_hex_id` | Copy unchanged into `d2b-contracts`; reference from Provider crate | These are contracts, not broker internals; safe to reference | +| `packages/d2bd/src/usbip_state_machine.rs` — `CANONICAL_STEPS`, `UsbipBusidStep`, step ordering | Adapt step ordering into `src/reconcile.rs` EffectPort model; remove all broker-call sites | Step semantics and idempotency invariants preserved | +| `packages/d2bd/src/usbip_reconcile_state.rs` — desired/carrier/bind/proxy state enums | Split into typed Service authority and per-Guest Binding status | Restart-safe reconcile model preserved without putting attachment state on Device | +| `packages/d2b-host/src/usbip_argv.rs` — argv generators | Remain in `d2b-host`; called by the core adapter only | Provider crate has no compile dependency on `d2b-host` | +| `packages/d2b-priv-broker/src/ops/usbip_firewall.rs` — `bind_firewall_rule`, audit structs | Adapter-internal only; Provider crate never imports this | Preserve the one closed `UsbipBindFirewallRule` request and extend it with exact action enum `Ensure|Remove` plus generation fencing; no separate release op; audit structs remain broker-internal and `UsbipBindFirewallRuleAudit` never becomes visible to Provider | +| `packages/d2b-priv-broker/src/ops/usbip_host.rs` — `withhold_device` impl | Adapter-internal | Same as above | +| `packages/d2b-priv-broker/src/ops/usbip_lock.rs` — OFD lock | Adapter-internal | Lock fd never leaves adapter | +| `packages/d2b-contract-tests/tests/usbip_policy_network_scoping.rs` | Split into fast Provider `tests/wrong_zone.rs` admission coverage and real `tests/host-integration/usbip-service.nix` firewall coverage | Old duplicate retires only after both successors pass | +| `nixos-modules/components/usbip.nix` — guest vhci_hcd + tools | Unchanged; guest runtime module stays; host-side bits removed at v3 reset | Remains under runtime-cloud-hypervisor Guest module | +| `packages/d2bd/src/usbipd_perenv_autostart.rs` — per-env autostart | Delete; replace with one Host backend and one typed TCP 3240 relay Endpoint per Network | No per-env systemd unit and no per-Device port collision | + +--- + +## Work items + +### ADR046-usbip-001: `UsbipEffectPort` trait definition + +| Field | Value | +| --- | --- | +| Dependency/owner | d2b-contracts crate shape stabilised by shared root contract; d2b-contracts owner | +| Current source | None — net-new v3 work; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | packages/d2b-contracts/src/usbip_effect_port.rs | +| Detailed design | Define UsbipEffectPort and UsbipGuestEffectPort in d2b-contracts with DeviceUid, NetworkUid, UsbBindingUid, LeaseToken, FirewallToken, FirewallGenerationFence, FirewallObservation, KernelModuleClass, DeviceProbeResult, and UsbipEffectError; export traits/types only with no implementation. `apply_firewall` and `release_firewall` both accept expected Network/Service generations; release also accepts NetworkUid and borrows the token so the controller can retain it until confirmed effect. Keep firewall apply/observe/release Network/busid-scoped, attach/detach Binding-addressed, and all fd/path/busid values private. `TransientDetail` derives `Clone, PartialEq, Eq` while retaining manual redacted Debug/Display so `UsbipEffectError`'s derives compile without disclosure. Primary reuse disposition: `create`. Preserved source-plan detail: net-new trait definition. | +| Integration | Provider/device-usbip controller depends on this trait for injected semantic effects; the framework core adapter implements it in ADR046-usbip-002. | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | d2b-contracts tests for trait object safety, generation-fenced firewall apply/observe/release signatures (release requires NetworkUid and token), `FirewallGenerationMismatch`, `UsbipEffectError: Clone + PartialEq + Eq`, `TransientDetail` clone/equality, redacted Debug/Display behavior, and no implementation leakage. | +| Removal proof | None — net-new; no prior owner to remove | + +Define the `UsbipEffectPort` async trait with the method set in § UsbipEffectPort. +Define `DeviceUid`, `NetworkUid`, `UsbBindingUid`, `LeaseToken`, +`FirewallToken`, `FirewallGenerationFence`, `FirewallObservation`, +`KernelModuleClass`, `DeviceProbeResult`, and +`UsbipEffectError`. Export from `d2b-contracts`. No implementation +in `d2b-contracts`; trait only. Add conformance tests in `d2b-contracts/tests/usbip_effect_port.rs`. + +--- + +### ADR046-usbip-002: Core adapter implementation + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-usbip-001, ADR046-zone-control-024; UsbipBindFirewallRule broker op; d2b-host usbip argv support; framework core adapter owner | +| Current source | packages/d2bd/src/usbip_state_machine.rs, packages/d2bd/src/usbip_reconcile_state.rs, packages/d2b-host/src/usbip_argv.rs, packages/d2b-priv-broker/src/ops/usbip_firewall.rs, usbip_host.rs, and usbip_lock.rs | +| Reuse action | adapt | +| Destination | packages/d2b-core/src/device_usbip_adapter.rs | +| Detailed design | Implement UsbipEffectPort in the core adapter: signed-bundle busid lookup, same-Zone validation, trusted physical-USB identity resolution, mandatory Core-derived `physical-usb-backing/v1` digest and exact `(Host, physical-usb-backing, opaqueKeyDigest)` claim shared with every security-key/USB Provider, exclusive OFD claim, sole ownership of all USBIP TCP/3240 and exact per-Network/per-busid effects through the existing closed `UsbipBindFirewallRule` request with action enum `Ensure|Remove`, ownership-scoped observe/release, anti-spoof probe, one shared Host module/backend authority, one Core-derived D097 relay Endpoint/firewall authority per Network, D097 authority-index preflight/adoption, and post-effect audit. `Ensure` applies; `Remove` uses the same request, binds expected Network/Service generations, validates exact ownership, succeeds idempotently on validated absence, and fails closed on a foreign marker. No separate release op exists. Complete the shared claim before any open, withhold, bind, module, relay, firewall, or attachment effect; never expose raw busid, identity digest, path, fd, bind address, nftables body, audit structs, or broker wire types. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt into framework-internal adapter. | +| Integration | Reconcile framework injects the adapter into Provider/device-usbip; D097 authority index gates effects; adapter calls privileged broker and d2b-host argv helpers behind the semantic trait. | +| Data migration | Full d2b 3.0 reset; adapter resumes from Service/Binding status and authority owner proofs rather than daemon-coupled snapshots | +| Validation | Fast packages/d2b-core/tests/device_usbip_adapter.rs covers same-Zone gate, exact shared physical backing tuple derivation, byte-identical keys for USB/security-key views of one fake token, Provider-private-class/digest bypass rejection, `physical-usb-backing-conflict` before effects, separate USBIP module/relay authorities, one-module/one-relay reuse, exact closed `Ensure|Remove` decoding and same-request broker mapping, unknown-action rejection, expected Network/Service generation binding, exact Network/busid firewall scoping, validated-absence `Remove`, foreign-marker failure without mutation, ownership-scoped drift/status, transient retry with retained token/authority, independent per-busid release, no network-local dependency, anti-spoof, redaction, and no digest/busid/path/fd exposure. | +| Removal proof | Old daemon-coupled adapter call sites are removed by ADR046-usbip-009 after Provider wiring and adapter tests pass. | + +Implement the adapter: busid lookup from signed bundle, same-Zone check, OFD lock +management, broker dispatch for `UsbipBindFirewallRule` actions +`Ensure|Remove`, sysfs withhold, post-effect +audit emission. The adapter MUST NOT expose any raw busid, lock path, or broker wire +type to the trait caller. Add unit tests for same-Zone gate and anti-spoof logic in +`packages/d2b-core/tests/device_usbip_adapter.rs`. + +--- + +### ADR046-usbip-003: Provider crate skeleton + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-usbip-001, ADR046-provider-004; Provider model crate structure; device-usbip provider owner | +| Current source | None — net-new Provider crate; no pre-ADR45 baseline equivalent | +| Reuse action | create | +| Destination | packages/d2b-provider-device-usbip/ | +| Detailed design | Create the required crate layout; bind the shared D098 `UsbService`/`UsbBinding` base versions/fingerprints from ADR046-provider-004 and implement only strict USBIP Provider extensions; sign/register extension schemas and advertise explicit export only for authority `UsbService` resources implemented by this Provider; implement validation.rs and compile-checked EffectPort injection. Declare the controller user/User resource in Nix activation. Primary reuse disposition: `create`. Preserved source-plan detail: net-new crate skeleton with contract reuse. | +| Integration | Workspace manifests, Provider artifact catalog, Nix module, and ProviderDeployment consume the crate and component descriptor. | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | make test-policy passes; Cargo.toml has no d2b-priv-broker dependency; fast schema/manifest tests consume the common fixtures, accept canonical minimal base without `spec.provider`, prove a fake direct-local Provider can implement the same base, and cover Service-only exportability, Binding non-exportability, Core projection ownerRef/base fields with explicit `spec.provider` rejection, D088 status layering, semantic factory-fingerprint stability across Provider/adapter identity changes, strict refs, and trait injection. | +| Removal proof | None — net-new; no prior owner to remove | + +Create the crate with the layout in § Crate layout. Implement `lib.rs`, `validation.rs` +(bus-id corpus from `d2b-contracts::usbip`), and stub controller with compile-checked +`UsbipEffectPort` dependency injection. Validate workspace policy passes +(`make test-policy`). Confirm `d2b-priv-broker` does NOT appear in `Cargo.toml`. + +The Nix module declares the `d2b-device-usbip-ctrl` system user and its +`User/d2b-device-usbip-ctrl` resource at NixOS activation time. The +controller component state Volume (`device-usbip--controller--state--host-system`) +is created by core ProviderDeployment — not by the device-usbip controller — +before the controller Process is started. Volume is not an exported ResourceType +of this Provider. + +--- + +### ADR046-usbip-004: Service/Binding controller and D096 adapters + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-usbip-001, ADR046-usbip-002, ADR046-usbip-003, and ADR046-zone-control-024; device-usbip controller owner | +| Current source | packages/d2bd/src/usbip_state_machine.rs and packages/d2bd/src/usbip_reconcile_state.rs | +| Reuse action | adapt | +| Destination | packages/d2b-provider-device-usbip/src/{controller,reconcile,export_import}.rs | +| Detailed design | Reconcile provider-neutral authority/projection `UsbService` and per-Guest `UsbBinding` resources through strict USBIP Provider extensions, consuming UsbipEffectPort, the exact shared Host-global physical USB tuple, USBIP-private D097 authorities, and signed D096 ExportAdapter/ImportAdapter. Enforce same-Zone base and provider refs; ResourceExport authority-Service-only target; same-type ResourceImport-owned projection with `providerRef`, semantic base/import fields, no `spec.provider`, and no physical fields/effects; route selection from the signed local descriptor/import record; semantic factory fingerprint independent of separately authenticated adapter identity; exclusive fair Binding admission; encrypted bounded named-stream control/data; D088 layered status; Service/Binding finalizers; restart adoption; declared/explicit modes; no session/transfer resources. Firewall reconcile maps apply to `UsbipBindFirewallRule` action `Ensure`; finalization maps release to action `Remove` on the same request with expected Network/Service generations. Finalization retains firewall token/status and relay authority until confirmed effect, retries transient failure, refreshes on generation mismatch, and blocks fail-closed on a foreign marker. Primary reuse disposition: `adapt`. Preserved source-plan detail: extract and adapt step machine into Provider reconcile loop. | +| Integration | Controller watches Device/Host/Network/Guest/Endpoint/Export/Import dependencies, calls injected EffectPorts, commits ResourceMutationBatch updates, coordinates children from ADR046-usbip-005, and delegates only semantic export/import admission to the Provider adapter while core owns D096 routing/lifecycle. | +| Data migration | Full d2b 3.0 reset; no direct import of d2bd usbip_reconcile_state snapshots | +| Validation | Fast tests/controller_state_machine.rs, service_binding_schema.rs, export_import.rs, authority_conflict.rs, async_loop.rs, finalizer.rs, and wrong_zone.rs cover authority/projection/Binding lifecycle, Service-only export, projection `spec.provider` rejection, semantic fingerprint stability under adapter identity changes plus separate descriptor authentication, D088 `status.resource`/`status.provider` placement, shared physical tuple collision before effects, encrypted fake streams, no physical projection effect, exclusivity, restart, WrongZone degradation, exact `Ensure|Remove` mapping, and firewall status/token/authority retention until `Remove` confirmation. | +| Removal proof | packages/d2bd/src/usbip_state_machine.rs and usbip_reconcile_state.rs are deleted by ADR046-usbip-009 once Provider parity tests pass. | + +Implement the Service authority/projection and Binding attachment step machines. +Map legacy desired/carrier/bind/proxy observations to Service/Binding status, +never Device attachment status. Implement finalizers, restart step skipping, +non-blocking watch dispatch, fair exclusive Binding admission, declared/explicit +mode, and D096 export/import stream revocation. + +Tests required: +- `tests/controller_state_machine.rs`: full bring-up / teardown with a + `FakeUsbipEffectPort`, including `Ensure`/`Remove`, generation mismatch, + validated absence, transient retry, and foreign-marker failure +- `tests/async_loop.rs`: receiver dispatches Service/Binding B while A awaits an effect +- `tests/finalizer.rs`: finalizer add/clear through partial progress; no + firewall status/token or relay authority release before confirmed `Remove` +- `tests/wrong_zone.rs`: WrongZone error → Degraded phase + correct error class + +--- + +### ADR046-usbip-005: Process resource management + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-usbip-003; Process ResourceType schema; device-usbip process lifecycle owner | +| Current source | None — net-new Process resources; templates derive from the Provider package descriptor | +| Reuse action | adapt | +| Destination | packages/d2b-provider-device-usbip/src/reconcile.rs | +| Detailed design | Create/adopt exactly one Host backend Process authority, exactly one D097 Network relay Process/Endpoint/firewall authority bound to TCP 3240 with a Core-derived Network/policy key, and one Binding-owned Guest proxy/private Endpoint per attached Binding. Restrict relay resolution to the Core adapter and exact active-lease Binding proxies; deliver their connected streams by LaunchTicket. Use canonical system-minijail specs, signed templates, bounded budgets/readiness/restart, no argv/path/address/fd fields; attach/detach remains a one-shot EffectPort operation, not a second Process. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt into singleton Host backend, per-Network relay, and per-Binding Guest proxy management. | +| Integration | Service controller registers physical busids with shared backend/relay; Binding controller creates its Guest proxy/private Endpoint; Process controller launches workers; UsbipGuestEffectPort attaches to the private Endpoint. | +| Data migration | Full d2b 3.0 reset; old per-env runners become Host/Network authorities and per-Device port-3240 workers are forbidden | +| Validation | Fast Process/Endpoint shape tests prove one backend per Host, one Core-derived multiplexed TCP 3240 Endpoint/firewall authority per Network, deterministic duplicate conflict, adapter/exact-Binding-only resolution, LaunchTicket-connected stream, no generic Network reader access, Binding ownership/private Guest policy, no per-Device listener, no raw address/argv/path/fd, and readiness before bind/attach. | +| Removal proof | Old per-env usbipd autostart and ProcessRole::Usbip paths are removed by ADR046-usbip-009 after Process resource lifecycle tests pass. | + +Implement the three worker classes in § Worker Process resources using +`ResourceMutationBatch`. Confirm that two Services on one Host/Network reuse +the same backend and relay Endpoint, while two Guest Bindings receive distinct +private proxies/Endpoints. Attach/detach remains an EffectPort call. + +--- + +### ADR046-usbip-006: Typed status.provider.details + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-usbip-003; Device provider details plus `UsbService` and `UsbBinding` base/provider status schema owner | +| Current source | packages/d2bd/src/usbip_reconcile_state.rs state fields | +| Reuse action | adapt | +| Destination | packages/d2b-provider-device-usbip/src/status.rs | +| Detailed design | Define provider-neutral `UsbService` `status.resource` with whole-device availability/access counts, authority-only `physical-usb-backing` claim state, and projection-only common import state; define `UsbBinding.status.resource` with generic attachment phase/queue/generation/timestamps. Define separate strict `status.provider` USBIP details: Device probe, owner backend/relay, USBIP-owned `firewallState`/opaque digest/observed generation, imported-route observations, and Binding proxy/private Endpoint/subphase. During `UsbipBindFirewallRule` action `Remove`, strict status remains `firewallState: releasing`; the token and relay authority remain held until confirmed success, including validated absence. No common access/import/attachment field appears directly under `status`; no USBIP module, Network, Endpoint, proxy, server/client, port, firewall, or busid field enters base status; no raw backing digest, busid, path, fd, address, session/transfer ID, remote identity, or payload appears anywhere. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt state fields to typed status.provider.details. | +| Integration | Controller writes each extension atomically with its resource's common status; dependency/update propagation is Device/Export → Service/projection → Binding. | +| Data migration | Full d2b 3.0 reset; current d2bd reconcile state is not imported | +| Validation | Fast tests/status_serde.rs covers generic base status plus three strict USBIP detail schemas, exact `status.resource` placement for access/import/attachment/backing claim, exact `status.provider` placement for relay/firewall implementation observations, drift transition and ownership, `releasing` retention until `Remove` confirmation, rejection of semantic fields directly under `status`, mode-dependent omissions, bounded counts/refs, unknown-field denial, and a deny corpus proving USBIP-only fields are rejected from base and Network status. | +| Removal proof | Old d2bd USBIP reconcile-state structs are removed by ADR046-usbip-009 after status extension coverage passes. | + +Define generic `UsbServiceStatus`/`UsbBindingStatus` base projections and strict +`UsbipDeviceDetails`, `UsbipServiceDetails::{Authority,Projection}`, and +`UsbipBindingDetails`; keep the mode union strict and path-free. Tests: +`tests/status_serde.rs`. The `Usbip*Details` names are Provider-extension DTOs, +not ResourceTypes or compatibility aliases. + +--- + +### ADR046-usbip-007: Hermetic and real-system validation + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-usbip-004 and ADR046-usbip-005; device-usbip integration owner | +| Current source | packages/d2b-contract-tests/tests/usbip_policy_network_scoping.rs plus new integration scenarios | +| Reuse action | adapt | +| Destination | packages/d2b-provider-device-usbip/{src,tests,integration/README.md}; tests/host-integration/usbip-service.nix; tests/host-integration/hardware/usbip-service.sh | +| Detailed design | Put provider-neutral Service/Binding base-schema separation, strict USBIP extensions, projection `spec.provider` rejection, D088 layered status, semantic factory-fingerprint independence from Provider/adapter identity, exact shared physical backing tuple/conflict, one Core-derived per-Network relay Endpoint/firewall authority, exact per-busid firewall ownership/drift/release through one `UsbipBindFirewallRule` request with closed `Ensure|Remove`, generation fencing, idempotent validated-absence `Remove`, foreign-marker fail-closed behavior, and status/token/authority retention until confirmation, arbitration, same-type export/import, encrypted fake-stream, and least-privilege process/Endpoint shape coverage in fast Layer-1 Rust tests. Include a fake direct-local Provider proving the same base contract has no USBIP dependency. Reserve runNixOSTest for real Linux usbip_host/vhci_hcd, usbipd, namespaces/nftables, TCP 3240, zero network-local carve-out, and Guest checks; reserve the hardware script for an approved physical device. Use existing Make gates only. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt existing network-scoping assertion and add new scenarios. | +| Integration | Layer-2 lanes exercise actual kernel/backend/relay/Guest/device paths and do not duplicate pure controller/schema cases. Cross-Zone protocol logic remains hermetic with fake peers; the runNixOSTest only proves its real-system integration. | +| Data migration | None — docs/tooling only; no runtime state | +| Validation | `make test-host-integration` runs the non-hardware real-kernel case on a capable host; `make test-hardware` runs the explicit manual device case. No Layer-1 test opens a device, loads a module, creates a namespace, or listens on a socket. | +| Removal proof | Old usbip_policy_network_scoping coverage is retired only after the fast wrong-Zone admission test and `tests/host-integration/usbip-service.nix` successor both pass and the migration ledger is updated. | + +Required tests: + +| Path | Scenario | Gate | +| --- | --- | --- | +| `tests/host-integration/usbip-service.nix` | Real modules, one Host backend, one Core-derived multiplexed Network TCP 3240 Endpoint/firewall authority, exact per-busid rules, `UsbipBindFirewallRule` `Ensure|Remove` only, generation-bound ownership-scoped drift/status/release, idempotent validated-absence `Remove`, foreign-marker fail-closed, authority retained until effect confirmation, zero network-local carve-out, wrong-Zone denial, and least-privilege Guest Binding proxy/attach/revocation with fake USB backend | `make test-host-integration` | +| `tests/host-integration/hardware/usbip-service.sh` | Approved physical USB device, exclusive busid, second-Binding fairness, shared Host-global USB/security-key authority conflict, data/detach, no fd/path crossing | `make test-hardware`; manual only | + +`packages/d2b-provider-device-usbip/integration/README.md` must document: +- how to run each scenario locally through its existing integration/hardware gate; +- what container/Host/KVM privileges are required; +- which exact cases require a real approved USB device and are manual-only; +- how to add a new scenario; +- the wrong-zone scenario's required assertions. + +--- + +### ADR046-usbip-008: Nix and eval assertions + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-usbip-003, ADR046-zone-control-024; ADR-046-nix-configuration; Nix integrator | +| Current source | nixos-modules/components/usbip.nix guest wiring and new Zone resource declarations | +| Reuse action | adapt | +| Destination | nixos-modules/components/usbip.nix, nixos-modules/options-zones.nix, nixos-modules/assertions.nix | +| Detailed design | Add Provider config; remove the old per-VM option; emit provider-neutral authority `UsbService`, per-Guest `UsbBinding`, strict USBIP Provider envelopes, and optional ResourceExport/ResourceImport authoring shapes; imported same-type projection Services remain Core-created with no `spec.provider`. Assert same-Zone base/provider refs, projection ownerRef/forbidden implementation and physical fields, Service-only export target, one Host backend/Network relay, exact Core-derived Host-global `(Host, physical-usb-backing, opaqueKeyDigest)` exclusion shared by USB/security-key, Provider-private-class bypass rejection, one `UsbipBindFirewallRule` contract with exact actions `Ensure|Remove` and no separate release operation, and retain guest vhci_hcd/tools. Primary reuse disposition: `adapt`. Preserved source-plan detail: adapt guest module, remove host-side option surface, and extend eval assertions. | +| Integration | Nix compiler emits Device plus provider-neutral `UsbService`/`UsbBinding` and explicit D096 resources consumed by core/Provider; guest runtime supplies USBIP proxy/attach tools; generated base and provider-extension schemas/fingerprints remain canonical. | +| Data migration | Full d2b 3.0 reset; operators reauthor old per-VM options as Device + authority/projection Service + per-Guest Binding | +| Validation | Fast tests/unit/nix/cases/usbip-*.nix cover schema shape, Core projection `spec.provider` rejection, D088 layered status fixtures including USBIP-owned firewall state and `releasing` retention, all reference/owner/export assertions, byte-identical USB/security-key tuple collision and private-class bypass rejection, one Core-derived 3240 Endpoint/firewall authority per Network, exact `Ensure|Remove` broker action surface with unknown values rejected and no second op, least-privilege consumer policy, absence of network-local 3240 rules, old-option removal, and guest module retention. | +| Removal proof | d2b.vms..usbip.yubikey and host-side USBIP module paths are removed at reset once Zone resource emitter coverage passes. | + +- Add `d2b.zones..providers.device-usbip.config.controllerExecutionRef` option. +- Remove `d2b.vms..usbip.yubikey` at v3 reset; add deprecation warning until removal. +- Add Zone resource shapes for Device, authority `UsbService`, `UsbBinding`, and + optional ResourceExport/ResourceImport; projection Services are Core-owned + and never contain `spec.provider`. +- Add eval assertions for same-Zone base/provider refs, same-type Service-only + export, projection restrictions, one Host/Network USBIP authority, and shared + Host-global USB/security-key backing exclusion. +- Retain guest-side `nixos-modules/components/usbip.nix` (vhci_hcd + tools) unchanged under runtime-cloud-hypervisor. +- Add or update `tests/unit/nix/cases/usbip-*.nix` for each new assertion path. + +--- + +### ADR046-usbip-009: Removal of v3 daemon-coupled USBIP + +| Field | Value | +| --- | --- | +| Dependency/owner | ADR046-usbip-004 and ADR046-usbip-008; Provider fully wired and validated; daemon cleanup owner | +| Current source | packages/d2bd/src/usbipd_perenv_autostart.rs, packages/d2bd/src/usbip_state_machine.rs, packages/d2bd/src/usbip_reconcile_state.rs, nixos-modules/network.nix USBIP firewall block, and ProcessRole::Usbip in packages/d2b-core/src/processes.rs | +| Reuse action | delete-after-cutover | +| Destination | packages/d2bd/src/, nixos-modules/network.nix, packages/d2b-core/src/processes.rs | +| Detailed design | Remove daemon-coupled USBIP after Provider tests and integration tests pass: delete per-env autostart, state machine, and reconcile state modules after migration; remove USBIP firewall block from network.nix; remove ProcessRole::Usbip; run Layer-1 gates and confirm no d2bd or network.nix references remain outside the adapter and contracts. Primary reuse disposition: `delete-after-cutover`. Preserved source-plan detail: delete after Provider replacement reaches parity. | +| Integration | Provider/device-usbip, core D096/D097 adapters, Nix Device plus provider-neutral USB resource emitter, USBIP authority workers, and Binding-owned children are the sole USBIP lifecycle path after deletion. | +| Data migration | Full d2b 3.0 reset; no daemon-coupled USBIP runtime state import | +| Validation | make test-unit and make test-flake plus grep or contract checks for removed symbols and no residual d2bd/network.nix USBIP lifecycle references. | +| Removal proof | usbipd_perenv_autostart.rs, usbip_state_machine.rs, usbip_reconcile_state.rs, network.nix USBIP firewall block, and ProcessRole::Usbip are deleted after parity. | + +Deletion sequence: +1. Confirm Provider tests and integration tests pass. +2. Delete `packages/d2bd/src/usbipd_perenv_autostart.rs`. +3. Remove `packages/d2bd/src/usbip_state_machine.rs` and `usbip_reconcile_state.rs` + after verifying all logic has been migrated. +4. Remove USBIP firewall block from `nixos-modules/network.nix`. +5. Remove `ProcessRole::Usbip` from `packages/d2b-core/src/processes.rs`. +6. Run `make test-unit` and `make test-flake`; confirm no USBIP references remain in + d2bd or network.nix outside the adapter and contracts. + +--- + +## Tests + +### Fast hermetic execution and test placement (D094) + +Per D094 and `ADR-046-validation-and-delivery` §10.16, this Provider's `src/` +unit tests and `tests/*.rs` hermetic suite are fast, in-process, deterministic, +and parallel-safe: an individual normal test has p95 ≤50 ms with no wall-clock +sleep, and `cargo test -p d2b-provider-device-usbip --lib --tests` completes in +≤2 s warm-cache execution time (compilation excluded). They use a deterministic +fake clock/RNG and the toolkit fakes/FakeEffectPort only — no process spawn, +container, network, DBus, systemd, broker daemon, Nix eval/build, KVM, +USB/GPU/TPM hardware, or live cloud, and no filesystem tree beyond tiny temp +fixtures. Any scenario genuinely needing a booted kernel/Guest lives only in +`tests/host-integration`; physical-device coverage lives only in +`tests/host-integration/hardware`. Such a need is moved to the matching existing +Layer-2 gate, never given a sleep, larger unit-test timeout, or `#[ignore]`. +Bounded crypto/property tests are the only classified exception, each named +with a capped case count and declared higher per-test budget. + +### Required unit tests (`tests/`) + +| File | Coverage | +| --- | --- | +| `service_binding_schema.rs` | Provider-neutral authority/projection `UsbService` and `UsbBinding` base schemas plus strict USBIP Provider envelopes; canonical minimal base; same-Zone refs; imported projection ResourceImport ownerRef, `spec.provider` rejection, and physical-field denial; same-type Service-only explicit export; Device/Endpoint/Binding export denial | +| `provider_neutral_base.rs` | Base specs/status reject USBIP port, Network, Endpoint, proxy, server/client, firewall, and busid fields; a fake direct-local Provider implements the same base types without USBIP schemas or aliases | +| `controller_state_machine.rs` | Authority and Binding sequences with fake ports; shared Host backend/Network relay; Binding-owned proxy/private Endpoint; declared/explicit attach; restart and reverse teardown; apply maps to `Ensure`, release maps to `Remove`, and teardown retains firewall token/status/relay authority until confirmation | +| `authority_conflict.rs` | USBIP, fake direct-local USB, and security-key Providers resolve one fake token to a byte-identical Core-derived `(Host, physical-usb-backing, opaqueKeyDigest)` tuple; private-class/digest bypass fails; the second claim returns `physical-usb-backing-conflict` before effects; one Host USBIP module/backend; one Core-derived TCP 3240 relay Endpoint/firewall authority per Network; multiplex reuse and deterministic conflicts | +| `export_import.rs` | ResourceExport targets authority `usb.d2bus.org.UsbService` only; ResourceImport preserves that exact type and creates an ownerRef projection with no `spec.provider`; semantic factory fingerprint is stable under Provider/adapter identity changes while signed descriptor authentication remains exact; encrypted bounded fake control/data streams; generation/fingerprint/revocation; no fd/path/busid; sessions/transfers remain internal | +| `effect_port_contract.rs` | `UsbipEffectPort` and `UsbipGuestEffectPort` trait object safety; firewall apply/observe/release signatures callable from Provider crate; release requires NetworkUid, borrowed FirewallToken, and FirewallGenerationFence; `FirewallGenerationMismatch`; no import of broker types or `d2b-priv-broker`; compile-time `UsbipEffectError: Clone + PartialEq + Eq`; `TransientDetail` clone/equality and manual Debug/Display both produce `` | +| `firewall_ownership.rs` | Exact per-Network/per-busid intent; one closed `UsbipBindFirewallRule` request; exact `Ensure|Remove` decode with unknown-action rejection; apply/release same-request mapping; expected Network/Service generation binding; one relay listener/base hook with independent busid entries; idempotent validated-absence `Remove`; foreign marker fail-closed without mutation; ownership-scoped drift and strict Service provider status; transient retry retains token/status/authority; release one busid preserves another; Network digest/status unaffected; no network-local generic rule dependency | +| `relay_endpoint_authority.rs` | Exactly one D097 relay Endpoint authority per Network; Core-derived key; multiplexed Service holders; duplicate conflict before listen/firewall effect; adapter/exact-active-Binding-only resolution; LaunchTicket connected stream; generic Network readers denied | +| `conformance.rs` | Device/`UsbService`/`UsbBinding` ResourceTypeSchema round-trip, signed USBIP extension fingerprints, deny_unknown_fields, and Provider capability advertisement | +| `state_volume.rs` | Controller Volume schema conformance: `stateSchema: {}`, layout `ownerRef: User/` (not ComponentPrincipal), `sensitivityClass: private`, single `state` view; no cross-component Volume; dirfd delivery to controller only | +| `status_serde.rs` | Provider-neutral whole-device Service/Binding `status.resource` plus strict USBIP `status.provider` details including firewall state/digest/generation; reject access/import/attachment/backing fields directly under `status`, reject module/Network/Endpoint/proxy/server/client/port/firewall/busid fields from base and Network status, and deny raw identity/path/fd/address/session/transfer/payload fields everywhere | +| `validation_corpus.rs` | Bus-id max length (31 chars); metachar rejection; leading-zero segment rejection; vendor/product id exactly 4 hex digits; `busClass != usb` → `unsupported-bus-class` | +| `mutual_exclusion.rs` | USBIP, fake direct-local USB, and security-key claims for one Host-global backing key conflict; second Binding queues fairly and causes no second bind/open | +| `wrong_zone.rs` | Every base and USBIP provider ref is same-Zone; cross-Zone use requires D096 import; wrong-Zone firewall causes Service degradation and no effect | +| `finalizer.rs` | Service/Binding finalizers start only after effect/lease and clear only after child/lease teardown; Service calls generation-fenced `Remove`, retains firewall token/status/relay authority on transient, stale-generation, and foreign-marker outcomes, clears them only after confirmation, and restart resumes partial teardown | +| `async_loop.rs` | Independent Service/Binding dispatch remains concurrent while per-Service single-flight preserves arbitration | + +All rows above are Layer-1 Rust tests in the crate's `src/` or `tests/`; they +use fake EffectPorts/streams/authority indexes and perform no module load, +network bind, namespace operation, process spawn, or device open. + +### Integration-only real kernel/device checks + +| Path | Scenario | Gate | +| --- | --- | --- | +| `tests/host-integration/usbip-service.nix` | Real NixOS boot, `usbip_host`/`vhci_hcd`, one Network TCP 3240 relay Endpoint authority multiplexing two fake USB backends, exact per-busid rules, closed `Ensure|Remove` request, drift/status/reapply, generation-bound independent release, validated-absence idempotency, foreign-marker fail-closed, status/authority retention until confirmed `Remove`, zero network-local TCP/3240 rule, wrong-Zone denial, least-privilege Binding proxy/attach/revocation | `make test-host-integration`; capable host | +| `tests/host-integration/hardware/usbip-service.sh` | Approved physical USB device: shared Host-global backing and exclusive busid claims, attach/data/detach, second Binding fairness, USBIP/security-key conflict, and no fd/path crossing | `make test-hardware`; manual hardware only | + +`packages/d2b-provider-device-usbip/integration/README.md` indexes these Layer-2 +scenarios, their existing Make targets, required privileges/KVM/modules, and +the approved-device/manual safety preconditions. Real kernel/device behavior +must not be moved into unit tests, hidden behind `#[ignore]`, or simulated as a +claim of hardware coverage. + +--- + +## Removal sequence + +When `Provider/device-usbip` is fully deployed and every USBIP Device has an +authority Service plus per-Guest Bindings/projections as needed: + +1. Remove `d2b.vms..usbip.yubikey` Nix option (deprecated at reset; removed + now). +2. Delete `packages/d2bd/src/usbipd_perenv_autostart.rs`. +3. Delete `packages/d2bd/src/usbip_state_machine.rs` and + `usbip_reconcile_state.rs` (logic migrated to Provider crate and adapter). +4. Remove `ProcessRole::Usbip` from `packages/d2b-core/src/processes.rs`. +5. Remove USBIP firewall block from `nixos-modules/network.nix`. +6. Run `make check` (Layer-1 gate); confirm zero remaining references to removed + symbols from outside `d2b-provider-device-usbip` and `d2b-core/src/device_usbip_adapter.rs`. +7. Update this dossier's `Supersedes` field to mark all removed targets as `removed`. + +Per D094, each replaced current-code test is retired with an explicit +keep/adapt/move/delete disposition and a removal gate: the minimum reusable +semantic assertions migrate into this crate's hermetic `tests/`, and the old +duplicate tests, shell gates, fixtures, static artifacts, CI jobs, and manifest +entries are deleted once successor coverage and the removal proof pass — +updating `tests/layer1-jobs.json`, the closed gate manifests, the +flake/matrix/Nix-unit pins, the generated ledgers, and the CI workflow shards. +Old and new suites never run in parallel indefinitely. diff --git a/docs/specs/providers/ADR-046-provider-display-wayland.md b/docs/specs/providers/ADR-046-provider-display-wayland.md new file mode 100644 index 000000000..33ba79a04 --- /dev/null +++ b/docs/specs/providers/ADR-046-provider-display-wayland.md @@ -0,0 +1,1948 @@ +# ADR 0046 Provider dossier: `display-wayland` + +| Field | Value | +| --- | --- | +| Spec ID | `ADR-046-provider-display-wayland` | +| Crate | `packages/d2b-provider-display-wayland/` | +| Provider ResourceRef | `Provider/display-wayland` | +| Parent | ADR 0046 | +| Status | Proposed | +| Version | 2 | +| Baseline | `b5ddbed67867d9244bf33390868101bd9b053e49` | +| Normative | Yes | +| Owners | display-wayland crate owner, Wayland proxy binary, Nix integration | +| Depends on | `ADR-046-provider-model-and-packaging`, `ADR-046-components-processes-and-sandbox`, `ADR-046-componentsession-and-bus`, `ADR-046-resources-volume`, `ADR-046-resources-device`, `ADR-046-resources-host-guest-process-user`, `ADR-046-nix-configuration`, `ADR-046-telemetry-audit-and-support`, `ADR-046-resource-reconciliation`, `ADR-046-resource-api-and-authorization` | +| Supersedes | `ProcessRole::WaylandProxy` in `packages/d2b-core/src/processes.rs`; `LocalCrossDomainWaylandProvider` in `packages/d2b-host-providers/src/lib.rs`; `generate_wayland_proxy_argv` in `packages/d2b-host/src/wayland_proxy_argv.rs`; `nixos-modules/components/graphics.nix` `graphics.waylandProxy.*` options; `nixos-modules/ui-colors.nix` VM border color resolution; current `pkgs/wl-cross-domain-proxy` guest binary | + +--- + +## 1. Purpose and scope + +`Provider/display-wayland` is the d2b 3.0 Wayland display Provider. It owns +the complete Wayland cross-domain display pipeline for local VM Guests: the +jailed Host proxy process that mediates between the virtio-gpu/cross-domain +channel and the host compositor, and the in-guest frontend process that drives +the guest-side virtio-gpu Wayland cross-domain transport. It manages endpoint +streams, UI identity metadata (color/border/label), and filter policy over +the cross-domain channel. + +This Provider does **not** own the GPU device allocation or the VMM process. +Those are owned by `Provider/device-gpu` and `Provider/runtime-cloud-hypervisor` +respectively. This Provider consumes `Device/display-wayland` endpoints exposed +by the GPU Provider and coordinates with the clipboard Provider +(`Provider/clipboard-wayland`) through the d2b-bus bridge socket. + +The Provider does **not** own any audio path. Audio remains in +`Provider/audio-pipewire`. + +The Provider does **not** implement a direct compositor fallback. When the +Host proxy component is not Ready, the cross-domain Wayland channel is +unavailable and the Guest application surface is not forwarded. There is no +transparent fallback to a direct compositor socket or a second IPC path. + +--- + +## 2. Provider identity + +```text +providerRef: Provider/display-wayland +artifactId: display-wayland +crate: packages/d2b-provider-display-wayland/ +``` + +Required crate layout: + +```text +packages/d2b-provider-display-wayland/ + src/ + tests/ + integration/ + README.md +``` + +All four paths are mandatory per `ADR-046-provider-model-and-packaging`. The +workspace policy gate enforces this. + +--- + +## 3. ResourceTypes + +`Provider/display-wayland` does not implement any of the standard ResourceTypes +(Volume, Network, Device, Process, Host, Guest). It implements and exports the +following Provider-specific ResourceTypes: + +| ResourceType | Short name | Responsibility | +| --- | --- | --- | +| `display-wayland.d2bus.org.WaylandSession` | `WaylandSession` | One active Wayland cross-domain display session between a Guest and the host compositor, including endpoint references, filter policy, identity metadata, and lifecycle | +| `display-wayland.d2bus.org.WaylandPolicy` | `WaylandPolicy` | Zone-scoped Wayland global filter policy template: classified global allowlist/denylist, version caps, dmabuf filters, identity rail settings | + +Both ResourceTypes are scoped to the owning Zone and follow the standard +resource envelope contract from `ADR-046-resource-object-model`. + +**D089 spec extension contract:** any implementation-only desired configuration +for these ResourceTypes is carried in `spec.provider.settings` under the +registered `display-wayland.d2bus.org//spec` schema; that schema is +signed in the manifest, deny-unknown, bounded, versioned, and validated against +`spec.providerRef` at Nix build and API admission. Base fields stay at `spec.*`; +shared semantics are promoted to the WaylandSession/WaylandPolicy base and never +placed in `spec.provider`. This Provider implements the exact base spec/status +schema version/fingerprint, accepts the canonical minimal valid base Spec, and +rejects an unsupported optional base capability only through its signed +capability matrix plus provider-neutral `unsupported-capability`. +`spec.provider` aligns with `status.provider` for `Provider/display-wayland`. + +--- + +## 4. Components + +### 4.1 Component table + +| ID | Type | Binary | Domain | Placement | Cardinality | +| --- | --- | --- | --- | --- | --- | +| `display-controller` | controller | `d2b-display-wayland-controller` | system | Zone singleton | 1 per Zone | +| `display-user-portal` | service | `d2b-display-wayland-user-portal` | user | one per active user login session | 1 per user with active sessions | + +Both components are declared as static entries in the signed ProviderDeployment +manifest; the framework instantiates them at Provider enrollment. There is no +implicit controller bootstrap. + +### 4.2 `display-controller` + +A Zone-singleton system-domain controller. Manages both +`display-wayland.d2bus.org.WaylandSession` and `display-wayland.d2bus.org.WaylandPolicy` +resources within its Zone. + +On `display-wayland.d2bus.org.WaylandPolicy` create/update: +1. Validates the policy schema. **Unknown interface names fail closed**: any + `allowGlobals` or `denyGlobals` entry not in the compiled global catalog is + rejected at admission with an actionable error. The `WaylandPolicy` resource + is not admitted. +2. Compiles an ordered canonical policy digest into controller-local in-memory + state. No durable Volume is required. + +On `display-wayland.d2bus.org.WaylandSession` create/update: +1. Verifies the referenced `WaylandPolicy` (fully qualified ResourceRef) is Ready. +2. Requests a cross-domain GPU endpoint attachment grant from `Provider/device-gpu` + via typed ComponentSession service. Receives an opaque grant handle. + `ProviderSupervisor` routes the fd directly from the device-gpu provider to the + target worker at launch; the controller never holds the fd. +3. Requests a per-session compositor connection attachment grant from + `display-user-portal` for the session's `userRef` via KK-profile + ComponentSession. Receives an opaque grant handle. `ProviderSupervisor` routes + the fd directly from the portal to the target worker at launch; the controller + never holds the compositor fd or any compositor socket path. +4. Assembles a `ProviderSupervisor` LaunchTicket carrying the opaque attachment + grant handles (compositor, GPU endpoint) and the sealed config descriptor. + `ProviderSupervisor` resolves each grant handle and installs the corresponding + fd at the declared in-jail fd number before the Process's first instruction. + The proxy receives no d2b-bus socket and never contacts d2b-bus. +5. Issues a separate LaunchTicket for the guest frontend Process for each + enabled Guest, with the sealed cross-domain config. +6. Writes `WaylandSession.status` with endpoint revision and readiness. + +### 4.3 `display-user-portal` + +A separately sandboxed user-domain service component. One instance per active +user login session. Declared as a static entry in the signed ProviderDeployment +manifest; instantiated by the user's session manager when the Provider is +enrolled and the user logs in. + +The portal: +1. Receives a pre-opened compositor connection attachment from the fixed user + session supervisor via KK-profile ComponentSession attachment (SCM_RIGHTS). + The portal **never reads `WAYLAND_DISPLAY`**, any compositor socket path, or + any ambient environment path. The fixed user session supervisor is the only + entity that holds the compositor socket path. +2. Validates that the delivering peer is the same-user supervisor via + `SO_PEERCRED`; rejects any delivery from a different uid. +3. Authenticates to `display-controller` via KK-profile ComponentSession (local + Unix transport; peer identity from `SO_PEERCRED`). +4. On request from `display-controller` for a specific `WaylandSession` and + `userRef`: validates same-user, then issues a bounded per-session compositor + connection attachment grant to `ProviderSupervisor`. `ProviderSupervisor` + routes the fd directly to the target worker without the controller or portal + holding a reference. +5. Never exposes the compositor socket path in any resource status, spec, log, + audit, or metric surface. Never grants more connections than there are active + sessions for the authenticated user. + +Neither the portal nor any other `display-wayland` component reads or holds the +compositor socket path after the portal's initial supervisor-delivered attachment +is accepted. + +### 4.4 ProviderStateSet and status-first operational state + +Per D087, `display-wayland` declares **no Provider state Volume** for either +static component. Its `ProviderStateSet` is the optional query-time grouping of +declared Provider state Volumes and is empty for this Provider. + +Bounded, non-secret operational state is written to the owning resource's +`status` subresource and to the core Operation ledger: `WaylandSession` carries +phase, conditions, proxy readiness, policy digest, pool-slot status, and finalizer +progress; `WaylandPolicy` carries policy admission/status. Portal compositor +connection grants are transient in-process attachments and never durable state. +After restart, the controller re-lists current resources and re-verifies external +Process/Device reality before writing materially changed status. + +Per D088, `WaylandSession` and `WaylandPolicy` status use the universal +`ResourceStatus` base at top-level `status.*` plus their ResourceType-common +typed `status.resource` objects. Optional `status.provider` is only for +implementation-only observation (`providerRef`, qualified immutable `schemaId`, +semver `schemaVersion`, numeric `observedProviderGeneration`, and strict +unknown-field-denied redacted `details` ≤32 KiB registered/signed in the +Provider manifest); shared fields are never duplicated there. The controller +writes all present layers atomically in one status mutation. + +Storage-need test rationale: display controller and portal state contains no +secret recovery material, no large or binary payload, no private data that must be +hidden from authorized status readers, and no bounded-but-revision-unsuitable +payload with a demonstrated recovery need, so an identity-only Volume is not +declared. + +--- + +## 5. WaylandSession ResourceSpec + + +```yaml +apiVersion: resources.d2bus.org/v3 +type: display-wayland.d2bus.org.WaylandSession +metadata: + name: corp-vm-display + zone: dev + uid: + generation: 1 + ownerRef: Guest/corp-vm + finalizers: [display-wayland.d2bus.org/proxy-stopped] +spec: + guestRef: Guest/corp-vm + hostRef: Host/host-system + userRef: User/alice # user whose compositor session the proxy connects to + policyRef: display-wayland.d2bus.org.WaylandPolicy/default + # UI identity metadata — compositor-agnostic + identity: + label: "corp-vm" # max 64 chars; validated against Guest name + activeColor: "#7fc8ff" # #rrggbb hex; required + inactiveColor: "#45475a" # #rrggbb hex; defaults to activeColor + urgentColor: "#f38ba8" # #rrggbb hex; defaults to activeColor + border: + enable: true + railWidth: 9 # logical pixels; 0 = no rail; max 64 + label: + enable: true + text: null # null = use identity.label; "" = suppress text + position: top-left # top-left | top-center + crossDomainTrusted: true # must be true; false is rejected at admission + virglVideo: false # opt-in experimental virglrenderer video path + filter: + debugLogging: false + denyGlobals: [] + allowGlobals: [] + maxVersions: {} + dmabufAllow: [] + dmabufDeny: [] +status: {} +``` + +### 5.1 WaylandSession spec field reference + +| Field | Type | Required | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | --- | +| `guestRef` | ResourceRef | yes | — | `Guest/` | Target Guest | +| `hostRef` | ResourceRef | yes | — | `Host/` | Host running the proxy Process | +| `userRef` | ResourceRef | yes | — | `User/` | User whose compositor session the proxy connects to; used by `display-user-portal` to acquire the compositor fd | +| `policyRef` | ResourceRef | yes | — | `display-wayland.d2bus.org.WaylandPolicy/` | Resolved policy template; fully qualified ResourceRef | +| `identity.label` | string | yes | — | 1..64 chars; `^[a-z][a-z0-9-]*$` | Authenticated display label; matches Guest name by default | +| `identity.activeColor` | string | yes | — | `^#[0-9a-fA-F]{6}$` | Active/focused identity color | +| `identity.inactiveColor` | string | no | `activeColor` | `^#[0-9a-fA-F]{6}$` | Inactive/unfocused identity color | +| `identity.urgentColor` | string | no | `activeColor` | `^#[0-9a-fA-F]{6}$` | Urgent state identity color | +| `identity.border.enable` | bool | no | `true` | — | Proxy-drawn identity rail | +| `identity.border.railWidth` | u32 | no | `9` | 0..64 | Left rail width in logical pixels; 0 disables the rail | +| `identity.label.enable` | bool | no | `true` | — | Proxy-drawn identity label | +| `identity.label.text` | string? | no | `null` | max 64 chars | `null` uses `identity.label`; `""` suppresses the label text | +| `identity.label.position` | enum | no | `top-left` | `top-left \| top-center` | Label position | +| `crossDomainTrusted` | bool | yes | — | must be `true` | Explicit opt-in for cross-domain Wayland forwarding; `false` is rejected at spec admission | +| `virglVideo` | bool | no | `false` | — | Opt-in experimental virglrenderer video path; gated by `device-gpu` descriptor | +| `filter.debugLogging` | bool | no | `false` | — | Verbose Wayland protocol trace logging; structurally redacted output only (message shape and global names; no payload bytes, argument values, or surface content); not for production | +| `filter.denyGlobals` | `[string]` | no | `[]` | max 128 items; each max 63 chars | Additional globals to deny beyond policy defaults | +| `filter.allowGlobals` | `[string]` | no | `[]` | max 128 items; each max 63 chars | Globals to allow; clipboard-boundary globals are ignored and produce audit advisory | +| `filter.maxVersions` | `map` | no | `{}` | max 128 entries | Per-interface version caps | +| `filter.dmabufAllow` | `[string]` | no | `[]` | max 64 items | dmabuf format/modifier allow rules: `FORMAT[:MODIFIER]` | +| `filter.dmabufDeny` | `[string]` | no | `[]` | max 64 items | dmabuf format/modifier deny rules: `FORMAT[:MODIFIER]` | + +`crossDomainTrusted: false` is rejected at spec admission because the +cross-domain Wayland channel is the only transport this Provider serves. +When cross-domain is not desired the operator should not declare a +`WaylandSession` for that Guest. + +### 5.2 WaylandSession status + +D088 status layering applies here: fields common to every resource remain in the +top-level `status.*` base, while the Wayland typed fields are +`status.resource`; `status.provider` is unnecessary unless a concrete +implementation has bounded redacted details that are not shared by all +display-wayland implementations. + +D091 currency and upgrade: the display-wayland controller implements +`assess_update`, `plan_upgrade`, and `execute_upgrade` for its qualified +ResourceTypes and semantic Wayland sessions. A `ProviderGenerationChanged`, +`ArtifactChanged`, `DependencyChanged`, or `SpecChanged` reason populates +universal `status.update` with +`UpdateAvailable` or `UpgradeRequired`; disruptive changes MUST return +`UpgradeRequired` rather than being applied in place, while non-disruptive +changes reconcile normally. These currency fields are universal/ResourceType +base fields, never `status.provider`. Upgrades recycle only the display +realization (owned `Process` resources, endpoints, semantic sessions, and +session attachments) with `disruption` set to `Reload`, `Restart`, or +`Recycle`; durable config is preserved, dependent sessions are drained and +restarted by the dependency-aware planner, and owned ephemeral session state +remains process memory. No surface content bytes, clipboard bytes, terminal +bytes, notification content bytes, session bytes, secrets, paths, or handles +may appear in `status.update`. + +D090 expedited reconcile: Create, UpdateSpec, and Delete requests that set +`waitForReconcile` perform no external effect, finalizer mutation, or status +mutation until Core supplies a typed `CommittedRevisionProof` +`{resourceUid,generation,revision,operationId}`. Abort produces no effect; a +durable commit is never rolled back on later reconcile timeout. The response is +the committed object plus one-pass projected layered status, a disposition +(`Converged`, `Progressing`, `Blocked`, `UpgradeRequired`, or `Failed`), and +`statusPersistence` (`pending` or `committed`); effect idempotency keys derive +from `(UID,generation,revision,operationId)` in the same per-resource +single-flight priority lane. + +```yaml +status: + phase: Pending | Ready | Degraded | Failed | Unknown + observedGeneration: 1 + conditions: + - type: ProxyReady + status: "True" + reason: proxy-process-ready + - type: GuestFrontendReady + status: "True" + reason: guest-process-ready + - type: PolicyApplied + status: "True" + reason: policy-digest-applied + - type: GpuEndpointAvailable + status: "True" + reason: device-gpu-endpoint-ready + lastReconciledAt: null + resource: + proxyProcessRef: Process/corp-vm-display-proxy + guestFrontendProcessRef: Process/corp-vm-display-guest + waylandEndpointRef: Endpoint/corp-vm-wayland-cross-domain + waylandEndpointGeneration: 1 + policyDigest: sha256: +``` + +`proxyProcessRef`, `guestFrontendProcessRef`, `waylandEndpointRef`, +`waylandEndpointGeneration`, and `policyDigest` are bounded observations in +`status.resource`. No socket path, compositor socket name, user identity, +window title, raw argv, PID, unit name, terminal/clipboard/notification byte, +secret, or authority-conferring handle appears in any status layer. + +### 5.3 Condition types + +| Type | Meaning | +| --- | --- | +| `ProxyReady` | Host proxy Process is running and has emitted a readiness event | +| `GuestFrontendReady` | Guest frontend Process is running and connected | +| `PolicyApplied` | Compiled filter policy digest is current | +| `GpuEndpointAvailable` | `device-gpu` Provider advertises cross-domain capability and an authorized `EndpointRef` | +| `UserPortalReady` | `display-user-portal` for `userRef` is available and has delivered a compositor fd | +| `ClipboardBridgeReady` | Internal clipboard bridge to `clipboard-wayland` Provider is established; optional | +| `CrossDomainTrusted` | `spec.crossDomainTrusted = true` and GPU sidecar advertises cross-domain context type | + +--- + +## 6. Process templates + +### 6.1 `display-controller` static Process + +The `display-controller` is a Zone-singleton system-domain controller Process +declared in the signed ProviderDeployment manifest. It is the sole d2b-bus actor +for this Provider; it has no worker sibling processes that share its authority. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: display-wayland-controller + zone: dev + ownerRef: Provider/display-wayland +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + processClass: controller + template: display-wayland-controller + sandbox: + namespaceClasses: [mount, pid] + capabilityClasses: [] + seccompClass: w1-display-wayland-controller + noNewPrivileges: true + startRoot: false + environmentClass: minimal + readOnlyRoot: true + umask: "0022" + oomScoreAdj: -100 + budget: + memory: + request: "16Mi" + limit: "64Mi" + pids: + limit: 32 + fds: + limit: 128 + networkUsage: null + deviceUsage: [] + readiness: + initialDelay: "0s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined + restartPolicy: + class: on-failure + backoffBase: "1s" + backoffMax: "60s" + backoffMultiplier: 2.0 + maxRestarts: null + resetAfter: "300s" + telemetry: + metricsEnabled: true + tracingEnabled: true + logLevel: info + sensitiveLabels: false +``` + +### 6.2 `display-user-portal` static Process + +The `display-user-portal` is a user-domain service Process declared in the signed +ProviderDeployment manifest. It is sandboxed separately from the controller and +has no d2b-bus authority. It receives pre-opened compositor connections from the +fixed user session supervisor and re-exports bounded per-session attachment grants. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: display-wayland-user-portal + zone: dev + ownerRef: Provider/display-wayland +spec: + providerRef: Provider/system-systemd-user + executionRef: Host/host-system + domain: user + processClass: service + template: display-wayland-user-portal + sandbox: + namespaceClasses: [] + capabilityClasses: [] + seccompClass: w1-display-wayland-portal + noNewPrivileges: true + startRoot: false + environmentClass: minimal + readOnlyRoot: true + umask: "0022" + oomScoreAdj: 0 + budget: + memory: + request: "8Mi" + limit: "32Mi" + pids: + limit: 16 + fds: + limit: 64 + networkUsage: null + deviceUsage: [] + readiness: + initialDelay: "0s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined + restartPolicy: + class: on-failure + backoffBase: "1s" + backoffMax: "60s" + backoffMultiplier: 2.0 + maxRestarts: null + resetAfter: "300s" + telemetry: + metricsEnabled: true + tracingEnabled: true + logLevel: info + sensitiveLabels: false +``` + +### 6.3 Host proxy worker Process (`wayland-proxy-worker`) + +The Host proxy is a long-lived `Process` resource owned by the `WaylandSession` +resource (via `metadata.ownerRef`). + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: corp-vm-display-proxy + zone: dev + ownerRef: display-wayland.d2bus.org.WaylandSession/corp-vm-display +spec: + providerRef: Provider/system-minijail + executionRef: Host/host-system + domain: system + processClass: worker + template: wayland-proxy-worker + sandbox: + namespaceClasses: [mount, pid] + capabilityClasses: [] + seccompClass: w1-wayland-proxy + noNewPrivileges: true + startRoot: false + environmentClass: minimal + readOnlyRoot: true + umask: "0022" + oomScoreAdj: 0 + budget: + memory: + request: "32Mi" + limit: "128Mi" + pids: + limit: 64 + fds: + limit: 256 + mounts: + - volumeRef: Volume/corp-vm-display-proxy-runtime + view: proxy-runtime + mountPath: /run/d2b-wlproxy + access: read-write + required: true + deviceUsage: + - deviceRef: Device/corp-vm-gpu + access: shared + purpose: wayland-cross-domain-endpoint + networkUsage: null + readiness: + initialDelay: "0s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined + restartPolicy: + class: on-failure + backoffBase: "1s" + backoffMax: "60s" + backoffMultiplier: 2.0 + maxRestarts: null + resetAfter: "300s" + telemetry: + metricsEnabled: true + tracingEnabled: true + logLevel: info + sensitiveLabels: false +``` + +**Process identity and naming:** + +The process title is `d2b--wlproxy` (derived from the authenticated +Guest resource name, max 63-char constraint). This title is the only +diagnostic-visible name for the process; it must not include compositor socket +names, user identities, or window content. + +**Binary:** `d2b-display-wayland-host-proxy` + +**Principal:** opaque hash-derived OS account (`d2b-wlp-` for +bundle-declared sessions; `d2b-wlp-p` for pool-allocated sessions; see §16.5). +No capabilities. No PipeWire/Pulse socket access. + +**Sandbox:** +- Mount namespace: the per-session Volume (§7.1) provides the only writable + surface at `/run/d2b-wlproxy`. +- All required fds (upstream compositor fd, cross-domain GPU endpoint fd) and + sealed config are installed by `ProviderSupervisor` at declared in-jail fd + numbers before the process's first instruction. `ProviderSupervisor` resolves + each attachment grant handle directly from the source (portal, device-gpu) to + this worker; the controller never holds the fds. The proxy never contacts + d2b-bus and holds no d2b-bus socket. +- No compositor socket path, GPU device node path, or cross-provider socket path + ever enters the proxy binary's address space. +- `seccompClass: w1-wayland-proxy` — Provider-signed seccomp catalog entry. +- `startRoot: false` — never holds root inside or outside the namespace. + +**Startup sequence (fail-closed):** +1. Read sealed config and compiled `WaylandPolicy` digest from the in-jail config + fd installed by `ProviderSupervisor`. Exit non-zero on parse errors. +2. Verify the upstream compositor fd (installed at the declared in-jail fd + number) is a live connected socket. Exit non-zero if invalid or already + broken. The per-session listen socket is **never** created before a live + upstream fd is confirmed. +3. Create the per-session listen socket inside the Volume mount. +4. Emit a `ProxyReadinessEvent` (bounded, path-free) over the readiness fd + installed by `ProviderSupervisor`. +5. Enter the dispatch loop. + +**No d2b-bus authority:** the proxy binary has no d2b-bus socket after launch. +It receives all required state and fds from the LaunchTicket. It never opens +a second compositor connection, an SSH path, or any out-of-band IPC fallback. +If the upstream compositor fd is invalid the proxy exits non-zero. The +`display-controller` detects the failed Process and sets +`WaylandSession.status.phase = Failed`. There is no retry to a different fd or +a fallback display path. + +### 6.4 Guest frontend worker Process (`wayland-frontend-worker`) + +The guest frontend is a long-lived `Process` resource running in the `system` +domain under the Guest. + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Process +metadata: + name: corp-vm-display-guest + zone: dev + ownerRef: display-wayland.d2bus.org.WaylandSession/corp-vm-display +spec: + providerRef: Provider/system-systemd + executionRef: Guest/corp-vm + domain: system + processClass: worker + template: wayland-frontend-worker + sandbox: + namespaceClasses: [] + capabilityClasses: [] + seccompClass: strict + noNewPrivileges: true + startRoot: false + environmentClass: minimal + readOnlyRoot: true + umask: "0022" + oomScoreAdj: 0 + budget: + memory: + request: "8Mi" + limit: "32Mi" + pids: + limit: 16 + fds: + limit: 64 + networkUsage: null + deviceUsage: [] + readiness: + initialDelay: "0s" + timeout: "30s" + failureThreshold: 3 + successThreshold: 1 + class: provider-defined + restartPolicy: + class: on-failure + backoffBase: "1s" + backoffMax: "60s" + backoffMultiplier: 2.0 + maxRestarts: null + resetAfter: "300s" + telemetry: + metricsEnabled: true + tracingEnabled: true + logLevel: info + sensitiveLabels: false +``` + +**Binary:** `wl-cross-domain-proxy` (from `pkgs/wl-cross-domain-proxy`; see +§17 for the v3 migration path). + +**Purpose:** drives the guest-side virtio-gpu Wayland cross-domain transport. +The binary connects to the in-guest virtio-gpu cross-domain context and +forwards Wayland protocol messages. Filtering, global hiding, and app-id +rewriting are performed by the Host proxy; the guest process performs no +filtering. + +The binary does not use `--tag` or any filtering flag. It does not +connect to the compositor socket directly; it relies on the virtio-gpu +cross-domain context established by `Provider/device-gpu` and +`Provider/runtime-cloud-hypervisor`. + +**Lifecycle gate:** this Process is not created until `crossDomainTrusted = +true` is confirmed in the `WaylandSession` spec. The `display-controller` +checks this field at reconcile time and sets a `CrossDomainTrusted` condition +on the `WaylandSession` before issuing the guest frontend LaunchTicket. A +missing GPU cross-domain context causes the Process to enter `Failed` phase; +the controller sets `WaylandSession` to `Degraded`. The guest frontend worker, +like the host proxy, has no d2b-bus authority after launch; all required fds +and config arrive in the LaunchTicket. + +### 6.5 Endpoint resources (D092) + +`Provider/display-wayland` declares standard `Endpoint` base-schema conformance. +Stable display endpoints are owned child resources, not inline `Process.spec` +fields. Consumers refer to `Endpoint/` and never receive raw Wayland +socket paths, GPU device locators, CIDs, ports, fd numbers, or credentials from +resource spec, status, CLI output, audit, or telemetry. Resolution occurs only +through an authorized EffectPort/LaunchTicket; unauthorized resolution returns +`endpoint-resolve-denied`. A producing Process restart bumps +`Endpoint.status.endpointGeneration`, which triggers dependent consumers with +`dependency-changed`. + +Representative cross-domain Wayland Endpoint resources: + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: corp-vm-wayland-cross-domain + zone: dev + ownerRef: display-wayland.d2bus.org.WaylandSession/corp-vm-display +spec: + providerRef: Provider/display-wayland + producerRef: Process/corp-vm-display-proxy + endpointClass: data + transport: fd-attachment + purpose: display-wayland.d2bus.org/wayland-cross-domain + serviceFingerprint: display-wayland.d2bus.org/wayland-data.v3 + locality: cross-domain + visibility: zone + attachmentPolicy: launch-ticket + consumerPolicy: + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Endpoint +metadata: + name: corp-vm-wayland-guest-frontend + zone: dev + ownerRef: display-wayland.d2bus.org.WaylandSession/corp-vm-display +spec: + providerRef: Provider/display-wayland + producerRef: Process/corp-vm-display-guest + endpointClass: transport + transport: vsock + purpose: display-wayland.d2bus.org/guest-cross-domain + serviceFingerprint: display-wayland.d2bus.org/guest-frontend.v3 + locality: cross-domain + visibility: zone + attachmentPolicy: launch-ticket + consumerPolicy: + allowedOperations: [resolve] + lifecyclePolicy: recycle-with-producer +status: + readiness: Ready + observedProducerGeneration: 1 + observedResourceGeneration: 1 + endpointGeneration: 1 + connectionAvailability: available + leaseAvailability: lease-required +``` + +Per-client Wayland connections are not `Endpoint` resources: they are +short-lived controller-internal connection handles below the D092 promotion +threshold. + +### 6.6 Retained opaque handles + +- pidfds: Process supervision tokens tied to kernel process identity; they are + not stable resource identities and remain authority-bearing handles. +- Per-connection/session handles: compositor attachment grants and client + connection IDs are high-churn and private to a single LaunchTicket. +- Named streams: clipboard/display helper streams are per-session payload + channels and never identify a stable service endpoint. +- `OwnedTransport`: authenticated ComponentSession transport ownership remains + an in-memory session object, not a resource locator. +- fd indexes: LaunchTicket-local descriptor numbers are meaningful only inside + one process launch and therefore stay opaque. + +--- + +## 7. Volume resources + +### 7.1 Host proxy runtime Volume + +```yaml +apiVersion: resources.d2bus.org/v3 +type: Volume +metadata: + name: corp-vm-display-proxy-runtime + zone: dev + ownerRef: display-wayland.d2bus.org.WaylandSession/corp-vm-display +spec: + providerRef: Provider/volume-local + source: + executionRef: Host/host-system + settings: + kind: tmpfs # backend mount type; distinct from Volume.kind lifecycle + sourcePolicyId: display-wayland.wlproxy-runtime.v1 # bound in Provider.spec.config.runtimeVolumePolicyId + kind: ephemeral # lifecycle: removed on Host restart / session deletion + layout: + - path: "" + type: directory + ownerRef: User/d2b-wlp- # opaque hash-derived principal; see §16.5 + groupRef: User/d2b-wlp- + mode: "0700" + sensitivity: private + createPolicy: create-if-never-provisioned + repairPolicy: exact-owner + cleanupPolicy: owner-controlled + noFollow: true + accessAcl: [] # no additional access ACL entries beyond mode bits + defaultAcl: [] # no default ACL entries propagated to new files + invariants: [no-symlink] + views: + proxy-runtime: + path: "" + rights: [read, write, create, delete, traverse] + attachments: [] # no process-scoped attachment grants; fd routing is via ProviderSupervisor grant handles + quota: + maxBytes: 4194304 # 4 MiB hard limit; sockets carry no payload bytes + maxInodes: 64 # listen socket, clipboard bridge socket, lock files + enforcement: hard +``` + +`source.settings.kind: tmpfs` selects the kernel tmpfs mount. `Volume.kind: +ephemeral` is the lifecycle classification: the volume is boot-scoped and +removed on Host restart or session deletion, which triggers `WaylandSession` +reconciliation. The tmpfs quota is enforced by the kernel mount options; the +charge is derived automatically from `source.executionRef` (`Host/host-system`) +without a separate `chargeTo` field. `source.settings.sourcePolicyId` binds the +volume to the Provider-declared source policy; if not globally fixed, it is +configurable via `Provider.spec.config.runtimeVolumePolicyId`. + +The `display-controller` creates this Volume resource as a child of the +`WaylandSession` (via `ownerRef`) when reconciling the session. `Provider/volume-local` +is the sole Volume reconciler: it materializes the tmpfs mount from the declared +spec; the controller submits the Volume resource object but does not perform the +mount itself. Session-owned Volumes are deleted by owner-cascade when the +`WaylandSession` is deleted. + +The actual listen socket path inside the mount is a private implementation +detail of the proxy binary; it is never placed in the Volume spec, resource +status, logs, or audit. No cross-provider path sharing occurs. + +### 7.2 `display-controller` Provider state + +`display-controller` declares **no Provider state Volume** and has no `/state` +mount. Its bounded non-secret operational data lives in `WaylandSession` and +`WaylandPolicy` `status` plus the core Operation ledger: observed generations, +conditions, policy digests, pool-slot summaries, finalizer progress, and launch +attempt results. Status remains observation-only, redacted, revisioned, bounded, +and reverified against Process/Device reality after restart; it never confers +host-mutation or repair authority. + +Storage-need test rationale: all controller runtime state is derivable from the +Zone resource store, Operation ledger, and live Process/Device observations, and +there is no secret, large, private, or revision-unsuitable recovery payload. + +### 7.3 `display-user-portal` Provider state + +`display-user-portal` declares **no Provider state Volume** and has no `/state` +mount. Same-user compositor connection grants, received compositor fds, and +attachment handles are transient in-process capabilities; they are never stored +in status, logs, audit, metrics, or durable Volumes. The portal reports only +bounded readiness and error conditions through the owning resource status and the +core Operation ledger. + +Storage-need test rationale: portal state is either transient authority-conferring +fd/handle data that must not be persisted or small non-secret readiness data that +fits the status-first model, so no durable Provider state Volume is declared. + +--- + +## 8. WaylandPolicy ResourceSpec + + +```yaml +apiVersion: resources.d2bus.org/v3 +type: display-wayland.d2bus.org.WaylandPolicy +metadata: + name: default + zone: dev + uid: + generation: 1 +spec: + denyGlobals: [] + allowGlobals: [] + maxVersions: {} + dmabufAllow: [] + dmabufDeny: [] + defaults: + acceleratedRendering: allow + clipboardBoundary: virtualize + highRisk: deny + appDefaults: allow + offDefaults: deny + unclassified: deny +status: {} +``` + +### 8.1 WaylandPolicy spec fields + +| Field | Type | Required | Default | Bounds | Notes | +| --- | --- | --- | --- | --- | --- | +| `denyGlobals` | `[string]` | no | `[]` | max 512 items; each max 63 chars | Zone-wide additional globals to deny | +| `allowGlobals` | `[string]` | no | `[]` | max 512 items; each max 63 chars | Zone-wide additional globals to allow; clipboard-boundary globals are ignored with advisory | +| `maxVersions` | `map` | no | `{}` | max 512 entries | Zone-wide per-interface version caps | +| `dmabufAllow` | `[string]` | no | `[]` | max 256 items | Zone-wide dmabuf allow rules | +| `dmabufDeny` | `[string]` | no | `[]` | max 256 items | Zone-wide dmabuf deny rules | +| `defaults.acceleratedRendering` | `allow \| deny` | no | `allow` | — | Default action for `AcceleratedRendering`-classified globals | +| `defaults.clipboardBoundary` | `virtualize \| deny` | no | `virtualize` | — | `virtualize`: synthesize clipboard locally; `deny`: hide all clipboard globals | +| `defaults.highRisk` | `allow \| deny` | no | `deny` | — | Default action for `HighRisk`-classified globals | +| `defaults.appDefaults` | `allow \| deny` | no | `allow` | — | Default action for `AppDefault`-classified globals | +| `defaults.offDefaults` | `allow \| deny` | no | `deny` | — | Default action for `OffDefault`-classified globals | +| `defaults.unclassified` | `allow \| deny` | no | `deny` | — | Default action for unclassified globals; overrides to `allow` produce audit advisory | + +`WaylandSession.spec.filter.*` fields override the Zone-wide `WaylandPolicy` +for that session. Per-session overrides take precedence over Zone policy. +Neither per-session nor Zone policy can override the Required-baseline layer +(the fixed set of globals d2b always needs for graphics). + +### 8.2 Classified global layers + +The policy engine uses a four-layer classified allowlist (from lowest to +highest priority): + +1. **Required-baseline** — core globals d2b needs for cross-domain Wayland + graphics. These cannot be denied from config; a per-session `denyGlobals` + entry for a Required-baseline global produces a `W-DENY-BASELINE` audit + advisory and the entry is silently ignored. + +2. **Secure feature defaults** — named classification bundles with safe + defaults. Defaults are governed by `WaylandPolicy.spec.defaults.*`. + +3. **Zone policy** — `WaylandPolicy` per-session overrides applied to all + sessions referencing this policy. + +4. **Session overrides** — `WaylandSession.spec.filter.*` per-session + overrides with highest priority. + +Unknown globals (not in any layer) are denied by default. **Unknown interface +names in `allowGlobals` or `denyGlobals` fail closed at spec admission**: any +entry not present in the Provider's compiled global catalog causes the +`WaylandPolicy` resource to be rejected with an actionable error. Only +interface names registered in the Provider's compiled catalog are accepted. + +### 8.3 Clipboard boundary + +`wl_data_device_manager` and related clipboard/DnD globals are owned by the +virtual clipboard architecture (`Provider/clipboard-wayland`). This Provider +synthesizes a guest-visible `wl_data_device_manager` locally. Downstream +`wl_data_*` objects are **never** bound into the host compositor clipboard +namespace from within this Provider. Host and cross-realm clipboard +materialization routes through `d2b-clipd`. + +Clipboard-boundary globals listed in `allowGlobals` are **ignored** (the +clipboard architecture does not permit forwarding) and produce a +`W-ALLOW-CLIPBOARD-BOUNDARY` audit advisory. + +--- + +## 9. GPU dependency + +`Provider/display-wayland` depends on `Provider/device-gpu` as a declared +optional dependency alias `gpu`. The GPU Provider must be installed and Ready +in the same Zone for `WaylandSession` resources to progress past `Pending`. + +The dependency contract: + +- `device-gpu` advertises cross-domain capability and an authorized + `Endpoint/` reference when the VMM advertises the + `cross-domain` context type. No socket path or fd is exposed in Device or + Endpoint status/spec. +- `display-wayland` acquires the exact connected cross-domain fd through a + typed `device-gpu` ComponentSession service/descriptor handoff, authenticated + via an enrolled KK-profile ComponentSession between the two Provider + controllers. The fd is passed as a ComponentSession attachment over the local + Unix transport. No socket path for the GPU endpoint appears in any resource + status, spec, or audit record. +- `virglVideo: true` in `WaylandSession.spec` is only valid when the GPU + Device's `providerDiagnostic` reports video decode support. The controller + produces a `VirglVideoUnsupported` condition if the GPU Device does not + advertise this capability. +- The GPU cross-domain context type (`cross-domain`) must be enabled in the + VMM configuration (`Provider/runtime-cloud-hypervisor`) for the session to + become Ready. The `CloudHypervisorRunner` Process receives the cross-domain + flag from the private bundle, not from a public resource field. +- When `Provider/device-gpu` is absent or the GPU Device is not Ready, the + `GpuEndpointAvailable` condition is False, `WaylandSession.status.phase = + Pending`, and the proxy Process is not created. + +The `display-wayland` Provider must not query the GPU device node path, open +any DRM/render fd, or interact with the GPU hardware directly. It receives only +the cross-domain endpoint fd through the typed `device-gpu` ComponentSession +service/descriptor handoff; no socket path or device node is exposed to this +Provider. + +--- + +## 10. ComponentSession transport and no direct compositor fallback + +`display-controller` is the sole d2b-bus actor for this Provider. It +communicates over KK-profile (`Noise_KK_25519_ChaChaPoly_SHA256`) or NN-profile +ComponentSession over local Unix transports as appropriate. Worker processes +(host proxy, guest frontend) have **no d2b-bus authority after launch**; all +required fds and sealed config arrive via the `ProviderSupervisor` LaunchTicket. + +**`display-controller` bus communication:** +- With `display-user-portal` over KK-profile ComponentSession (local Unix + seqpacket; peer identity from `SO_PEERCRED` + process identity): the portal + issues an opaque per-session compositor connection attachment grant handle. + `ProviderSupervisor` resolves the handle and routes the fd directly from + the portal to the target worker. The controller receives only the opaque grant + handle and a status confirmation; it never holds the compositor fd. +- With `Provider/device-gpu` controller over KK-profile ComponentSession: the + device-gpu provider issues an opaque cross-domain GPU endpoint attachment grant + handle. `ProviderSupervisor` resolves the handle and routes the fd directly + from device-gpu to the target worker. The controller receives only the opaque + grant handle and a status confirmation; it never holds the GPU endpoint fd. +- With `Provider/clipboard-wayland` over NN-profile ComponentSession for + per-session clipboard bridge establishment (opaque endpoint handle routing; + no socket paths exchanged). +- With Zone runtime for resource status updates and lifecycle. + +**LaunchTicket composition:** `display-controller` assembles a +`ProviderSupervisor` LaunchTicket carrying: +- the opaque compositor connection attachment grant handle (from portal); +- the opaque GPU endpoint attachment grant handle (from device-gpu); +- the sealed config descriptor (policy digest, identity, filter settings). + +`ProviderSupervisor` resolves each grant handle and installs the corresponding +fd at the declared in-jail fd number before the worker's first instruction. +No fd transits through the controller process. No socket path is embedded in +the LaunchTicket, spec, status, audit, or any log line. + +**Cross-provider fd routing:** `ProviderSupervisor` routes attachment grants +directly from the issuing endpoint (portal, device-gpu) to the exact target +worker Process. The crosvm-facing proxy listen socket path, if the existing wire +protocol requires a filesystem path, resides only in the Volume mount (§7.1); +it is never shared with `device-gpu`, the VMM, or any other Provider through the +bundle, resource API, or LaunchTicket. + +**No compositor fallback:** if the compositor fd installed by the LaunchTicket +is invalid or the connection breaks: +- The proxy emits a bounded `ProxyReadinessEvent` with + `failure: upstream-unavailable` over the readiness fd. +- The proxy exits non-zero. +- `display-controller` detects the failed Process, sets + `WaylandSession.status.conditions.ProxyReady = False` and + `WaylandSession.status.phase = Failed`. +- No second compositor fd, path, environment variable, or out-of-band channel + is tried. + +The proxy jailed principal (opaque hash-derived OS account; see §16.5) holds no +capabilities and no authority to open compositor connections independently. + +--- + +## 11. UI color and border metadata + +`Provider/display-wayland` is the authoritative consumer of the per-Guest +UI identity color and border metadata produced by the Zone resource bundle. + +### 11.1 Color source of truth + +In v3, the `WaylandSession.spec.identity.*` fields are the single source of +truth for the proxy identity colors. There is no separate `ui-colors.json` +artifact read by the proxy at runtime. The controller injects the resolved +colors into the Process template as sealed config (a `configRef` Volume +mounted read-only in the proxy's sandbox). + +### 11.2 Color derivation + +Colors are resolved at Nix eval time by the Zone resource bundle emitter: + +1. If `d2b.zones..resources..spec.identity.activeColor` is set + explicitly, use that value. +2. Otherwise derive from the deterministic palette: `sha256("-")` + selects a palette index from the 12-color set. This mirrors the current + `colorForName "d2b-niri-border"` logic in `nixos-modules/ui-colors.nix`. + +Colors must satisfy `^#[0-9a-fA-F]{6}$`. Values outside this pattern are +rejected at eval time. + +### 11.3 Identity rail contract + +The proxy-drawn identity rail (left vertical band, fixed `railWidth` logical +pixels) and optional label use only the resolved color triplet and a maximum +64-character label. The proxy binary: + +- reads the sealed config Volume for color/rail/label settings; +- draws only wl_shm ARGB8888 surfaces using the memfd-backed in-process render + path (current `packages/d2b-wayland-proxy/src/decoration.rs`); +- does not read app-ids, titles, or other compositor-managed metadata to + determine the decoration color; +- always uses the authenticated Guest name as the fallback label when + `identity.label.text` is null; +- logs neither the label text nor any guest surface title. + +### 11.4 App-id prefix + +The Host proxy prefixes every guest-originated xdg_toplevel app-id with +`d2b..`. This prefix is injected using the `--app-id-prefix` +mechanism currently in `packages/d2b-host/src/wayland_proxy_argv.rs`. In v3 +the prefix is derived from the authenticated `WaylandSession.spec.guestRef` +name and injected via sealed config; it is never a freeform string from an +untrusted source. + +App-id values and window titles are not logged, not included in audit events, +and not exposed in resource status or metrics. + +### 11.5 Title prefix + +The Host proxy prepends a bounded title prefix (currently `[] `) to every +guest-originated window title forwarded to the host compositor. The title +prefix is derived from `identity.label` and formatted at runtime as +`[