@sabaiway/agent-workflow-kit 14.23.0 → 14.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 14.24.0 — the bridge tier also writes the placed bridges' hosts into the project sandbox, so where the host honours the settings sandbox keys a review the harness keeps sandboxed is not refused them (AD-172)
8
+
9
+ **`velocity-profile --bridge-tier` now merges the declared hosts of every placed bridge (each bundled manifest's
10
+ `networkHosts`) into the project `.claude/settings.json` `sandbox.network.allowedDomains`.** Before, the tier wrote
11
+ only its allow rules (the review wrappers' code mode and, with agy-review placed, the grounding pre-step) and the
12
+ wrappers' `sandbox.excludedCommands` entries, and a harness that kept a wrapper
13
+ sandboxed anyway (Claude Code 2.1.291, observed 2026-10-06) refused its hosts until the user added them by hand.
14
+
15
+ - **What the consent covers.** Where the host honours the settings sandbox keys, every sandboxed command of the
16
+ project can then reach those hosts, not only the wrappers. The printed tier notice, `--help`, the velocity and
17
+ recommendations mode docs and a new `bridge-tier` posture note say so; the `--autonomy` preview keeps reporting the
18
+ hosts as a network weakening. `sandbox.filesystem.allowWrite` stays unseeded: the bridges' writable state dirs stay
19
+ hand-apply.
20
+ - **Same writer rules as the other surfaces.** Dry-run by default; foreign hosts and other `sandbox.network` keys
21
+ kept; a host already present left where it is; a second `--apply` changes no byte. The dry-run prints
22
+ `would add sandbox.network.allowedDomains entries: N`, the hosts and `already present (allowedDomains): M`.
23
+ - **Stops with nothing written.** A non-object `sandbox.network` or a non-array `allowedDomains` in the project file
24
+ ends a `--bridge-tier` run as `VELOCITY_MALFORMED` naming the key; an unreadable or unparsable bundled manifest ends
25
+ it naming the manifest. The flagless, `--kit-tools` and `--autonomy` runs read those keys as before.
26
+ - **The advisor offers the re-run.** The Recommendations `bridge-tier` item now also counts missing declared hosts,
27
+ so a project that ran the tier before this release is offered the one re-run at its next upgrade:
28
+ `bridge-wrappers tier incomplete — {n} entr(ies) missing (allow rules, sandbox exclusions or declared hosts of placed bridges)`.
29
+ - **One reader.** The manifest reader moved unchanged into `tools/bridge-sandbox-recipe.mjs`; the tier and the
30
+ sandbox-lane recipe both read it. The tarball holds 359 files.
31
+
7
32
  ## 14.23.0 — a delegated codex or agy run uses Jev as your own run does; ships codex-cli-bridge 5.0.0 and antigravity-cli-bridge 7.0.0 (AD-171)
8
33
 
9
34
  **Every bridge wrapper now passes `TYPESAFE_API_KEY` to the run, and on Linux with codex-cli 0.160.0 or newer a
package/README.md CHANGED
@@ -246,7 +246,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
246
246
  | `/agent-workflow-kit core-evidence` | any time | **the ONE loop-evidence writer** (strip-the-kit) — every core evidence record lands in a single append-only JSONL store inside the git dir (never committable; versioned schema, latest-per-key supersession, byte-identical duplicates refused, malformed lines fail every reader closed). `red-proof "<file>#<pattern>"` declares an observed-red **BEFORE a bugfix** (N/N red runs + content custody + base + the pre-fix fingerprint; green/mixed/timeout are DISTINGUISHED refusals — nothing written); `degrade --backend --reason` is the ONLY escape for an unavailable review backend (per-tree, never all backends); `summary` renders the whole loop state statelessly (gate result · per-backend verdicts · red-proofs · degrades) — no ledger, no rounds, nothing remembered. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
247
247
  | `/agent-workflow-kit coverage-check` | any time | **the final-run checker** (D3(c)+(d)) — **certifies coverage ONLY inside the `--final` run that owns the lcov** (ownership is exclusive by CONVENTION over the fixed path, not enforced — a concurrent writer to it is a stated residual, queued as LCOV-EXCLUSIVE-OWNERSHIP): an artifact on disk proves nothing about the tree it came from, so a standalone run prints its findings and states `attested=no` / `NO VERDICT` rather than a PASS (an lcov that predates an edit would otherwise certify a line the suite never executed). `attested=` claims a verdict was ISSUED, pass or fail — a run over uncovered lines still reads `attested=yes` and still exits 1, and a run whose coverage arm never executed (no lcov) reads `attested=no` even inside `--final`, because nothing was read and nothing is certified. The runner passes a nonce whose one-way commitment over `{nonce, fingerprint, base}` is the `final-start.attempt` it recorded; a context describing another tree, or matching no recorded attempt, is a REFUSAL, never a verdict. Findings are unchanged — reads the lcov the declared `unit-tests` gate produced at the FIXED git-dir path and fails on any uncovered CHANGED executable Node line (listed `file:line`; a changed file absent from the map is a file-level red; out-of-domain/unsupported files are LISTED — the claim narrowed honestly); VERIFIES every current-base red-proof declaration (bound test exists · custody hash unchanged · green N/N now · pre-fix fingerprint differs); prints `lcov-sha256=<hex|none>` of the exact bytes it consumed — the sha the `--final` receipt binds and re-hashes. An absent lcov is a LOUD `skipped-no-lcov`; a symlinked path is a refusal. `--check` is the gate exit code — declare it as the LAST gate (`run-gates --final` refuses otherwise). Read-only. |
248
248
  | `/agent-workflow-kit commit-guard` | any time | **the read-only pre-commit guard** (D10) — makes the commit capture the whole current working tree, so «verified» and «about to be committed» are the same bytes (the receipt itself has a stated residual — see the mode doc). FIRST it refuses an **INDEX that lags the verified working tree** (the gates and the fingerprint describe the WORKING tree while `git commit` takes the INDEX alone, and the fingerprint cannot tell them apart — so a lagging index used to ship a strict subset of what was verified): unstaged tracked paths or reviewable untracked-not-ignored paths, named up to a bounded cap with the remainder stated, a dirty tracked **submodule** named separately with its own recovery, and fail-closed on an undecidable probe. This deliberately blocks a partial commit. Then it binds the LATEST completed `run-gates --final` receipt to the EXACT current tree: refuses on a missing/red/stale receipt, fingerprint drift under the run, a dangling later attempt, declaration content drift, evidence-hash or lcov drift, or unsatisfied review obligations (the same review-state decision, recomputed over a sanitized env — forged out-of-repo stores never satisfy). Re-runs NO gate or test. Wire it into `.git/hooks/pre-commit` (the installer writes the RESOLVED invocation). `git commit --no-verify` stays the stated residual. |
249
- | `/agent-workflow-kit recommendations` | any time (every `upgrade` ends with it) | **read-only deployment advisor** (AD-044) — computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, OS sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe, a feature-spec layer never adopted) and renders **verdict-first**: one composed verdict line (does anything need attention?), then each item as **{severity · what · one-line benefit · an optional `recipe:` line (the sandbox-lane live recipe, the worktrees-dir hand-apply-first grant advice, the agents hidden-mode reconcile follow-up, the spec-adoption decline preview, or another offer's decline preview) · the exact consent-gated apply one-liner}**. The agent PRESENTS the section in the user's conversational language — every fact and count, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request — and runs EXACTLY the rendered one-liners only on your yes, surfacing each item's posture note first. Renders **present-even-when-empty** (`no recommendations — flow optimal.`); a failed probe degrades to a stated skip line. Registry strings are fact-true frozen one-line data (posture/risk notes live in the mode doc at the consent moment); the kit never seeds `sandbox.network.allowedDomains` / `filesystem.allowWrite` (**HAND-APPLY** territory), and the sandbox-lane item's convergence is a neutral fingerprint acknowledgement recorded by a consent-gated ack writer into `docs/ai/acks.json` — never a security key (the recipe is documented per bridge in `capability.json` `networkHosts` + `writableDirs`). `--cwd` is required (the target project is explicit); never writes, never commits, never runs a subscription CLI. |
249
+ | `/agent-workflow-kit recommendations` | any time (every `upgrade` ends with it) | **read-only deployment advisor** (AD-044) — computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, OS sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe, a feature-spec layer never adopted) and renders **verdict-first**: one composed verdict line (does anything need attention?), then each item as **{severity · what · one-line benefit · an optional `recipe:` line (the sandbox-lane live recipe, the worktrees-dir hand-apply-first grant advice, the agents hidden-mode reconcile follow-up, the spec-adoption decline preview, or another offer's decline preview) · the exact consent-gated apply one-liner}**. The agent PRESENTS the section in the user's conversational language — every fact and count, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request — and runs EXACTLY the rendered one-liners only on your yes, surfacing each item's posture note first. Renders **present-even-when-empty** (`no recommendations — flow optimal.`); a failed probe degrades to a stated skip line. Registry strings are fact-true frozen one-line data (posture/risk notes live in the mode doc at the consent moment); `velocity --bridge-tier` seeds the placed bridges' declared hosts into `sandbox.network.allowedDomains`, `filesystem.allowWrite` stays **HAND-APPLY** territory, and the sandbox-lane item's convergence is a neutral fingerprint acknowledgement recorded by a consent-gated ack writer into `docs/ai/acks.json` — never a security key (the recipe is documented per bridge in `capability.json` `networkHosts` + `writableDirs`). `--cwd` is required (the target project is explicit); never writes, never commits, never runs a subscription CLI. |
250
250
  | `/agent-workflow-kit doc-parity` | any time | **read-only doc-parity lint** (AD-049) — kills the doc-drift class where a mode-contract doc silently lags a code constant (a `--check` doc still reading `300` after the diff cap moved to `400`): a **closed, exported registry** binds each live constant (review caps, schema versions, the ledger's own class/scope vocabulary, and the autonomy-doctor EXIT/status/trusted-dir contract) to the exact token its `references/modes/*.md` contract must carry, and asserts the CURRENT value renders into every bound file — a drifted doc, an unreadable file, or an absent token **fails closed**. The values are sourced from the live imports (never re-typed), so the lint can't itself go stale; adding a binding is adding a checked entry (closed-world, edit-safe). `--check` is the gate exit code a kit maintainer runs: the lint checks the kit's own package. Never writes, never commits, never runs a subscription CLI. |
251
251
  | `/agent-workflow-kit dispatch` | any time | **the delegation engine** — makes "how much does delegating a sub-task actually buy?" a measured number instead of a feeling. `check <file>` validates a sub-task brief's contract block and exits 0/1 naming the first violated field — **form only**: whether the task is genuinely bounded, its design decided and its acceptance adequate stays your judgment, and a well-formed absurdity passes here by construction. `advise --step-class <c>` answers the question that comes BEFORE that one — which vehicle should carry this kind of sub-task, whether it is even present on this machine, and what the ledger has already recorded for threads of that class (finished, failed, closed without a fold, and still open — counted separately) — and it decides nothing: it refuses no dispatch, gates no verb, and a choice that diverges from the advice is noted rather than blocked. It reads the filesystem only: nothing is launched to find out what is installed, an absent ledger prints "no recorded history", an unreadable one prints the store's own words, and either way the advice still prints. The same block appears as a footer under a form-valid `check` — never under a refusal, so it can never hide one. `register` pre-registers an acceptance wave (step classes, pairing key, minimum observations per class, the mean and first-pass thresholds) so the bar can never be chosen after the results it judges — immutable per wave. `observe` records ONE hand-written observation (`solo-construction`, the baseline whose ratio is 1 by construction — except over a scope measuring zero bytes, which has no ratio at all and is recorded ineligible by name — or `self-reported`) — delegated numbers are never hand-written, they are derived. The writer verbs are where that derivation happens: `open` puts a delegated thread on the record with every mint-time field copied from the brief's own header, refusing a deadline the wrapper's cap plus its kill grace would not fit inside, and recording whether the tree it started from was clean against the thread's base — HEAD by default, or the tree `--checkpoint <oid>` names, so the second task of a story is measured over the first one's uncommitted work instead of opening dirty (a dirty one makes the result honestly unmeasurable rather than quietly counted), while `--task <brief>` also records which task brief the thread carries out and the files that brief lists, refusing, since the last commit, a task thread whose files overlap by path prefix those of an open task thread or of a folded thread of another brief (`files-overlap`), and a mix of the two kinds — an untasked open beside an open task thread or, for a `code` open of backend `codex`, after a task fold, and a task open beside an open untasked thread (`mixed-open`), refusing at open a Files path the snapshot scope can never hold (`task-files-scope`), and measuring a task thread over its files alone, so `return` and `fold` refuse a changed path that no open or folded task thread on the same base claims (`out-of-files`); `await` waits for that one dispatch to answer and writes nothing — only the finished receipt satisfies it, a run still holding the nonce means keep waiting, and a wait that runs out says so with its own exit code, names whether the deadline or your own timeout ended it, and authorizes nothing: a wait that ended without an answer is a question for you, never permission to dispatch again; `return` absorbs the receipt the wrapper minted — only a finished one, checked against the contract it actually ran, its deadline and its own report, and refused outright when the tree hides changes from git or moves while the return is being computed — and derives the bytes from git rather than from anyone's claim; `fold` is the acceptance, and it refuses if the tree moved since the return — precisely, it re-confirms **equality of the visible canonical payload**, which is a change detector rather than a cryptographic identity of the tree: under the default head base that payload's untracked section is unframed and carries no file mode, so a content or symlink target that imitates the marker opening the next entry can alias two trees, and making an untracked file executable moves nothing (both named as residuals in the mode doc, with the fix queued); under a checkpoint base it is one indexed diff that carries modes. A change set carrying a binary, a non-regular path, a submodule, or a symlink whose target is not valid UTF-8 is **refused** at `return` and `fold` — CONSERVATIVELY, rather than promise what the default head payload may not be able to follow — and the same refusals hold for a checkpoint thread. `degrade` closes a thread that never earned a fold, on the record and with its reason. `aggregate` prints one wave: the registered thresholds, every observation as context, and per step class the finished threads — a folded success with git-provable bytes contributes its ratio, one whose bytes are unprovable is excluded from the mean but still counts in the first-pass rate, and a failed thread, a degrade-closed one, or one folded after failing acceptance each count as a real zero. Below the registered minimum nothing is computed (insufficient); at or above it is computed and labeled PILOT evidence. It REFUSES rather than guessing: no pre-registration, an unfinished thread in scope, a recorded refusal-to-delegate that never opened a thread, or an ambiguous wave each stop the computation by name. `handoff-return --slug <s>` closes the worktree loop after `land --prepare`: it re-attests that main still holds exactly the prepared tree under the same HEAD (both recorded in the handoff at prepare time), prints the satellite handoff's user-owned content byte for byte with its boundaries, byte lengths and the main-owned destinations each part folds into, prints the handoff digest and both OIDs as its proof, states the after-the-fold order (a fold landed after the gates leaves them stale), and records ONE self-reported worktree-stream observation only when the whole prepared change set is measurable — a deletion, a rename, a symlink, a submodule or a mode-only change ends with a named NOT RECORDED instead, never a partial number. Appends only to its own ledger inside the git dir (never committable); it also writes `handoff-return`'s `write-tree` tree object and, for a checkpoint measurement, the leaf's OWN temporary index file and the blobs its forced adds store — never a ref, never the real index; never commits, never runs a subscription CLI. |
252
252
  | `/agent-workflow-kit worktrees` | any time | **parallel feature worktrees** — run several features in DIFFERENT agent sessions on one repo, zero interference on working-tree files (the ONE exception is the dependency cache, below): `provision <slug> --plan <file>` creates a sibling git worktree on branch `aw/<slug>` and populates it (registry-derived footprint copy-if-missing — a tracked file is never overwritten; EXACTLY ONE seeded feature plan; the `handoff-<slug>.md` record from minute zero; `node_modules` symlinked where the link stays ignored — a shared MUTABLE dependency cache: writes through it hit MAIN's node_modules; for isolation run the printed isolated-install command (`--install` only PRINTS it; on `--resume` run the printed unlink-first recovery first — printed only for a link whose RAW TARGET is MAIN's `node_modules`, since a foreign or unreadable link is reported as such with no removal advised); absolute root-pinned gate commands rebased on untracked copies only, and only while their bytes equal the MAIN source or its rebased form — user-modified copies stay untouched); `list` is read-only (slug, branch, base, dirty, handoff); `prompt <slug>` is read-only too and re-prints the satellite's cold-start prompt — the same text `provision` ends its report with: where the worktree is, its ONE seeded plan, the handoff as the one channel back, MAIN's orientation (shared series index, landing) and, under its own heading because it is probed on the satellite, that checkout's install posture — every value derived LIVE rather than replayed from the frozen provision record, a recorded value that no longer matches NAMED as a divergence (with the cause its source makes likely) instead of printed as the runnable one, every offered command marked with WHO runs it (`MAIN $ …` for the landing, `HERE $ …` for this checkout's own install, a posture with nothing to run staying prose), and a control character in any rendered value a typed STOP rather than a forged prompt line; `land <slug> --prepare` locks the common git dir, fail-closes on divergence or incomplete satellite state, transfers the complete accepted satellite diff onto a CLEAN main, runs sync plus the declared gates, and reports HEAD/TRANSFER/PREPARED OIDs — the commit ALWAYS stays a dialogue ask; `cleanup <slug>` takes the same lock and removes a LANDED worktree only after live landed-verification against main HEAD, while `--abandon` is the ONE destructive arm (destroys unlanded work; **no preview step** on any writer). The parent dir is the `docs/ai/worktrees.json` `parentDir` setting (default: the repo's sibling parent); an unwritable parent degrades to printed maintainer-pasted commands, and the one-time host consent that makes it promptless surfaces via `recommendations`. Never commits, never pushes, never runs a subscription CLI. |
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow". New to the epic, story and task tier? Run `/agent-workflow-kit tier`. A typed decision with a confidence from Jev? Run `/agent-workflow-kit jev`.
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '14.23.0'
6
+ version: '14.24.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "14.23.0",
6
+ "version": "14.24.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "14.23.0",
3
+ "version": "14.24.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -24,7 +24,8 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
24
24
 
25
25
  **Per-item posture notes (the consent moment — surface BEFORE running or handing over the apply):**
26
26
 
27
- - `sandbox-lane` — surface this note TOGETHER with the sandbox-lanes ladder below (the ladder IS the practical half of the note — inline, never a pointer). Pure DISCOVERABILITY: it surfaces the manifest-declared observed session-sandbox recipe (egress hosts ∪ resolved writable state dirs — `networkHosts` ∪ `writableDirs` of the wired bridges' `capability.json`, the single documentation source) and converges on a NEUTRAL fingerprint acknowledgement recorded by the consent-gated **ack writer** into the family-owned `docs/ai/acks.json` (`sandboxLaneAck`; a changed recipe re-fires the item). The store is family-owned so no host settings validator guards it (AD-055 relocated the ack off the Claude Code settings schema, which rejected the unknown key); the legacy `"agentWorkflow": { "sandboxLaneAck": … }` settings-scope key is still READ for one deprecation window (until the next kit MAJOR). It never claims the settings security keys take effect on any host class, never recommends writing them, and the kit never seeds `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` (bridge council 2026-07-11, both backends concur: a network pre-allow widens egress for EVERY sandboxed command; a write allowance on CLI state dirs would expose credential dirs). Posture history: an IDE-managed session sandbox was live-observed (2026-07-11/12) ignoring hand-applied settings security keys in BOTH scopes, and codex needs a writable HOME (EROFS `~/.codex` in-sandbox); whether a session's sandbox honors the settings keys is runtime-unknowable from the advisor (a denial-only signal) — which is exactly why the item states only detectable facts and no zero-prompt promise on any host class.
27
+ - `bridge-tier` — the rendered line IS the mutation: `velocity-profile --apply --bridge-tier` writes the project `.claude/settings.json` on that one run. It seeds, for each placed bridge only, the review wrapper's `code`-mode allow rule (a wrapper then runs UNATTENDED: it reads any repo file it is pointed at and sends the assembled payload to its subscription backend), the quoted grounding pre-step rule when `agy-review` is placed, the wrapper names in `sandbox.excludedCommands`, and the declared hosts of the placed bridges (each bundled manifest's `networkHosts`) in `sandbox.network.allowedDomains`. Where the host honours the settings sandbox keys, every sandboxed command of the project can then reach those hosts, not only the wrappers, and the `--autonomy` preview reports them as a network weakening. The writable state dirs (`writableDirs`) stay hand-apply (the sandbox lanes below). The item fires while an allow rule, an exclusion or a declared host of a placed bridge is missing, so a project wired before the tier seeded hosts is offered the one re-run.
28
+ - `sandbox-lane` — surface this note TOGETHER with the sandbox-lanes ladder below (the ladder IS the practical half of the note — inline, never a pointer). Pure DISCOVERABILITY: it surfaces the manifest-declared observed session-sandbox recipe (egress hosts ∪ resolved writable state dirs — `networkHosts` ∪ `writableDirs` of the wired bridges' `capability.json`, the single documentation source) and converges on a NEUTRAL fingerprint acknowledgement recorded by the consent-gated **ack writer** into the family-owned `docs/ai/acks.json` (`sandboxLaneAck`; a changed recipe re-fires the item). The store is family-owned so no host settings validator guards it (AD-055 relocated the ack off the Claude Code settings schema, which rejected the unknown key); the legacy `"agentWorkflow": { "sandboxLaneAck": … }` settings-scope key is still READ for one deprecation window (until the next kit MAJOR). It never claims the settings security keys take effect on any host class; the tier seeds `sandbox.network.allowedDomains` with the placed bridges' declared hosts only through the consented `--bridge-tier` (AD-172, which reverses the bridge council of 2026-07-11 for those hosts alone; the egress they widen for every sandboxed command is stated at that consent), and `sandbox.filesystem.allowWrite` stays unseeded (that council: a write allowance on CLI state dirs would expose credential dirs). Posture history: an IDE-managed session sandbox was live-observed (2026-07-11/12) ignoring hand-applied settings security keys in BOTH scopes, and codex needs a writable HOME (EROFS `~/.codex` in-sandbox); whether a session's sandbox honors the settings keys is runtime-unknowable from the advisor (a denial-only signal) — which is exactly why the item states only detectable facts and no zero-prompt promise on any host class.
28
29
 
29
30
  - `read-lane` — enabling the opt-in read-only compound lane auto-approves *compounds* (and singles) of the seeded read-only core that carry ZERO shell metaprogramming: an UNATTENDED trust extension, bounded by the audited read-only core (never a command outside it; prompt-bypass only, never a sandbox bypass) and applied regardless of which of those core commands you seeded as individual settings rules. It is a PROJECT-PERSISTENT declaration in `docs/ai/lanes.json` — every future session, subagents' Bash too where the host fires hooks on subagent Bash, and (committed) every checkout. The apply depends on state: when the lane is OFF, it is the `gate-hook --read-lane` preview (whose own currency check refuses a stale hook — a pre-1.48 hook never reads `lanes.json`); when the placed hook is STALE (an enabled lane over an old hook) or MISSING, the item instead surfaces a **delete-to-reseed** / re-place recovery (a destructive `rm` + `--apply`, an attention item — never the safe preview). Risk profile: a bounded read-only trust-posture extension — no write/exec exposure beyond the audited core.
30
31
  - `worktrees-dir` — on a settings-native host that honors the key, the HAND-APPLY line widens the OS-sandbox WRITE surface to the whole worktrees parent dir: every sibling path under it (other repositories included) becomes agent-writable, and the widening persists for every later session. A harness-managed host may ignore that project setting; grant the narrow parent through host/session controls or use the provision terminal fallback instead. When that scope is wider than you want, narrow it FIRST: create a dedicated dir yourself (outside the agent's write surface), point `docs/ai/worktrees.json` `parentDir` at it, then re-run recommendations — the item re-renders with the narrowed dir. The kit never writes sandbox filesystem allowances itself; the line is always yours to paste. **Convergence** is two-path: a declared `sandbox.filesystem.allowWrite` entry covering the probed dir (either settings scope — `~` and `~/…` resolve against home, and coverage is path-segment-aware, so a grant on a sibling or on a child never counts), or — for a host that ignores that key — the neutral dir-bound acknowledgement recorded by this item's consent-gated apply one-liner (`ack-write --lane worktrees-dir` — a dry-run preview that prints the exact `--apply`; recorded as `worktreesDirAck` in the family-owned `docs/ai/acks.json`, never a security key), while the grant advice itself rides the `recipe:` line as the labeled FIRST step; against a trusted host NO the apply stays the HAND-APPLY grant advice and no ack is offered. Neither is proof of write CAPABILITY: the provision preflight's real create+delete probe stays the runtime truth, and the fingerprint is bound to the **resolved probe dir**, so the item re-fires only when that resolved dir changes (two absent `parentDir` values sharing an existing ancestor resolve to the same dir and keep the same ack). Risk profile: a real write-surface widening where honored — scope it deliberately.
@@ -44,10 +45,10 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
44
45
 
45
46
  **Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
46
47
 
47
- - **Settings-native sandbox** (the harness reads the `.claude/settings.json` sandbox keys): where the harness honours the `--bridge-tier` wiring's `excludedCommands`, the wrappers run OUTSIDE the sandbox and never consult `allowedDomains`/`allowWrite` — nothing beyond the tier. A harness may not honour that entry (observed 2026-10-06: Claude Code 2.1.291 ran an agent's plain `codex-review` inside its sandbox with the project entry present, and that run still reached `chatgpt.com`, which only `allowedDomains` admitted); there a recipe host outside `allowedDomains` is refused by the harness proxy or held for a network prompt (a refusal reads `Tunnel connection failed: 403` or `CONNECT tunnel failed, response 403` in the run), and the lane is the hand-apply `allowedDomains` shape in (2) below, filled from this recipe — the agent does not add it, the kit never writes these keys. Record the ack (the ack-write one-liner) once the lane is confirmed.
48
- - **Harness-managed sandbox** (an IDE/session-imposed sandbox that ignores the settings keys) — a NARROWEST-SCOPE ladder: (1) prefer wrapper/command-SCOPED sandbox rules where the harness offers them (only the review wrappers gain the egress + state-dir writes); (2) a SESSION-scoped allowance (the manifest hosts + the resolved state dirs for the whole session) is an INFORMED WIDENING — it carries the same blast-radius class the settings security keys were rejected for (every sandboxed command in the session gains that egress and those writable credential dirs), acceptable only as the maintainer's deliberate choice for a review-heavy session. Its **paste-ready hand-apply shape** (the kit never writes these keys) is, in `.claude/settings.json`, `"sandbox": { "network": { "allowedDomains": ["<egress host>", …] }, "filesystem": { "allowWrite": ["<writable state dir>", …] } }` — fill `<egress host>` / `<writable state dir>` from THIS recipe (the wired bridges' `capability.json` `networkHosts` ∪ resolved `writableDirs`); one consent then lands durably in config. (3) Else the per-run consented bypass. Record the ack (the ack-write one-liner) once a lane is chosen — choosing the bypass, or consciously declining, also counts.
48
+ - **Settings-native sandbox** (the harness reads the `.claude/settings.json` sandbox keys): where the harness honours the `--bridge-tier` wiring's `excludedCommands`, the wrappers run OUTSIDE the sandbox and never consult `allowedDomains`/`allowWrite` — nothing beyond the tier. A harness may not honour that entry (observed 2026-10-06: Claude Code 2.1.291 ran an agent's plain `codex-review` inside its sandbox with the project entry present, and that run still reached `chatgpt.com`, which only `allowedDomains` admitted); there a recipe host outside `allowedDomains` is refused by the harness proxy or held for a network prompt (a refusal reads `Tunnel connection failed: 403` or `CONNECT tunnel failed, response 403` in the run). The lane for the hosts is the `--bridge-tier` re-run (the `bridge-tier` item offers it while a declared host of a placed bridge is missing): it seeds the placed bridges' declared hosts into `sandbox.network.allowedDomains`, and the agent never hand-adds them. For a wrapper the harness keeps sandboxed the hand-apply `allowWrite` step for the writable state dirs stays: the `filesystem.allowWrite` half of the shape in (2) below, filled from this recipe — the agent does not add it and the kit never writes it. Record the ack (the ack-write one-liner) once the lane is confirmed.
49
+ - **Harness-managed sandbox** (an IDE/session-imposed sandbox that ignores the settings keys) — a NARROWEST-SCOPE ladder: (1) prefer wrapper/command-SCOPED sandbox rules where the harness offers them (only the review wrappers gain the egress + state-dir writes); (2) a SESSION-scoped allowance (the manifest hosts + the resolved state dirs for the whole session) is an INFORMED WIDENING — it carries the same blast-radius class the settings security keys were rejected for (every sandboxed command in the session gains that egress and those writable credential dirs), acceptable only as the maintainer's deliberate choice for a review-heavy session. Its **paste-ready hand-apply shape** (the kit writes `allowedDomains` only via `--bridge-tier`, and never `allowWrite`) is, in `.claude/settings.json`, `"sandbox": { "network": { "allowedDomains": ["<egress host>", …] }, "filesystem": { "allowWrite": ["<writable state dir>", …] } }` — fill `<egress host>` / `<writable state dir>` from THIS recipe (the wired bridges' `capability.json` `networkHosts` ∪ resolved `writableDirs`); one consent then lands durably in config. (3) Else the per-run consented bypass. Record the ack (the ack-write one-liner) once a lane is chosen — choosing the bypass, or consciously declining, also counts.
49
50
  - Either way the recipe is **observed-minimal, honestly incomplete** — a blocked host names itself at run time; extend by hand and re-ack (the fingerprint moves with the recipe, not with your extensions).
50
51
 
51
- **Invariants:** read-only (never writes, never commits, never runs a subscription CLI) · `--cwd` required · present-even-when-empty · verdict-first from frozen templates · probe failures degrade to stated skip lines · apply one-liners are cwd-independent (absolute tool paths + a pinned `--cwd`; the sandbox-provision item pins via a `cd <root> &&` prefix — the doctor reads its cwd; the ONE exception is the `set-autonomy` item, a conversational skill invocation explicitly labeled *run IN the target project*) · the kit never seeds `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` (hand-apply territory) · the sandbox-lane convergence is a neutral acknowledgement recorded into `docs/ai/acks.json` by the consent-gated ack writer (the legacy settings-scope key read for one deprecation window), never a security key · the `worktrees-dir` item converges on a declared `allowWrite` entry covering its probed dir, or on its own `worktreesDirAck` lane in that same store (recorded by its apply one-liner, never by the `recipe:` line).
52
+ **Invariants:** read-only (never writes, never commits, never runs a subscription CLI) · `--cwd` required · present-even-when-empty · verdict-first from frozen templates · probe failures degrade to stated skip lines · apply one-liners are cwd-independent (absolute tool paths + a pinned `--cwd`; the sandbox-provision item pins via a `cd <root> &&` prefix — the doctor reads its cwd; the ONE exception is the `set-autonomy` item, a conversational skill invocation explicitly labeled *run IN the target project*) · `sandbox.filesystem.allowWrite` stays hand-apply territory, and `sandbox.network.allowedDomains` is seeded only by the consented `velocity-profile --bridge-tier` · the sandbox-lane convergence is a neutral acknowledgement recorded into `docs/ai/acks.json` by the consent-gated ack writer (the legacy settings-scope key read for one deprecation window), never a security key · the `worktrees-dir` item converges on a declared `allowWrite` entry covering its probed dir, or on its own `worktreesDirAck` lane in that same store (recorded by its apply one-liner, never by the `recipe:` line).
52
53
 
53
54
  **Exit codes:** `0` report rendered (items or the empty state); `1` error (e.g. `--cwd` is not a directory); `2` usage.
@@ -50,7 +50,7 @@ Honesty notes: tier entries get **NO PreToolUse-hook residual coverage — with
50
50
 
51
51
  **The boundary of that qualifier, stated so it is not guessed.** It covers claims about `sandbox.*` keys — the block whose honouring was observed to vary by host. It deliberately does NOT cover `permissions.*` semantics (an allow rule auto-approving a command, `local > project` precedence for a non-sandbox key): that is the harness's own documented permission model, the layer this whole mode rests on, and hedging it would say nothing true. **Stated residual:** which sentences count as settings-derived runtime claims is decided by review, not by a checker — deciding whether a sentence asserts a runtime effect is a question about prose meaning, and this family deliberately has no mechanism that judges that (the same limit that retired the byte-scanning deny rung). A new claim-bearing line therefore has to carry the qualifier by authorship; the acceptance criterion is that every line stating what a `sandbox.*` key DOES carries it, and the tests pin the surfaces that exist today. What does NOT change is the CLASSIFICATION: which red-line a key would weaken, and that a proven bridge-tier exclusion is a note rather than a weakening, are properties of the declaration and stay flat statements.
52
52
 
53
- **The `--bridge-tier` (own opt-in, AD-044).** Seeds what a promptless council review run needs — BOTH surfaces: `permissions.allow` prefix rules AND the wrapper names in `sandbox.excludedCommands` (where the host honors the settings sandbox keys the harness runs an excluded command OUTSIDE the sandbox — the wrappers need network — so a plain allowlisted invocation triggers no sandbox-bypass approval; where it does not, the wrapper starts sandboxed and simply prompts or fails loudly — fail-safe, never a silent widening). Both land in the **project** `.claude/settings.json` — the file this writer owns; an exclusion placed only in `settings.local.json` was live-observed NOT to route the command outside the sandbox (2026-07-11: the wrapper then starts sandboxed and dies on a read-only HOME + a network prompt), so hand-wiring the local file is not a working substitute for this tier. **Honesty note:** a session whose sandbox is imposed by the harness runtime itself (e.g. an IDE-managed session sandbox) may ignore settings-level exclusions entirely — there the wrappers need the session-level sandbox config (or a per-run consented bypass); the tier's seeded posture is correct for a settings-native sandbox that honours the exclusion and simply prompts again elsewhere (fail-safe, never a silent widening). A harness that reads the settings sandbox keys may still drop this exclusion (observed 2026-10-06: Claude Code 2.1.291 ran an agent's plain `codex-review` inside its sandbox with the project entry present); its lane is the settings-native one in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Each bridge's observed egress hosts are declared in its `capability.json` `networkHosts` — the single documentation source (observed-minimal — a blocked host names itself at run time; read the manifests, this doc deliberately retypes no host list). The kit **never seeds** `sandbox.network.allowedDomains` or `sandbox.filesystem.allowWrite` (bridge council 2026-07-11, both backends concur): a network pre-allow widens egress for EVERY sandboxed command, and a write allowance on CLI state dirs (`~/.codex`, `~/.gemini/…`) would make credential dirs writable to every sandboxed command. Per-bridge picture under a harness-managed sandbox (live-observed 2026-07-11/12): an IDE-managed session sandbox ignores hand-applied `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` in BOTH settings scopes — its own per-host network consents govern egress; the durable zero-prompt lanes there are the session/host sandbox config (hosts from `networkHosts` + the CLI state-dir writes) or the per-run consented bypass. codex additionally needs a writable HOME (EROFS `~/.codex`); note the apex-vs-wildcard nuance — an apex domain is NOT covered by its `*.`-wildcard form, so the manifests carry both forms where observed (the blocked host names itself at run time). The upgrade Recommendations advisor surfaces exactly this recipe (hosts ∪ resolved `writableDirs`) as the `sandbox-lane` discoverability item — a consent-gated **ack writer** converging on a neutral fingerprint acknowledgement recorded into the family-owned `docs/ai/acks.json` (the kit still never seeds the security keys — those stay hand-apply), with the posture notes at the consent moment (`${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). Membership is the FROZEN review-wrapper constant, **never** the execution/probe wrappers (`codex-exec`, `agy-run` keep their human prompt — delegated execution is not covered by this consent; codex-exec's nested-sandbox recovery is the canon's observed-failure lane, not a preemptive tier seed), and only the **`code` review mode** — a `plan`/`diff` invocation takes a file argument that can point OUTSIDE the repo, so those modes keep their prompt; each wrapper entry derives ONLY when its bridge is **placed on PATH** (an absent bridge is a stated skip). The seeded byte-forms (this list IS the documented-invocation source for the bridge tier):
53
+ **The `--bridge-tier` (own opt-in, AD-044).** Seeds what a promptless council review run needs — three surfaces: `permissions.allow` prefix rules, the wrapper names in `sandbox.excludedCommands` (where the host honors the settings sandbox keys the harness runs an excluded command OUTSIDE the sandbox — the wrappers need network — so a plain allowlisted invocation triggers no sandbox-bypass approval; where it does not, the wrapper starts sandboxed and simply prompts or fails loudly — fail-safe, never a silent widening), and the placed bridges' declared hosts in `sandbox.network.allowedDomains` (below). All three land in the **project** `.claude/settings.json` — the file this writer owns; an exclusion placed only in `settings.local.json` was live-observed NOT to route the command outside the sandbox (2026-07-11: the wrapper then starts sandboxed and dies on a read-only HOME + a network prompt), so hand-wiring the local file is not a working substitute for this tier. **Honesty note:** a session whose sandbox is imposed by the harness runtime itself (e.g. an IDE-managed session sandbox) may ignore settings-level exclusions entirely — there the wrappers need the session-level sandbox config (or a per-run consented bypass); the tier's seeded posture is correct for a settings-native sandbox that honours the exclusion and simply prompts again elsewhere (fail-safe, never a silent widening). A harness that reads the settings sandbox keys may still drop this exclusion (observed 2026-10-06: Claude Code 2.1.291 ran an agent's plain `codex-review` inside its sandbox with the project entry present); its lane is the settings-native one in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Each bridge's observed egress hosts are declared in its `capability.json` `networkHosts` — the single documentation source (observed-minimal — a blocked host names itself at run time; read the manifests, this doc deliberately retypes no host list). The tier ALSO seeds the placed bridges' declared hosts — the `networkHosts` of every bundled manifest whose review wrapper is placed — into the project's `sandbox.network.allowedDomains` (AD-172, which reverses the bridge council of 2026-07-11 for those hosts only): where the host honors the settings sandbox keys, a wrapper a harness keeps sandboxed despite the exclusion is no longer refused them, and every sandboxed command of the project can then reach those hosts, not only the wrappers — that widening is part of this consent, and the `--autonomy` preview reports it as a network weakening. `sandbox.filesystem.allowWrite` stays unseeded: a write allowance on CLI state dirs (`~/.codex`, `~/.gemini/…`) would make credential dirs writable to every sandboxed command, so the writable state dirs stay hand-apply. Per-bridge picture under a harness-managed sandbox (live-observed 2026-07-11/12): an IDE-managed session sandbox ignores hand-applied `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` in BOTH settings scopes — its own per-host network consents govern egress; the durable zero-prompt lanes there are the session/host sandbox config (hosts from `networkHosts` + the CLI state-dir writes) or the per-run consented bypass. codex additionally needs a writable HOME (EROFS `~/.codex`); note the apex-vs-wildcard nuance — an apex domain is NOT covered by its `*.`-wildcard form, so the manifests carry both forms where observed (the blocked host names itself at run time). The upgrade Recommendations advisor surfaces exactly this recipe (hosts ∪ resolved `writableDirs`) as the `sandbox-lane` discoverability item — a consent-gated **ack writer** converging on a neutral fingerprint acknowledgement recorded into the family-owned `docs/ai/acks.json` (the writable state dirs stay hand-apply; the hosts are the tier's), with the posture notes at the consent moment (`${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). Membership is the FROZEN review-wrapper constant, **never** the execution/probe wrappers (`codex-exec`, `agy-run` keep their human prompt — delegated execution is not covered by this consent; codex-exec's nested-sandbox recovery is the canon's observed-failure lane, not a preemptive tier seed), and only the **`code` review mode** — a `plan`/`diff` invocation takes a file argument that can point OUTSIDE the repo, so those modes keep their prompt; each wrapper entry derives ONLY when its bridge is **placed on PATH** (an absent bridge is a stated skip). The seeded byte-forms (this list IS the documented-invocation source for the bridge tier):
54
54
 
55
55
  - `Bash(codex-review code:*)` — the code-mode prefix, args wildcard; plus `codex-review` in `sandbox.excludedCommands`
56
56
  - `Bash(agy-review code:*)` — the code-mode prefix, args wildcard; plus `agy-review` in `sandbox.excludedCommands`
@@ -4,7 +4,7 @@
4
4
  // ANSWERED renders THIS tool's PREVIEW one-liner; the preview prints the exact `--apply` command;
5
5
  // the agent runs `--apply` only after the mode doc's §3 informed-consent confirmation. It records a
6
6
  // NEUTRAL fingerprint acknowledgement — never a security key; the kit never writes sandbox
7
- // network/filesystem allowances.
7
+ // filesystem allowances, and allowedDomains only through velocity-profile --bridge-tier.
8
8
  //
9
9
  // The wording is per-LANE or genuinely neutral, never "recipe": the sandbox lane acknowledges a
10
10
  // session-sandbox recipe, but the coverage-domain lane acknowledges a census FACT and the
@@ -0,0 +1,40 @@
1
+ // bridge-sandbox-recipe.mjs — the one reader of the bundled bridges' declared sandbox surfaces
2
+ // (contract: bridges/velocity-profile). The bridge tier seeds the hosts it returns and the
3
+ // Recommendations sandbox-lane recipe renders hosts and dirs from it, so the two cannot drift.
4
+ //
5
+ // Dependency-free, Node >= 22. No side effects on import.
6
+
7
+ import { readFileSync, readdirSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
10
+
11
+ // The manifest-declared session-sandbox recipe surfaces of every BUNDLED bridge whose review
12
+ // wrapper is in the wired set — networkHosts ∪ writableDirs, derived from the manifests (the
13
+ // single documentation source), never hardcoded here.
14
+ export const bundledSandboxRecipe = (placedWrappers, deps) => {
15
+ const readFile = deps.readFile ?? readFileSync;
16
+ const readDir = deps.readdir ?? readdirSync;
17
+ const bundleRoot = deps.bundleRoot ?? DEFAULT_BUNDLE_ROOT;
18
+ const hosts = [];
19
+ const dirEntries = [];
20
+ for (const dir of readDir(bundleRoot)) {
21
+ // An unreadable/unparsable bundled manifest must NOT thin the recipe silently — a partial
22
+ // recipe rendered as complete is worse than none. The throw reaches the caller: a stated skip
23
+ // in the advisor, a STOP with zero writes in the bridge tier.
24
+ let manifest;
25
+ try {
26
+ manifest = JSON.parse(readFile(join(bundleRoot, dir, 'capability.json'), 'utf8'));
27
+ } catch (err) {
28
+ // ENOTDIR = the entry is a stray regular file (.DS_Store, a README), not a bridge bundle.
29
+ if (err?.code === 'ENOTDIR') continue;
30
+ throw new Error(`bundled manifest unreadable: ${join(dir, 'capability.json')} — ${err?.message ?? err}`);
31
+ }
32
+ const reviewCmd = manifest?.roles?.review?.cmd;
33
+ if (!reviewCmd || !placedWrappers.includes(reviewCmd)) continue;
34
+ if (Array.isArray(manifest.networkHosts)) {
35
+ for (const h of manifest.networkHosts) if (!hosts.includes(h)) hosts.push(h);
36
+ }
37
+ if (Array.isArray(manifest.writableDirs)) dirEntries.push(...manifest.writableDirs);
38
+ }
39
+ return { hosts, dirEntries };
40
+ };
@@ -99,18 +99,18 @@ drift-guarded set-equal to this block by the bridge `bin/*.test.mjs` suites (hel
99
99
 
100
100
  `networkHosts` declares the hosts the backend CLI is **observed** to contact (synthetic examples:
101
101
  `*.api.backend.example`, `accounts.backend.example`; the real observed lists live in each bridge's
102
- `capability.json`) — the **single documentation
103
- source** for a hand-applied sandbox/network allowlist (session sandbox config, or a hand-pasted
104
- `sandbox.network.allowedDomains` entry). Rules:
102
+ `capability.json`) — the **single source** of the hosts `velocity-profile --bridge-tier` seeds
103
+ into the project's `sandbox.network.allowedDomains` for every placed bridge, and of a hand-applied
104
+ session sandbox allowlist. Rules:
105
105
 
106
106
  - Each entry is a bare dotted hostname or a `*.family` wildcard — never a scheme/path/port.
107
107
  Entries must be unique. A malformed list **fails** `--strict` (the entries are pasted verbatim
108
108
  into allowlist lines by the Recommendations advisor).
109
- - **The kit never seeds these into settings** (bridge council 2026-07-11, both backends concur):
110
- a network pre-allow widens egress for **every** sandboxed command, so running the wrappers
111
- **outside** the sandbox (`sandbox.excludedCommands`, the `--bridge-tier` wiring) stays the
112
- primary lane; the hosts list exists for the **hand-apply** fallback under a sandbox that does
113
- not honour the exclusion (harness-managed, or a settings-native harness that drops it).
109
+ - **The `--bridge-tier` seed is the primary lane** (AD-172): beside the wrappers' exclusion it
110
+ merges the placed bridges' hosts into the project's `sandbox.network.allowedDomains`, so a
111
+ settings-native harness that drops the exclusion still admits them; that pre-allow widens egress
112
+ for **every** sandboxed command, which the tier's consent states. The **hand-apply** fallback
113
+ stays for a harness-managed sandbox that ignores the settings keys.
114
114
  - Observed-minimal, honestly incomplete: a blocked host names itself at run time — extend the
115
115
  hand-applied list by hand; the manifest list records what was actually observed.
116
116
 
@@ -132,7 +132,7 @@ declared path is only the **default under no override**. Rules:
132
132
  relative path. The advisor anchors those to the **target project root** (its pinned `--cwd`),
133
133
  matching what a wrapper invoked from the project root resolves — the documented dispatch form
134
134
  (the wrapper itself anchors to its invocation `$PWD`); else the `default` applies.
135
- - Like `networkHosts`, this is a **documentation source**: the kit never seeds
135
+ - Unlike `networkHosts`, this is a **documentation source** only: the kit never seeds
136
136
  `sandbox.filesystem.allowWrite` or any other filesystem allowance — the entries feed the
137
137
  sandbox-lane discoverability item (session/host sandbox config is hand-applied territory).
138
138
 
@@ -638,10 +638,10 @@ export const validateManifest = (skillDir) => {
638
638
  }
639
639
 
640
640
  // `networkHosts` (AD-044 Plan 4, consult-locked): the backend CLI's OBSERVED egress host
641
- // families — a DOCUMENTATION source for hand-applied sandbox/network allowlists. The kit never
642
- // seeds these into settings (exclusion stays primary; a network pre-allow widens egress for
643
- // EVERY sandboxed command), but a malformed list FAILS --strict like `settings`: the
644
- // Recommendations advisor renders it verbatim, so a bad entry would corrupt a pasted line.
641
+ // families. `velocity-profile --bridge-tier` seeds them into sandbox.network.allowedDomains for
642
+ // placed bridges (AD-172; the pre-allow widens egress for EVERY sandboxed command, stated at that
643
+ // consent), and a malformed list FAILS --strict like `settings`: the tier writes it and the
644
+ // Recommendations advisor renders it verbatim, so a bad entry would corrupt a seeded line.
645
645
  const networkHosts = manifest.networkHosts;
646
646
  if (networkHosts != null) {
647
647
  if (!Array.isArray(networkHosts) || networkHosts.length === 0) {
@@ -661,7 +661,7 @@ export const validateManifest = (skillDir) => {
661
661
 
662
662
  // `writableDirs` (REC-UX-REWORK, D6): the backend CLI's writable state-dir declarations —
663
663
  // {env, default} entries the Recommendations advisor RESOLVES at run time (a NON-EMPTY env
664
- // value wins, else the default) and renders into the sandbox-lane recipe. Like `networkHosts`
664
+ // value wins, else the default) and renders into the sandbox-lane recipe. Unlike `networkHosts`
665
665
  // this is a DOCUMENTATION source (the kit never seeds filesystem allowances), and a malformed
666
666
  // list FAILS --strict for the same reason: the resolved dir is rendered into a hand-applied line.
667
667
  const writableDirs = manifest.writableDirs;
@@ -19,9 +19,9 @@
19
19
  // below) and VERDICT-FIRST (D1): every non-optimal state opens with ONE composed verdict line.
20
20
  // Registry strings are frozen tool DATA, fact-true, one line under the shape cap (D2); posture/
21
21
  // risk prose lives in the mode doc at the consent moment (D3). A probe failure is a stated
22
- // skipped-item line — never a crash, never a fabricated item. The kit never seeds
23
- // sandbox.network.allowedDomains / filesystem.allowWrite (HAND-APPLY territory; bridge council
24
- // 2026-07-11, both backends concur); the sandbox-lane item's convergence is a NEUTRAL
22
+ // skipped-item line — never a crash, never a fabricated item. sandbox.filesystem.allowWrite stays
23
+ // HAND-APPLY territory; sandbox.network.allowedDomains is seeded only by the consented
24
+ // `velocity-profile --bridge-tier` (AD-172); the sandbox-lane item's convergence is a NEUTRAL
25
25
  // fingerprint-bound acknowledgement recorded by the consent-gated ack writer into the family-owned
26
26
  // docs/ai/acks.json (AD-055 relocated it off the host settings schema), never a security key (D4).
27
27
  //
@@ -79,7 +79,7 @@ import { CHECKER_CLAIM, INITIAL_ADOPTION_REASON, SOURCE_SIZE_GATE_ID, classifySo
79
79
  import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
80
80
  import { resolveGitHooksPath } from './commit-guard.mjs';
81
81
  import { loadConfig } from './orchestration-config.mjs';
82
- import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
82
+ import { bundledSandboxRecipe } from './bridge-sandbox-recipe.mjs';
83
83
  import { assertContainedRealPath } from './fs-safe.mjs';
84
84
  import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
85
85
  import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
@@ -252,7 +252,7 @@ export const composeVerdict = ({ attention, optional, skipped }) => {
252
252
  export const WHATS = Object.freeze({
253
253
  'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
254
254
  'kit-tools-tier': "the kit's own read-only tools still prompt — {n} kit-tools tier entr(ies) not seeded",
255
- 'bridge-tier': 'council review runs prompt per bridge invocation — {n} bridge-wrappers tier entr(ies) not seeded (placed bridges only, code mode only)',
255
+ 'bridge-tier': 'bridge-wrappers tier incomplete — {n} entr(ies) missing (allow rules, sandbox exclusions or declared hosts of placed bridges)',
256
256
  'autonomy-policy': 'no {path} — the computed defaults apply implicitly (red-lines ask/deny; every activity floors at prompt)',
257
257
  'autonomy-render': 'the declared autonomy policy is not rendered into .claude/settings.json — drift: {drift}',
258
258
  'sandbox-provision': 'the OS sandbox is unavailable: {reason}',
@@ -415,7 +415,7 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
415
415
  { id: 'executor-vehicle', mode: 'agents', advisorKey: 'executor-vehicle' },
416
416
  // Exempt, not un-audited. `acceptEdits` auto-applies Edit/Write and auto-runs mkdir/touch/mv/cp:
417
417
  // a TRUST-POSTURE change. The kit never nudges a user toward weakening their approval posture (the
418
- // same doctrine that keeps sandbox network/filesystem allowances HAND-APPLY); velocity presents the
418
+ // same doctrine that keeps sandbox filesystem allowances HAND-APPLY); velocity presents the
419
419
  // full honest posture at its own consent moment, where the user is already deciding.
420
420
  {
421
421
  id: 'accept-edits',
@@ -465,8 +465,8 @@ const probeVelocityItems = ({ root, deps, add, skip }) => {
465
465
  skip('kit-tools-tier', err);
466
466
  }
467
467
  try {
468
- const bt = planVelocityProfile(preflight, { bridgeTier: true, findWrapper: deps.findWrapper });
469
- const delta = bt.bridgeToAdd.length + bt.excludedToAdd.length;
468
+ const bt = planVelocityProfile(preflight, { bridgeTier: true, findWrapper: deps.findWrapper, bundleRoot: deps.bundleRoot, readdir: deps.readdir, readFile: deps.readFile });
469
+ const delta = bt.bridgeToAdd.length + bt.excludedToAdd.length + bt.hostsToAdd.length;
470
470
  if (delta > 0) {
471
471
  add('bridge-tier', fillTemplate(WHATS['bridge-tier'], { n: delta }), applyLine(' --bridge-tier'));
472
472
  }
@@ -1157,37 +1157,6 @@ const probeMasksItem = ({ root, deps, add, skip }) => {
1157
1157
  // knob is retired in the wrappers (recognized, arms nothing) and an oversized code review is now a
1158
1158
  // chunked feed with a per-part delivery proof, so there is nothing left to offer.
1159
1159
 
1160
- // The manifest-declared session-sandbox recipe surfaces of every BUNDLED bridge whose review
1161
- // wrapper is in the wired set — networkHosts ∪ writableDirs, derived from the manifests (the
1162
- // single documentation source), never hardcoded here.
1163
- const bundledSandboxRecipe = (placedWrappers, deps) => {
1164
- const readFile = deps.readFile ?? readFileSync;
1165
- const readDir = deps.readdir ?? readdirSync;
1166
- const bundleRoot = deps.bundleRoot ?? DEFAULT_BUNDLE_ROOT;
1167
- const hosts = [];
1168
- const dirEntries = [];
1169
- for (const dir of readDir(bundleRoot)) {
1170
- // An unreadable/unparsable bundled manifest must NOT thin the recipe silently — a partial
1171
- // recipe rendered as complete is worse than no item. The throw reaches the probe's catch and
1172
- // becomes a stated skip.
1173
- let manifest;
1174
- try {
1175
- manifest = JSON.parse(readFile(join(bundleRoot, dir, 'capability.json'), 'utf8'));
1176
- } catch (err) {
1177
- // ENOTDIR = the entry is a stray regular file (.DS_Store, a README), not a bridge bundle.
1178
- if (err?.code === 'ENOTDIR') continue;
1179
- throw new Error(`bundled manifest unreadable: ${join(dir, 'capability.json')} — ${err?.message ?? err}`);
1180
- }
1181
- const reviewCmd = manifest?.roles?.review?.cmd;
1182
- if (!reviewCmd || !placedWrappers.includes(reviewCmd)) continue;
1183
- if (Array.isArray(manifest.networkHosts)) {
1184
- for (const h of manifest.networkHosts) if (!hosts.includes(h)) hosts.push(h);
1185
- }
1186
- if (Array.isArray(manifest.writableDirs)) dirEntries.push(...manifest.writableDirs);
1187
- }
1188
- return { hosts, dirEntries };
1189
- };
1190
-
1191
1160
  // D6 resolution, mirroring the wrappers' byte-semantics (`${VAR:-default}` + the exact case-arms:
1192
1161
  // `~` / `~/…` / `/…` ride as-given; EVERY other form — including `~user/…`, which the wrappers
1193
1162
  // never resolve as a home path — anchors like a relative path). The advisor anchors to the TARGET
@@ -1322,7 +1291,7 @@ const readReadLaneToggle = (root, deps) => {
1322
1291
  // D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
1323
1292
  // at the consent moment; the static contract test asserts EXACT bidirectional coverage
1324
1293
  // (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
1325
- export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement', 'profile-gap', 'jev-connect', 'jev-skill']);
1294
+ export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', 'sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement', 'profile-gap', 'jev-connect', 'jev-skill']);
1326
1295
 
1327
1296
  // The feature-spec layer's adoption state (contract: kit/spec-adoption). The canon lets a plan cite
1328
1297
  // zero governing specs while a project adopts the layer, and nothing ever said whether adoption had
@@ -1362,9 +1331,11 @@ const probeSandboxLane = ({ root, deps, add, skip }) => {
1362
1331
  const sandbox = settings.data?.sandbox;
1363
1332
  const excluded = Array.isArray(sandbox?.excludedCommands) ? sandbox.excludedCommands : [];
1364
1333
  const probePlaced = deps.findWrapper ?? ((cmd) => findOnPath(cmd, deps).state === 'present');
1365
- // Wired = the FULL two-surface tier proof (excludedCommands + the code-mode allow rule, either
1366
- // scope) — surfacing the recipe while the tier is half-configured would front-run the
1367
- // bridge-tier item (codex terminal). Byte-form from the tier's own constants.
1334
+ // Wired = the two-surface proof the tier wrote before AD-172 (the wrapper in the PROJECT
1335
+ // excludedCommands + its code-mode allow rule, either scope) — surfacing the recipe before those
1336
+ // exist would front-run the bridge-tier item (codex terminal). The hosts surface (AD-172) is
1337
+ // deliberately not required: the recipe also carries the writable dirs no tier seeds, so it may
1338
+ // render beside a bridge-tier item offering the hosts re-run. Byte-form from the tier's own constants.
1368
1339
  const allowRules = [
1369
1340
  ...(Array.isArray(settings.data?.permissions?.allow) ? settings.data.permissions.allow : []),
1370
1341
  ...(Array.isArray(localSettings.data?.permissions?.allow) ? localSettings.data.permissions.allow : []),
@@ -1372,7 +1343,7 @@ const probeSandboxLane = ({ root, deps, add, skip }) => {
1372
1343
  const wired = BRIDGE_REVIEW_WRAPPERS.filter(
1373
1344
  (w) => excluded.includes(w) && probePlaced(w) && allowRules.includes(`Bash(${w} ${BRIDGE_REVIEW_MODE}:*)`),
1374
1345
  );
1375
- if (wired.length === 0) return; // the tier is not (fully) wired — the bridge-tier item covers first
1346
+ if (wired.length === 0) return; // the tier's rules and exclusions are not wired — the bridge-tier item covers first
1376
1347
  const { hosts, dirEntries } = bundledSandboxRecipe(wired, deps);
1377
1348
  const env = deps.getenv ?? process.env;
1378
1349
  const home = deps.home ?? homedir();
@@ -1797,8 +1768,8 @@ section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a pro
1797
1768
  stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
1798
1769
  the doctor item pins via a cd prefix; the ONE exception is the set-autonomy item — a
1799
1770
  conversational skill invocation labeled "run IN the target project") and preserve each writer's
1800
- own consent semantics; the kit never seeds sandbox network/filesystem allowances (HAND-APPLY
1801
- territory), and the sandbox-lane convergence is a neutral fingerprint acknowledgement recorded by
1771
+ own consent semantics; sandbox filesystem allowances stay HAND-APPLY territory, the placed bridges' declared hosts reach sandbox.network.allowedDomains only through the
1772
+ consented velocity-profile --bridge-tier, and the sandbox-lane convergence is a neutral fingerprint acknowledgement recorded by
1802
1773
  the consent-gated ack writer into docs/ai/acks.json (never a security key).
1803
1774
 
1804
1775
  Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
@@ -14,6 +14,7 @@ import { findOnPath } from './detect-backends.mjs';
14
14
  import { SHELL_METACHARACTERS, hasShellMetacharacter, isSeedablePathToken } from './repo-lex.mjs';
15
15
  import { isDirectRun } from './direct-run.mjs';
16
16
  import { compareSemver } from './semver-lite.mjs';
17
+ import { bundledSandboxRecipe } from './bridge-sandbox-recipe.mjs';
17
18
  // The declared-path resolution + segment containment the allowWrite degrade shares with the
18
19
  // advisor's worktrees-dir convergence lane — ONE leaf, so the two readings cannot drift.
19
20
  import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
@@ -388,9 +389,11 @@ dry-run preview byte-strings. Never touches settings.local.json.
388
389
  --bridge-tier (own consent) seeds the bridge REVIEW wrappers' CODE mode for PLACED bridges
389
390
  (codex-review code, agy-review code - never the execution/probe wrappers, never plan/diff modes)
390
391
  + the quoted grounding pre-step rule, and the wrapper names into sandbox.excludedCommands (they
391
- need network - ${HOST_HONORS_QUALIFIER} the harness runs them outside the sandbox). Consented
392
- posture: an auto-allowed review wrapper runs UNATTENDED and sends the assembled repo payload to its
393
- subscription backend (see the printed tier notice).
392
+ need network - ${HOST_HONORS_QUALIFIER} the harness runs them outside the sandbox). It also
393
+ seeds the placed bridges' declared hosts into sandbox.network.allowedDomains - ${HOST_HONORS_QUALIFIER}
394
+ every sandboxed command of the project can then reach those hosts; the writable state dirs stay
395
+ hand-apply. Consented posture: an auto-allowed review wrapper runs UNATTENDED and sends the
396
+ assembled repo payload to its subscription backend (see the printed tier notice).
394
397
 
395
398
  --autonomy renders docs/ai/autonomy.json into the settings blocks it OWNS — the sandbox block +
396
399
  permissions.ask/deny red-lines + permissions.defaultMode. POLICY-ONLY: never seeds the allowlist and
@@ -634,7 +637,7 @@ const assertTargetWritable = (absPath, deps = {}) => {
634
637
 
635
638
  const formatJson = (data, eol) => `${JSON.stringify(data, null, SETTINGS_JSON_INDENT).replace(JSON_NEWLINE_PATTERN, eol)}${eol}`;
636
639
 
637
- const mergeProjectSettings = (projectData, toAdd, acceptEdits, excludedToAdd = []) => {
640
+ const mergeProjectSettings = (projectData, toAdd, acceptEdits, excludedToAdd = [], hostsToAdd = []) => {
638
641
  const base = projectData ?? {};
639
642
  const permissions = getPermissions(base);
640
643
  const allow = getAllowEntries(base);
@@ -644,15 +647,26 @@ const mergeProjectSettings = (projectData, toAdd, acceptEdits, excludedToAdd = [
644
647
  allow: mergedAllow,
645
648
  ...(acceptEdits === true ? { defaultMode: ACCEPT_EDITS_MODE } : {}),
646
649
  };
647
- // The bridge tier's second surface: sandbox.excludedCommands (merge-don't-clobber — foreign
648
- // entries and sandbox sub-keys preserved; only the tier's wrapper names append, deduped). The
649
- // flagless/kit-tools paths pass no entries, so the sandbox block stays untouched for them.
650
+ // The bridge tier's second and third surfaces: sandbox.excludedCommands and
651
+ // sandbox.network.allowedDomains (merge-don't-clobber — foreign entries and sandbox sub-keys
652
+ // preserved; only the tier's entries append, deduped). The flagless/kit-tools paths pass no
653
+ // entries, so the sandbox block stays untouched for them.
650
654
  const existingSandbox = isJsonObject(base.sandbox) ? base.sandbox : {};
651
655
  const existingExcluded = Array.isArray(existingSandbox.excludedCommands) ? existingSandbox.excludedCommands : [];
652
656
  const newExcluded = excludedToAdd.filter((cmd) => !existingExcluded.includes(cmd));
653
- const sandboxBlock = newExcluded.length
654
- ? { sandbox: { ...existingSandbox, excludedCommands: [...existingExcluded, ...newExcluded] } }
655
- : {};
657
+ const existingNetwork = isJsonObject(existingSandbox.network) ? existingSandbox.network : {};
658
+ const existingHosts = Array.isArray(existingNetwork.allowedDomains) ? existingNetwork.allowedDomains : [];
659
+ const newHosts = hostsToAdd.filter((host) => !existingHosts.includes(host));
660
+ const sandboxBlock =
661
+ newExcluded.length || newHosts.length
662
+ ? {
663
+ sandbox: {
664
+ ...existingSandbox,
665
+ ...(newExcluded.length ? { excludedCommands: [...existingExcluded, ...newExcluded] } : {}),
666
+ ...(newHosts.length ? { network: { ...existingNetwork, allowedDomains: [...existingHosts, ...newHosts] } } : {}),
667
+ },
668
+ }
669
+ : {};
656
670
  return { ...base, permissions: mergedPermissions, ...sandboxBlock };
657
671
  };
658
672
 
@@ -683,9 +697,11 @@ const formatKitTier = (result) =>
683
697
  // resolution states the exfiltration surface, never pretends it away.
684
698
  export const KIT_BRIDGE_TIER_NOTICE =
685
699
  'bridge-wrappers tier: seeds the REVIEW wrappers only, and only their CODE mode (`codex-review code`, `agy-review code` — never codex-exec/agy-run: delegated execution keeps its human prompt; never the plan/diff modes: their file arguments can point outside the repo, so they keep their prompt), each derived ONLY when its bridge is PLACED on PATH, plus the grounding pre-step rule in its rendered quoted byte-form. POSTURE (what this consent covers): an auto-allowed review wrapper runs UNATTENDED — it reads any repo file it is pointed at and sends the assembled payload to its subscription backend, and prefix rules cannot inspect arguments, so a code-mode argument that names a readable file (agy\'s --facts/--decided) rides the same consent — the same documented residual class as the autonomy red-line rules; that is the tier\'s PURPOSE (unattended council review runs) and its residual — tier entries get NO PreToolUse-hook coverage. The grounding entry\'s writer surface is bounded by grounding.mjs\'s OWN scratch-destination guard (a tracked or in-repo-not-ignored --out is refused by the tool). The wrapper names are ALSO seeded into sandbox.excludedCommands IN THE PROJECT settings.json (an exclusion only in settings.local.json was live-observed NOT to route — the wrapper then runs sandboxed and dies on a read-only HOME): ' +
686
- // The one interpolated seam in this notice: the shared host-conditional qualifier, so the tier's
700
+ // The interpolated seams in this notice: the shared host-conditional qualifier, so the tier's
687
701
  // routing promise and the render's degrade lines can never drift apart.
688
- `${HOST_HONORS_QUALIFIER} the harness runs an excluded command OUTSIDE the sandbox (the wrappers need network), so a plain allowlisted invocation triggers no sandbox-bypass approval — whether a host honors them is not knowable from here, and where it does not the wrapper simply starts sandboxed (fail-safe, never a silent widening). INVOCATION SHAPE: a prefix rule matches only a PLAIN invocation starting with the wrapper name — an env-var prefix or a compound chain never matches (redirects are fine).`;
702
+ `${HOST_HONORS_QUALIFIER} the harness runs an excluded command OUTSIDE the sandbox (the wrappers need network), so a plain allowlisted invocation triggers no sandbox-bypass approval — whether a host honors them is not knowable from here, and where it does not the wrapper simply starts sandboxed (fail-safe, never a silent widening). ` +
703
+ `The placed bridges' declared hosts (each bundled manifest's networkHosts) are seeded into sandbox.network.allowedDomains IN THE PROJECT settings.json: ${HOST_HONORS_QUALIFIER}, a wrapper the harness keeps sandboxed still reaches them, and every sandboxed command of the project can then reach those hosts, not only the wrappers — that widening is part of this consent, and the --autonomy preview reports it as a network weakening; the writable state dirs (writableDirs) stay hand-apply. ` +
704
+ 'INVOCATION SHAPE: a prefix rule matches only a PLAIN invocation starting with the wrapper name — an env-var prefix or a compound chain never matches (redirects are fine).';
689
705
 
690
706
  const formatBridgeTier = (result) =>
691
707
  result.bridgeTier
@@ -696,6 +712,13 @@ const formatBridgeTier = (result) =>
696
712
  `${result.wrote ? 'added' : 'would add'} sandbox.excludedCommands entries: ${result.excludedToAdd.length}`,
697
713
  ...formatEntryList(result.excludedToAdd),
698
714
  `already present (excludedCommands): ${result.excludedAlreadyPresent.length}`,
715
+ ...(result.hostsToAdd.length + result.hostsAlreadyPresent.length
716
+ ? [
717
+ `${result.wrote ? 'added' : 'would add'} sandbox.network.allowedDomains entries: ${result.hostsToAdd.length}`,
718
+ ...formatEntryList(result.hostsToAdd),
719
+ `already present (allowedDomains): ${result.hostsAlreadyPresent.length}`,
720
+ ]
721
+ : []),
699
722
  ...result.bridgeSkips.map((s) => ` skipped: ${s.reason}`),
700
723
  KIT_BRIDGE_TIER_NOTICE,
701
724
  ]
@@ -947,7 +970,19 @@ export const preflightVelocityProfile = ({ cwd }, deps = {}) => {
947
970
  };
948
971
  };
949
972
 
950
- export const planVelocityProfile = (preflight, { acceptEdits, kitTools, bridgeTier, findWrapper } = {}) => {
973
+ // The tier merges into the project's sandbox.network.allowedDomains, so a wrong-typed value there
974
+ // STOPs the tier with zero writes; every other mode keeps today's reading of the two keys.
975
+ const assertNetworkMergeable = (sandbox) => {
976
+ if (sandbox.network !== undefined && !isJsonObject(sandbox.network)) {
977
+ throw makeVelocityProfileError(VELOCITY_MALFORMED, `${SETTINGS_FILE}: sandbox.network must be a JSON object`);
978
+ }
979
+ if (sandbox.network?.allowedDomains !== undefined && !Array.isArray(sandbox.network.allowedDomains)) {
980
+ throw makeVelocityProfileError(VELOCITY_MALFORMED, `${SETTINGS_FILE}: sandbox.network.allowedDomains must be an array`);
981
+ }
982
+ return sandbox.network?.allowedDomains ?? [];
983
+ };
984
+
985
+ export const planVelocityProfile = (preflight, { acceptEdits, kitTools, bridgeTier, findWrapper, bundleRoot, readdir, readFile } = {}) => {
951
986
  const projectAllow = getAllowEntries(preflight.projectSettings?.data);
952
987
  const toAdd = UNIVERSAL_READONLY_ALLOWLIST.filter((entry) => !projectAllow.includes(entry));
953
988
  const alreadyPresent = UNIVERSAL_READONLY_ALLOWLIST.filter((entry) => projectAllow.includes(entry));
@@ -961,6 +996,10 @@ export const planVelocityProfile = (preflight, { acceptEdits, kitTools, bridgeTi
961
996
  const existingExcluded = Array.isArray(existingSandbox.excludedCommands) ? existingSandbox.excludedCommands : [];
962
997
  const excludedToAdd = bridge.excludedCommands.filter((cmd) => !existingExcluded.includes(cmd));
963
998
  const excludedAlreadyPresent = bridge.excludedCommands.filter((cmd) => existingExcluded.includes(cmd));
999
+ const existingHosts = bridgeTier === true ? assertNetworkMergeable(existingSandbox) : [];
1000
+ const hosts = bridgeTier === true ? bundledSandboxRecipe(bridge.placed, { bundleRoot, readdir, readFile }).hosts : [];
1001
+ const hostsToAdd = hosts.filter((host) => !existingHosts.includes(host));
1002
+ const hostsAlreadyPresent = hosts.filter((host) => existingHosts.includes(host));
964
1003
  return {
965
1004
  toAdd,
966
1005
  alreadyPresent,
@@ -973,6 +1012,8 @@ export const planVelocityProfile = (preflight, { acceptEdits, kitTools, bridgeTi
973
1012
  bridgeSkips: bridge.skips,
974
1013
  excludedToAdd,
975
1014
  excludedAlreadyPresent,
1015
+ hostsToAdd,
1016
+ hostsAlreadyPresent,
976
1017
  setsDefaultMode: acceptEdits === true,
977
1018
  };
978
1019
  };
@@ -980,7 +1021,15 @@ export const planVelocityProfile = (preflight, { acceptEdits, kitTools, bridgeTi
980
1021
  export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true, kitTools = false, bridgeTier = false } = {}, deps = {}) => {
981
1022
  const projectDir = cwd ?? deps.cwd ?? process.cwd();
982
1023
  const preflight = preflightVelocityProfile({ cwd: projectDir }, deps);
983
- const plan = planVelocityProfile(preflight, { acceptEdits, kitTools, bridgeTier, findWrapper: deps.findWrapper });
1024
+ const plan = planVelocityProfile(preflight, {
1025
+ acceptEdits,
1026
+ kitTools,
1027
+ bridgeTier,
1028
+ findWrapper: deps.findWrapper,
1029
+ bundleRoot: deps.bundleRoot,
1030
+ readdir: deps.readdir,
1031
+ readFile: deps.readFile,
1032
+ });
984
1033
  // Drift guard runs on BOTH dry-run and apply (so a dry-run faithfully predicts the apply) and
985
1034
  // validates the FULL audited core, not just the to-add delta — a drifted core entry is caught even
986
1035
  // when it is already present in the user's allow list (and toAdd is a subset of the core anyway).
@@ -1016,6 +1065,7 @@ export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true,
1016
1065
  [...plan.toAdd, ...plan.tierToAdd, ...plan.bridgeToAdd],
1017
1066
  acceptEdits,
1018
1067
  plan.excludedToAdd,
1068
+ plan.hostsToAdd,
1019
1069
  );
1020
1070
  fs.writeFile(settingsPath, formatJson(merged, preflight.projectSettings.eol ?? LF), UTF8);
1021
1071
  return { wrote: true, dryRun: false, settingsPath, ...resultBase };