diff --git a/docs/evidence/m11-security-trust-boundary-audit-2026-10-21.md b/docs/evidence/m11-security-trust-boundary-audit-2026-10-21.md new file mode 100644 index 000000000..80cc5fc94 --- /dev/null +++ b/docs/evidence/m11-security-trust-boundary-audit-2026-10-21.md @@ -0,0 +1,624 @@ +# CP #190 independent trust-boundary audit — 2026-10-21 + +Status: **audit complete for the Control Plane surface; acceptance NOT granted.** +The whole-cloud and whole-runtime trust acceptance remains **held**. Every live +profile gate is still open, and the independent human penetration review required by +the issue acceptance criteria has not been performed. + +Revision 2 (2026-10-21): calibrated against root's counterevidence. Six findings were +re-checked against exact source; F-01, F-02, F-04, F-05 and F-09 were reduced in +severity and restated with an explicit reachability class, and F-03's citation was +corrected. Every finding now states what the code does and what that makes +reachable, and no finding claims a demonstrated deployed credential leak, a deployed +unauthenticated approval path, or a cross-UID read. The unresolved gates in §8 are +unchanged. + +Revision 3 (2026-10-21): every finding was traced from its entrypoint through the +authority and effect path to the point where a control is applied, and every probe +was re-read as a synthetic demonstration rather than as deployed behaviour. Five +findings were restated with their traced path, F-09 was split so the provider +observation and the adapter-owned directory are reported separately, CP190-F29 was +added for the adapter-owned directory, and five static reachability probes +(CP190-P21 to CP190-P25) were added. The finding count is now 29 and the probe count 25. No severity changed in this round and no gate was released. + +| Field | Value | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Issue | [#190 — M19.06 Complete the security, trust-boundary, and adversarial audit](https://github.com/adea-ai/control-plane/issues/190) | +| Repository | `adea-ai/control-plane` | +| Branch | `f2w4/cp190-trust-boundary` | +| Audited head | `27f17bd55e0266439b5b3089163195c9e7a669be` (workspace `1.105.5`) | +| Base | `main` at `6d16a495186d2a4d84d422c936a02b4abdc93928` | +| Reviewer | Fleet 2 / F2W4, independent of every code owner whose fix this report assesses | +| Machine-readable matrix | [`docs/evidence/m11-security-trust-boundary-matrix-2026-10-21.v1.json`](m11-security-trust-boundary-matrix-2026-10-21.v1.json) | +| Executable probes | [`scripts/security-trust-boundary-probes.mjs`](../../scripts/security-trust-boundary-probes.mjs) — `bun scripts/security-trust-boundary-probes.mjs` | +| Requirements source | [`docs/requirements/security-trust-atomic-inventory.md`](../requirements/security-trust-atomic-inventory.md) (`STA-L*` atoms, Security & Trust Model revision 78) | +| Prior source audit | [`docs/requirements/security-trust-source-audit.md`](../requirements/security-trust-source-audit.md) (`STM-001`..`STM-035`) | + +## 1. Scope and method + +The audit maps every Control Plane threat and control surface that the issue names +onto reachable entrypoints and then onto evidence, then probes the reachable ones +with credential-free synthetic inputs. + +What this lane did: + +- re-read the current issue acceptance text (retrieved 2026-10-21, issue updated + 2026-10-18), the atomic security/trust inventory, and the existing bounded source + audit; +- traced the workspace, project, runtime, session, tools, models, artifacts and admin + domains from the public entrypoint through authority, effect, persistence and + cleanup, recording `path:line` for every decisive decision; +- assessed native grant and effect authorization, secret-canary sinks, direct-local + IPC and path handling, the remote Runtime Gateway and the product relay separately, + HPKE AAD/recipient/replay/expiry, SQLite/backup/import, provider and secret + providers, sandbox boundaries and supply chain; +- ran **20 credential-free synthetic probes** against the audited head. Every probe + reproduces a confirmed finding at this head. All 20 reproduce; 0 harness failures. + +What this lane deliberately did **not** do: + +- no secret, credential, token, key or connection string was read; +- no production or deployed profile was contacted and no penetration traffic was sent; +- no source file, existing test, canonical ledger or architecture artifact was edited; +- no owner fix is approved by this reviewer — each routed owner must verify its own fix + on its own exact head, and an independent reviewer must then re-verify it. + +### 1.1 Custody of the three new paths + +The shared cross-fleet index +(`/tmp/opencode-three-fleet-20261020-fresh/data/cross-fleet-ownership.json`, +SHA-256 `561ccd22bdd6d5a4abb1ebbecc489d8ee055c046074df680262547ad52784f05`) was +searched for each candidate path. It contains **no** reference to +`scripts/run-bun-test-group.mjs`, `tests/m11-security-probes.test.mjs`, +`packages/credential-vault`, `packages/remote-control-relay`, `packages/sandbox`, +`packages/deployment/src/checkpoint` or `packages/profile-portability`. The four open +pull requests (1212, 1210, 1204, 1048) touch none of the three new paths, and no +registered worktree has any of them dirty or untracked. + +Claude-lead custody is recorded as `unverified` in the shared index, so this lane used +**new, unique, additive paths only** and modified no existing file. A later custody +reversal can drop the branch without touching another owner. + +| New path | Purpose | +| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `scripts/security-trust-boundary-probes.mjs` | Executable credential-free synthetic probes; exits `0` while every recorded finding still reproduces, `2` when an owner fix lands, `1` on a harness failure | +| `docs/evidence/m11-security-trust-boundary-matrix-2026-10-21.v1.json` | Machine-readable threat/control → entrypoint → evidence matrix and the finding register | +| `docs/evidence/m11-security-trust-boundary-audit-2026-10-21.md` | This independent review report | + +`tests/m11-security-probes.test.mjs` was **not** modified: it is a shared lane file and +its custody is not established here. The 20 new probes are therefore a separate, +dedicated artifact rather than an edit to the existing probe suite. + +## 2. Threat/control → entrypoint → evidence matrix + +The full matrix, including the per-control obligation mapping and the evidence index, +is in the JSON artifact. The summary by domain: + +| Domain | Verified | Partial | Gap | Findings | +| --------- | -------- | ------- | --- | ---------- | +| Workspace | 2 | 0 | 2 | F-05, F-10 | +| Project | 0 | 1 | 1 | F-03 | +| Runtime | 2 | 0 | 2 | F-07, F-24 | +| Session | 1 | 0 | 1 | F-26 | +| Tools | 1 | 0 | 1 | F-18 | +| Models | 1 | 0 | 1 | F-20 | +| Artifacts | 2 | 0 | 1 | F-08 | +| Admin | 1 | 0 | 2 | F-14, F-19 | + +By the thirteen audit areas the issue requires: + +| #190 required area | Result | Findings | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- | +| 1. Every security objective/trust boundary/credential class maps to a testable control | partial | the inventory maps the obligations; the Control Plane cannot evidence several of them (F-03, F-13, F-19) | +| 2. Authorization matrices for every operation class | partial | F-05, F-18, F-19 leave workspace, tool and credential matrices open | +| 3. Instruction injection, malicious output, memory poisoning, scope broadening, plan substitution, child-plan expansion, approval bypass, destructive coercion | gap | F-20, F-18; the injection corpus remains with the #191 lane | +| 4. Managed-cloud trust (Railway/Neon/R2/Restate, SSRF, ingress, cross-service auth, leakage) | partial | F-14, F-15, F-25, F-26, F-28 | +| 5. Direct-local RuntimeTransport/native trust | gap | F-03, F-05, F-23, F-24 | +| 6. Runtime Gateway and product relay, assessed separately | gap | F-01, F-02, F-07, F-16, F-17 | +| 7. HPKE envelopes: recipient, AAD, rotation, replay, expiry, tamper, downgrade, substitution | **verified** | only the replay window is affected by F-02 | +| 8. Local SQLite and self-hosted persistence | gap | F-08, F-09, F-10 | +| 9. SecretsProvider adapters across profiles | gap | F-04, F-06, F-20, F-21, F-22 | +| 10. Sandbox boundaries, SSRF/metadata, output limits, cleanup, promotion, ambient credentials | gap | F-11, F-12, F-13 | +| 11. Supply chain for containers, Local binaries and self-hosted images | **verified** | none; see §5 | +| 12. DoS/abuse boundaries | gap | F-14, F-27 | +| 13. Independent threat-model review and targeted manual penetration | partial | this report is the independent read-only threat review; the penetration half is still open | + +## 3. Confirmed findings + +Severity is the reviewer's assessment against the release, not a CVSS score. +"Release impact" states what the finding blocks. **Reachability** states how a +reader may use the finding: + +| Reachability class | Meaning | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------- | +| `deployed-now` | Present in a shipped profile today, subject to the preconditions in the finding | +| `configured-only` | Reachable only after an operator configures the optional component; it ships disabled or is not composed by default | +| `library-contract` | A library-level contract gap. No composed path in this repository reaches it, so no deployed behaviour is claimed | +| `defense-in-depth` | A hardening gap. The described attack is not demonstrated as reachable; a stated control prevents it in the current configuration | +| `evidence-gap` | The control is a contract with no composed implementation, so there is nothing to attack and nothing to accept | + +After the two calibration rounds the distribution is **0 critical, 2 high (F-03, +F-13), 22 medium, 2 low (F-04, F-09), 3 low-medium (F-23, F-27, F-29)** across 29 +findings. No finding claims a demonstrated deployed credential leak, a deployed +unauthenticated approval path, or a cross-UID read. + +| ID | Severity | Reachability | Finding | Release impact | Probe | +| ---- | ---------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | +| F-01 | Medium | `configured-only` | The relay host processor applies no authentication of its own to metadata-only commands | does not block the release; the Control Plane ships no relay client, no adapter and no metadata effect at all | P01, P02, P21 | +| F-02 | Medium | `configured-only` | The relay receipt layer adds no idempotency beyond the command identifier, and only in-memory receipt repositories ship | bounds the remote-control idempotency claim; `create_execution` is idempotent through the CommandInbox | P03, P04, P22 | +| F-03 | High | `deployed-now` | The direct-local runtime path never consults a `LocalProjectGrant`, and the production discovery state is a hard-coded constant | blocks the Local filesystem/project acceptance claim: the working directory is bounded by operator configuration, not by a grant | P08, P09, P10, P24 | +| F-13 | High | `evidence-gap` | The sandbox provider contract is composed into no application and no live provider exists | there is no sandbox boundary in any shipped profile | P18 | +| F-04 | Low | `library-contract` | The credential-vault returned-value screen does not descend into `Error`, `Map`, `Set` or byte containers | no deployed credential leakage is claimed; the one composed leased path normalizes the result before the screen sees it | P05, P23 | +| F-05 | Medium | `deployed-now` | The Local private API derives workspace and project scope from the request body | leaves the Local cross-workspace fail-closed claim unproven; the Local profile binds loopback and uses an owner-only token | P07 | +| F-06 | Medium | `deployed-now` | Redaction is key-name based and misses compound credential key names | secrets reach logs and events under such keys | P06 | +| F-07 | Medium | `configured-only` | Runtime Gateway runtime commands are not bounded to 24 hours | remote commands outlive their authority; the gateway is used only for non-co-located nodes | P11 | +| F-08 | Medium | `deployed-now` | A Local filesystem checkpoint copies the private-file secrets directory in cleartext | operator backups carry plaintext secrets | P12 | +| F-09 | Low | `defense-in-depth` | SQLite WAL/SHM sidecars keep the pre-restriction file mode inside an owner-only parent directory | no cross-UID read is claimed and there is no supported traversal path in the provider | P13, P25 | +| F-10 | Medium | `deployed-now` | Portable import scans with an empty sensitive-value list; portable secret references are not workspace scoped | exports can carry another workspace's secret locator | P14, P15 | +| F-11 | Medium | `configured-only` | Sandbox credential bindings are injected after the ambient-credential denial runs | a raw secret reaches the sandbox environment; the sandbox is composed into no application | P16 | +| F-12 | Medium | `configured-only` | The sandbox metadata-service deny list is incomplete | non-default metadata endpoints stay reachable; the sandbox is composed into no application | P17 | +| F-14 | Medium | `deployed-now` | The only public control-plane service has no HTTP rate limit and no inbound deadline | an authenticated principal can saturate the pool; unauthenticated callers are rejected first | P19 | +| F-15 | Medium | `configured-only` | The Restate ingress submission carries no request credential | workflow submission trusts placement only; the Cloud profile relies on private networking and the self-hosted profile binds all interfaces | P20 | +| F-16 | Medium | `configured-only` | The repository ships a RuntimeNode credential issuer | the issuer private key lives on Control Plane infrastructure; the CLI is operator-run | source reading | +| F-17 | Medium | `evidence-gap` | The synthetic RuntimeNode authority merges credential classes and drops the binding fence | gateway auth tests do not exercise production separation; the authority is test-only | source reading | +| F-18 | Medium | `deployed-now` | The pi-durable tool path does not cross-check the accepted plan's tool grants | a call can present a grant the plan does not grant | source reading | +| F-19 | Medium | `evidence-gap` | No policy decision point is composed into any application | the authorized-executor path cannot be demonstrated; the deny path is proven | source reading | +| F-20 | Medium | `deployed-now` | Provider contributions and ProjectState items carry free-form content with no secret filter or untrusted-data marking | retrieved content can carry a credential or an instruction | source reading | +| F-21 | Medium | `deployed-now` | The environment and private-file secret providers ignore the `SecretUse` context | a resolved secret is not bound to a workspace or purpose | source reading | +| F-22 | Medium | `configured-only` | The Hosted server profile composes no credential vault; the host-secure provider has no resolver | the self-hosted server profile has no dynamic credential lifecycle | source reading | +| F-23 | Low-Medium | `defense-in-depth` | The spawn-policy check is not pinned to the executed path | a symlink swap race could launch an unlisted executable; it requires local write access to the executable path | source reading | +| F-24 | Medium | `configured-only` | The exported direct-local ACP factory applies no spawn policy by default | an embedder can start any absolute executable at any absolute cwd; the production factory does set a policy | source reading | +| F-25 | Medium | `configured-only` | Marketplace registry egress accepts any HTTPS URL with no metadata-range denial | a misconfigured registry URL can reach a metadata endpoint; the base URL is operator-controlled | source reading | +| F-26 | Medium | `configured-only` | The Restate interaction handler resolves by name without a responding-principal check | an approval delivered over the workflow ingress is not re-authorized on that path | source reading | +| F-28 | Medium | `configured-only` | Staging and production share one R2 bucket credential slot, separated by prefix only | the asserted environment credential separation is a convention | source reading | +| F-27 | Low-Medium | `deployed-now` | Tool rate-limit events are only deleted for the tuple being checked | unbounded row growth for abandoned tuples | source reading | +| F-29 | Low-Medium | `defense-in-depth` | The pi-durable adapter creates its own data directory without an owner-only mode | the guarantee is inherited from an ancestor the composition happens to create owner-only, not enforced by the component that owns the data | P25 | ### 3.1 Final-candidate re-check against every available head | + +Every branch in the fleet's shared origin was compared against the audited head for +the files each finding depends on. The comparison covered +`packages/remote-control-relay/src`, `packages/credential-vault/src`, +`packages/sandbox/src`, `packages/deployment/src/checkpoint.ts`, +`packages/profile-portability/src`, `packages/secrets/src`, +`apps/control-api/src/auth`, `apps/runtime-gateway/src`, +`apps/workflow-worker/src/runtime-attempt-router.ts`, +`apps/workflow-worker/src/managed-pi-remote-command.ts`, `packages/execution-plan/src`, +`packages/telemetry/src`, `packages/events/src`, +`packages/sqlite-persistence/src/provider.ts`, `packages/context/src`, +`packages/domain/src/project-state.ts`, `packages/memory-writeback/src`, +`apps/control-api/src/pi-durable/current-tool-authority.ts`, +`apps/control-api/src/executions/execution-acceptance.service.ts`, +`apps/control-api/src/application.ts`, `apps/control-api/src/marketplace/registry.ts`, +`packages/workflow-runtime/src/restate-endpoint.ts`, +`scripts/runtime-node-identity-admin.mjs`, `apps/local-control-plane/src/acp-runtime.ts`, +`packages/acp-adapter/src`, `packages/managed-pi-adapter/src`, +`packages/deployment/src/process-runtime.ts` and +`apps/hosted-control-plane/src/composition.ts`. + +Result: **no owner fix for any of the 28 findings exists on any currently available +head.** Four files differ on unmerged branches, and in every case the difference is +cosmetic or belongs to an unrelated change: + +| File | Branch | Difference | Effect on a finding | +| ---------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `packages/remote-control-relay/src/relay.ts` | `origin/test/m612-tool-replay-qualification` | the same `#612` byte-format fix under different variable names | none; the processor still adds no provenance check of its own, so F-01 and F-02 stand | +| `packages/deployment/src/checkpoint.ts` | `origin/test/m612-tool-replay-qualification` | the same `#612` digest-format wording | none; the secrets directory is still copied, so F-08 stands | +| `packages/profile-portability/src/migration.ts` | `origin/test/m612-tool-replay-qualification` | record-equivalence comparison refactor | none; F-10 stands | +| `packages/sqlite-persistence/src/provider.ts` | `origin/test/m612-tool-replay-qualification` | scope-trigger statements moved | none; F-09 stands | +| `apps/workflow-worker/src/runtime-attempt-router.ts` | `origin/w1/cp1048-restore-generation-authority-repair` | the per-model harness map from #678 | none; the grant-state check is unchanged, so F-03 stands | + +The verification step required by the issue — "verify owner fixes on their new exact +heads after they are available" — therefore has nothing to verify yet. It stays open +until an owner lands a fix and records its exact head. + +### 3.2 Calibration review + +Root reviewed the counterevidence for the findings this revision originally stated +too strongly. Each counterevidence item was checked against the exact source before +any wording changed, and the finding was rewritten to state what the code does and +what that makes reachable — never a deployed impact the source contradicts. + +| Finding | Was | Now | Counterevidence accepted | Exact source checked | +| ------- | ----------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| F-01 | Critical | Medium | The relay-client authentication obligation is documented and the adapters are opt-in; no concrete configured attacker-controlled ingress supports a deployed unauthenticated-approval claim | `docs/remote-control-relay.md`; `apps/local-control-plane/src/composition.ts:152,249-250,508-512`; `apps/hosted-control-plane/src/composition.ts:164,220-223,371-375`; `packages/remote-control-relay/src/relay.ts:45` | +| F-02 | High | Medium | `create_execution` uses the normal CommandInbox idempotency, so the generic callback-before-receipt ordering does not establish that all three processors duplicate live effects; and receipt-before-effect would be unsafe | `apps/control-api/src/executions/execution-acceptance.service.ts:51,58`; `packages/domain/src/command-inbox.ts:53-58,684-705`; `packages/remote-control-relay/src/adapter.ts:37,79` | +| F-04 | Medium-High | Low | Ordinary thrown errors are sanitized and the Control API vault denies leases without a policy decision point, so this is a bounded library callback-result egress contract gap, not demonstrated deployed credential leakage | `packages/credential-vault/src/vault.ts:456-467,550-557`; `apps/control-api/src/cloud-composition.ts:100-104`; `packages/credential-vault/src/vault.ts:326-332` | +| F-05 | Medium-High | Medium | The Local profile binds loopback by default and the bearer credential is an owner-only file for the trusted local product, so the finding is an authorization-completeness gap | `apps/local-control-plane/src/index.ts:82-88`; `apps/control-api/src/auth/private-api-authentication.ts:14-43`; `apps/control-api/src/auth/service-authentication.ts:310-313` | +| F-09 | Medium | Low | The parent data directory is `chmod 0700` before SQLite and its WAL/SHM sidecars are created, so the sidecar mode alone does not establish that another UID can read; no supported path defeats the parent traversal control | `packages/sqlite-persistence/src/provider.ts:209-210,211,216,221` | +| F-03 | High | High | Severity unchanged; the previous revision cited a module that does not exist. The managed-Pi direct driver is `packages/managed-pi-adapter/src/index.ts:322` | `packages/managed-pi-adapter/src/index.ts:322`; `packages/acp-adapter/src/index.ts:69`; `packages/acp-adapter/src/acp-driver-execution.ts` | + +The probe file was corrected with the findings. `CP190-P09` and `CP190-P10` named a +module that does not exist and therefore reported a missing grant check for a +missing file; they now read `packages/managed-pi-adapter/src/index.ts` and fail +loudly when a named file is missing or empty, so a nonexistent path can never be +reported as a finding. `CP190-P05` now asserts both halves of the boundary — a +returned `Error` carrier is not screened while a thrown error is sanitized to +`PROVIDER_OPERATION_FAILED`. `CP190-P13` now records the mitigating parent-directory +control next to the non-owner-only sidecar mode. `CP190-P02`, `CP190-P03` and +`CP190-P04` now carry their reachability bounds and no longer imply a deployed +unauthenticated approval path or a universal duplicate-effect path. + +### 3.3 Round two — reachability traced to the authority and effect path + +Every finding was traced from its public entrypoint through the authority check and +the effect, to the point where a control is actually applied. Five findings were +restated with that traced path, F-09 was split so that two different SQLite +directories are no longer described as one observation, CP190-F29 was added for the +adapter-owned directory, and five static reachability probes were added. + +| Finding | What the trace changed | Decisive source | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| F-01 | The earlier wording implied a live approve, deny or cancel effect inside the Control Plane. The trace shows the opposite: the Control Plane ships **no metadata effect implementation at all** — the effect is an injected callback — and no application constructs the relay host adapter or the metadata processor. | `packages/remote-control-relay/src/relay.ts:376-379`; `packages/remote-control-relay/src/adapter.ts:154-173`; `packages/remote-control-relay/src/relay.ts:45`; `apps/local-control-plane/src/composition.ts:152,249-250,508-512`; `apps/hosted-control-plane/src/composition.ts:164,220-223,371-375` | +| F-02 | The idempotency is not uniform across payload types. `create_execution` is idempotent in the CommandInbox because the acceptance request extends the context schema that carries `idempotencyKey`; `submit_input` carries **no** idempotency key at all; the metadata effect is an injected callback with no shipped implementation. | `packages/contracts/src/control-api.ts:45,668`; `packages/remote-control-relay/src/execution.ts:38-52,209-223,260-268`; `apps/control-api/src/executions/execution-acceptance.service.ts:51,58`; `packages/domain/src/command-inbox.ts:53-58,684-705` | +| F-03 | The direct-local bound is **operator configuration, not a grant**. The managed-Pi child runs in a per-attempt directory under the data directory and the composed spawn policy names only `allowedExecutables`, so the working-directory dimension is unconstrained; the ACP child instead runs in an operator-configured absolute directory. | `packages/managed-pi-adapter/src/index.ts:322`; `packages/managed-pi-adapter/src/process-client.ts:288-298`; `apps/local-control-plane/src/managed-pi-runtime.ts:66`; `apps/local-control-plane/src/index.ts:300`; `packages/deployment/src/process-runtime.ts:10`; `apps/local-control-plane/src/installed-acp-runtime.ts:88-95` | +| F-04 | A positive control the earlier revision missed: the one composed leased path **normalizes** the callback result and rejects a non-plain carrier before the vault screen can miss it. The gap is therefore confined to the two callers that return a raw result, and neither is composed. | `packages/model-gateway/src/selection-service.ts:269,341-380`; `packages/credential-vault/src/tool-broker.ts:91-99`; `packages/tool-execution/src/mcp-adapter.ts:257-277`; `apps/control-api/src/cloud-composition.ts:100-104` | +| F-09 / F-29 | One observation was two directories. The provider creates and restricts its parent to `0700` **before** any file exists, so it has no traversal path. The pi-durable adapter creates its own directory with no mode and never restricts the database file, so its protection is inherited from an ancestor. | `packages/sqlite-persistence/src/provider.ts:209-210,211,216,221`; `packages/pi-durable-adapter/src/journal.ts:31`; `packages/pi-durable-adapter/src/lease.ts:20`; `apps/control-api/src/pi-durable/node-composition.ts:89`; `apps/control-api/src/models/production-model-composition.ts:230` | + +### 3.4 Detail for the findings whose severity, reachability or wording changed + +**F-01 — The relay host processor applies no authentication of its own (Medium, +`configured-only`).** +The metadata command schema +(`packages/remote-control-relay/src/protocol.ts:68-81`) is a `.strict()` object with +exactly `schemaVersion, workspaceId, hostId, commandId, operation, issuedAt, +expiresAt, targetId` — no signature, MAC, nonce, sequence or key field — and +`RelayMetadataCommandProcessor.process` +(`packages/remote-control-relay/src/relay.ts:379-424`) validates only the schema, the +`hostId`/`workspaceId` equality and the time bounds before calling the injected +`accept` (`:376-379`). The obligation is documented and delegated: +`docs/remote-control-relay.md` states that metadata-only operations use a separate +strict, authenticated command schema and that "authentication of that metadata +transport remains the responsibility of the concrete relay client". The traced +reachability is what bounds this finding: the only consumer of a metadata command is +`RemoteControlHostAdapter.#pollOnce` +(`packages/remote-control-relay/src/adapter.ts:154-173`), the adapter itself is an +injected option of both compositions +(`apps/local-control-plane/src/composition.ts:249-250,508-512`, +`apps/hosted-control-plane/src/composition.ts:220-223,371-375`) reported as +`'disabled'` when absent (`:152`, `:164`), **no application constructs +`RelayMetadataCommandProcessor` or `RemoteControlHostAdapter`**, and the only +`OutboundOpaqueRelayClient` implementation is the test fake `FakeOpaqueRelay` +(`packages/remote-control-relay/src/relay.ts:45`). Because the effect is an injected +callback, **the Control Plane ships no approve, deny or cancel effect at all** — +those are schema operation names, not shipped behaviour. An integrator must supply +the relay client, the effect and the adapter. **No deployed attacker-controlled +ingress exists, so no deployed unauthenticated approval path is claimed.** What +remains is that the processor adds no provenance check of its own. + +**F-02 — The relay receipt layer adds no idempotency beyond the command identifier +(Medium, `configured-only`).** +The receipt is keyed by `commandId` and compared byte for byte +(`packages/remote-control-relay/src/relay.ts:400-409`), and the only receipt +repositories in the package are in-memory +(`packages/remote-control-relay/src/relay.ts:227,343`, +`packages/remote-control-relay/src/execution.ts:135`), while +`docs/remote-control-relay.md` already records the durable repository as a +composition requirement. The effect-before-receipt order is correct and must not be +inverted: a receipt written first would record an effect that never happened. What +the trace adds is that the durable guarantee is **not uniform across payload +types**. `create_execution` parses `ExecutionAcceptanceRequestSchema` +(`packages/remote-control-relay/src/execution.ts:209-223,260-268`), which extends +`CommandContextSchema` and therefore carries the caller's `idempotencyKey` +(`packages/contracts/src/control-api.ts:45,668`); the acceptance service then admits +it through the CommandInbox +(`apps/control-api/src/executions/execution-acceptance.service.ts:51,58`, +`packages/domain/src/command-inbox.ts:53-58,684-705`), so a redelivery is deduped +there. `submit_input` carries **no** idempotency key +(`packages/remote-control-relay/src/execution.ts:38-52`), so its dedupe must come +from the interaction domain. The metadata operations have no shipped effect at all. +So the finding is precisely: the relay's own receipt layer adds no idempotency +beyond the command identifier, the two writes are not atomic, and the durable +guarantee for the metadata and input paths is inherited from a callback and a domain +this package does not own. + +**F-03 — The direct-local path never consults a grant (High, `deployed-now`).** +The grant exists only as an opaque reference plus a tri-state +(`packages/runtime-gateway-protocol/src/protocol.ts:562-567`, +`packages/acp-adapter/src/acp-gateway-types.ts:18`), and the grant checks live only in +the _gateway_ drivers +(`packages/managed-pi-adapter/src/managed-pi-gateway-driver.ts:108-120`, +`packages/acp-adapter/src/acp-gateway-driver.ts:127-139`). The direct driver is +`ManagedPiDriver` (`packages/managed-pi-adapter/src/index.ts:322`): it parses the +plan, enforces the runtime version range and the capability evaluation, and forwards +to the client, with no grant reference and no path check anywhere in it. The traced +path shows what actually bounds the child: the managed-Pi process runs with its +working directory set to a per-attempt directory under the data directory +(`apps/local-control-plane/src/managed-pi-runtime.ts:66`; +`packages/managed-pi-adapter/src/process-client.ts:288-298`), created `0700` with the +system prompt written `0600`, and the composed spawn policy names only +`allowedExecutables` (`apps/local-control-plane/src/index.ts:300`) — and +`NodeProcessSpawnPolicy` documents that "absent constraints leave the corresponding +dimension unconstrained" (`packages/deployment/src/process-runtime.ts:10`), so the +working-directory dimension is unconstrained by policy. The ACP path differs: the +child runs in the operator-configured absolute `CONTROL_PLANE_CODEX_ACP_CWD` and the +policy mirrors that directory +(`apps/local-control-plane/src/installed-acp-runtime.ts:88-95`), with absolute-path +validation as the only constraint. The only production writer of the grant state is a +hard-coded constant (`apps/runtime-gateway/src/runtime-inventory-ingestion.ts:387`) +consumed at `apps/workflow-worker/src/runtime-attempt-router.ts:179-184`. The +accurate statement is therefore that **the direct-local working directory is bounded +by operator configuration, not by a grant**, that the grant carries no path +semantics so `STA-L42-10` has nothing to attach to, and that the Control Plane +mediates no filesystem operation on the harness's behalf — the ambient-access denial +that `STA-L17-01` also requires remains a desktop-host obligation outside this +repository. + +**F-04 — The returned-value screen misses non-plain carriers (Low, +`library-contract`).** +`packages/credential-vault/src/vault.ts:456-467` screens the value a leased callback +**returns** with `containsSecret`, and `containsSecret` (`:550-557`) walks own +enumerable string-keyed properties only, so a returned `Error`, `Map`, `Set` or byte +container carrying the secret is not detected. Ordinary thrown errors are already +sanitized: the `catch` at `:462-466` converts any non-`CredentialVaultError` throw +into `PROVIDER_OPERATION_FAILED`. The trace adds the control the earlier revision +missed: the one composed leased path is the model-selection service, which returns +`safeCredentialResult(await operation(secret))` +(`packages/model-gateway/src/selection-service.ts:269`), and `safeCredentialResult` +(`:341-380`) rejects any object whose prototype is not `Object.prototype` or `null`, +rejects symbol keys, rejects sparse arrays and rejects cycles — so an `Error`, a +`Map`, a `Set` or a typed array cannot survive that path. The two callers that +return the raw result are the credential broker +(`packages/credential-vault/src/tool-broker.ts:91-99`) and the MCP adapter +(`packages/tool-execution/src/mcp-adapter.ts:257-277`, which reduces thrown transport +errors to a bounded code but returns the invoke output raw); **neither is composed +into any application**, and no application composes a `VaultToolCredentialBroker`. The +composed profiles also do not lease: the Control API vault composes no policy +decision point (`apps/control-api/src/cloud-composition.ts:100-104`), so leases deny +before a secret is resolved (`packages/credential-vault/src/vault.ts:326-332`). +**No deployed credential leakage is claimed or demonstrated.** + +**F-09 — SQLite sidecar modes in the provider (Low, `defense-in-depth`).** +`packages/sqlite-persistence/src/provider.ts:209-210` creates the parent directory +and `chmod`s it to `0700` **before** the database is opened at `:211` and before the +WAL pragma at `:216`; `:221` then restricts the main file to `0600`. The sidecars +therefore keep the pre-restriction mode, but **another UID cannot traverse the +`0700` parent, and no supported path defeats that control**, so the sidecar mode +alone does not establish a cross-UID read. The residual is defence in depth: the +sidecar files are not owner-only, so a later relaxation of the directory mode, a copy +of a sidecar on its own, or a backup that captures a sidecar would expose recent +writes that the `0600` main file would have protected. The adapter-owned directory is +a different observation and is reported separately as F-29. + +**F-29 — The pi-durable adapter creates its own directory without an owner-only mode +(Low-Medium, `defense-in-depth`).** +`packages/pi-durable-adapter/src/journal.ts:31` and +`packages/pi-durable-adapter/src/lease.ts:20` create their directories with +`mkdirSync(..., { recursive: true })` and **no mode**, so the directory takes the +process umask default, and the database file is never `chmod`ed. +`apps/control-api/src/pi-durable/node-composition.ts:89` does the same for the +admission database. In the composed managed-cloud path an ancestor happens to be +created owner-only (`apps/control-api/src/models/production-model-composition.ts:230`), +which blocks traversal today — so the guarantee is **inherited from an unrelated +composition rather than enforced by the component that owns the data**. Any other +wiring would place a `0644` database in a `0755` directory, which is a supported +traversal path; no such wiring ships, so this is defence in depth rather than a +demonstrated cross-UID read. + +**F-13 — There is no sandbox boundary in any shipped profile (High, `evidence-gap`).** +`@control-plane/sandbox` is a root dev dependency only; no application or runtime +package depends on it, and `SandboxCoordinator` appears nowhere outside the package +and its tests. The package has no `@e2b/*` dependency +(`packages/sandbox/package.json:24-27`) and only a provider-neutral contract, an +adapter over an `E2bClientPort`, and fakes. `docs/sandbox.md:12-18` already states +that the in-process network check is a precheck and that provider-level isolation is +the required enforcement boundary. `STA-L77-01` and `STA-L83-02` therefore have no +implementation to assess, and the live E2B gate stays unproven. + +## 4. Reproducible regression packet + +Each entry names the owner, the existing file in that owner's package where the +regression belongs, the failing condition expressed with credential-free synthetic +inputs, and the assertion the owner must add. The **proposed file** column names +where the owner would add the test; it is a proposal, not a custody claim — no +existing test file is edited by this lane, and the owner must confirm custody of +its own package before editing. + +The `CP190-Pnn` identifiers are the probes in +`scripts/security-trust-boundary-probes.mjs`; running that script today prints +`25 open, 0 resolved, 0 harness failures`. Fifteen probes execute a synthetic +runtime path and ten read composed source; none performs a live call, reads a +credential or contacts a deployed profile. Every probe result is a **synthetic +demonstration** of a code-level property; none of them is a deployed observation, +and the report claims no deployed behaviour anywhere. + +| ID | Owner | Proposed regression file | Failing condition (credential-free synthetic) | Assertion the owner must add | +| ------------------- | -------------------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| F-01 / P01-P02, P21 | W1 | `packages/remote-control-relay/src/index.test.mjs` | Submit a `RelayMetadataCommand` with only the eight schema fields and `operation: 'approve'` for a known `targetId` | The processor must not accept a metadata command its caller has not authenticated; a composition must refuse to start a relay adapter whose client declares no authentication | +| F-02 / P03-P04, P22 | W1 | `packages/remote-control-relay/src/index.test.mjs` | Re-submit the same logical effect under a new `commandId`; or make the receipt store's write fail | The effect and its receipt must be indivisible; a receipt-write failure must leave the effect unapplied; a receipt written before the effect is **not** an acceptable repair | +| F-03 / P08-P10, P24 | W1 | `packages/managed-pi-adapter/src/index.test.mjs`, `packages/acp-adapter/src/gateway.test.mjs` | Drive a direct-local dispatch with no grant, a revoked grant, and a symlinked path that escapes the grant root | The direct-local driver must require a current grant and derive the working directory from it; a revoked or absent grant and an escaping symlink are rejected | +| F-04 / P05, P23 | W1 — **custody pending, see §7** | `packages/credential-vault/src/index.test.mjs` | Lease a credential and **return** `new Error(secret)`; separately, throw the same error | The returned-value screen must traverse `Error` message, stack and cause, `Map`, `Set`, `Buffer` and typed arrays, or reject a non-plain result; a thrown error must keep surfacing `PROVIDER_OPERATION_FAILED`; the model-selection normalizer must keep rejecting a non-plain carrier | +| F-05 / P07 | W1 | `apps/control-api/src/application.test.mjs` | Call the Local API with a valid token and a foreign `workspaceId` | The Local principal must be bound to an authorized workspace and project set and compared against the request | +| F-06 / P06 | W1 | `packages/telemetry/src/index.test.mjs` | `redactTelemetryValue({ sessionToken: canary })` | `sessionToken`, `passphrase`, `bearerToken`, `connectionString` and `apiSecret` must be redacted, or the production value-based canary guard must run at every sink | +| F-07 / P11 | W1 | `apps/runtime-gateway/src/context-command-delivery.test.mjs` | Enqueue a runtime command with a one-year lifetime | The runtime family must apply the same 24-hour bound and reject with `EXPIRY_TOO_LONG` | +| F-08 / P12 | W1 | `packages/deployment/src/checkpoint.test.mjs` | Checkpoint a data directory that contains `secrets/provider-key` | The checkpoint must exclude or seal the secrets directory; the cleartext must not appear in the copy | +| F-09 / P13, P25 | W1 | `packages/sqlite-persistence/src/index.test.mjs` | Open a provider in a 0755 directory and inspect the sidecar modes | The `-wal` and `-shm` sidecars must be mode 0600, so the file mode does not depend on the parent directory mode | +| F-10 / P14-P15 | W1 | `packages/profile-portability/src/credentials.test.mjs` | Import a manifest whose record value carries a secret under a non-prohibited key; export a manifest with a foreign workspace's secret reference | The import path must scan with the export's sensitive values, and secret references must carry a workspace scope | +| F-11 / P16 | W1 | `packages/sandbox/src/index.test.mjs` | Execute a sandbox command with `credentialBindings: [{ leaseId, environmentName: 'AWS_SESSION' }]` | The ambient credential denial must apply to the effective environment including credential bindings | +| F-12 / P17 | W1 | `packages/sandbox/src/index.test.mjs` | Execute a sandbox command that targets `169.254.170.2` | The metadata denial must cover the link-local range and the known provider endpoints | +| F-13 / P18 | W1 + operations | `packages/sandbox/src/index.test.mjs` | Compose a hosted profile | A hosted composition must resolve a sandbox provider, and a composition without one must fail closed; the live E2B template, network, resource and cleanup evidence must be attached | +| F-14 / P19 | W1 | `apps/control-api/src/application.test.mjs` | Send concurrent authenticated requests to the public API | A per-principal quota and an inbound deadline must exist; an over-quota request must get a bounded `429` without reaching the database | +| F-15 / P20 | W1 + deployment | `apps/control-api/src/application.test.mjs` | Submit a workflow without the Restate request identity | The ingress submission must be authenticated, and the self-hosted ingress must bind to the private interface | +| F-16 | W1 | `scripts/runtime-node-identity-admin.test.mjs` | Inspect the production Control Plane image | The image must not contain an issuer private key path; issuance must live on a separate operator surface | +| F-17 | W1 | `apps/runtime-gateway/src/authentication.test.mjs` | Present a device-signed credential with an issuer key id | The synthetic authority must sign with a distinct issuer key and enforce the node, workspace and generation fence | +| F-18 | W1 | `apps/control-api/src/pi-durable/current-tool-authority.test.mjs` | Submit a tool call whose grant names a tool absent from the accepted plan | The pi-durable path must cross-check `plan.constraints.tools.grants` | +| F-19 | W1 + Cedar policy owner | `packages/policy/src/index.test.mjs` | Compose a profile that leases a connector credential | A composed profile must resolve a decision point, and the permit matrix must be recorded | +| F-20 | W1 | `packages/context/src/provider.test.mjs` | Admit a provider contribution whose content carries a canary | Provider and ProjectState content must be filtered and marked as untrusted data | +| F-21 | W1 | `packages/secrets/src/index.test.mjs` | Resolve the same reference under two different `use` contexts | Every provider must accept and enforce the `SecretUse` context | +| F-22 | W1 + desktop owner | `apps/hosted-control-plane/src/index.test.mjs` | Compose the hosted server profile; resolve a packaged Local handle | The hosted server profile must resolve a credential lifecycle; packaged Local must resolve an opaque OS-secure handle | +| F-23 | W1 | `packages/deployment/src/process-runtime.test.mjs` | Swap the allowlisted executable for a symlink after the check | The spawn must execute the resolved canonical path with a device and inode re-check | +| F-24 | W1 | `apps/local-control-plane/src/acp-runtime.test.mjs` | Create the exported ACP factory without a spawn policy | The factory must require an explicit spawn policy and fail closed without it | +| F-25 | W1 | `apps/control-api/src/marketplace/registry.test.mjs` | Point the marketplace registry base URL at a metadata address | The registry fetch must deny the link-local and metadata ranges | +| F-26 | W1 | `packages/workflow-runtime/src/restate-endpoint.test.mjs` | Deliver an interaction response from a principal outside `allowedPrincipalIds` | The workflow ingress must re-check the responding principal before resolving | +| F-27 | W1 | `packages/database/src/tool-rate-limiter.test.mjs` | Stop calling a principal, tool or operation tuple and wait out the window | The retention sweep must remove expired `tool_rate_limit_events` rows for abandoned tuples | +| F-28 | W1 + operations | `tests/railway-production-plan.test.mjs` | Use the staging credential against a production object key | Staging and production must hold distinct bucket-scoped R2 credentials; cross-environment reads must be denied | +| F-29 / P25 | W1 | `packages/pi-durable-adapter/src/journal.test.mjs` | Construct the adapter with a fresh directory under a 0755 parent | The created directory must be 0700 and the database file 0600; the adapter must not inherit the guarantee from an ancestor | + +## 5. What the audit confirmed as sound + +These are recorded so the next reviewer does not re-derive them. + +- **HPKE envelopes (`packages/remote-control-relay/src/crypto.ts`).** RFC 9180 Base + mode with `DHKEM(X25519, HKDF-SHA256)`, `HKDF-SHA256` and `AES-128-GCM`. The AAD + (`packages/remote-control-relay/src/protocol.ts:132-151`) binds envelope version, + suite, key id, workspace, host, command id, payload type and schema version, + issued and expiry times, and the content digest. Expiry is checked before + decryption, the 1 MiB cap is exact, wrong recipient, tamper, downgrade and digest + mismatch all fail closed, and the ciphertext cap is enforced on both sides. The + only HPKE weakness found is the replay _window_, which is bounded by F-02, not the + envelope itself. +- **Supply chain.** Frozen lockfile with integrity hashes, digest-pinned base images + and GitHub Actions, a pinned patched dependency with upstream provenance in + `patches/README.md`, a Trivy image scan with a CycloneDX SBOM before publication, + build-provenance attestation, least-privilege workflow permissions, + `persist-credentials: false` on every checkout, and `pull_request_target` + workflows that never check out PR code. +- **Deny-by-default policy.** `CedarPolicyDecisionPoint` + (`packages/policy/src/index.ts:209-248`) denies on an unmapped action, a + workspace mismatch, a missing or revoked policy, an evaluator failure, and a + forbidden override. The credential vault denies leases outright when no decision + point is composed (`packages/credential-vault/src/vault.ts:326-332`). The gap is + that no profile composes one (F-19). +- **Child-plan privilege narrowing.** `packages/execution-plan/src/index.ts:280-340` + and `:1162-1210` reject every expansion dimension, and + `assertExecutionPlanDerivedFrom` re-derives and compares on persistence. +- **Memory-write safety.** `packages/memory-writeback/src/index.ts:208-215` defaults + to `disabled`; approval is exhaustive (`:480-506`) and ambiguous writes park in + `reconciliation_required` instead of retrying. +- **Gateway channel hardening.** Ed25519 credential plus device proof, a one-time + transactional consumption fence bound to node/workspace/generation, durable + revocation re-checked on every command, strict frame-size and backpressure limits, + and workspace/node binding on every frame. +- **Existing probes.** `tests/m11-security-probes.test.mjs` passes at this head + (42 tests, 129 assertions, seed 1104). It covers policy denial, grant-handle + shape, secret-file traversal and symlink escape, spawn-policy limits, object-store + content addressing, sandbox network and metadata denial, and HPKE tamper, expiry + and cap behaviour. It does **not** cover any of the 28 findings in §3, which is + why the dedicated probe file was necessary. +- **Relay ordering.** The effect-before-receipt order in the relay processors is + correct and must not be inverted: a receipt written before the effect would record + an effect that never happened. The defect in F-02 is that the two writes are not + atomic, not their order. +- **Relay idempotency for `create_execution`.** The acceptance request extends the + context schema that carries `idempotencyKey` + (`packages/contracts/src/control-api.ts:45,668`), so the caller's key reaches the + CommandInbox (`packages/domain/src/command-inbox.ts:53-58,684-705`) and a + redelivery is deduped there. +- **The composed leased path.** The model-selection service normalizes the leased + callback result and rejects a non-plain carrier before the vault screen can miss + it (`packages/model-gateway/src/selection-service.ts:269,341-380`), and thrown + errors are already sanitized to `PROVIDER_OPERATION_FAILED` + (`packages/credential-vault/src/vault.ts:462-466`). +- **Local SQLite file permissions.** The provider creates and restricts its parent + directory to `0700` before the database and its sidecars exist + (`packages/sqlite-persistence/src/provider.ts:209-210`), so there is no supported + traversal path to the sidecars. +- **The direct-local managed-Pi working directory.** The child runs in a per-attempt + directory created `0700` under the data directory with the system prompt written + `0600` (`packages/managed-pi-adapter/src/process-client.ts:288-298`), and the + harness is launched with `--no-tools --no-extensions --no-skills + --no-context-files --no-approve`. + +## 6. Targeted evidence questions + +Each question is the specific external fact this lane could not establish from the +repository, with the owner who holds it. They are the fastest route to closing the +calibrated findings; none of them requires a credential. + +| ID | Finding | Question | Why it decides the finding | Owner | +| ----- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| Q-F04 | F-04 | Which profile, if any, is expected to lease a connector credential in the release candidate, and what result shapes can a tool executor return from a leased callback? | The returned-value screen only matters once a profile composes a policy decision point. The answer sets whether this is a release blocker or a library hardening item | W1 (profile compositions) with the credential-vault owner | +| Q-F09 | F-09 | Does any supported deployment capture the SQLite `-wal` or `-shm` sidecar independently of the database file, and can the data directory mode be relaxed by configuration? | The parent is `0700` before creation, so the sidecar mode is defence in depth. It becomes material only if a backup or a configuration change separates a sidecar from its parent | W1 (`packages/sqlite-persistence`) with the operations owner | +| Q-F01 | F-01 | Which concrete relay client will be configured for a release profile, and how does that client authenticate the transport and the store it reads from? | The documented delegation is only as strong as the concrete client. Until one is named, the host processor's missing check is a defence-in-depth gap and not a deployed path | W1 with the Agent HQ product relay owner | +| Q-F02 | F-02 | Does the injected metadata `accept` callback provide its own idempotency for approve, deny, cancel and resume, and is the command-result repository bound to the durable persistence provider in the intended composition? | `create_execution` is already idempotent through the CommandInbox. The receipt path matters for the metadata operations, whose effect idempotency is the injected callback's property | W1 (`packages/remote-control-relay`) | +| Q-F03 | F-03 | Where is the `LocalProjectGrant` issued and validated on the product side, and which Control Plane component is intended to consume it for a direct-local dispatch? | The Control Plane has no grant authority of its own. The answer decides whether the fix is a grant check in the direct drivers or a different composition boundary | W1 with the Agent HQ desktop owner | +| Q-F05 | F-05 | Which component is authoritative for the Local workspace and project authorization set that the private API should compare a request against? | The hosted path compares signed claims. The Local path needs the equivalent authoritative set before the body-derived scope can be replaced | W1 (`apps/control-api` authentication) with the Agent HQ product owner | +| Q-F13 | F-13 | Which profile is expected to compose a `SandboxProvider`, and what E2B template, network, resource and cleanup policy is the intended baseline? | Without a composed provider there is no sandbox boundary to accept, and the live E2B gate cannot be closed | W1 (profile composition) with the operations owner | +| Q-F20 | F-20 | Is the value-based `SecretCanaryGuard` intended to run at every content sink, and what is the authoritative protected-value set? | The production guard exists but has no call site, and the key-name redaction misses compound credential names. The answer decides filter placement and ownership | W1 (`packages/telemetry`, `packages/events`, `packages/context`) | +| Q-F29 | F-29 | Which composition owns the pi-durable data directory, and is it ever created outside an owner-only ancestor? | The adapter inherits the guarantee today. If a profile can point the adapter at a directory that is not inside an owner-only ancestor, the adapter-owned directory becomes a supported traversal path | W1 (`packages/pi-durable-adapter` and the `apps/control-api` pi-durable composition) | + +## 7. F-04 implementation ownership — exact-path custody result + +This lane does **not** own the F-04 repair and made no change to the proposed paths. +The result below is a custody _observation_, reported for root to verify and admit. + +| Proposed path | Purpose of the future edit | +| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `packages/credential-vault/src/vault.ts` | Extend the returned-value egress screen so it descends into `Error`, `Map`, `Set` and byte containers, or reject a non-plain result | +| `packages/credential-vault/src/index.test.mjs` | Add the owner regression test: a leased callback that returns `new Error(secret)` must raise `SECRET_EGRESS_BLOCKED`, and a leased callback that throws must still surface the bounded `PROVIDER_OPERATION_FAILED` | + +Custody check performed 2026-10-21: + +- shared cross-fleet index `/tmp/opencode-three-fleet-20261020-fresh/data/cross-fleet-ownership.json` + (SHA-256 `561ccd22bdd6d5a4abb1ebbecc489d8ee055c046074df680262547ad52784f05`): **0 + occurrences** of either proposed path, and 0 occurrences of `packages/credential-vault` + or `credential-vault` anywhere in the index; +- all twelve open pull requests (1048, 1204, 1210, 1214–1220, 1222, 1223): **0** + touch either proposed path. Several branches change + `packages/credential-vault/CHANGELOG.md` and `packages/credential-vault/package.json` + for release metadata; none changes the two proposed source or test paths; +- every registered worktree, including the fleet and Fleet 1 checkouts: **0** dirty or + untracked path under `packages/credential-vault`. + +Result: **no exact-path owner found** for either proposed path. Absence of an +exact-path claim is **not** ownership approval. Root must verify and admit the exact +paths before any edit. This lane will not touch either path, and it will not touch +`packages/sqlite-persistence`, until that admission exists. + +## 8. Unresolved gates — the reasons acceptance stays held + +1. **No permitted live profile evidence.** The managed cloud, Local and self-hosted + profiles have no permitted evidence in this lane. Whole-cloud and whole-runtime + trust acceptance stays held until it exists. +2. **Agent HQ owns the other half of the trust model.** LocalProjectGrant issuance, + desktop key storage, Event Gateway authorization and replay, durable + `ContentSyncDevice`/`ContentKeyEpoch` flows, and the product relay's own + credential are product-owned. The inventory already records them as `NONE-AHQ`; + this audit cannot close them. +3. **No live E2B sandbox.** F-13 leaves the sandbox boundary as a contract only. +4. **No live managed-cloud vault rotation and restore evidence** for the selected + credential-vault provider. +5. **No packaged Local OS-secure handle resolution** and no packaged binary signing + or rollback evidence (F-22, and the inventory's `STA-L40-06`/`STA-L40-14`). +6. **No malware scanner composition, quarantine or fail-closed outage evidence.** +7. **No independent human penetration review.** This report is an independent + read-only threat review with synthetic probes. The issue's "independent + security-review evidence" criterion also needs the human penetration pass that + this lane is not authorized to perform. +8. **The reviewer approves none of the routed fixes.** Each routed owner must verify + its own fix on its own exact head, and an independent reviewer must then re-verify + it on that exact head before the finding is closed. +9. **The calibration review did not release any gate.** It narrowed five findings and + corrected one citation. The live-profile, product-owned and human-review gates + above are unchanged, so the acceptance hold is unchanged. + +## 9. Routing + +No source path is taken by this lane. Each finding is routed to the owner that already +holds the affected package, and the routing table is in the JSON artifact +(`findings[].routedTo`). + +| Owner | Findings | +| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| W1 — runtime and profile composition | F-01, F-02, F-03, F-05, F-07, F-08, F-09, F-10, F-11, F-12, F-13, F-15, F-16, F-17, F-18, F-19, F-20, F-21, F-22, F-23, F-24, F-25, F-26, F-28, F-29 | +| W2 — policy, contracts, decision resolution | F-19 (Cedar matrix and the per-profile authorization matrix) | +| W4 — coverage and runner paths | F-27 (retention sweep coverage), plus the probe-to-regression-test conversion for the packages it owns | +| Agent HQ / product owner | F-01 (the product relay's own transport authentication) and the `NONE-AHQ` controls in §8.2 | +| Operations | F-13 (E2B template, network, resource and cleanup thresholds), F-14 (deployed limit values), F-28 (per-environment R2 credentials) | + +### 9.1 Next actions + +1. Root assigns each finding to the owner above and the owner converts the + `CP190-Pnn` probe into a regression test inside the package they own. +2. The owner records the fix head; this reviewer re-verifies each fix on that exact + head and updates this report and the JSON matrix. +3. `scripts/security-trust-boundary-probes.mjs` must be updated in the same change as + any fix that stops a probe from reproducing; a probe that stops reproducing + without that update is a CI-visible drift, not a silent pass. +4. The live profile evidence in §8.1–§8.6 is requested from its owner; until it + exists, `STA-L*` rows marked open in the inventory stay open and the whole-cloud + trust acceptance stays held. +5. The eight questions in §6 are answered by their owners; each answer either + promotes a finding, retires it, or narrows it further. +6. F-04's repair waits for root to verify and admit + `packages/credential-vault/src/vault.ts` and + `packages/credential-vault/src/index.test.mjs` (see §7). This lane will not edit + either path before that admission. diff --git a/docs/evidence/m11-security-trust-boundary-matrix-2026-10-21.v1.json b/docs/evidence/m11-security-trust-boundary-matrix-2026-10-21.v1.json new file mode 100644 index 000000000..a961fcb3b --- /dev/null +++ b/docs/evidence/m11-security-trust-boundary-matrix-2026-10-21.v1.json @@ -0,0 +1,1114 @@ +{ + "schemaVersion": 1, + "artifactId": "cp190-security-trust-boundary-matrix", + "issue": 190, + "milestone": "M19", + "generatedAt": "2026-10-21", + "candidate": { + "repository": "adea-ai/control-plane", + "branch": "f2w4/cp190-trust-boundary", + "head": "27f17bd55e0266439b5b3089163195c9e7a669be", + "mainBase": "6d16a495186d2a4d84d422c936a02b4abdc93928", + "workspace": "1.105.5" + }, + "provenance": { + "author": "Fleet 2 / F2W4 independent reviewer", + "independence": "The reviewer did not author any Control Plane source in this lane and approves none of the routed fixes.", + "requirementsSource": "docs/requirements/security-trust-atomic-inventory.md (Security & Trust Model revision 78, STA-L* atoms)", + "sourceAudit": "docs/requirements/security-trust-source-audit.md (STM-001..STM-035)", + "probeRunner": "scripts/security-trust-boundary-probes.mjs", + "probeCommand": "bun scripts/security-trust-boundary-probes.mjs", + "readOnly": true, + "credentialsRead": 0, + "productionTrafficSent": 0, + "deployedProfileEvidenceUsed": false, + "finalCandidateRecheck": { + "method": "every branch in the fleet's shared origin was compared against the audited head for the files each finding depends on", + "branchesCompared": 45, + "ownerFixesAvailable": 0, + "filesDifferingOnUnmergedBranches": [ + "packages/remote-control-relay/src/relay.ts", + "packages/deployment/src/checkpoint.ts", + "packages/profile-portability/src/migration.ts", + "packages/sqlite-persistence/src/provider.ts", + "apps/workflow-worker/src/runtime-attempt-router.ts" + ], + "findingImpact": "none; every difference is cosmetic or belongs to an unrelated change, so all 28 findings stand on the audited head and on every currently available unmerged branch", + "ownerFixVerification": "pending; no owner fix for any finding exists on an available head yet" + } + }, + "domains": [ + { + "domain": "workspace", + "controls": [ + { + "control": "Service credential scope is compared against signed claims", + "obligations": ["STA-L08-01", "STA-L38-03"], + "entrypoints": ["apps/control-api/src/auth/service-authentication.ts:302-328"], + "evidence": [ + "apps/control-api/src/application.test.mjs:1095", + "apps/control-api/src/application.test.mjs:1154" + ], + "status": "verified" + }, + { + "control": "Local private API derives workspace and project scope", + "obligations": ["STA-L08-01", "STA-L46-06", "STA-L46-17"], + "entrypoints": ["apps/control-api/src/auth/private-api-authentication.ts:86-94"], + "evidence": [], + "status": "finding", + "finding": "CP190-F05" + }, + { + "control": "Durable records carry explicit workspace scope at the database layer", + "obligations": ["STA-L38-01", "STA-L38-06"], + "entrypoints": ["packages/sqlite-persistence/src/migrations.ts:37-154"], + "evidence": ["packages/sqlite-persistence/src/index.test.mjs:337"], + "status": "verified" + }, + { + "control": "Portable export secret references carry no workspace scope", + "obligations": ["STA-L08-01", "STA-L76-03"], + "entrypoints": ["packages/profile-portability/src/manifest.ts:79-90"], + "evidence": [], + "status": "finding", + "finding": "CP190-F10" + } + ] + }, + { + "domain": "project", + "controls": [ + { + "control": "Local filesystem/project operations require a current LocalProjectGrant", + "obligations": ["STA-L17-01", "STA-L42-09", "STA-L69-02"], + "entrypoints": [ + "apps/runtime-gateway/src/runtime-inventory-ingestion.ts:387", + "apps/workflow-worker/src/runtime-attempt-router.ts:179-184", + "packages/managed-pi-adapter/src/managed-pi-gateway-driver.ts:108-120", + "packages/acp-adapter/src/acp-gateway-driver.ts:127-139" + ], + "evidence": [ + "tests/m11-security-probes.test.mjs:504", + "tests/m11-security-probes.test.mjs:511", + "tests/m11-security-probes.test.mjs:518" + ], + "status": "finding", + "finding": "CP190-F03" + }, + { + "control": "Canonical path containment for filesystem/project operations", + "obligations": ["STA-L42-10", "STA-L69-01"], + "entrypoints": [ + "packages/secrets/src/index.ts:137-161", + "packages/deployment/src/process-runtime.ts:58-86", + "packages/object-store/src/filesystem.ts:468-493" + ], + "evidence": [ + "tests/m11-security-probes.test.mjs:335", + "tests/m11-security-probes.test.mjs:385", + "tests/m11-security-probes.test.mjs:409" + ], + "status": "partial", + "finding": "CP190-F03" + } + ] + }, + { + "domain": "runtime", + "controls": [ + { + "control": "Runtime Gateway node identity is operator-signed and verified against durable records", + "obligations": ["STA-L15-01", "STA-L46-01", "STA-L46-04", "STA-L66-01"], + "entrypoints": [ + "apps/runtime-gateway/src/postgres-runtime-node-identity.ts:176-247", + "apps/runtime-gateway/src/authentication.ts:176-271", + "packages/database/src/runtime-node-identity-repository.ts:218-316" + ], + "evidence": [ + "apps/runtime-gateway/src/postgres-runtime-node-identity.test.mjs:182", + "tests/runtime-node-credential-revocation.integration.test.mjs" + ], + "status": "verified", + "note": "The gateway service verifies only. The repository also ships an issuer CLI (CP190-F16)." + }, + { + "control": "Runtime Gateway commands are workspace/node/generation bound on every frame", + "obligations": ["STA-L29-01", "STA-L68-02"], + "entrypoints": [ + "apps/runtime-gateway/src/authentication.ts:117-127", + "apps/runtime-gateway/src/websocket-lifecycle.ts:302-314", + "packages/runtime-gateway-protocol/src/protocol.ts:186-224" + ], + "evidence": ["tests/m5-runtime-gateway.test.mjs"], + "status": "verified" + }, + { + "control": "Remote command expiry is bounded to 24 hours", + "obligations": ["STA-L61-08"], + "entrypoints": [ + "apps/runtime-gateway/src/context-command-delivery.ts:52-53", + "apps/runtime-gateway/src/runtime-command-delivery.ts:69-91" + ], + "evidence": [], + "status": "finding", + "finding": "CP190-F07" + }, + { + "control": "Direct-local execution cannot bypass Control Plane policy because components are co-located", + "obligations": ["STA-L27-03", "STA-L77-01", "STA-L65-01"], + "entrypoints": [ + "apps/local-control-plane/src/acp-runtime.ts:30-53", + "packages/acp-adapter/src/stdio-client.ts:124-137", + "packages/deployment/src/process-runtime.ts:37-87" + ], + "evidence": ["apps/local-control-plane/src/acp-runtime.test.mjs:39"], + "status": "finding", + "finding": "CP190-F24" + } + ] + }, + { + "domain": "session", + "controls": [ + { + "control": "Native external sessions are listed as scoped opaque references", + "obligations": ["STA-L44-05", "STA-L75-01"], + "entrypoints": ["packages/acp-adapter/src/external-sessions.ts"], + "evidence": ["packages/acp-adapter/src/external-sessions.test.mjs:183"], + "status": "verified" + }, + { + "control": "Remote-control interaction responses are principal-checked", + "obligations": ["STA-L42-11", "STA-L66-01"], + "entrypoints": [ + "packages/domain/src/interaction-delivery.ts:36-68", + "packages/workflow-runtime/src/restate-endpoint.ts:56-69" + ], + "evidence": [], + "status": "finding", + "finding": "CP190-F26" + } + ] + }, + { + "domain": "tools", + "controls": [ + { + "control": "Tool invocation requires a matching grant, policy decision, approval and rate limit", + "obligations": ["STA-L42-05", "STA-L42-06", "STA-L42-09", "STA-L42-11"], + "entrypoints": [ + "packages/tool-execution/src/tool-registry.ts:287-307", + "packages/tool-execution/src/tool-execution.ts:117-182", + "packages/tool-execution/src/tool-execution.ts:351-446" + ], + "evidence": [ + "packages/tool-execution/src/tool-execution.test.mjs:581", + "packages/tool-execution/src/tool-execution.test.mjs:613" + ], + "status": "verified" + }, + { + "control": "Accepted plan tool grants are cross-checked at invocation time", + "obligations": ["STA-L42-01", "STA-L42-07"], + "entrypoints": [ + "apps/local-control-plane/src/local-graph-tool-authority.ts:88-123", + "apps/hosted-control-plane/src/hosted-graph-tool-operations.ts:727", + "apps/control-api/src/pi-durable/current-tool-authority.ts:120-201" + ], + "evidence": [], + "status": "finding", + "finding": "CP190-F18" + } + ] + }, + { + "domain": "models", + "controls": [ + { + "control": "Model credentials are supplied as a separate credential reference, never as model context", + "obligations": ["STA-L10-01", "STA-L42-08", "STA-L56-03"], + "entrypoints": [ + "packages/model-gateway/src/index.ts:187", + "packages/model-gateway/src/litellm-http.ts:246-273" + ], + "evidence": [ + "packages/model-gateway/src/index.test.mjs:127", + "packages/model-gateway/src/index.test.mjs:198" + ], + "status": "verified" + }, + { + "control": "Provider output is untrusted data that cannot widen caller authority", + "obligations": ["STA-L32-03", "STA-L54-12", "STA-L63-01"], + "entrypoints": [ + "packages/context/src/provider.ts:406-454", + "packages/context/src/index.ts:25-43" + ], + "evidence": ["packages/context/src/provider.test.mjs"], + "status": "finding", + "finding": "CP190-F20" + } + ] + }, + { + "domain": "artifacts", + "controls": [ + { + "control": "Artifact keys are content-addressed and the store root is canonical-pinned", + "obligations": ["STA-L18-01", "STA-L70-03"], + "entrypoints": [ + "packages/object-store/src/filesystem.ts:362", + "packages/object-store/src/filesystem.ts:468-493" + ], + "evidence": [ + "tests/m11-security-probes.test.mjs:987", + "tests/m11-security-probes.test.mjs:1011" + ], + "status": "verified" + }, + { + "control": "Sandbox outputs become artifacts only through an authorised promotion gate", + "obligations": ["STA-L56-07"], + "entrypoints": ["packages/sandbox/src/index.ts:230-251"], + "evidence": ["tests/m11-security-probes.test.mjs:967"], + "status": "verified" + }, + { + "control": "Local backups and portable exports exclude protected secret material", + "obligations": ["STA-L76-01", "STA-L76-03"], + "entrypoints": [ + "packages/deployment/src/checkpoint.ts:119-170", + "packages/profile-portability/src/migration.ts:230", + "packages/profile-portability/src/migration.ts:344-364" + ], + "evidence": ["packages/deployment/src/checkpoint.test.mjs:103"], + "status": "finding", + "finding": "CP190-F08" + } + ] + }, + { + "domain": "admin", + "controls": [ + { + "control": "Administrative routes require a scoped service credential of the service class", + "obligations": ["STA-L21-01", "STA-L42-01", "STA-L59-13"], + "entrypoints": [ + "apps/control-api/src/auth/service-authentication.ts:228-328", + "apps/control-api/src/credentials/credential-administration.controller.ts:22-65" + ], + "evidence": ["apps/control-api/src/application.test.mjs:1118"], + "status": "verified" + }, + { + "control": "The control plane composes a deny-by-default policy decision point", + "obligations": ["STA-L42-02", "STA-L40-18"], + "entrypoints": [ + "packages/policy/src/index.ts:209-248", + "apps/control-api/src/cloud-composition.ts:100-104" + ], + "evidence": [ + "tests/m11-security-probes.test.mjs:210", + "packages/credential-vault/src/durable-vault.test.mjs:70" + ], + "status": "finding", + "finding": "CP190-F19" + }, + { + "control": "Deployed public ingress is bounded against request floods", + "obligations": ["STA-L54-11", "STA-L57-01"], + "entrypoints": ["apps/control-api/src/application.ts:22-27"], + "evidence": [], + "status": "finding", + "finding": "CP190-F14" + } + ] + } + ], + "auditAreas": [ + { + "area": "native grant and effect authorization", + "result": "gap", + "findings": ["CP190-F03", "CP190-F18", "CP190-F19"] + }, + { + "area": "secret-canary sinks", + "result": "gap", + "findings": ["CP190-F04", "CP190-F06", "CP190-F08", "CP190-F10", "CP190-F20"] + }, + { + "area": "direct-local IPC and path handling", + "result": "gap", + "findings": ["CP190-F03", "CP190-F05", "CP190-F23", "CP190-F24"], + "notes": "The direct-local working directory is bounded by operator configuration, not by a grant: the managed-Pi child runs in a per-attempt directory under the data directory and the composed spawn policy leaves the working-directory dimension unconstrained, while the ACP child runs in an operator-configured absolute directory." + }, + { + "area": "remote Runtime Gateway versus product relay", + "result": "gap", + "findings": ["CP190-F01", "CP190-F02", "CP190-F07", "CP190-F16", "CP190-F17"], + "notes": "Traced per payload type. create_execution is idempotent through the CommandInbox because the acceptance request extends the context schema that carries idempotencyKey. submit_input carries no idempotency key. The metadata effect is an injected callback with no shipped implementation, and no composition builds the relay host adapter. The relay receipt layer adds no idempotency beyond the command identifier and only in-memory receipt repositories ship." + }, + { + "area": "HPKE AAD, recipient, replay and expiry", + "result": "verified", + "notes": "RFC 9180 DHKEM(X25519, HKDF-SHA256)/HKDF-SHA256/AES-128-GCM; AAD binds version, suite, key id, workspace, host, command id, payload type and schema, issued/expiry and content digest; 1 MiB cap; wrong recipient, tamper, downgrade and digest mismatch all fail closed.", + "findings": ["CP190-F02"] + }, + { + "area": "SQLite, backup and import", + "result": "gap", + "findings": ["CP190-F08", "CP190-F09", "CP190-F10", "CP190-F29"], + "notes": "The provider path creates and restricts its parent directory to 0700 before the database and its sidecars exist, so there is no supported traversal path. The adapter-owned pi-durable directory is the conditional case." + }, + { + "area": "provider and secrets", + "result": "gap", + "findings": ["CP190-F04", "CP190-F06", "CP190-F20", "CP190-F21", "CP190-F22"] + }, + { + "area": "sandbox", + "result": "gap", + "findings": ["CP190-F11", "CP190-F12", "CP190-F13"] + }, + { + "area": "supply chain", + "result": "verified", + "notes": "Frozen lockfile with integrity hashes, digest-pinned base images and actions, pinned patched dependency with upstream provenance, Trivy image scan with CycloneDX SBOM, build provenance attestation, least-privilege workflow permissions, persist-credentials disabled, pull_request_target workflows never check out PR code.", + "findings": [] + }, + { + "area": "managed-cloud trust", + "result": "partial", + "findings": ["CP190-F14", "CP190-F15", "CP190-F25", "CP190-F26", "CP190-F28"] + } + ], + "findings": [ + { + "id": "CP190-F01", + "title": "The relay host processor applies no authentication of its own to metadata-only commands", + "severity": "medium", + "releaseImpact": "Does not block the current release. The Control Plane ships the transport and validation layer only: no composition builds the relay host adapter or the metadata processor, the only relay client implementation is a test fake, and the approve, deny, cancel, resume and status effects are injected callbacks with no shipped implementation. What remains is that the processor offers no defence in depth of its own, so a claim that the Control Plane verifies the metadata transport cannot be made.", + "obligations": ["STA-L46-09", "STA-L66-01", "STA-L68-02", "STA-L52-04"], + "entrypoints": [ + "packages/remote-control-relay/src/relay.ts:379-424", + "packages/remote-control-relay/src/protocol.ts:68-81", + "packages/remote-control-relay/src/adapter.ts:154-173" + ], + "probes": ["CP190-P01", "CP190-P02", "CP190-P21"], + "routedTo": "W1 (packages/remote-control-relay) with the Agent HQ product relay owner", + "regression": "The processor must not be able to accept a metadata command that its caller has not authenticated, and a composition must refuse to start a relay adapter whose client does not assert authentication. Regression test: a metadata command with no authenticated-transport provenance is rejected by the processor, and the composition fails closed when the supplied client declares no authentication.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default.", + "reachabilityNote": "Traced end to end: the only consumer of a metadata command is RemoteControlHostAdapter.#pollOnce (packages/remote-control-relay/src/adapter.ts:154-173), which is itself an injected option of the Local and Hosted compositions (apps/local-control-plane/src/composition.ts:249-250,508-512; apps/hosted-control-plane/src/composition.ts:220-223,371-375) and is reported as 'disabled' when absent (:152, :164). No application constructs RelayMetadataCommandProcessor or RemoteControlHostAdapter, and the only OutboundOpaqueRelayClient implementation is the test fake FakeOpaqueRelay (packages/remote-control-relay/src/relay.ts:45). The metadata effect callback is injected (packages/remote-control-relay/src/relay.ts:376-379), so the Control Plane ships no approve, deny or cancel effect at all: those names are schema operations, not shipped behaviour. An integrator must supply the relay client, the effect and the adapter. **No deployed attacker-controlled ingress exists and no deployed unauthenticated approval path is claimed.** The residual is that the processor adds no provenance check of its own and relies on its caller having authenticated the transport, which docs/remote-control-relay.md assigns to the concrete relay client.", + "counterevidence": "docs/remote-control-relay.md states that metadata-only operations use a separate strict, authenticated command schema and that authentication of that metadata transport remains the responsibility of the concrete relay client. The obligation is documented and delegated, so this is a contract and defence-in-depth gap rather than a deployed unauthenticated-approval path." + }, + { + "id": "CP190-F02", + "title": "The relay receipt layer adds no idempotency beyond the command identifier, and only in-memory receipt repositories ship", + "severity": "medium", + "releaseImpact": "Does not block the current release. The durable idempotency guarantee for create_execution comes from the CommandInbox, not from the relay receipt, and the metadata effect is an injected callback whose own idempotency is outside the Control Plane.", + "obligations": ["STA-L16-01", "STA-L68-03", "STA-L78-01", "STA-L61-08"], + "entrypoints": [ + "packages/remote-control-relay/src/relay.ts:296-303", + "packages/remote-control-relay/src/relay.ts:417-437", + "packages/remote-control-relay/src/execution.ts:187-197", + "packages/remote-control-relay/src/relay.ts:376", + "packages/remote-control-relay/src/execution.ts:135" + ], + "probes": ["CP190-P03", "CP190-P04", "CP190-P22"], + "routedTo": "W1 (packages/remote-control-relay)", + "regression": "Bind the effect and its receipt atomically, or bind the receipt to a stable effect key that the injected callback also uses. Do not move the receipt before the effect. Regression test: a receipt-store write that fails must leave the effect unapplied; a receipt and its effect must be indivisible; and a redelivery after a restart must be rejected as a duplicate by the durable receipt.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default.", + "reachabilityNote": "Traced per payload type. create_execution: the relay parses ExecutionAcceptanceRequestSchema (packages/remote-control-relay/src/execution.ts:209-223,260-268), which extends CommandContextSchema and therefore carries the caller's idempotencyKey (packages/contracts/src/control-api.ts:45,668); ExecutionAcceptanceService then admits it through the CommandInbox (apps/control-api/src/executions/execution-acceptance.service.ts:51,58; packages/domain/src/command-inbox.ts:53-58,684-705), so a redelivery of the same command is deduped there. submit_input: RelaySubmitInputRequestSchema carries no idempotency key at all (packages/remote-control-relay/src/execution.ts:38-52), so any dedupe must come from the interaction domain. Metadata operations: the effect is an injected callback (packages/remote-control-relay/src/relay.ts:376-379) with no shipped implementation. The effect-before-receipt order is correct and must not be inverted; the defect is that the two writes are not atomic and that only in-memory receipt repositories ship (packages/remote-control-relay/src/relay.ts:227,343; packages/remote-control-relay/src/execution.ts:135), while docs/remote-control-relay.md already records the durable repository as a composition requirement.", + "counterevidence": "create_execution goes through ExecutionAcceptanceService and the CommandInbox idempotency key, so it is not duplicated by the callback ordering. The processor offers no cross-commandId idempotency, but the commandId is a relay-chosen identifier and the effect's own idempotency is the injected callback's property." + }, + { + "id": "CP190-F03", + "title": "The direct-local runtime path never consults a LocalProjectGrant, and the production discovery state is a hard-coded constant", + "severity": "high", + "releaseImpact": "Blocks the Local filesystem/project acceptance claim: the discovery projection reports a grant state that no authority issued, and the direct-local working directory is bounded by operator configuration rather than by a grant.", + "obligations": ["STA-L17-01", "STA-L42-09", "STA-L42-10", "STA-L69-02", "STA-L27-03"], + "entrypoints": [ + "apps/runtime-gateway/src/runtime-inventory-ingestion.ts:387", + "apps/workflow-worker/src/runtime-attempt-router.ts:179-184", + "packages/acp-adapter/src/acp-driver-execution.ts", + "packages/managed-pi-adapter/src/managed-pi-driver.ts", + "packages/managed-pi-adapter/src/process-client.ts" + ], + "probes": ["CP190-P08", "CP190-P09", "CP190-P10", "CP190-P24"], + "routedTo": "W1 (runtime composition and adapter boundary) with the Agent HQ desktop grant owner", + "regression": "The direct-local driver must require a current grant, and every path must be contained against a canonical grant root after symlink resolution. Regression test: a direct-local dispatch with a revoked or absent grant is rejected; a symlinked path that escapes the grant root is rejected; and the direct-local working directory is derived from the grant rather than from operator configuration alone.", + "reachability": "deployed-now", + "reachabilityClass": "Reachable in a shipped profile today, subject to the preconditions stated in the finding.", + "reachabilityNote": "Traced for the actual managed-Pi driver. ManagedPiDriver (packages/managed-pi-adapter/src/index.ts:322) parses the execution plan, enforces the runtime version range and the capability evaluation, and forwards to the client; it contains no grant reference and no path check. The child then runs with its working directory set to a per-attempt directory under the data directory (apps/local-control-plane/src/managed-pi-runtime.ts:66; packages/managed-pi-adapter/src/process-client.ts:288-298), created 0700 with the system prompt written 0600. The composed spawn policy names only allowedExecutables (apps/local-control-plane/src/index.ts:300), and NodeProcessSpawnPolicy documents absent dimensions as unconstrained (packages/deployment/src/process-runtime.ts:10), so the working-directory dimension is unconstrained by policy. The ACP path differs: the child runs in the operator-configured absolute CONTROL_PLANE_CODEX_ACP_CWD and the policy mirrors that directory (apps/local-control-plane/src/installed-acp-runtime.ts:88-95), with absolute-path validation as the only constraint. The gateway path is the only one that checks a grant (packages/managed-pi-adapter/src/managed-pi-gateway-driver.ts:108-120; packages/acp-adapter/src/acp-gateway-driver.ts:127-139), and the only production writer of the grant state is a hard-coded constant (apps/runtime-gateway/src/runtime-inventory-ingestion.ts:387) consumed at apps/workflow-worker/src/runtime-attempt-router.ts:179-184. Because the grant carries no path semantics, there is no grant-scoped project root for STA-L42-10 to attach to, and the Control Plane mediates no filesystem operation on the harness's behalf, so the residual Control Plane risk is a dispatch-authority and acceptance gap while the ambient-access denial remains a desktop-host obligation outside this repository.", + "counterevidence": "None offered. The citation error in the previous revision is corrected here: the managed-Pi direct driver is packages/managed-pi-adapter/src/index.ts:322 (export class ManagedPiDriver), not a separate managed-pi-driver.ts module, which does not exist. packages/acp-adapter/src/acp-driver-execution.ts does exist and holds no grant reference." + }, + { + "id": "CP190-F04", + "title": "The credential-vault returned-value screen does not descend into Error, Map, Set or byte containers", + "severity": "low", + "releaseImpact": "No deployed credential leakage is claimed or demonstrated. The one composed leased path normalizes the callback result before it is screened, and the two callers that return a raw result are not composed into any application.", + "obligations": ["STA-L40-17", "STA-L40-18", "STA-L76-03"], + "entrypoints": [ + "packages/credential-vault/src/vault.ts:457-462", + "packages/credential-vault/src/vault.ts:550-557" + ], + "probes": ["CP190-P05", "CP190-P23"], + "routedTo": "W1 (packages/credential-vault)", + "regression": "The returned-value screen must descend into Error message, stack and cause, Map, Set, Buffer and typed arrays, or reject a non-plain result. Regression test: a leased callback that returns new Error(secret) must raise SECRET_EGRESS_BLOCKED; a leased callback that throws must still surface the bounded PROVIDER_OPERATION_FAILED code; and the model-selection normalizer must continue to reject a non-plain carrier.", + "reachability": "library-contract", + "reachabilityClass": "A library-level contract gap. No composed path in this repository reaches it, so no deployed behaviour is claimed.", + "reachabilityNote": "Bounded to the returned value of a leased callback. packages/credential-vault/src/vault.ts:456-467 screens the value the callback returns with containsSecret, and containsSecret (:550-557) walks own enumerable string-keyed properties only, so a returned Error, Map, Set or byte container carrying the secret is not detected. Ordinary thrown errors are already sanitized: the catch at :462-466 converts any non-CredentialVaultError throw into PROVIDER_OPERATION_FAILED. The composed leased path is protected by a second control: the model-selection service returns safeCredentialResult(await operation(secret)) (packages/model-gateway/src/selection-service.ts:269), and safeCredentialResult (:341-380) rejects any object whose prototype is not Object.prototype or null, rejects symbol keys, rejects sparse arrays and rejects cycles, so a non-plain carrier cannot survive it. The two callers that return the raw result are the credential broker (packages/credential-vault/src/tool-broker.ts:91-99) and the MCP adapter (packages/tool-execution/src/mcp-adapter.ts:257-277, which reduces thrown transport errors to a bounded code but returns the invoke output raw); neither is composed into any application, and no application composes a VaultToolCredentialBroker. The composed profiles also do not lease: the Control API vault composes no policy decision point (apps/control-api/src/cloud-composition.ts:100-104), so leases deny before a secret is resolved (packages/credential-vault/src/vault.ts:326-332).", + "counterevidence": "The previous revision implied a deployed credential-leakage path. It does not: thrown errors are sanitized, the Control API vault denies leases without a policy decision point, and the demonstrated case is a library contract gap in the returned-value screen only." + }, + { + "id": "CP190-F05", + "title": "The Local private API derives workspace and project scope from the request body", + "severity": "medium", + "releaseImpact": "Does not block the current release. It leaves the Local cross-workspace fail-closed claim unproven, because the Control Plane does not verify the requested scope against an authorized set.", + "obligations": ["STA-L08-01", "STA-L46-06", "STA-L46-17", "STA-L38-03"], + "entrypoints": ["apps/control-api/src/auth/private-api-authentication.ts:86-94"], + "probes": ["CP190-P07"], + "routedTo": "W1 (apps/control-api authentication)", + "regression": "Bind the Local principal to an authorized workspace and project set and compare the envelope against it. Regression test: a valid local token with a foreign workspaceId must be rejected.", + "reachability": "deployed-now", + "reachabilityClass": "Reachable in a shipped profile today, subject to the preconditions stated in the finding.", + "reachabilityNote": "Present in the shipped Local profile. The default bind is loopback (apps/local-control-plane/src/index.ts:82-88), the bearer token is a 0600 file in a 0700 directory (apps/control-api/src/auth/private-api-authentication.ts:14-43) and the caller must be svc_agent-hq, so a bearer holder is the trusted local product. The gap is authorization completeness, not a demonstrated cross-tenant read: the hosted authenticator performs the missing comparison (apps/control-api/src/auth/service-authentication.ts:310-313) and the Local one does not.", + "counterevidence": "None offered; the severity is reduced from medium-high because the Local profile binds loopback by default, the credential is an owner-only file and the caller is the trusted local product. The finding stands as an authorization-completeness gap." + }, + { + "id": "CP190-F06", + "title": "Log, event and checkpoint redaction is key-name based and misses compound credential key names", + "severity": "medium", + "releaseImpact": "A secret stored under a key such as sessionToken is written to logs and events. STA-L76-03 acceptance stays open.", + "obligations": ["STA-L40-17", "STA-L76-03", "STA-L59-14"], + "entrypoints": [ + "packages/telemetry/src/redaction.ts:3-73", + "packages/events/src/index.ts:119-124", + "packages/langgraph-adapter/src/index.ts:912-916" + ], + "probes": ["CP190-P06"], + "routedTo": "W1 (packages/telemetry, packages/events)", + "regression": "Extend the sensitive key set to compound credential names, or apply the production value-based canary guard at every sink. Regression test: a secret under sessionToken, passphrase or bearerToken must not appear in a redacted log payload.", + "reachability": "deployed-now", + "reachabilityClass": "Reachable in a shipped profile today, subject to the preconditions stated in the finding." + }, + { + "id": "CP190-F07", + "title": "Runtime Gateway runtime commands are not bounded to the 24 hour remote-command maximum", + "severity": "medium", + "releaseImpact": "A runtime command stays dispatchable for an unbounded lifetime and survives credential rotation. STA-L61-08 acceptance stays open.", + "obligations": ["STA-L61-08", "STA-L68-01"], + "entrypoints": [ + "apps/runtime-gateway/src/runtime-command-delivery.ts:69-91", + "apps/runtime-gateway/src/context-command-delivery.ts:52-53" + ], + "probes": ["CP190-P11"], + "routedTo": "W1 (apps/runtime-gateway)", + "regression": "Apply the same 24 hour bound to the runtime command family. Regression test: enqueuing a runtime command with a one-year lifetime must fail with EXPIRY_TOO_LONG.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default." + }, + { + "id": "CP190-F08", + "title": "A Local filesystem checkpoint copies the private-file secrets directory in cleartext", + "severity": "medium", + "releaseImpact": "Operator backups carry plaintext provider secrets. STA-L76-01 and STA-L76-03 acceptance stay open for the Local profile.", + "obligations": ["STA-L76-01", "STA-L76-03", "STA-L61-02"], + "entrypoints": [ + "packages/deployment/src/checkpoint.ts:119-170", + "scripts/control-plane-checkpoint.mjs:24-28", + "apps/local-control-plane/src/composition.ts:385-395" + ], + "probes": ["CP190-P12"], + "routedTo": "W1 (packages/deployment, scripts/control-plane-checkpoint.mjs)", + "regression": "Exclude the secrets directory from the checkpoint inventory or seal it under the vault key. Regression test: a checkpoint of a data directory containing secrets/provider-key must not contain the secret bytes.", + "reachability": "deployed-now", + "reachabilityClass": "Reachable in a shipped profile today, subject to the preconditions stated in the finding." + }, + { + "id": "CP190-F09", + "title": "SQLite WAL and SHM sidecars keep the pre-restriction file mode inside an owner-only parent directory", + "severity": "low", + "releaseImpact": "No cross-UID readability is demonstrated and none is claimed. The parent data directory is created and restricted to 0700 before the database and its sidecars exist, so there is no supported traversal path in the provider.", + "obligations": ["STA-L76-01", "STA-L23-03"], + "entrypoints": ["packages/sqlite-persistence/src/provider.ts:209-221"], + "probes": ["CP190-P13"], + "routedTo": "W1 (packages/sqlite-persistence)", + "regression": "Restrict the sidecar files to owner-only, so the file mode does not depend on the parent directory mode. Regression test: after opening a provider in a 0755 directory the -wal and -shm files must be mode 0600.", + "reachability": "defense-in-depth", + "reachabilityClass": "A hardening gap. The described attack is not demonstrated as reachable; the stated control prevents it in the current configuration.", + "reachabilityNote": "packages/sqlite-persistence/src/provider.ts:209-210 creates the parent directory and chmods it to 0700 before the database is opened at :211 and before the WAL pragma at :216, and :221 then restricts the main file to 0600. The sidecars keep the pre-restriction mode, but another UID cannot traverse the 0700 parent, and no supported path defeats that control. The residual is defence in depth: the sidecar files are not owner-only, so a later relaxation of the directory mode, a copy of a sidecar on its own, or a backup that captures a sidecar would expose recent writes that the 0600 main file would have protected. The adapter-owned directory case is reported separately as CP190-F29.", + "counterevidence": "The previous revision implied a cross-UID read. The parent directory is 0700 before any file is created, so the sidecar mode is a defense-in-depth gap only." + }, + { + "id": "CP190-F10", + "title": "The portable import path scans with an empty sensitive-value list and portable secret references are not workspace scoped", + "severity": "medium", + "releaseImpact": "A profile export can carry another workspace's secret locator, and the import path does not re-check secret values. STA-L08-01 and STA-L76-03 acceptance stay open.", + "obligations": ["STA-L08-01", "STA-L76-03"], + "entrypoints": [ + "packages/profile-portability/src/migration.ts:230", + "packages/profile-portability/src/migration.ts:344-364", + "packages/profile-portability/src/manifest.ts:79-90" + ], + "probes": ["CP190-P14", "CP190-P15"], + "routedTo": "W1 (packages/profile-portability)", + "regression": "Scan the import path with the same sensitive values used at export and scope secret references to a workspace. Regression test: a manifest whose record value carries a secret under a non-prohibited key must be rejected on import.", + "reachability": "deployed-now", + "reachabilityClass": "Reachable in a shipped profile today, subject to the preconditions stated in the finding." + }, + { + "id": "CP190-F11", + "title": "Sandbox credential bindings are injected after the ambient-credential denial runs", + "severity": "medium", + "releaseImpact": "The ambient credential denial never sees credential-bound names, so a raw secret reaches the sandbox environment. STA-L77-01 acceptance stays open.", + "obligations": ["STA-L46-17", "STA-L77-01", "STA-L10-01"], + "entrypoints": [ + "packages/sandbox/src/index.ts:198-215", + "packages/sandbox/src/index.ts:367-379" + ], + "probes": ["CP190-P16"], + "routedTo": "W1 (packages/sandbox)", + "regression": "Apply the ambient credential denial to the effective environment, including credential bindings, or deny any credential-shaped bound name. Regression test: a credential bound to AWS_SESSION must be denied or must not reach the provider environment.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default." + }, + { + "id": "CP190-F12", + "title": "The sandbox metadata-service deny list is incomplete", + "severity": "medium", + "releaseImpact": "Non-default cloud metadata endpoints are reachable from a sandbox command. STA-L77-01 acceptance stays open.", + "obligations": ["STA-L77-01", "STA-L26-02"], + "entrypoints": ["packages/sandbox/src/index.ts:605-607"], + "probes": ["CP190-P17"], + "routedTo": "W1 (packages/sandbox)", + "regression": "Deny the link-local metadata range and the known provider metadata endpoints. Regression test: a command targeting 169.254.170.2 must be denied with POLICY_DENIED.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default." + }, + { + "id": "CP190-F13", + "title": "The sandbox provider contract is not composed into any application and no live provider exists", + "severity": "high", + "releaseImpact": "There is no sandbox boundary in any shipped profile. STA-L77-01 and STA-L83-02 acceptance stay open and the live E2B gate remains unproven.", + "obligations": ["STA-L77-01", "STA-L83-02", "STA-L26-02"], + "entrypoints": ["packages/sandbox/src/index.ts:174-280", "packages/sandbox/package.json"], + "probes": ["CP190-P18"], + "routedTo": "W1 (profile composition) and the operations owner for the E2B template and cleanup thresholds", + "regression": "Compose a SandboxProvider in the hosted profile and record the live template, resource, network and cleanup evidence. Regression test: a hosted composition must resolve a sandbox provider, and a composition without one must fail closed.", + "reachability": "evidence-gap", + "reachabilityClass": "The control is a contract with no composed implementation, so there is nothing to attack and nothing to accept." + }, + { + "id": "CP190-F14", + "title": "The only public control-plane service has no HTTP request rate limit and no inbound deadline", + "severity": "medium", + "releaseImpact": "An authenticated principal can saturate the PostgreSQL pool. Abuse-boundary acceptance for the cloud profile stays open.", + "obligations": ["STA-L54-11", "STA-L57-01", "STA-L78-01"], + "entrypoints": [ + "apps/control-api/src/application.ts:22-27", + "packages/database/src/connection.ts:19-31" + ], + "probes": ["CP190-P19"], + "routedTo": "W1 (apps/control-api) with the operations owner for the deployed limit values", + "regression": "Add a per-principal request quota and an inbound request deadline to the Fastify adapter. Regression test: a principal exceeding the quota receives a bounded 429 without reaching the database.", + "reachability": "deployed-now", + "reachabilityClass": "Reachable in a shipped profile today, subject to the preconditions stated in the finding." + }, + { + "id": "CP190-F15", + "title": "The Restate ingress submission carries no request credential", + "severity": "medium", + "releaseImpact": "Workflow submission trusts network placement only. STA-L24-01 and STA-L40-09 acceptance stay open for the self-hosted profile, where the ingress binds to all interfaces.", + "obligations": ["STA-L40-09", "STA-L24-01", "STA-L23-05"], + "entrypoints": [ + "apps/control-api/src/executions/execution-acceptance.service.ts:291-303", + "scripts/validate-railway.mjs:121", + "infrastructure/compose/compose.yaml:152" + ], + "probes": ["CP190-P20"], + "routedTo": "W1 (execution acceptance) and the deployment owner for the self-hosted ingress binding", + "regression": "Authenticate the ingress submission with the Restate request identity, and bind the self-hosted ingress to the private interface. Regression test: a submission without the request identity must be rejected.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default." + }, + { + "id": "CP190-F16", + "title": "The Control Plane repository ships a RuntimeNode credential issuer", + "severity": "medium", + "releaseImpact": "The operator issuer private key lives on Control Plane infrastructure. Credential-class separation and rotation acceptance stay open.", + "obligations": ["STA-L15-01", "STA-L46-04", "STA-L46-05", "STA-L59-03"], + "entrypoints": [ + "scripts/runtime-node-identity-admin.mjs:303-399", + "scripts/runtime-node-identity-admin.mjs:540-549" + ], + "probes": [], + "routedTo": "W1 (runtime node identity operations)", + "regression": "Move issuance to a separate operator-controlled surface and keep the gateway verifier-only. Regression test: the Control Plane image must not contain an issuer private key path.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default." + }, + { + "id": "CP190-F17", + "title": "The synthetic RuntimeNode authority merges credential classes and drops the binding fence", + "severity": "medium", + "releaseImpact": "Gateway authentication tests do not exercise the production issuer and device separation. STA-L46-01 and STA-L46-04 acceptance rest on code reading, not on the test suite.", + "obligations": ["STA-L46-01", "STA-L46-04", "STA-L15-01"], + "entrypoints": [ + "apps/runtime-gateway/src/synthetic-node-identity.ts:104-133", + "apps/runtime-gateway/src/runtime-node-identity-port.ts:8-11" + ], + "probes": [], + "routedTo": "W1 (apps/runtime-gateway)", + "regression": "Give the synthetic authority a distinct issuer key and enforce the node, workspace and generation fence. Regression test: a device-signed credential presented with an issuer key id must be rejected, and a consumed credential for another workspace must be refused.", + "reachability": "evidence-gap", + "reachabilityClass": "The control is a contract with no composed implementation, so there is nothing to attack and nothing to accept." + }, + { + "id": "CP190-F18", + "title": "The pi-durable tool path does not cross-check the accepted plan tool grants", + "severity": "medium", + "releaseImpact": "A tool call can present a self-consistent grant for a tool and operation the accepted plan does not grant. STA-L42-01 and STA-L42-07 acceptance stay open.", + "obligations": ["STA-L42-01", "STA-L42-07", "STA-L42-02"], + "entrypoints": [ + "apps/control-api/src/pi-durable/current-tool-authority.ts:120-201", + "apps/local-control-plane/src/local-graph-tool-authority.ts:88-123", + "apps/hosted-control-plane/src/hosted-graph-tool-operations.ts:727" + ], + "probes": [], + "routedTo": "W1 (apps/control-api pi-durable tool authority)", + "regression": "Cross-check the accepted plan constraints.tools.grants on the pi-durable path. Regression test: a grant for a tool absent from the accepted plan must be denied.", + "reachability": "deployed-now", + "reachabilityClass": "Reachable in a shipped profile today, subject to the preconditions stated in the finding." + }, + { + "id": "CP190-F19", + "title": "No policy decision point is composed into any application, so the credential lease positive path does not exist", + "severity": "medium", + "releaseImpact": "The deny-by-default behaviour is proven, but the narrowly authorized executor path in STA-L40-18 cannot be demonstrated in this repository.", + "obligations": ["STA-L42-02", "STA-L40-18", "STA-L25-02"], + "entrypoints": [ + "packages/policy/src/index.ts:209-248", + "apps/control-api/src/cloud-composition.ts:100-104", + "apps/local-control-plane/src/index.ts:153-162" + ], + "probes": [], + "routedTo": "W1 (profile compositions) and the Cedar policy owner", + "regression": "Compose the decision point where a credential lease is required and record the permit matrix. Regression test: a leased connector credential in a composed profile must be allowed only for its own workspace and operation.", + "reachability": "evidence-gap", + "reachabilityClass": "The control is a contract with no composed implementation, so there is nothing to attack and nothing to accept." + }, + { + "id": "CP190-F20", + "title": "Provider contributions and ProjectState items carry free-form content with no secret filter and no untrusted-data marking", + "severity": "medium", + "releaseImpact": "A provider or retrieved memory value can carry a credential into a ContextPackage, a ProjectState item or model context. STA-L40-17 and STA-L54-12 acceptance stay open.", + "obligations": ["STA-L40-17", "STA-L54-12", "STA-L32-03", "STA-L19-01"], + "entrypoints": [ + "packages/context/src/index.ts:25-43", + "packages/contracts/src/context-providers.ts:53-85", + "packages/domain/src/project-state.ts:86-120" + ], + "probes": [], + "routedTo": "W1 (packages/context, packages/domain)", + "regression": "Filter provider and ProjectState content against the protected value set and mark retrieved content as untrusted data. Regression test: a contribution whose content carries a canary must be rejected or redacted before admission.", + "reachability": "deployed-now", + "reachabilityClass": "Reachable in a shipped profile today, subject to the preconditions stated in the finding." + }, + { + "id": "CP190-F21", + "title": "The environment and private-file secret providers ignore the SecretUse context", + "severity": "medium", + "releaseImpact": "A secret resolved through the env or file provider is not bound to a workspace, operation or purpose. STA-L40-18 and STA-L42-08 acceptance stay open.", + "obligations": ["STA-L40-18", "STA-L42-08", "STA-L15-01"], + "entrypoints": [ + "packages/secrets/src/index.ts:89", + "packages/secrets/src/index.ts:134", + "packages/secrets/src/index.ts:219" + ], + "probes": [], + "routedTo": "W1 (packages/secrets)", + "regression": "Accept and enforce the SecretUse context in every provider. Regression test: resolving the same reference under two different use contexts must not produce the same unbound lease.", + "reachability": "deployed-now", + "reachabilityClass": "Reachable in a shipped profile today, subject to the preconditions stated in the finding." + }, + { + "id": "CP190-F22", + "title": "The Hosted server profile composes no credential vault and the host-secure handle provider has no resolver", + "severity": "medium", + "releaseImpact": "The self-hosted server profile has no dynamic credential lifecycle and no OS-secure handle path. STA-L15-01, STA-L40-11 and STA-L40-14 acceptance stay open.", + "obligations": ["STA-L15-01", "STA-L40-11", "STA-L40-14", "STA-L40-16"], + "entrypoints": [ + "apps/hosted-control-plane/src/composition.ts", + "packages/secrets/src/index.ts:211-238" + ], + "probes": [], + "routedTo": "W1 (apps/hosted-control-plane composition) and the desktop owner for the OS resolver", + "regression": "Compose the vault for the hosted server profile and wire a concrete host-secure resolver for packaged Local. Regression test: a hosted server composition must resolve a credential lifecycle, and a packaged Local profile must resolve an opaque handle.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default." + }, + { + "id": "CP190-F23", + "title": "The spawn policy check is not pinned to the executed path", + "severity": "low-medium", + "releaseImpact": "A symlink swap between the allowlist check and the spawn can launch an unlisted executable. STA-L69-01 and STA-L77-01 acceptance stay open.", + "obligations": ["STA-L69-01", "STA-L77-01", "STA-L27-03"], + "entrypoints": [ + "packages/deployment/src/process-runtime.ts:58-86", + "packages/acp-adapter/src/stdio-client.ts:124-137", + "packages/managed-pi-adapter/src/process-client.ts:818-830" + ], + "probes": [], + "routedTo": "W1 (packages/deployment spawn policy)", + "regression": "Execute the resolved canonical path and re-verify the device and inode identity, as the secrets provider already does. Regression test: swapping the allowlisted executable for a symlink after the check must not launch the replacement.", + "reachability": "defense-in-depth", + "reachabilityClass": "A hardening gap. The described attack is not demonstrated as reachable; the stated control prevents it in the current configuration.", + "reachabilityNote": "The check and the spawn are two separate steps, so a symlink swap in the window between them could launch an unlisted executable. The window is narrow, it requires local write access to the executable path, and no supported path widens it, so this is defence in depth rather than a demonstrated privilege gain." + }, + { + "id": "CP190-F24", + "title": "The exported direct-local ACP runtime factory applies no spawn policy by default", + "severity": "medium", + "releaseImpact": "An embedder can start an arbitrary absolute executable at an arbitrary absolute working directory. STA-L27-03 and STA-L77-01 acceptance stay open for embedders.", + "obligations": ["STA-L27-03", "STA-L77-01", "STA-L42-09"], + "entrypoints": [ + "apps/local-control-plane/src/acp-runtime.ts:30-53", + "packages/acp-adapter/src/stdio-client.ts:124" + ], + "probes": [], + "routedTo": "W1 (apps/local-control-plane ACP runtime)", + "regression": "Require an explicit spawn policy in the exported factory. Regression test: creating the factory without a spawn policy must fail closed.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default." + }, + { + "id": "CP190-F25", + "title": "Marketplace registry egress accepts any HTTPS URL with no metadata-range denial", + "severity": "medium", + "releaseImpact": "A misconfigured registry base URL can reach a metadata endpoint. STA-L31-02 and STA-L33-01 acceptance stay open.", + "obligations": ["STA-L31-02", "STA-L33-01", "STA-L40-16"], + "entrypoints": ["apps/control-api/src/marketplace/registry.ts:383-387"], + "probes": [], + "routedTo": "W1 (apps/control-api marketplace)", + "regression": "Deny the link-local and metadata ranges on the registry fetch path. Regression test: a registry base URL resolving to 169.254.169.254 must be refused.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default." + }, + { + "id": "CP190-F26", + "title": "The Restate interaction response handler resolves by name without a responding-principal check", + "severity": "medium", + "releaseImpact": "An approval or input delivered over the workflow ingress is not re-checked against the authorized principal set on that path. STA-L66-01 and STA-L42-11 acceptance stay open.", + "obligations": ["STA-L66-01", "STA-L42-11", "STA-L59-09"], + "entrypoints": [ + "packages/workflow-runtime/src/restate-endpoint.ts:56-69", + "packages/domain/src/interaction-delivery.ts:36-68", + "apps/workflow-worker/src/remote-workflow-runtime.ts:102-133" + ], + "probes": [], + "routedTo": "W1 (packages/workflow-runtime and the workflow worker)", + "regression": "Re-check the responding principal against the interaction allowed principal set before resolving. Regression test: a response from a principal outside allowedPrincipalIds must be rejected.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default." + }, + { + "id": "CP190-F27", + "title": "Tool rate-limit events are only deleted for the tuple being checked", + "severity": "low-medium", + "releaseImpact": "Rows for abandoned principal, tool or operation tuples accumulate without bound. Retention acceptance stays open.", + "obligations": ["STA-L54-11", "STA-L61-03", "STA-L86-01"], + "entrypoints": ["packages/database/src/tool-rate-limiter.ts:68-78"], + "probes": [], + "routedTo": "W1 (packages/database retention)", + "regression": "Sweep expired tool rate-limit rows in the retention pass. Regression test: the retention sweep must remove expired tool_rate_limit_events rows for a tuple that is never called again.", + "reachability": "deployed-now", + "reachabilityClass": "Reachable in a shipped profile today, subject to the preconditions stated in the finding." + }, + { + "id": "CP190-F28", + "title": "Staging and production share one R2 bucket credential slot and are separated by a key prefix only", + "severity": "medium", + "releaseImpact": "The asserted environment credential separation is a naming convention, not an enforced control. STA-L31-01, STA-L31-02 and STA-L40-11 acceptance stay open.", + "obligations": ["STA-L31-01", "STA-L31-02", "STA-L40-11"], + "entrypoints": [ + ".railway/railway.ts:82-89", + ".railway/railway.ts:133-139", + "packages/object-store/src/index.ts:140-142" + ], + "probes": [], + "routedTo": "W1 (deployment) with the operations owner", + "regression": "Provision distinct bucket-scoped R2 credentials per environment. Regression test: the production credential must not read a staging object and the staging credential must not read a production object.", + "reachability": "configured-only", + "reachabilityClass": "Reachable only after an operator configures the optional component; the component ships disabled or is not composed by default." + }, + { + "id": "CP190-F29", + "title": "The pi-durable adapter creates its own data directory without an owner-only mode", + "severity": "low-medium", + "releaseImpact": "No cross-UID read is demonstrated and none is claimed. In the composed managed-cloud path an ancestor directory is created owner-only, which blocks traversal today. The control that the adapter relies on is therefore owned by a different component than the data.", + "obligations": ["STA-L76-01", "STA-L76-02", "STA-L23-03"], + "entrypoints": [ + "packages/pi-durable-adapter/src/journal.ts:31", + "packages/pi-durable-adapter/src/lease.ts:20", + "apps/control-api/src/pi-durable/node-composition.ts:89" + ], + "probes": ["CP190-P25"], + "reachability": "defense-in-depth", + "reachabilityClass": "A hardening gap. The described attack is not demonstrated as reachable; a stated control prevents it in the current configuration.", + "reachabilityNote": "packages/pi-durable-adapter/src/journal.ts:31 and packages/pi-durable-adapter/src/lease.ts:20 create their directories with mkdirSync(..., { recursive: true }) and no mode, so the directory takes the process umask default, and the database file is never chmod'ed. apps/control-api/src/pi-durable/node-composition.ts:89 does the same for the admission database. In the composed managed-cloud path the ancestor is created owner-only (apps/control-api/src/models/production-model-composition.ts:230), which blocks traversal, so the guarantee is inherited from an unrelated composition rather than enforced by the component that owns the data. Any other wiring would place a 0644 database in a 0755 directory, which is a supported traversal path; no such wiring ships.", + "counterevidence": "No counterevidence was offered. The finding is bounded by the inherited ancestor mode and by the absence of any wiring that defeats it.", + "routedTo": "W1 (packages/pi-durable-adapter and the apps/control-api pi-durable composition)", + "regression": "The adapter must create its own directory owner-only and restrict the database file, rather than inheriting the guarantee from an ancestor. Regression test: construct the adapter with a fresh directory under a 0755 parent and assert the created directory is 0700 and the database file is 0600." + } + ], + "openGates": [ + "No permitted live profile evidence exists for the managed cloud, Local, or self-hosted profiles, so whole-cloud and whole-runtime trust acceptance stays held.", + "Agent HQ owns LocalProjectGrant issuance, desktop key storage, Event Gateway authorization, durable ContentSyncDevice and ContentKeyEpoch flows, and the product relay credential; none of those controls can be closed from this repository.", + "No live E2B sandbox provider, template, cleanup threshold or credential-denial evidence exists.", + "No live managed-cloud vault rotation and restore evidence exists for the selected credential-vault provider.", + "Packaged Local OS-secure handle resolution and packaged binary signing evidence remain absent.", + "Malware scanner composition, quarantine and fail-closed outage evidence remain a product-owned gate.", + "The independent human penetration review required by the issue acceptance criteria has not been performed; the probes in this lane are credential-free synthetic evidence only.", + "This reviewer approves none of the routed fixes; each routed owner must verify its own fix on its own exact head and an independent reviewer must then re-verify." + ], + "custody": { + "sharedIndex": "/tmp/opencode-three-fleet-20261020-fresh/data/cross-fleet-ownership.json", + "sharedIndexSha256": "561ccd22bdd6d5a4abb1ebbecc489d8ee055c046074df680262547ad52784f05", + "checkedPaths": [ + "scripts/security-trust-boundary-probes.mjs", + "docs/evidence/m11-security-trust-boundary-audit-2026-10-21.md", + "docs/evidence/m11-security-trust-boundary-matrix-2026-10-21.v1.json" + ], + "exactPathOwnerFound": false, + "openPullRequestsChecked": [1212, 1210, 1204, 1048], + "registeredWorktreesChecked": 11, + "claudeCustody": "unverified in the shared index; the three paths are new and unique, and no existing file was modified, so a custody reversal can drop the branch without touching another owner", + "existingFilesModified": [] + }, + "severitySummary": { + "high": 2, + "medium": 22, + "low": 2, + "low-medium": 3, + "total": 29, + "note": "Critical 0. Two high findings remain: CP190-F03 and CP190-F13. No finding claims a demonstrated deployed credential leak, a deployed unauthenticated approval path, or a cross-UID read.", + "beforeCalibration": { + "critical": 1, + "high": 2, + "medium-high": 2, + "medium": 21, + "low-medium": 2 + } + }, + "calibration": { + "reviewedBy": "root, against the original 40444c70a5d688628f4ddbc792c91ce957a2c2e9 report", + "reviewedAt": "2026-10-21", + "rule": "A finding states what the code does and what that makes reachable. It never asserts a deployed impact that the counterevidence contradicts.", + "changes": [ + { + "finding": "CP190-F01", + "from": "critical", + "to": "medium", + "because": "The relay-client authentication obligation is documented (docs/remote-control-relay.md) and the adapters are opt-in; no concrete configured attacker-controlled ingress supports the deployed unauthenticated-approval claim.", + "evidence": [ + "docs/remote-control-relay.md (metadata transport authentication is the concrete relay client's responsibility)", + "apps/local-control-plane/src/composition.ts:249-250,508-512 (opt-in adapter)", + "apps/hosted-control-plane/src/composition.ts:220-223,371-375 (opt-in adapter)", + "packages/remote-control-relay/src/relay.ts:45 (only relay client implementation is a test fake)" + ] + }, + { + "finding": "CP190-F02", + "from": "high", + "to": "medium", + "because": "create_execution uses the normal CommandInbox idempotency, so the generic callback-before-receipt ordering does not establish that all three processors duplicate live effects; and receipt-before-effect would be unsafe, so the ordering itself is not the defect.", + "evidence": [ + "apps/control-api/src/executions/execution-acceptance.service.ts:51,58 (CommandInbox-backed acceptance)", + "packages/domain/src/command-inbox.ts:53-58,684-705 (idempotency key binding)", + "packages/remote-control-relay/src/adapter.ts:37,79 (the metadata effect callback is injected)" + ] + }, + { + "finding": "CP190-F04", + "from": "medium-high", + "to": "low", + "because": "Ordinary thrown errors are sanitized and the Control API vault denies leases without a policy decision point, so this is a bounded library callback-result egress contract gap and not a demonstrated deployed credential leakage.", + "evidence": [ + "packages/credential-vault/src/vault.ts:462-466 (a thrown error becomes PROVIDER_OPERATION_FAILED)", + "packages/credential-vault/src/vault.ts:550-557 (containsSecret walks own enumerable string keys only)", + "apps/control-api/src/cloud-composition.ts:100-104 (no policy decision point composed)", + "packages/credential-vault/src/vault.ts:326-332 (leases deny without a decision point)" + ] + }, + { + "finding": "CP190-F05", + "from": "medium-high", + "to": "medium", + "because": "The Local profile binds loopback by default and the bearer credential is an owner-only file for the trusted local product, so the finding is an authorization-completeness gap rather than a demonstrated cross-tenant read.", + "evidence": [ + "apps/local-control-plane/src/index.ts:82-88 (default loopback bind)", + "apps/control-api/src/auth/private-api-authentication.ts:14-43 (0600 token in a 0700 directory)", + "apps/control-api/src/auth/service-authentication.ts:310-313 (the hosted comparison that is missing locally)" + ] + }, + { + "finding": "CP190-F09", + "from": "medium", + "to": "low", + "because": "The parent data directory is chmod 0700 before SQLite and its WAL/SHM sidecars are created, so the sidecar mode alone does not establish that another UID can read; the residual is defense in depth.", + "evidence": [ + "packages/sqlite-persistence/src/provider.ts:209-210 (parent chmod 0700 precedes creation)", + "packages/sqlite-persistence/src/provider.ts:211,216 (database and WAL created afterwards)", + "packages/sqlite-persistence/src/provider.ts:221 (main file restricted to 0600)" + ] + }, + { + "finding": "CP190-F03", + "from": "high", + "to": "high", + "because": "Severity unchanged; the previous revision cited a module that does not exist. The managed-Pi direct driver is packages/managed-pi-adapter/src/index.ts:322.", + "evidence": [ + "packages/managed-pi-adapter/src/index.ts:322 (export class ManagedPiDriver)", + "packages/acp-adapter/src/acp-driver-execution.ts (exists; holds no grant reference)" + ] + } + ], + "probeInterpretations": [ + "CP190-P05 now asserts both halves of the boundary: a thrown error is sanitized to PROVIDER_OPERATION_FAILED, and a returned Error carrier is not screened.", + "CP190-P09 and CP190-P10 now read packages/managed-pi-adapter/src/index.ts and fail loudly when the file is missing or empty, so a nonexistent path can never be reported as a missing grant check.", + "CP190-P13 now records both the mitigating parent-directory control and the non-owner-only sidecar mode, and classifies the result as defense in depth.", + "CP190-P02, CP190-P03 and CP190-P04 now state the reachability bound and the documented delegation, and no longer imply a deployed unauthenticated approval path or a universal duplicate-effect path.", + "CP190-P21 classifies the relay path: it proves that no application constructs the relay host adapter or the metadata processor, that the only relay client is a test fake and that the metadata effect is injected, so the finding is a contract and defence-in-depth gap rather than a deployed approval path.", + "CP190-P22 classifies the relay idempotency: it proves that the acceptance request carries the caller's idempotency key while submit_input carries none, so the durable guarantee is the CommandInbox's, not the receipt's.", + "CP190-P23 classifies the vault egress gap: it proves that the composed model-selection path normalizes the leased result and rejects a non-plain carrier, so only the uncomposed raw-result callers can reach the screen with an exotic container.", + "CP190-P24 classifies the direct-local path: it proves that the managed-Pi driver checks no grant and that the composed spawn policy leaves the working directory unconstrained, so the bound is operator configuration.", + "CP190-P25 splits the SQLite observation: it proves that the provider creates an owner-only parent before any file exists, and that the pi-durable adapter creates its own directory without a mode." + ], + "rounds": [ + { + "round": 1, + "reviewedBy": "root, against the original 40444c70a5d688628f4ddbc792c91ce957a2c2e9 report", + "changes": [ + "F-01 Critical -> Medium", + "F-02 High -> Medium", + "F-04 Medium-High -> Low", + "F-05 Medium-High -> Medium", + "F-09 Medium -> Low", + "F-03 citation corrected to packages/managed-pi-adapter/src/index.ts:322" + ] + }, + { + "round": 2, + "reviewedBy": "root, against the calibrated 43295261c6a8a5ef112e1e319961f3601f6de71b report", + "method": "Each finding was traced from its entrypoint through the authority and effect path to the point where a control is applied, and each probe was re-read as a synthetic demonstration rather than as deployed behaviour.", + "changes": [ + "F-01: the reachability statement now names the traced path and states that the Control Plane ships no metadata effect implementation at all; the earlier wording implied a live approve, deny or cancel effect that the package does not contain.", + "F-02: the reachability statement now separates create_execution, which is idempotent through the CommandInbox because the acceptance request extends the context schema that carries idempotencyKey, from submit_input, whose schema carries no idempotency key at all.", + "F-03: the reachability statement now traces the actual managed-Pi driver and shows that the child working directory is a per-attempt directory under the data directory, that the spawn policy leaves the working-directory dimension unconstrained, and that the ACP path instead uses an operator-configured absolute directory.", + "F-04: the reachability statement now names the positive control that the earlier revision missed, safeCredentialResult in the model-selection service, which rejects a non-plain carrier before the vault screen sees it, and names the two uncomposed raw-result callers.", + "F-09: split. The provider limb stays a defence-in-depth observation with no traversal path, and the adapter-owned directory is reported separately as CP190-F29 with the conditional path stated.", + "CP190-F29 added: the pi-durable adapter creates its own directory without an owner-only mode, so its protection is inherited from an ancestor rather than enforced locally.", + "Five static reachability probes added: CP190-P21 to CP190-P25." + ], + "findingsRestated": ["CP190-F01", "CP190-F02", "CP190-F03", "CP190-F04", "CP190-F09"], + "findingsAdded": ["CP190-F29"], + "probesAdded": ["CP190-P21", "CP190-P22", "CP190-P23", "CP190-P24", "CP190-P25"], + "severityChanges": [] + } + ] + }, + "targetedEvidenceQuestions": [ + { + "id": "Q-F04", + "finding": "CP190-F04", + "question": "Which profile, if any, is expected to lease a connector credential in the release candidate, and what result shapes can a tool executor return from a leased callback?", + "why": "The returned-value screen only matters once a profile composes a policy decision point. The answer sets whether the egress contract is a release blocker or a library hardening item.", + "owner": "W1 (profile compositions) with the credential-vault owner" + }, + { + "id": "Q-F09", + "finding": "CP190-F09", + "question": "Does any supported deployment capture the SQLite -wal or -shm sidecar independently of the database file, and can the data directory mode be relaxed by configuration?", + "why": "The parent directory is 0700 before creation, so the sidecar mode is defense in depth. It becomes material only if a backup or a configuration change separates a sidecar from its parent.", + "owner": "W1 (packages/sqlite-persistence) with the operations owner" + }, + { + "id": "Q-F01", + "finding": "CP190-F01", + "question": "Which concrete relay client will be configured for a release profile, and how does that client authenticate the transport and the store it reads from?", + "why": "The documented delegation is only as strong as the concrete client. Until one is named, the host processor's lack of its own check is a defence-in-depth gap and not a deployed path.", + "owner": "W1 with the Agent HQ product relay owner" + }, + { + "id": "Q-F02", + "finding": "CP190-F02", + "question": "Does the injected metadata accept callback provide its own idempotency for approve, deny, cancel and resume, and is the command-result repository bound to the durable persistence provider in the intended composition?", + "why": "create_execution is already idempotent through the CommandInbox. The receipt path matters for the metadata operations, whose effect idempotency is the injected callback's property.", + "owner": "W1 (packages/remote-control-relay)" + }, + { + "id": "Q-F03", + "finding": "CP190-F03", + "question": "Where is the LocalProjectGrant issued and validated on the product side, and which Control Plane component is intended to consume it for a direct-local dispatch?", + "why": "The Control Plane has no grant authority of its own. The answer decides whether the fix is a grant check in the direct drivers or a different composition boundary.", + "owner": "W1 with the Agent HQ desktop owner" + }, + { + "id": "Q-F05", + "finding": "CP190-F05", + "question": "Which component is authoritative for the Local workspace and project authorization set that the private API should compare a request against?", + "why": "The hosted path compares signed claims. The Local path needs the equivalent authoritative set before the body-derived scope can be replaced.", + "owner": "W1 (apps/control-api authentication) with the Agent HQ product owner" + }, + { + "id": "Q-F13", + "finding": "CP190-F13", + "question": "Which profile is expected to compose a SandboxProvider, and what E2B template, network, resource and cleanup policy is the intended baseline?", + "why": "Without a composed provider there is no sandbox boundary to accept, and the live E2B gate cannot be closed.", + "owner": "W1 (profile composition) with the operations owner" + }, + { + "id": "Q-F20", + "finding": "CP190-F20", + "question": "Is the value-based SecretCanaryGuard intended to run at every content sink, and what is the authoritative protected-value set?", + "why": "The production guard exists but has no call site, and the key-name redaction misses compound credential names. The answer decides filter placement and ownership.", + "owner": "W1 (packages/telemetry, packages/events, packages/context)" + }, + { + "id": "Q-F29", + "finding": "CP190-F29", + "question": "Which composition owns the pi-durable data directory, and is it ever created outside an owner-only ancestor?", + "why": "The adapter inherits the guarantee today. If a profile can point the adapter at a directory that is not inside an owner-only ancestor, the adapter-owned directory becomes a supported traversal path.", + "owner": "W1 (packages/pi-durable-adapter and the apps/control-api pi-durable composition)" + } + ], + "ownershipCustody": { + "checkedAt": "2026-10-21", + "purpose": "Exact-path custody result for the only proposed future source and test edit, reported without taking the paths.", + "proposedPaths": [ + "packages/credential-vault/src/vault.ts", + "packages/credential-vault/src/index.test.mjs" + ], + "sharedCrossFleetIndex": { + "path": "/tmp/opencode-three-fleet-20261020-fresh/data/cross-fleet-ownership.json", + "sha256": "561ccd22bdd6d5a4abb1ebbecc489d8ee055c046074df680262547ad52784f05", + "occurrencesOfProposedPaths": 0, + "occurrencesOfPackagePath": 0 + }, + "openPullRequests": { + "numbers": [1048, 1204, 1210, 1214, 1215, 1216, 1217, 1218, 1219, 1220, 1222, 1223], + "pathsTouchingProposedPaths": 0, + "note": "Several branches change packages/credential-vault/CHANGELOG.md and packages/credential-vault/package.json for release metadata; none changes the two proposed source or test paths." + }, + "registeredWorktrees": { + "dirtyPathsTouchingProposedPaths": 0 + }, + "result": "no exact-path owner found for either proposed path in the shared index, in any open pull request or in any registered worktree", + "interpretation": "Absence of an exact-path claim is not ownership approval. Root must verify and admit the exact paths before any edit. This lane made no change to either path, and it must not make one until that admission exists.", + "notes": "Unchanged by the second calibration round. The two proposed F-04 paths still have no exact-path owner in the shared index, in any open pull request or in any registered worktree, and this lane made no change to either path." + } +} diff --git a/scripts/security-trust-boundary-probes.mjs b/scripts/security-trust-boundary-probes.mjs new file mode 100644 index 000000000..3ccc54b7a --- /dev/null +++ b/scripts/security-trust-boundary-probes.mjs @@ -0,0 +1,950 @@ +#!/usr/bin/env bun +// CP #190 independent trust-boundary probes. +// +// These probes are credential-free and synthetic. They never read a real secret, +// never contact a deployed profile, and never send production traffic. Every +// input is generated in-process (throwaway keys, temporary directories, in-memory +// fakes) so the suite can run on a developer machine or in CI without authority. +// +// Each probe demonstrates one confirmed finding from +// docs/evidence/m11-security-trust-boundary-audit-2026-10-21.md. A probe that +// still reproduces is an OPEN finding. A probe that no longer reproduces means +// the owning team changed the behaviour: the owner must convert that probe into +// a regression test inside the package they own, and this file must be updated +// in the same change so the audit record stays truthful. +// +// Usage: bun scripts/security-trust-boundary-probes.mjs +// Exit codes: 0 = every probe reproduced the recorded open finding, +// 2 = at least one probe stopped reproducing (owner fix landed), +// 1 = a probe could not run (harness problem, not a finding). +import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { + InMemoryRelayMetadataCommandResultRepository, + RelayCommandProcessor, + RelayMetadataCommandProcessor, + RelayMetadataCommandSchema, + encryptRelayPayload, + generateHostEncryptionKeyPair, + publicEncryptionKey, +} from '../packages/remote-control-relay/src/index.ts' +import { + CredentialVault, + InMemorySecretProvider, + containsSecret, +} from '@control-plane/credential-vault' +import { redactTelemetryValue } from '../packages/telemetry/src/index.ts' +import { createFilesystemCheckpoint } from '@control-plane/deployment' +import { E2bSandboxAdapter, SandboxCoordinator } from '@control-plane/sandbox' + +const CANARY = 'cp190-synthetic-canary-0001' + +/** @type {{ id: string, title: string, obligations: string[], run: () => Promise<{ reproduced: boolean, detail: string, harness?: boolean }> }[]} */ +const probes = [] + +function probe(id, title, obligations, run) { + probes.push({ id, title, obligations, run }) +} + +async function readSource(relativePath) { + return readFile(new URL(`../${relativePath}`, import.meta.url), 'utf8') +} + +/** + * Reads a source file that a probe depends on. A missing or empty file is a + * harness failure, never a finding: a probe must never report "this module has + * no grant check" because the module it named does not exist. + */ +async function readSourceStrict(relativePath) { + const source = await readSource(relativePath).catch(() => undefined) + if (source === undefined) throw new Error(`probe source file is missing: ${relativePath}`) + if (source.trim().length === 0) throw new Error(`probe source file is empty: ${relativePath}`) + return source +} + +async function readJson(relativePath) { + return JSON.parse(await readSource(relativePath)) +} + +// --------------------------------------------------------------------------- +// Relay / product remote-control boundary (STA-L46-09, STA-L66-01, STA-L68-02) +// --------------------------------------------------------------------------- + +function metadataCommand(overrides = {}) { + return { + schemaVersion: 1, + workspaceId: 'wsp_synthetic_workspace', + hostId: 'host_synthetic', + commandId: 'cmd_synthetic_1', + operation: 'approve', + issuedAt: '2026-10-21T00:00:00.000Z', + expiresAt: '2026-10-21T01:00:00.000Z', + targetId: 'exe_synthetic_pending_interaction', + ...overrides, + } +} + +probe( + 'CP190-P01', + 'The relay metadata command schema carries no authentication material of its own', + ['STA-L46-09', 'STA-L66-01', 'STA-L68-02'], + async () => { + const parsed = RelayMetadataCommandSchema.safeParse(metadataCommand()) + if (!parsed.success) { + return { reproduced: false, detail: 'metadata command schema rejected the synthetic command' } + } + const fields = Object.keys(parsed.data).toSorted() + const authenticators = fields.filter((field) => + /signature|mac|hmac|token|credential|key|nonce|sequence|proof/i.test(field) + ) + const documented = await readSource('docs/remote-control-relay.md') + const delegated = documented.includes( + 'Authentication of that metadata transport remains the responsibility of the concrete relay client' + ) + return { + reproduced: authenticators.length === 0, + detail: `metadata command fields = ${fields.join(', ')}; authentication fields present = ${authenticators.length}; the obligation is documented as delegated to the concrete relay client = ${delegated}`, + } + } +) + +probe( + 'CP190-P02', + 'The relay host processor applies no authentication of its own to a metadata command', + ['STA-L46-09', 'STA-L66-01'], + async () => { + const effects = [] + const processor = new RelayMetadataCommandProcessor( + 'host_synthetic', + 'wsp_synthetic_workspace', + async (command) => { + effects.push(command.operation) + return { accepted: true } + }, + new InMemoryRelayMetadataCommandResultRepository() + ) + const result = await processor.process(metadataCommand(), new Date('2026-10-21T00:30:00.000Z')) + const local = await readSource('apps/local-control-plane/src/composition.ts') + const hosted = await readSource('apps/hosted-control-plane/src/composition.ts') + const optIn = + local.includes('readonly remoteControl?: RemoteControlHostAdapter') && + hosted.includes('readonly remoteControl?: RemoteControlHostAdapter') + const relay = await readSource('packages/remote-control-relay/src/relay.ts') + const onlyFakeClient = relay.includes('export class FakeOpaqueRelay') + return { + reproduced: result.outcome === 'accepted' && effects.length === 1, + detail: `outcome=${result.outcome}, applied effects=${effects.join(',') || 'none'}; the processor adds no provenance check of its own. Reachability: the relay adapter is opt-in in both compositions = ${optIn}; the only relay client in the repository is the test fake = ${onlyFakeClient}. No deployed attacker-controlled ingress is claimed.`, + } + } +) + +probe( + 'CP190-P03', + 'The relay processor offers no idempotency across command identifiers', + ['STA-L16-01', 'STA-L68-03', 'STA-L78-01'], + async () => { + const effects = [] + const processor = new RelayMetadataCommandProcessor( + 'host_synthetic', + 'wsp_synthetic_workspace', + async (command) => { + effects.push(command.targetId) + return { accepted: true } + }, + new InMemoryRelayMetadataCommandResultRepository() + ) + const now = new Date('2026-10-21T00:30:00.000Z') + await processor.process(metadataCommand({ commandId: 'cmd_synthetic_a' }), now) + await processor.process(metadataCommand({ commandId: 'cmd_synthetic_b' }), now) + const adapter = await readSource('packages/remote-control-relay/src/adapter.ts') + const injected = adapter.includes( + 'readonly metadataCommands?: RelayMetadataCommandProcessor' + ) + return { + reproduced: effects.length === 2, + detail: `effects applied for one target under two command ids = ${effects.length}; the effect callback is injected, so its own idempotency is the callback's property = ${injected}. This is not a duplicate-effect claim: create_execution is idempotent through the CommandInbox.`, + } + } +) + +probe( + 'CP190-P04', + 'The relay effect and its idempotency receipt are two separate writes with no atomic binding', + ['STA-L16-01', 'STA-L68-03', 'STA-L78-01'], + async () => { + const keyPair = await generateHostEncryptionKeyPair( + 'host_synthetic', + new Date('2026-10-21T00:00:00.000Z') + ) + const plaintext = new TextEncoder().encode('{"synthetic":true}') + const envelope = await encryptRelayPayload({ + recipient: publicEncryptionKey(keyPair), + workspaceId: 'wsp_synthetic_workspace', + commandId: 'cmd_synthetic_content', + payloadType: 'create_execution', + payloadSchemaVersion: 1, + issuedAt: '2026-10-21T00:00:00.000Z', + expiresAt: '2026-10-21T01:00:00.000Z', + plaintext, + }) + let effects = 0 + let receiptPersisted = false + const processor = new RelayCommandProcessor( + 'host_synthetic', + 'wsp_synthetic_workspace', + () => keyPair, + { + get: async () => undefined, + put: async () => { + // The receipt write fails, as in a crash between the effect and the receipt. + receiptPersisted = false + throw new Error('synthetic_receipt_write_failure') + }, + }, + async () => { + effects += 1 + return { accepted: true } + } + ) + let thrown + try { + await processor.process(envelope, new Date('2026-10-21T00:30:00.000Z')) + } catch (error) { + thrown = error + } + const relay = await readSource('packages/remote-control-relay/src/relay.ts') + const execution = await readSource('packages/remote-control-relay/src/execution.ts') + const onlyInMemory = + relay.includes('export class InMemoryRelayCommandResultRepository') && + relay.includes('export class InMemoryRelayMetadataCommandResultRepository') && + execution.includes('InMemoryRelayCommandResultRepository') + const documented = await readSource('docs/remote-control-relay.md') + const delegation = documented.includes( + 'the in-memory repository is a test/default seam, not restart durability' + ) + return { + reproduced: + effects === 1 && receiptPersisted === false && thrown !== undefined && onlyInMemory, + detail: `effects=${effects}, receipt persisted=${receiptPersisted}, threw=${thrown !== undefined}; the two writes are not atomic. The effect-before-receipt order is deliberate and safe: a receipt written first would record an effect that never happened. Only in-memory receipt repositories ship = ${onlyInMemory}; the durable repository is documented as a composition requirement = ${delegation}. create_execution is idempotent through the CommandInbox, so no duplicate live effect is claimed for it.`, + } + } +) + +// --------------------------------------------------------------------------- +// Credential vault egress guard (STA-L40-17, STA-L40-18, STA-L76-03) +// --------------------------------------------------------------------------- + +probe( + 'CP190-P05', + 'The credential-vault egress screen misses a returned carrier but a thrown error is sanitized', + ['STA-L40-17', 'STA-L40-18', 'STA-L76-03'], + async () => { + const now = '2026-10-21T00:00:00.000Z' + const allowDecisionPoint = { + requests: [], + async authorize(request) { + this.requests.push(request) + return { + effect: 'allow', + decisionId: `sha256:${'b'.repeat(64)}`, + reasonCode: 'CEDAR_PERMIT', + policySnapshot: request.policySnapshot, + evaluatedAt: request.context.requestedAt, + } + }, + } + const vault = new CredentialVault({ + provider: new InMemorySecretProvider(), + now: () => now, + decisionPoint: allowDecisionPoint, + }) + await vault.create({ + credentialId: 'crd_01JABCDEF0123456789ABCDEFG', + workspaceId: 'wsp_01JABCDEF0123456789ABCDEFG', + connectorRef: 'connector:synthetic', + provider: 'synthetic', + secret: CANARY, + createdAt: now, + }) + const scope = { + workspaceId: 'wsp_01JABCDEF0123456789ABCDEFG', + operation: 'synthetic.call', + resourceRef: 'tool:synthetic', + } + const first = await vault.lease({ + credentialLeaseId: 'crl_01JABCDEF0123456789ABCDEFG', + credentialId: 'crd_01JABCDEF0123456789ABCDEFG', + requestId: 'req_01JABCDEF0123456789ABCDEFG', + workspaceId: scope.workspaceId, + principalRef: 'service:tool-gateway', + operation: scope.operation, + resourceRef: scope.resourceRef, + requestedAt: now, + expiresAt: '2026-10-21T00:05:00.000Z', + policySnapshot: { + policyId: 'workspace-standard', + version: 1, + digest: `sha256:${'a'.repeat(64)}`, + }, + }) + // Half one: a callback that RETURNS an Error carrier. The screen walks own + // enumerable string keys, so the carrier is not inspected. + let returned + let blocked = false + try { + returned = await vault.use(first.capabilityRef, scope, async (secret) => new Error(secret)) + } catch (error) { + blocked = error instanceof Error && /SECRET_EGRESS_BLOCKED/.test(error.message) + } + const carrierMissed = containsSecret(new Error(CANARY), CANARY) === false + const carried = returned instanceof Error && returned.message.includes(CANARY) + // Half two: a callback that THROWS. The vault converts the throw into a + // bounded code, so the secret never leaves through the error path. + const second = await vault.lease({ + credentialLeaseId: 'crl_01JABCDEF0123456789ABCDEF2', + credentialId: 'crd_01JABCDEF0123456789ABCDEFG', + requestId: 'req_01JABCDEF0123456789ABCDEF2', + workspaceId: scope.workspaceId, + principalRef: 'service:tool-gateway', + operation: scope.operation, + resourceRef: scope.resourceRef, + requestedAt: now, + expiresAt: '2026-10-21T00:05:00.000Z', + policySnapshot: { + policyId: 'workspace-standard', + version: 1, + digest: `sha256:${'a'.repeat(64)}`, + }, + }) + let thrownCode + try { + await vault.use(second.capabilityRef, scope, async (secret) => { + throw new Error(secret) + }) + } catch (error) { + thrownCode = error instanceof Error ? error.message : String(error) + } + const thrownSanitized = thrownCode === 'PROVIDER_OPERATION_FAILED' + const composition = await readSource('apps/control-api/src/cloud-composition.ts') + const deniesWithoutDecisionPoint = composition.includes( + 'no policy decision point is composed, so leases deny' + ) + return { + reproduced: carrierMissed && carried && blocked === false && thrownSanitized, + detail: `returned Error carrier not screened = ${carrierMissed && carried && blocked === false}; thrown error sanitized to PROVIDER_OPERATION_FAILED = ${thrownSanitized}; the Control API composition composes no decision point so leases deny = ${deniesWithoutDecisionPoint}. This is a bounded library callback-result contract gap: no deployed credential leakage is claimed or demonstrated.`, + } + } +) + +probe( + 'CP190-P06', + 'Structured-log redaction misses compound credential key names', + ['STA-L40-17', 'STA-L76-03'], + async () => { + const redacted = redactTelemetryValue({ sessionToken: CANARY }) + const leaked = JSON.stringify(redacted).includes(CANARY) + return { + reproduced: leaked, + detail: `redactTelemetryValue({ sessionToken }) leaked the value = ${leaked}`, + } + } +) + +// --------------------------------------------------------------------------- +// Local / private API trust boundary (STA-L08-01, STA-L46-06, STA-L46-17) +// --------------------------------------------------------------------------- + +probe( + 'CP190-P07', + 'The Local private API derives workspace scope from the request body', + ['STA-L08-01', 'STA-L46-06', 'STA-L46-17'], + async () => { + const source = await readSource('apps/control-api/src/auth/private-api-authentication.ts') + const bodyDerived = + /workspaceIds:\s*typeof workspaceId === 'string' \? \[workspaceId\] : \[\]/.test(source) + const hosted = await readSource('apps/control-api/src/auth/service-authentication.ts') + const claimChecked = hosted.includes('claims.workspaceIds.includes(envelope.workspaceId)') + return { + reproduced: bodyDerived && claimChecked, + detail: `local authenticator copies body workspace ids = ${bodyDerived}; hosted authenticator compares signed claims = ${claimChecked}`, + } + } +) + +// --------------------------------------------------------------------------- +// LocalProjectGrant reachability (STA-L17-01, STA-L42-09, STA-L42-10, STA-L69-02) +// --------------------------------------------------------------------------- + +probe( + 'CP190-P08', + 'LocalProjectGrant discovery state is a hard-coded constant in the production path', + ['STA-L17-01', 'STA-L42-09', 'STA-L69-02'], + async () => { + const source = await readSource('apps/runtime-gateway/src/runtime-inventory-ingestion.ts') + const constant = source.includes( + "localProjectGrant: { required: false, state: 'not_required' }" + ) + return { + reproduced: constant, + detail: `runtime-inventory-ingestion.ts hard-codes the localProjectGrant state = ${constant}`, + } + } +) + +probe( + 'CP190-P09', + 'The direct-local ACP and managed-Pi drivers never consult a project grant', + ['STA-L17-01', 'STA-L42-09', 'STA-L69-02'], + async () => { + // The managed-Pi direct driver lives in the package entry module; an + // earlier revision of this probe named a module that does not exist and + // therefore reported a missing grant check for a missing file. + const direct = [ + 'packages/acp-adapter/src/index.ts', + 'packages/acp-adapter/src/acp-driver-execution.ts', + 'packages/managed-pi-adapter/src/index.ts', + ] + const findings = [] + for (const path of direct) { + const source = await readSourceStrict(path) + const mentionsGrant = /grantState|LocalProjectGrant|localProjectGrant/.test(source) + findings.push(`${path}: grantChecked=${mentionsGrant}`) + } + return { + reproduced: findings.every((finding) => finding.endsWith('grantChecked=false')), + detail: findings.join('; '), + } + } +) + +probe( + 'CP190-P10', + 'The canonical path containment a LocalProjectGrant would need does not exist', + ['STA-L42-10', 'STA-L69-01'], + async () => { + const sources = [ + 'packages/acp-adapter/src/index.ts', + 'packages/acp-adapter/src/acp-driver-execution.ts', + 'packages/managed-pi-adapter/src/index.ts', + 'packages/managed-pi-adapter/src/process-client.ts', + ] + const findings = [] + for (const path of sources) { + const source = await readSourceStrict(path) + findings.push(`${path}: hasRealpath=${source.includes('realpath')}`) + } + return { + reproduced: findings.every((finding) => finding.endsWith('hasRealpath=false')), + detail: findings.join('; '), + } + } +) + +// --------------------------------------------------------------------------- +// Runtime Gateway command lifetime (STA-L61-08) +// --------------------------------------------------------------------------- + +probe( + 'CP190-P11', + 'Runtime Gateway runtime commands are not bounded to the 24 h remote-command maximum', + ['STA-L61-08'], + async () => { + const runtime = await readSource('apps/runtime-gateway/src/runtime-command-delivery.ts') + const context = await readSource('apps/runtime-gateway/src/context-command-delivery.ts') + const contextBounded = context.includes('EXPIRY_TOO_LONG') + const runtimeBounded = runtime.includes('EXPIRY_TOO_LONG') + return { + reproduced: contextBounded && !runtimeBounded, + detail: `context family bounded = ${contextBounded}; runtime family bounded = ${runtimeBounded}`, + } + } +) + +// --------------------------------------------------------------------------- +// Local persistence and backups (STA-L76-01, STA-L76-03) +// --------------------------------------------------------------------------- + +probe( + 'CP190-P12', + 'A Local filesystem checkpoint copies the private-file secrets directory in cleartext', + ['STA-L76-01', 'STA-L76-03'], + async () => { + const root = await mkdtemp(join(tmpdir(), 'cp190-checkpoint-')) + try { + const source = join(root, 'data') + const destination = join(root, 'checkpoint') + await mkdir(join(source, 'secrets'), { recursive: true, mode: 0o700 }) + await writeFile(join(source, 'secrets', 'provider-key'), `${CANARY}\n`, { mode: 0o600 }) + await writeFile(join(source, 'control-plane.sqlite'), 'sqlite-placeholder', { mode: 0o600 }) + await createFilesystemCheckpoint({ + sourceDirectory: source, + destinationDirectory: destination, + profile: 'local', + }) + const copied = await readFile(join(destination, 'secrets', 'provider-key'), 'utf8') + return { + reproduced: copied.includes(CANARY), + detail: `checkpoint contains the cleartext provider secret = ${copied.includes(CANARY)}`, + } + } finally { + await rm(root, { recursive: true, force: true }) + } + } +) + +probe( + 'CP190-P13', + 'SQLite WAL sidecars keep the pre-restriction file mode, inside an owner-only parent directory', + ['STA-L76-01', 'STA-L23-03'], + async () => { + const source = await readSource('packages/sqlite-persistence/src/provider.ts') + const parentMode = source.indexOf('await chmod(dirname(this.#path), 0o700)') + const opened = source.indexOf('new DatabaseSync(this.#path)') + const walIndex = source.indexOf('journal_mode = WAL') + const chmodIndex = source.indexOf('chmod(this.#path, 0o600)', walIndex) + const sidecarChmod = /chmod\([^)]*-wal/.test(source) + // The parent directory is restricted before any file exists, so another UID + // cannot traverse into it. The sidecar mode is a defense-in-depth gap, not + // a demonstrated cross-UID read. + const parentRestrictedFirst = parentMode >= 0 && parentMode < opened && parentMode < walIndex + return { + reproduced: + parentRestrictedFirst && walIndex > opened && chmodIndex > walIndex && !sidecarChmod, + detail: `parent chmod 0700 at ${parentMode} precedes the database at ${opened} and the WAL pragma at ${walIndex}; the main file is restricted to 0600 at ${chmodIndex}; a sidecar chmod exists = ${sidecarChmod}. Another UID cannot traverse the 0700 parent, so this is defense in depth and no cross-UID read is claimed.`, + } + } +) + +// --------------------------------------------------------------------------- +// Portable export / import (STA-L08-01, STA-L76-03) +// --------------------------------------------------------------------------- + +probe( + 'CP190-P14', + 'The portable import path scans with an empty sensitive-value list', + ['STA-L76-03'], + async () => { + const source = await readSource('packages/profile-portability/src/migration.ts') + const importScan = source.includes('assertSafeExport(manifest, [])') + const exportScan = /assertSafeExport\(manifest, options\.sensitiveValues \?\? \[\]\)/.test( + source + ) + return { + reproduced: importScan && exportScan, + detail: `import uses an empty sensitive list = ${importScan}; export honours the caller list = ${exportScan}`, + } + } +) + +probe( + 'CP190-P15', + 'Portable export secret references carry no workspace scope', + ['STA-L08-01', 'STA-L76-03'], + async () => { + const source = await readSource('packages/profile-portability/src/manifest.ts') + const start = source.indexOf('PortableSecretReferenceSchema') + const schema = source.slice(start, start + 700) + const scoped = /workspaceId/.test(schema) + return { + reproduced: !scoped, + detail: `PortableSecretReferenceSchema declares workspaceId = ${scoped}`, + } + } +) + +// --------------------------------------------------------------------------- +// Sandbox (STA-L46-17, STA-L77-01, STA-L83-02) +// --------------------------------------------------------------------------- + +probe( + 'CP190-P16', + 'Sandbox credential bindings are injected after the ambient-credential denial runs', + ['STA-L46-17', 'STA-L77-01'], + async () => { + const sandboxId = 'sbx_01JABCDEF0123456789ABCDEFG' + const provider = { + providerRef: 'synthetic', + observed: undefined, + create: async (request) => ({ + id: sandboxId, + createdAt: '2026-10-21T00:00:00.000Z', + expiresAt: '2026-10-21T01:00:00.000Z', + metadata: { + workspaceId: request.metadata.workspaceId, + executionId: request.metadata.executionId, + attemptId: request.metadata.attemptId, + }, + }), + execute: async (_sandboxId, request) => { + provider.observed = request + return { + exitCode: 0, + stdout: 'ok', + stderr: '', + timedOut: false, + durationMs: 1, + truncated: false, + } + }, + upload: async () => {}, + download: async () => new Uint8Array(), + status: async () => ({ sandboxId, state: 'ready' }), + destroy: async () => {}, + list: async () => [], + } + const adapter = new E2bSandboxAdapter({ + client: provider, + resolveCredential: async () => CANARY, + }) + const coordinator = new SandboxCoordinator({ + provider: adapter, + promoter: { + promote: async () => ({ + artifactId: 'art_synthetic', + locator: 'synthetic', + digest: `sha256:${'0'.repeat(64)}`, + sizeBytes: 0, + }), + }, + now: () => '2026-10-21T00:00:00.000Z', + }) + const handle = await coordinator.create({ + workspaceId: 'wsp_01JABCDEF0123456789ABCDEFG', + executionId: 'exe_01JABCDEF0123456789ABCDEFG', + attemptId: 'att_01JABCDEF0123456789ABCDEFG', + policy: { + template: 'base', + timeoutMs: 60_000, + limits: { cpuCount: 1, memoryMb: 256, storageMb: 128, outputBytes: 4096 }, + network: { mode: 'deny_all' }, + }, + }) + let denied = false + let failure + try { + await coordinator.execute({ + sandboxId: handle.sandboxId, + command: ['sh', '-c', 'env'], + environment: {}, + credentialBindings: [ + { leaseId: 'crl_01JABCDEF0123456789ABCDEFG', environmentName: 'AWS_SESSION' }, + ], + timeoutMs: 5_000, + }) + } catch (error) { + failure = error instanceof Error ? error.message : String(error) + denied = failure.includes('POLICY_DENIED') + } + const injected = JSON.stringify(provider.observed?.environment ?? {}).includes(CANARY) + return { + reproduced: denied === false && injected, + detail: `policy denied = ${denied}; failure = ${failure ?? 'none'}; raw credential reached the provider environment = ${injected}`, + } + } +) + +probe( + 'CP190-P17', + 'The sandbox metadata-service denial list is incomplete', + ['STA-L77-01'], + async () => { + const source = await readSource('packages/sandbox/src/index.ts') + const start = source.indexOf('function isMetadataHost') + const body = source.slice(start, start + 400) + const absent = ['169.254.170.2', 'fd00:ec2::254', '100.100.100.200', '169.254.169.253'].filter( + (host) => !body.includes(host) + ) + return { + reproduced: absent.length > 0, + detail: `known metadata endpoints absent from the deny list = ${absent.join(', ')}`, + } + } +) + +probe( + 'CP190-P18', + 'The sandbox package is not composed into any application or runtime package', + ['STA-L77-01', 'STA-L83-02'], + async () => { + const consumers = [] + for (const root of ['apps', 'packages']) { + const entries = await readdir(new URL(`../${root}/`, import.meta.url), { + withFileTypes: true, + }) + for (const entry of entries) { + if (!entry.isDirectory()) continue + const manifestPath = `${root}/${entry.name}/package.json` + const manifest = await readJson(manifestPath).catch(() => undefined) + if (!manifest) continue + const dependencies = { + ...manifest.dependencies, + ...manifest.devDependencies, + } + if (dependencies['@control-plane/sandbox'] !== undefined) consumers.push(manifestPath) + } + } + return { + reproduced: consumers.length === 0, + detail: `workspace packages depending on @control-plane/sandbox = ${consumers.length}`, + } + } +) + +// --------------------------------------------------------------------------- +// Managed cloud / abuse boundaries (STA-L40-09, STA-L54-11, STA-L57-01) +// --------------------------------------------------------------------------- + +probe( + 'CP190-P19', + 'The public control API has no HTTP request rate limit', + ['STA-L54-11', 'STA-L57-01'], + async () => { + const manifest = await readJson('apps/control-api/package.json') + const dependencies = { ...manifest.dependencies, ...manifest.devDependencies } + const rateLimit = Object.keys(dependencies).some((name) => /rate-limit/i.test(name)) + const source = await readSource('apps/control-api/src/application.ts') + const configured = /rateLimit|rate_limit/.test(source) + return { + reproduced: rateLimit === false && configured === false, + detail: `rate-limit dependency = ${rateLimit}; adapter rate-limit configuration = ${configured}`, + } + } +) + +probe( + 'CP190-P20', + 'The Restate ingress submission carries no request credential', + ['STA-L40-09', 'STA-L24-01'], + async () => { + const source = await readSource( + 'apps/control-api/src/executions/execution-acceptance.service.ts' + ) + const start = source.indexOf('const response = await this.#fetch(') + const block = start >= 0 ? source.slice(start, start + 900) : '' + const carriesCredential = /authorization|x-restate|bearer|token/i.test(block) + return { + reproduced: carriesCredential === false, + detail: `restate ingress POST carries an authorization header = ${carriesCredential}`, + } + } +) + +// --------------------------------------------------------------------------- +// Reachability probes. These are static: they read composed source and prove +// where a control is or is not wired, without executing a product path. +// --------------------------------------------------------------------------- + +probe( + 'CP190-P21', + 'No composition builds the relay host adapter or the metadata processor, and no metadata effect implementation ships', + ['STA-L46-09', 'STA-L52-01'], + async () => { + const roots = ['apps'] + const hits = [] + for (const root of roots) { + const entries = await readdir(new URL(`../${root}/`, import.meta.url), { + withFileTypes: true, + }) + for (const entry of entries) { + if (!entry.isDirectory()) continue + const source = await readSource(`${root}/${entry.name}/src/`).catch(() => undefined) + void source + const files = await readdir(new URL(`../${root}/${entry.name}/src/`, import.meta.url), { + withFileTypes: true, + }).catch(() => []) + for (const file of files) { + if (!file.isFile() || !file.name.endsWith('.ts')) continue + const text = await readSource(`${root}/${entry.name}/src/${file.name}`).catch(() => '') + for (const marker of [ + 'new RelayMetadataCommandProcessor', + 'new RemoteControlHostAdapter', + ]) { + if (text.includes(marker)) + hits.push(`${root}/${entry.name}/src/${file.name}: ${marker}`) + } + } + } + } + const relay = await readSource('packages/remote-control-relay/src/relay.ts') + const adapter = await readSource('packages/remote-control-relay/src/adapter.ts') + const onlyFake = relay.includes('export class FakeOpaqueRelay') + const injectedEffect = adapter.includes( + 'readonly metadataCommands?: RelayMetadataCommandProcessor' + ) + const injectedAccept = relay.includes( + 'readonly accept: (command: RelayMetadataCommand) => Promise' + ) + return { + reproduced: hits.length === 0 && onlyFake && injectedEffect && injectedAccept, + detail: `application constructions of the relay adapter or metadata processor = ${hits.length}; the only relay client implementation is the test fake = ${onlyFake}; the metadata processor is injected = ${injectedEffect}; the metadata effect callback is injected = ${injectedAccept}. No shipped approve, deny or cancel effect exists, so no deployed unauthenticated approval path is claimed.`, + } + } +) + +probe( + 'CP190-P22', + 'The relay create_execution request carries the caller idempotency key and submit_input carries none', + ['STA-L16-01', 'STA-L68-03'], + async () => { + const contracts = await readSource('packages/contracts/src/control-api.ts') + const contextSchema = contracts.slice( + contracts.indexOf('CommandContextSchema'), + contracts.indexOf('CommandContextSchema') + 900 + ) + const carriesKey = /idempotencyKey/.test(contextSchema) + const acceptance = contracts.slice( + contracts.indexOf('ExecutionAcceptanceRequestSchema'), + contracts.indexOf('ExecutionAcceptanceRequestSchema') + 200 + ) + const extendsContext = /CommandContextSchema\.extend/.test(acceptance) + const execution = await readSource('packages/remote-control-relay/src/execution.ts') + const submit = execution.slice( + execution.indexOf('RelaySubmitInputRequestSchema'), + execution.indexOf('RelaySubmitInputRequestSchema') + 600 + ) + const submitHasKey = /idempotencyKey/.test(submit) + const inbox = await readSource('packages/domain/src/command-inbox.ts') + const inboxKey = /idempotencyKey/.test(inbox) + return { + reproduced: carriesKey && extendsContext && submitHasKey === false && inboxKey, + detail: `the acceptance request extends the context schema that carries idempotencyKey = ${carriesKey && extendsContext}; the relay submit_input schema carries an idempotency key = ${submitHasKey}; the CommandInbox binds an idempotency key = ${inboxKey}. create_execution is therefore idempotent in the CommandInbox, and the relay receipt adds no idempotency beyond the command id.`, + } + } +) + +probe( + 'CP190-P23', + 'The composed model-selection path normalizes the leased callback result and rejects a non-plain carrier', + ['STA-L40-17', 'STA-L40-18'], + async () => { + const service = await readSource('packages/model-gateway/src/selection-service.ts') + const normalizer = service.slice( + service.indexOf('function safeCredentialResult'), + service.indexOf('function safeCredentialResult') + 2200 + ) + const rejectsPrototype = + normalizer.includes('Object.getPrototypeOf(input) !== Object.prototype') && + normalizer.includes('Object.getPrototypeOf(input) !== null') + const rejectsSymbols = normalizer.includes('Object.getOwnPropertySymbols(input).length') + const usedOnTheLeasedPath = service.includes( + 'return safeCredentialResult(await operation(secret))' + ) + const vault = await readSource('packages/credential-vault/src/vault.ts') + const screen = vault.slice(vault.indexOf('export function containsSecret')) + const ownEnumerableOnly = screen.includes('Object.entries(value).some(') + const broker = await readSource('packages/credential-vault/src/tool-broker.ts') + const rawResult = broker.includes('return this.#vault.use(') + return { + reproduced: + rejectsPrototype && rejectsSymbols && usedOnTheLeasedPath && ownEnumerableOnly && rawResult, + detail: `the model-selection leased path normalizes the result = ${usedOnTheLeasedPath}; the normalizer rejects a non-plain prototype = ${rejectsPrototype}; it rejects symbol keys = ${rejectsSymbols}. The vault screen itself is an own-enumerable-key traversal = ${ownEnumerableOnly}, and the credential broker returns the raw callback result = ${rawResult}. The composed path is protected; only the uncomposed raw-result callers can carry a non-plain container.`, + } + } +) + +probe( + 'CP190-P24', + 'The managed-Pi direct driver checks no grant and its spawn policy leaves the working directory unconstrained', + ['STA-L17-01', 'STA-L42-09', 'STA-L42-10'], + async () => { + const driver = await readSource('packages/managed-pi-adapter/src/index.ts') + const driverStart = driver.slice( + driver.indexOf('export class ManagedPiDriver'), + driver.indexOf('export class ManagedPiDriver') + 6000 + ) + const checksGrant = /grantState|LocalProjectGrant|localProjectGrant/.test(driverStart) + const local = await readSource('apps/local-control-plane/src/index.ts') + const policy = local.slice( + local.indexOf('spawnPolicy: { allowedExecutables: [executablePath] }') - 200, + local.indexOf('spawnPolicy: { allowedExecutables: [executablePath] }') + 200 + ) + const workingDirectories = /allowedWorkingDirectories/.test(policy) + const runtime = await readSource('packages/deployment/src/process-runtime.ts') + const optional = runtime.includes( + 'Absent constraints leave the corresponding dimension unconstrained' + ) + const acp = await readSource('apps/local-control-plane/src/installed-acp-runtime.ts') + const acpCwd = + acp.includes('cwd: options.cwd') && acp.includes('allowedWorkingDirectories: [options.cwd]') + return { + reproduced: checksGrant === false && workingDirectories === false && optional && acpCwd, + detail: `the managed-Pi direct driver consults a grant = ${checksGrant}; its spawn policy names an allowed working directory = ${workingDirectories}; the policy documents absent dimensions as unconstrained = ${optional}; the ACP runtime runs the child in the operator-configured cwd and mirrors it into the allowlist = ${acpCwd}. The direct-local working directory is bounded by operator configuration, not by a grant.`, + } + } +) + +probe( + 'CP190-P25', + 'The SQLite provider creates an owner-only parent, while the pi-durable adapter creates its own directory without one', + ['STA-L76-01', 'STA-L23-03'], + async () => { + const provider = await readSource('packages/sqlite-persistence/src/provider.ts') + const parentMode = provider.indexOf('await chmod(dirname(this.#path), 0o700)') + const opened = provider.indexOf('new DatabaseSync(this.#path)') + const wal = provider.indexOf('journal_mode = WAL') + const providerOwnerOnly = parentMode >= 0 && parentMode < opened && parentMode < wal + const journal = await readSource('packages/pi-durable-adapter/src/journal.ts') + const journalDir = journal.slice( + journal.indexOf('constructor(path: string)'), + journal.indexOf('constructor(path: string)') + 400 + ) + const journalMode = /mkdirSync\(dirname\(path\), \{ recursive: true \}\)/.test(journalDir) + const journalFileMode = /chmod\(/.test(journalDir) + const lease = await readSource('packages/pi-durable-adapter/src/lease.ts') + const leaseMode = /mkdirSync\(directory, \{ recursive: true \}\)/.test(lease) + return { + reproduced: providerOwnerOnly && journalMode && journalFileMode === false && leaseMode, + detail: `the provider creates and restricts its parent to 0700 before the database and WAL exist = ${providerOwnerOnly}; the pi-durable journal creates its own directory without a mode = ${journalMode}; it never restricts the database file = ${journalFileMode}; the session lease creates its directory without a mode = ${leaseMode}. The provider path has no traversal path; the adapter-owned directory is the conditional one, because its protection is inherited from an ancestor the composition happens to create owner-only.`, + } + } +) + +// --------------------------------------------------------------------------- +// Runner +// --------------------------------------------------------------------------- + +const results = [] +let harnessFailures = 0 +for (const entry of probes) { + let outcome + try { + outcome = await entry.run() + } catch (error) { + outcome = { + reproduced: false, + detail: `probe could not run: ${error instanceof Error ? error.message : String(error)}`, + harness: true, + } + } + if (outcome.harness) harnessFailures += 1 + results.push({ ...entry, ...outcome }) +} + +const width = Math.max(...results.map((entry) => entry.id.length)) +for (const entry of results) { + const status = entry.harness ? 'HARNESS' : entry.reproduced ? 'OPEN' : 'RESOLVED' + process.stdout.write(`${entry.id.padEnd(width)} ${status.padEnd(8)} ${entry.detail}\n`) + process.stdout.write( + `${' '.repeat(width)} obligations: ${entry.obligations.join(', ')}\n` + ) +} + +const open = results.filter((entry) => entry.reproduced).length +const resolved = results.filter((entry) => !entry.reproduced && !entry.harness).length +process.stdout.write( + `\nCP #190 trust-boundary probes: ${open} open, ${resolved} resolved, ${harnessFailures} harness failures.\n` +) +process.stdout.write( + 'Every probe used synthetic in-process inputs; no secret was read and no traffic was sent.\n' +) + +if (harnessFailures > 0) process.exit(1) +if (resolved > 0) { + process.stdout.write( + 'At least one recorded finding no longer reproduces. Convert that probe into an owner regression test and update this file.\n' + ) + process.exit(2) +} +process.exit(0)