@sabaiway/agent-workflow-kit 14.27.0 → 14.28.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.
@@ -22,9 +22,7 @@
22
22
  // skipped-item line — never a crash, never a fabricated item. The consented
23
23
  // `velocity-profile --bridge-tier` seeds used bridges' allow rules, <wrapper> * exclusions
24
24
  // (codex-exec * where an execute slot delegates), hosts in sandbox.network.allowedDomains and
25
- // state dirs in sandbox.filesystem.allowWrite; the sandbox-lane item's convergence is a NEUTRAL
26
- // fingerprint-bound acknowledgement recorded by the consent-gated ack writer into the family-owned
27
- // docs/ai/acks.json (AD-055 relocated it off the host settings schema), never a security key (D4).
25
+ // state dirs in sandbox.filesystem.allowWrite; its recipe: line names the missing entries.
28
26
  //
29
27
  // Read-only: never writes, never commits, never runs a subscription CLI. The reused probes are all
30
28
  // exported read-only surfaces of their owning tools (velocity/autonomy/doctor/backends/recipes/
@@ -43,15 +41,13 @@ import {
43
41
  probeSandboxAvailability,
44
42
  isExecutableFile,
45
43
  readSettingsFile,
46
- BRIDGE_REVIEW_WRAPPERS,
47
- BRIDGE_REVIEW_MODE,
48
44
  SETTINGS_FILE,
49
45
  SETTINGS_LOCAL_FILE,
50
46
  } from './velocity-profile.mjs';
51
47
  import { loadAutonomy, isSparseSeedConfig, AUTONOMY_REL } from './autonomy-config.mjs';
52
48
  import { deriveDoctorPlan, TRUSTED_DIRS } from './autonomy-doctor.mjs';
53
49
  import { laneOf, applySlot } from './write-lanes.mjs';
54
- import { detectBackends, findOnPath } from './detect-backends.mjs';
50
+ import { detectBackends } from './detect-backends.mjs';
55
51
  import { isDirectRun } from './direct-run.mjs';
56
52
  import { ACTIVITIES, resolveActivityRecipe, composeReadiness, safeLine } from './recipes.mjs';
57
53
  import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-registry.mjs';
