@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.
- package/CHANGELOG.md +35 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/recipes.md +2 -0
- package/references/modes/recommendations.md +8 -9
- package/references/modes/set-recipe.md +1 -1
- package/references/modes/velocity.md +2 -3
- package/references/shared/report-footer.md +29 -32
- package/tools/ack-write.mjs +11 -11
- package/tools/bridge-sandbox-recipe.mjs +1 -1
- package/tools/bridge-wiring.mjs +196 -0
- package/tools/manifest/schema.md +12 -11
- package/tools/manifest/validate.mjs +11 -11
- package/tools/recipes.mjs +75 -15
- package/tools/recommendations.mjs +43 -89
- package/tools/velocity-profile.mjs +26 -13
- package/tools/write-lanes.mjs +0 -1
|
@@ -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;
|
|
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
|
|
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 {
|
|
84
|
-
import {
|
|
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
|
-
|
|
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
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
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', '
|
|
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
|
|
1437
|
-
//
|
|
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
|
|
1658
|
-
//
|
|
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
|
|
1750
|
-
|
|
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.
|
|
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,
|
|
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
|
|
312
|
-
//
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
|
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
|
|
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
|
package/tools/write-lanes.mjs
CHANGED
|
@@ -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,
|