@@ -80,8 +76,8 @@ import { CHECKER_CLAIM, INITIAL_ADOPTION_REASON, SOURCE_SIZE_GATE_ID, classifySo
80
76
  import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
81
77
  import { resolveGitHooksPath } from './commit-guard.mjs';
82
78
  import { loadConfig } from './orchestration-config.mjs';
83
- import { bundledSandboxRecipe } from './bridge-sandbox-recipe.mjs';
84
- import { homeRelative, resolveWritableDir } from './bridge-state-dirs.mjs';
79
+ import { homeRelative } from './bridge-state-dirs.mjs';
80
+ import { sandboxEnabled, HOST_HONORS_QUALIFIER } from './bridge-wiring.mjs';
85
81
  import { assertContainedRealPath } from './fs-safe.mjs';
86
82
  import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
87
83
  import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
@@ -199,7 +195,6 @@ export const SEVERITIES = Object.freeze({
199
195
  'adr-store-migration': SEVERITY_ATTENTION,
200
196
  'sandbox-masks': SEVERITY_OPTIONAL,
201
197
  'sandbox-masks.unfenced-mount': SEVERITY_ATTENTION,
202
- 'sandbox-lane': SEVERITY_OPTIONAL,
203
198
  'worktrees-dir': SEVERITY_OPTIONAL,
204
199
  // The layer is opt-in, so both arms are OFFERS under the frozen registry (attention is a CONFIGURED
205
200
  // declaration that is broken): an absent store offers the seed, a store with no live contract offers
@@ -281,7 +276,6 @@ export const WHATS = Object.freeze({
281
276
  'adr-store-migration': 'still on the retired 3-tier ADR layout — {shape}',
282
277
  'sandbox-masks': '{n} sandbox mask(s) clutter git status — the managed exclude block is absent or stale',
283
278
  'sandbox-masks.stale-real': '{n} sandbox mask(s) clutter git status — the exclude block is stale; {m} fenced entr(ies) are REAL paths (a fresh apply drops them)',
284
- 'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
285
279
  'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
286
280
  'spec-adoption': 'feature-spec store absent (docs/ai/specs) — no feature contract can govern a plan here yet; seed the store, or record the decline',
287
281
  'profile-gap': '{what}',
@@ -345,7 +339,6 @@ export const BENEFITS = Object.freeze({
345
339
  'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
346
340
  'adr-store-migration': 'durability — every decision becomes its own file with a generated navigator, instead of one hand-rotated pile',
347
341
  'sandbox-masks': 'zero clutter — a mask git ignores through the block leaves git status; a mount-only one leaves git add -A and the review fingerprint too',
348
- 'sandbox-lane': 'discoverability — the manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
349
342
  'worktrees-dir': 'parallel features — the host-specific write allowance or terminal fallback is surfaced before provision',
350
343
  'spec-adoption': 'contracts — a plan names the contract it builds to, and a change to a governed slice is visible at review instead of after it',
351
344
  'profile-gap': 'full flow — each difference from the reference profile is previewed by its own writer; a declined one returns only at a lineage step',
@@ -373,7 +366,6 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
373
366
  { id: 'kit-tools-tier', mode: 'velocity', advisorKey: 'kit-tools-tier' },
374
367
  { id: 'bridge-tier', mode: 'velocity', advisorKey: 'bridge-tier' },
375
368
  { id: 'autonomy-render', mode: 'velocity', advisorKey: 'autonomy-render' },
376
- { id: 'sandbox-lane', mode: 'velocity', advisorKey: 'sandbox-lane' },
377
369
  { id: 'autonomy-policy', mode: 'set-autonomy', advisorKey: 'autonomy-policy' },
378
370
  { id: 'sandbox-provision', mode: 'autonomy-doctor', advisorKey: 'sandbox-provision' },
379
371
  { id: 'gates-declaration', mode: 'gates', advisorKey: 'gates-declaration' },
@@ -469,7 +461,34 @@ const probeVelocityItems = ({ root, deps, add, skip }) => {
469
461
  });
470
462
  const delta = bt.bridgeToAdd.length + bt.excludedToAdd.length + bt.hostsToAdd.length + bt.dirsToAdd.length;
471
463
  if (delta > 0) {
472
- add('bridge-tier', fillTemplate(WHATS['bridge-tier'], { n: delta }), applyLine(' --bridge-tier'));
464
+ const entrySeparator = ', ';
465
+ const missing = [
466
+ ['allow rules', bt.bridgeToAdd],
467
+ ['exclusions', bt.excludedToAdd],
468
+ ['hosts', bt.hostsToAdd],
469
+ ['state dirs', bt.dirsToAdd],
470
+ ].filter(([, entries]) => entries.length > 0)
471
+ .map(([kind, entries]) => `${kind}: ${entries.join(entrySeparator)}`).join('; ');
472
+ const executeMissing = [...bt.executeToAdd.excludedCommands, ...bt.executeToAdd.hosts, ...bt.executeToAdd.dirs];
473
+ const executeDetail = executeMissing.length > 0
474
+ ? `; the execution wrapper codex-exec needs: ${executeMissing.join(entrySeparator)}`
475
+ : '';
476
+ const hostCondition = (() => {
477
+ try {
478
+ return sandboxEnabled(preflight.projectSettings.data, preflight.localSettings.data)
479
+ ? ` — once added, wired ${HOST_HONORS_QUALIFIER}`
480
+ : '';
481
+ } catch (err) {
482
+ return ` — host condition unchecked: ${oneLineOf(err?.message ?? String(err))}`;
483
+ }
484
+ })();
485
+ add(
486
+ 'bridge-tier',
487
+ fillTemplate(WHATS['bridge-tier'], { n: delta }),
488
+ applyLine(' --bridge-tier'),
489
+ 'bridge-tier',
490
+ `missing — ${missing}${executeDetail}${hostCondition}`,
491
+ );
473
492
  }
474
493
  } catch (err) {
475
494
  skip('bridge-tier', err);
@@ -1177,10 +1196,9 @@ export const recipeFingerprint = ({ hosts, dirs, home }) => {
1177
1196
  return factFingerprint(JSON.stringify({ hosts: [...hosts].sort(), dirs: [...new Set(dirs.map(norm))].sort() }));
1178
1197
  };
1179
1198
 
1180
- // The ack store (D4; AD-055 Part I) is the kit-owned PRIMARY ack channel; the legacy settings-scope
1181
- // keys below are read for one deprecation window. An ack lane exists for a state the maintainer can
1182
- // only ANSWER, never converge — a dead checker/producer pair is fixed, not acknowledged, so no lane
1183
- // names it. The store's path, keys, lane registry and reader are ack-store.mjs (re-exported above).
1199
+ // The ack store (D4; AD-055 Part I) is the kit-owned ack channel. An ack lane exists for a state the
1200
+ // maintainer can only ANSWER, never converge — a dead checker/producer pair is fixed, not acknowledged,
1201
+ // so no lane names it. The store's path, keys, lane registry and reader are ack-store.mjs.
1184
1202
 
1185
1203
  // The opt-in read-lane toggle file (AD-055 Part II) — the SAME kit-owned docs/ai/lanes.json the
1186
1204
  // placed hook reads live. The read-lane item offers to enable it once the hook is placed+wired.
@@ -1255,11 +1273,6 @@ const declarationCarriesMarker = (root, deps) => {
1255
1273
  return declaration.outcome === 'loaded' && declaration.gates.some((gate) => Object.hasOwn(gate, LCOV_PRODUCER_KEY));
1256
1274
  };
1257
1275
 
1258
- // The LEGACY neutral ack namespace (pre-AD-055): read from BOTH settings scopes until the next kit
1259
- // MAJOR (2.0.0) so a never-migrated host stays converged across the deprecation window (Decisions 3).
1260
- export const SANDBOX_LANE_ACK_PARENT = 'agentWorkflow';
1261
- export const SANDBOX_LANE_ACK_KEY = 'sandboxLaneAck';
1262
-
1263
1276
  // Read the opt-in read-lane toggle for the read-lane item. An ABSENT file (or absent docs/ai) →
1264
1277
  // false (the lane is off — offer it). `readLane === true` → enabled (converged). A parse/IO error on
1265
1278
  // an EXISTING file, a symlinked ancestor/leaf, an escape, or a non-object root THROWS — the probe
@@ -1290,7 +1303,7 @@ const readReadLaneToggle = (root, deps) => {
1290
1303
  // D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
1291
1304
  // at the consent moment; the static contract test asserts EXACT bidirectional coverage
1292
1305
  // (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
1293
- 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']);
1306
+ export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', '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
1307
 
1295
1308
  // The feature-spec layer's adoption state (contract: kit/spec-adoption). The canon lets a plan cite
1296
1309
  // zero governing specs while a project adopts the layer, and nothing ever said whether adoption had
@@ -1323,63 +1336,6 @@ export const probeSpecAdoption = ({ root, deps, add, skip }) => {
1323
1336
  }
1324
1337
  };
1325
1338
 
1326
- const probeSandboxLane = ({ root, deps, add, skip }) => {
1327
- try {
1328
- const settings = readSettingsFile(join(root, SETTINGS_FILE), { ...deps, cwd: root });
1329
- const localSettings = readSettingsFile(join(root, SETTINGS_LOCAL_FILE), { ...deps, cwd: root });
1330
- const sandbox = settings.data?.sandbox;
1331
- const excluded = Array.isArray(sandbox?.excludedCommands) ? sandbox.excludedCommands : [];
1332
- const probePlaced = deps.findWrapper ?? ((cmd) => findOnPath(cmd, deps).state === 'present');
1333
- // Wired = the two-surface proof the tier wrote before AD-172 (the wrapper in the PROJECT
1334
- // excludedCommands + its code-mode allow rule, either scope) — surfacing the recipe before those
1335
- // exist would front-run the bridge-tier item (codex terminal). The hosts surface (AD-172) is
1336
- // deliberately not required: hosts and state dirs may still be missing, so the recipe may
1337
- // render beside a bridge-tier item offering the re-run. Byte-form from the tier's own constants.
1338
- const allowRules = [
1339
- ...(Array.isArray(settings.data?.permissions?.allow) ? settings.data.permissions.allow : []),
1340
- ...(Array.isArray(localSettings.data?.permissions?.allow) ? localSettings.data.permissions.allow : []),
1341
- ];
1342
- const wired = BRIDGE_REVIEW_WRAPPERS.filter(
1343
- (w) => (excluded.includes(w) || excluded.includes(`${w} *`)) && probePlaced(w) && allowRules.includes(`Bash(${w} ${BRIDGE_REVIEW_MODE}:*)`),
1344
- );
1345
- if (wired.length === 0) return; // the tier's rules and exclusions are not wired — the bridge-tier item covers first
1346
- const { hosts, dirEntries } = bundledSandboxRecipe(wired, deps);
1347
- const env = deps.getenv ?? process.env;
1348
- const home = deps.home ?? homedir();
1349
- const dirs = [];
1350
- for (const entry of dirEntries) {
1351
- const resolved = resolveWritableDir(entry, { env, root });
1352
- if (!dirs.includes(resolved)) dirs.push(resolved);
1353
- }
1354
- const fingerprint = recipeFingerprint({ hosts, dirs, home });
1355
- // Convergence is the NEUTRAL fingerprint-bound acknowledgement: the item converges iff the
1356
- // CURRENT fingerprint equals the ack in ANY consulted store — the family-owned acks.json FIRST,
1357
- // then the legacy settings scopes; a stale value in one store is ignored when another matches
1358
- // (Decisions 2). A changed recipe (hosts, dirs, or an env override) re-fires the item (D4).
1359
- const acks = [
1360
- readAckValue(root, deps, ACKS_LANE_KEY),
1361
- settings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY],
1362
- localSettings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY],
1363
- ];
1364
- if (acks.includes(fingerprint)) return; // the acknowledged recipe — the item converged
1365
- // The item joins the CONSENT-GATED WRITER class (Decisions 4): the apply is the ack writer's
1366
- // PREVIEW one-liner (pure executable, cwd-independent), carrying the neutral fingerprint — never
1367
- // a security key. The LIVE recipe (egress hosts + resolved writable dirs) rides a separate
1368
- // rendered `recipe:` line (the fill source for the mode doc's lane-(2) hand-apply block); the
1369
- // fingerprint encodes it, so a changed recipe re-fires with a fresh command.
1370
- const recipe = `egress hosts [${hosts.join(', ')}]; writable state dirs [${dirs.join(', ')}] (observed-minimal; a blocked host names itself at run time)`;
1371
- add(
1372
- 'sandbox-lane',
1373
- fillTemplate(WHATS['sandbox-lane'], {}),
1374
- `node ${q(toolPath('ack-write.mjs'))} --fingerprint ${fingerprint} --cwd ${q(root)}`,
1375
- 'sandbox-lane',
1376
- recipe,
1377
- );
1378
- } catch (err) {
1379
- skip('sandbox-lane', err);
1380
- }
1381
- };
1382
-
1383
1339
  // D7 lane 1 — the DECLARATION confirmation. A settings entry is not proof of writable CAPABILITY
1384
1340
  // (runtime truth stays with the provision preflight's real create+delete probe); it is proof the
1385
1341
  // maintainer applied this item's own advice, which is what the item may converge on. Both scopes
@@ -1433,8 +1389,8 @@ const declaresWritableDir = (root, probeDir, deps) => {
1433
1389
  };
1434
1390
 
1435
1391
  // D7 lane 2 — the ack fallback for a host that ignores the settings key, bound to the PROBED DIR
1436
- // through the shared neutral fingerprint (the same home-symbolic canonicalization the sandbox-lane
1437
- // ack uses, so a home-anchored dir acks portably). The binding is to the RESOLVED probe dir, so the
1392
+ // through the worktrees-dir fingerprint's home-symbolic canonicalization, so a home-anchored dir
1393
+ // acks portably. The binding is to the RESOLVED probe dir, so the
1438
1394
  // item re-fires only when that resolved dir changes — two ABSENT parentDir values sharing an
1439
1395
  // existing ancestor resolve to the same dir and keep the same ack.
1440
1396
  export const worktreesDirFingerprint = (probeDir, home) => recipeFingerprint({ hosts: [], dirs: [probeDir], home });
@@ -1630,7 +1586,6 @@ const PROBES = Object.freeze([
1630
1586
  probeFamilyFreshness,
1631
1587
  probeAdrStore,
1632
1588
  probeMasksItem,
1633
- probeSandboxLane,
1634
1589
  probeWorktreesDir,
1635
1590
  probeMcpChannel,
1636
1591
  probeSpecAdoption,
@@ -1654,8 +1609,8 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
1654
1609
  // smoke asserting a specific false-green never returns) can assert the exact outcome instead of
1655
1610
  // pattern-matching prose that is free to be reworded.
1656
1611
  // `detail` (optional) is an extra rendered `recipe:` line — factual context that is TOO LONG for
1657
- // the capped WHAT and does NOT belong in the pure-command apply (the sandbox-lane live recipe:
1658
- // egress hosts + resolved writable dirs; the worktrees-dir hand-apply-first grant advice; the
1612
+ // the capped WHAT and does NOT belong in the pure-command apply (the bridge-tier missing-entries
1613
+ // line; the worktrees-dir hand-apply-first grant advice; the
1659
1614
  // agents hidden-mode reconcile follow-up). Single-line like apply; absent for every other item.
1660
1615
  // Returns whether the item really RENDERED. One probe's disposition depends on another's having
1661
1616
  // spoken (the marker-stale ⟷ read-lane.stale precedence), and "the conditions still look right"
@@ -1746,8 +1701,8 @@ Usage:
1746
1701
 
1747
1702
  Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
1748
1703
  one composed verdict line opens every non-optimal render, then per item {severity · what is
1749
- sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (the sandbox-lane live
1750
- recipe, the worktrees-dir hand-apply-first grant advice, or the agents hidden-mode reconcile
1704
+ sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (the bridge-tier
1705
+ missing-entries line, the worktrees-dir hand-apply-first grant advice, or the agents hidden-mode reconcile
1751
1706
  follow-up) · the harness check line · the exact consent-gated apply one-liner}. --cwd is
1752
1707
  REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
1753
1708
  section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
@@ -1760,8 +1715,7 @@ sandbox.network.allowedDomains and state dirs in sandbox.filesystem.allowWrite.
1760
1715
  honors the settings sandbox keys, an excluded wrapper runs outside the sandbox, every sandboxed
1761
1716
  command of the project can reach each host, and every sandboxed command can write each state dir;
1762
1717
  plan/diff review modes and delegated execution keep their prompt. Items marked HAND-APPLY stay
1763
- the user's own step. The sandbox-lane convergence is a neutral fingerprint acknowledgement recorded by
1764
- the consent-gated ack writer into docs/ai/acks.json (never a security key).
1718
+ the user's own step.
1765
1719
 
1766
1720
  Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
1767
1721
  rendered (items or empty); 1 error; 2 usage.`;
@@ -13,10 +13,12 @@ import { SHELL_METACHARACTERS, hasShellMetacharacter, isSeedablePathToken } from
13
13
  import { isDirectRun } from './direct-run.mjs';
14
14
  import { compareSemver } from './semver-lite.mjs';
15
15
  import { BRIDGE_REVIEW_WRAPPERS, BRIDGE_REVIEW_MODE, KIT_GROUNDING_TOOL, usedBridges, bundledSandboxRecipe, exclusionOf } from './bridge-sandbox-recipe.mjs';
16
- import { stateDirsOf, missingStateDirs, mergeAllowWrite } from './bridge-state-dirs.mjs';
16
+ import { stateDirsOf, mergeAllowWrite } from './bridge-state-dirs.mjs';
17
+ import { HOST_HONORS_QUALIFIER, sandboxSurfaceDelta } from './bridge-wiring.mjs';
17
18
  import { loadConfig } from './orchestration-config.mjs';
18
19
  import { validateManifest } from './manifest/validate.mjs';
19
20
  export { BRIDGE_REVIEW_WRAPPERS, BRIDGE_REVIEW_MODE, KIT_GROUNDING_TOOL };
21
+ export { HOST_HONORS_QUALIFIER };
20
22
  // The declared-path resolution + segment containment the allowWrite degrade shares with the
21
23
  // advisor's worktrees-dir convergence lane — ONE leaf, so the two readings cannot drift.
22
24
  import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
@@ -303,18 +305,19 @@ const DO_NOT_ADD_WARNING = 'do not add';
303
305
  const ADD_BY_HAND = true;
304
306
  const MUTATING_SCRIPT_NAME_PATTERN = /(release|publish|deploy|push|version|commit|tag)/iu;
305
307
  const MUTATING_SCRIPT_HOOK_PATTERN = /^(pre|post)/iu;
308
+ const CODEX_BRIDGE = 'codex-cli-bridge';
309
+ const EXECUTE_ROLE = 'execute';
306
310
  // The one thing velocity must never seed — an explicit, greppable expression of the load-bearing
307
311
  // invariant (kept deliberately even though the read-only screen already rejects these, so the
308
312
  // refusal is named, tested, and produces a clear message).
309
313
  const MUTATING_ALLOW_COMMAND_PATTERN = /^(?:git\s+(?:commit|push)|npm\s+publish)(?:\s|$)/iu;
310
314
  // Whether a host APPLIES a `sandbox.*` settings key is not knowable from here: a settings-native
311
- // host honors it, while an IDE/session-imposed sandbox was observed ignoring the hand-applied
312
- // security keys in BOTH scopes (the advisor records the same limit in
315
+ // host honors it, while an IDE/session-imposed sandbox was observed ignoring the security keys
316
+ // in BOTH scopes (the advisor records the same limit in
313
317
  // references/modes/recommendations.md). So every claim about the RUNTIME EFFECT of a settings key —
314
318
  // the tier's exclusion routing and every weakening detail alike — carries this qualifier. The
315
319
  // CLASSIFICATION is untouched: which red-line a key would weaken, and that a proven tier exclusion
316
320
  // is a note rather than a weakening, are properties of the DECLARATION, not of a host.
317
- export const HOST_HONORS_QUALIFIER = 'where the host honors the settings sandbox keys';
318
321
  export const HOST_HONORS_NOTICE =
319
322
  `host-conditional: whether a host applies the sandbox.* settings keys is not knowable from here — a settings-native host honors them, a harness-managed session sandbox may ignore them in BOTH scopes, so each runtime effect above is stated "${HOST_HONORS_QUALIFIER}" rather than promised; what a key would weaken is a property of the declaration and is stated flat.`;
320
323
 
@@ -986,16 +989,25 @@ export const planVelocityProfile = (preflight, opts = {}) => {
986
989
  const bridgeToAdd = bridge.allow.filter((entry) => !projectAllow.includes(entry));
987
990
  const bridgeAlreadyPresent = bridge.allow.filter((entry) => projectAllow.includes(entry));
988
991
  const existingSandbox = isJsonObject(preflight.projectSettings?.data?.sandbox) ? preflight.projectSettings.data.sandbox : {};
989
- const existingExcluded = Array.isArray(existingSandbox.excludedCommands) ? existingSandbox.excludedCommands : [];
990
- const excludedToAdd = bridge.excludedCommands.filter((cmd) => !existingExcluded.includes(cmd));
991
- const excludedAlreadyPresent = bridge.excludedCommands.filter((cmd) => existingExcluded.includes(cmd));
992
- const existingHosts = bridgeTier === true ? assertNetworkMergeable(existingSandbox) : [];
993
- const hostsToAdd = bridge.hosts.filter((host) => !existingHosts.includes(host));
994
- const hostsAlreadyPresent = bridge.hosts.filter((host) => existingHosts.includes(host));
995
- const existingDirs = bridgeTier === true ? assertFilesystemMergeable(existingSandbox) : [];
992
+ if (bridgeTier === true) {
993
+ assertNetworkMergeable(existingSandbox);
994
+ assertFilesystemMergeable(existingSandbox);
995
+ }
996
996
  const dirs = derived?.dirs ?? [];
997
- const dirsToAdd = derived ? missingStateDirs(dirs, existingDirs, { home: derived.home, root: preflight.cwd }) : [];
997
+ const delta = derived
998
+ ? sandboxSurfaceDelta({ excludedCommands: bridge.excludedCommands, hosts: bridge.hosts, dirs }, existingSandbox, { home: derived.home, root: preflight.cwd })
999
+ : { excludedCommands: [], hosts: [], dirs: [] };
1000
+ const { excludedCommands: excludedToAdd, hosts: hostsToAdd, dirs: dirsToAdd } = delta;
1001
+ const excludedAlreadyPresent = bridge.excludedCommands.filter((cmd) => !excludedToAdd.includes(cmd));
1002
+ const hostsAlreadyPresent = bridge.hosts.filter((host) => !hostsToAdd.includes(host));
998
1003
  const dirsAlreadyPresent = dirs.filter((dir) => !dirsToAdd.includes(dir));
1004
+ const executeRecipe = derived?.used.some((entry) => entry.bridge === CODEX_BRIDGE && entry.roles.includes(EXECUTE_ROLE))
1005
+ ? bundledSandboxRecipe([{ bridge: CODEX_BRIDGE, roles: [EXECUTE_ROLE] }], opts)
1006
+ : null;
1007
+ const executeDirs = executeRecipe ? stateDirsOf(executeRecipe.dirEntries, { env: derived.env, root: preflight.cwd, home: derived.home }) : [];
1008
+ const executeToAdd = executeRecipe
1009
+ ? sandboxSurfaceDelta({ excludedCommands: executeRecipe.excludedCommands, hosts: executeRecipe.hosts, dirs: executeDirs }, existingSandbox, { home: derived.home, root: preflight.cwd })
1010
+ : { excludedCommands: [], hosts: [], dirs: [] };
999
1011
  return {
1000
1012
  toAdd,
1001
1013
  alreadyPresent,
@@ -1012,6 +1024,7 @@ export const planVelocityProfile = (preflight, opts = {}) => {
1012
1024
  hostsAlreadyPresent,
1013
1025
  dirsToAdd,
1014
1026
  dirsAlreadyPresent,
1027
+ executeToAdd,
1015
1028
  setsDefaultMode: acceptEdits === true,
1016
1029
  };
1017
1030
  };
@@ -1568,7 +1581,7 @@ const collectSandboxWeakenings = (sources, { root, home, tmp }, tier) => {
1568
1581
  const foreign = fsb.allowWrite.filter((entry) => !known.includes(entry));
1569
1582
  addNotes(`${SANDBOX_KEY}.filesystem.allowWrite`, known, 'every sandboxed command can write it');
1570
1583
  // Only the entries that RESOLVE outside the repo and $TMPDIR are a weakening at all — an entry
1571
- // pointing INSIDE the repo grants nothing the red-line withholds, so reporting it would be an
1584
+ // pointing INSIDE the repo grants nothing beyond the red-line's own bound, so reporting it would be an
1572
1585
  // over-report. All-contained ⇒ no line at all.
1573
1586
  // TWO records, never one: a resolved external path IS a weakening, an unresolvable entry is
1574
1587
  // UNVERIFIABLE, and a single line carrying both would assert an effect for entries it could
@@ -41,7 +41,6 @@ export const VARIANT_LANES = Object.freeze({
41
41
  'adr-store-migration': LANE_CHAT,
42
42
  'sandbox-masks': LANE_CHAT,
43
43
  'sandbox-masks.unfenced-mount': LANE_CHAT,
44
- 'sandbox-lane': LANE_CHAT,
45
44
  'worktrees-dir': LANE_CHAT,
46
45
  'spec-adoption': LANE_CHAT,
47
46
  'profile-gap': LANE_CHAT,