@sabaiway/agent-workflow-kit 1.44.0 → 1.45.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 +52 -0
- package/README.md +6 -3
- package/SKILL.md +9 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +87 -16
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +3 -0
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +4 -2
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +3 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +90 -19
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +4 -0
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/references/driving-codex.md +3 -2
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +3 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/grounding.md +8 -7
- package/references/modes/recommendations.md +14 -0
- package/references/modes/review-state.md +1 -1
- package/references/modes/sandbox-masks.md +15 -0
- package/references/modes/upgrade.md +10 -7
- package/references/modes/velocity.md +13 -2
- package/references/shared/composition-handoff.md +21 -16
- package/references/shared/report-footer.md +5 -1
- package/references/templates/AGENTS.md +2 -1
- package/references/templates/autonomy.json +3 -0
- package/tools/autonomy-config.mjs +13 -3
- package/tools/commands.mjs +15 -1
- package/tools/delegation.mjs +9 -5
- package/tools/detect-backends.mjs +2 -2
- package/tools/doc-parity.mjs +8 -0
- package/tools/engine-source.mjs +6 -0
- package/tools/family-registry.mjs +21 -7
- package/tools/fold-completeness-run.mjs +5 -0
- package/tools/grounding.mjs +139 -22
- package/tools/inject-methodology.mjs +117 -28
- package/tools/manifest/schema.md +20 -0
- package/tools/manifest/validate.mjs +26 -0
- package/tools/procedures.mjs +61 -8
- package/tools/recipes.mjs +94 -15
- package/tools/recommendations.mjs +469 -0
- package/tools/review-ledger-write.mjs +14 -0
- package/tools/review-ledger.mjs +3 -2
- package/tools/review-state.mjs +101 -24
- package/tools/run-gates.mjs +3 -0
- package/tools/sandbox-masks.mjs +370 -0
- package/tools/set-recipe.mjs +13 -1
- package/tools/velocity-profile.mjs +228 -22
|
@@ -6,6 +6,9 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
6
6
|
// it never imports the policy fs-writer (autonomy-write.mjs) — the render owns the settings file, not
|
|
7
7
|
// the policy file.
|
|
8
8
|
import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, COMMAND_REDLINES } from './autonomy-config.mjs';
|
|
9
|
+
// The bridge-wrappers tier's placement probe (AD-044 Plan 4, Decision 2): a tier entry derives ONLY
|
|
10
|
+
// for a PLACED bridge wrapper — findOnPath is the same read-only PATH scan the backend detector uses.
|
|
11
|
+
import { findOnPath } from './detect-backends.mjs';
|
|
9
12
|
|
|
10
13
|
// Velocity-profile core + writer: a fixed, audited read-only allowlist that an onboarding step seeds
|
|
11
14
|
// into `.claude/settings.json` so routine read-only commands stop idling on approval prompts.
|
|
@@ -122,7 +125,7 @@ export const SHELL_READONLY = Object.freeze([
|
|
|
122
125
|
// seed time from the RUNNING tool's own location — resolved-absolute, so a moved or reinstalled
|
|
123
126
|
// skill leaves a stale rule that FAIL-SAFE prompts again (never a silent widening).
|
|
124
127
|
//
|
|
125
|
-
// Membership (
|
|
128
|
+
// Membership (10, frozen): the read-only kit tools plus run-gates.mjs — which is NOT read-only but
|
|
126
129
|
// project-exec (it runs the project's OWN declared gates.json commands), so it seeds as ONE exact
|
|
127
130
|
// byte-string pinned to this project root (`--cwd <resolved root>`): a wildcard would be BROADER
|
|
128
131
|
// than the AD-037 hook boundary (`--cwd <dir>` executes an arbitrary OTHER project's gates.json)
|
|
@@ -136,6 +139,7 @@ export const KIT_READONLY_TOOLS = Object.freeze([
|
|
|
136
139
|
'tools/detect-backends.mjs',
|
|
137
140
|
'tools/commands.mjs',
|
|
138
141
|
'tools/review-state.mjs',
|
|
142
|
+
'tools/recommendations.mjs',
|
|
139
143
|
KIT_RUN_GATES_TOOL,
|
|
140
144
|
'tools/manifest/validate.mjs',
|
|
141
145
|
'tools/release-scan.mjs',
|
|
@@ -156,6 +160,71 @@ const KIT_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
|
156
160
|
const KIT_TOOL_INVOKER = 'node';
|
|
157
161
|
const RUN_GATES_CWD_FLAG = '--cwd';
|
|
158
162
|
|
|
163
|
+
// ── the opt-in --bridge-tier (AD-044 Plan 4, Decision 2) ────────────────────────────────
|
|
164
|
+
// The FROZEN membership source: review-role wrapper NAMES only, spelled bare — that is how the
|
|
165
|
+
// wrappers are invoked (setup-backends places them as symlinks in ~/.local/bin). Roles come from
|
|
166
|
+
// THIS constant, never from the detector's wrapperCmds (which carries no role labels):
|
|
167
|
+
// codex-exec (execution role) and agy-run (probe role) are deliberately ABSENT — delegated
|
|
168
|
+
// execution keeps its human prompt; the velocity pain this tier closes is council REVIEW runs.
|
|
169
|
+
export const BRIDGE_REVIEW_WRAPPERS = Object.freeze(['codex-review', 'agy-review']);
|
|
170
|
+
// Only the `code` review mode is auto-allowed (codex R2, Segment A): a bare `Bash(<wrapper>:*)`
|
|
171
|
+
// prefix would also cover the plan/diff file-argument modes, whose targets can point OUTSIDE the
|
|
172
|
+
// repo — the tier's stated surface is the unattended council CODE review, so the seeded prefix is
|
|
173
|
+
// `<wrapper> code`. Plan/diff invocations keep their prompt.
|
|
174
|
+
export const BRIDGE_REVIEW_MODE = 'code';
|
|
175
|
+
// The grounding pre-step tool (the reviews' facts assembler). Its tier entry is seeded in the
|
|
176
|
+
// EXACT byte-form the procedures advisor renders — a DOUBLE-QUOTED absolute path (deliberate
|
|
177
|
+
// there: a skill dir with a space must stay copy-pasteable) — so seeded↔rendered byte-parity
|
|
178
|
+
// holds; the screen accepts the quoted form ONLY for this one tool (a tier-scoped acceptance,
|
|
179
|
+
// never a general quote allowance).
|
|
180
|
+
export const KIT_GROUNDING_TOOL = 'tools/grounding.mjs';
|
|
181
|
+
const WRAPPER_PLACED = 'present';
|
|
182
|
+
|
|
183
|
+
// The quoted-grounding token: "<seedable-abs-path>/tools/grounding.mjs" — quotes stripped, the
|
|
184
|
+
// inner path must itself be seedable and end on the grounding tail. No other node tool may ride
|
|
185
|
+
// this class (a NEGATIVE screen pin).
|
|
186
|
+
const isQuotedGroundingToken = (token) => {
|
|
187
|
+
if (typeof token !== 'string' || token.length < 3 || !token.startsWith('"') || !token.endsWith('"')) return false;
|
|
188
|
+
const inner = token.slice(1, -1);
|
|
189
|
+
return isSeedablePathToken(inner) && inner.endsWith(`/${KIT_GROUNDING_TOOL}`);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Derive the opt-in bridge-wrappers tier: one bare-name wildcard allow rule per PLACED review
|
|
194
|
+
* wrapper (the frozen constant is the membership source; placement is a probe, never a role
|
|
195
|
+
* source), the SAME wrapper names for sandbox.excludedCommands (the harness runs an excluded
|
|
196
|
+
* command OUTSIDE the sandbox, so a plain allowlisted invocation needs no sandbox-bypass
|
|
197
|
+
* approval — the zero-prompt wiring), and the grounding pre-step rule in its rendered quoted
|
|
198
|
+
* byte-form (derived only when ≥1 review bridge is placed; an unseedable kit path is a stated
|
|
199
|
+
* skip). An absent bridge is a stated skip — its entry never derives.
|
|
200
|
+
*/
|
|
201
|
+
export const deriveBridgeTierAllowlist = ({ findWrapper, groundingAbsPath } = {}) => {
|
|
202
|
+
const probe = findWrapper ?? ((cmd) => findOnPath(cmd).state === WRAPPER_PLACED);
|
|
203
|
+
const placed = BRIDGE_REVIEW_WRAPPERS.filter((cmd) => probe(cmd));
|
|
204
|
+
const skips = BRIDGE_REVIEW_WRAPPERS.filter((cmd) => !placed.includes(cmd)).map((cmd) => ({
|
|
205
|
+
entry: cmd,
|
|
206
|
+
reason: `bridge wrapper "${cmd}" is not placed on PATH — its allow rule is not seeded (place the bridge with /agent-workflow-kit setup, then re-run)`,
|
|
207
|
+
}));
|
|
208
|
+
const allow = placed.map((cmd) => `Bash(${cmd} ${BRIDGE_REVIEW_MODE}:*)`);
|
|
209
|
+
const excludedCommands = [...placed];
|
|
210
|
+
// The grounding pre-step exists FOR agy (its grounded --facts reviews; codex grounds natively via
|
|
211
|
+
// the AGENTS.md auto-merge) — a codex-only install must not auto-allow an unused writer (codex R9).
|
|
212
|
+
if (placed.includes('agy-review')) {
|
|
213
|
+
// groundingAbsPath is a TEST seam only (an unseedable kit path — spaces — is not constructible
|
|
214
|
+
// from a test against the real checkout); production callers never pass it.
|
|
215
|
+
const groundingAbs = groundingAbsPath ?? join(KIT_ROOT, KIT_GROUNDING_TOOL);
|
|
216
|
+
if (isSeedablePathToken(groundingAbs)) {
|
|
217
|
+
allow.push(`Bash(${KIT_TOOL_INVOKER} "${groundingAbs}":*)`);
|
|
218
|
+
} else {
|
|
219
|
+
skips.push({
|
|
220
|
+
entry: groundingAbs,
|
|
221
|
+
reason: 'the kit path is not a POSIX absolute path free of spaces/metacharacters/quoting — the grounding pre-step rule is not seeded (its prompt stays); add a hand-picked entry if you accept the spelling',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return Object.freeze({ allow: Object.freeze(allow), excludedCommands: Object.freeze(excludedCommands), skips: Object.freeze(skips), placed: Object.freeze(placed) });
|
|
226
|
+
};
|
|
227
|
+
|
|
159
228
|
// Characters that make a pattern NOT a single read-only command, so the screen rejects them.
|
|
160
229
|
// Per current Claude Code permission semantics: the recognized command SEPARATORS are
|
|
161
230
|
// `&& || ; | |& &` plus newline (each segment must be independently permitted, so a chained
|
|
@@ -228,6 +297,7 @@ const FLAG_DRY_RUN = '--dry-run';
|
|
|
228
297
|
const FLAG_APPLY = '--apply';
|
|
229
298
|
const FLAG_ACCEPT_EDITS = '--accept-edits';
|
|
230
299
|
const FLAG_KIT_TOOLS = '--kit-tools';
|
|
300
|
+
const FLAG_BRIDGE_TIER = '--bridge-tier';
|
|
231
301
|
const FLAG_AUTONOMY = '--autonomy';
|
|
232
302
|
const FLAG_CHECK = '--check';
|
|
233
303
|
const FLAG_CWD = '--cwd';
|
|
@@ -244,22 +314,29 @@ const MUTATING_ALLOW_COMMAND_PATTERN = /^(?:git\s+(?:commit|push)|npm\s+publish)
|
|
|
244
314
|
const RESIDUAL_NOTICE =
|
|
245
315
|
'residual: seeded read-only allow entries are a trust-posture convenience, NOT a sandbox; settings-level rules cannot inspect runtime redirection/command-substitution/--output writes; commit/push/publish are never allowlisted (a DIRECT invocation still ASKs, but the runtime residual is not closed here); the residual guard ships as the opt-in PreToolUse hook — Mode: hook (/agent-workflow-kit hook). floor (never auto-approved, with or without the tier): every writer --apply/--write/--yes still prompts; clobber-protection STOPs still stop; the three release asks (commit/push/publish) stay maintainer-owned.';
|
|
246
316
|
|
|
247
|
-
const USAGE = `usage: velocity-profile [--dry-run | --apply] [--kit-tools] [--accept-edits] [--cwd <dir>] [--help]
|
|
317
|
+
const USAGE = `usage: velocity-profile [--dry-run | --apply] [--kit-tools] [--bridge-tier] [--accept-edits] [--cwd <dir>] [--help]
|
|
248
318
|
velocity-profile --autonomy [--apply] [--cwd <dir>] (render the autonomy policy)
|
|
249
319
|
velocity-profile --autonomy --check [--cwd <dir>] (read-only drift gate)
|
|
250
320
|
|
|
251
321
|
Allowlist mode (default): seeds the fixed read-only Claude Code allowlist into .claude/settings.json.
|
|
252
322
|
Default is --dry-run. --apply writes; --accept-edits only sets defaultMode when applying.
|
|
253
|
-
--kit-tools additionally seeds the audited kit-tool tier:
|
|
323
|
+
--kit-tools additionally seeds the audited kit-tool tier: 9 read-only kit tools by resolved
|
|
254
324
|
absolute path (args wildcard), run-gates.mjs as ONE exact project-root-pinned byte-string
|
|
255
325
|
(project-exec - it runs YOUR declared gates.json), and the writers' exact arg-free dry-run
|
|
256
326
|
preview byte-strings. Never touches settings.local.json.
|
|
327
|
+
--bridge-tier (own consent) seeds the bridge REVIEW wrappers' CODE mode for PLACED bridges
|
|
328
|
+
(codex-review code, agy-review code - never the execution/probe wrappers, never plan/diff modes)
|
|
329
|
+
+ the quoted grounding pre-step rule, and the wrapper names into sandbox.excludedCommands (they
|
|
330
|
+
need network - the harness runs them outside the sandbox). Consented posture: an auto-allowed
|
|
331
|
+
review wrapper runs UNATTENDED and sends the assembled repo payload to its subscription backend
|
|
332
|
+
(see the printed tier notice).
|
|
257
333
|
|
|
258
334
|
--autonomy renders docs/ai/autonomy.json into the settings blocks it OWNS — the sandbox block +
|
|
259
335
|
permissions.ask/deny red-lines + permissions.defaultMode. POLICY-ONLY: never seeds the allowlist and
|
|
260
336
|
leaves permissions.allow untouched. Preview by default; --apply writes; --check is a read-only drift
|
|
261
|
-
gate (exit 1 on drift). --autonomy cannot combine with --accept-edits
|
|
262
|
-
flags). Refuses an absent policy (seed one first with set-autonomy). Never touches
|
|
337
|
+
gate (exit 1 on drift). --autonomy cannot combine with --accept-edits, --kit-tools, or --bridge-tier
|
|
338
|
+
(allowlist-mode flags). Refuses an absent policy (seed one first with set-autonomy). Never touches
|
|
339
|
+
settings.local.json.`;
|
|
263
340
|
|
|
264
341
|
const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
265
342
|
|
|
@@ -328,6 +405,12 @@ const validateSettingsShape = (data, relPath) => {
|
|
|
328
405
|
if (data.sandbox !== undefined && !isJsonObject(data.sandbox)) {
|
|
329
406
|
throw makeVelocityProfileError(VELOCITY_MALFORMED, `${relPath}: sandbox must be a JSON object`);
|
|
330
407
|
}
|
|
408
|
+
// The bridge tier merges into sandbox.excludedCommands — a malformed (non-array) value is a STOP
|
|
409
|
+
// with ZERO writes (the same fail-closed posture as permissions.allow above), never a silent
|
|
410
|
+
// treat-as-empty overwrite (codex R1, Segment A).
|
|
411
|
+
if (isJsonObject(data.sandbox) && data.sandbox.excludedCommands !== undefined && !Array.isArray(data.sandbox.excludedCommands)) {
|
|
412
|
+
throw makeVelocityProfileError(VELOCITY_MALFORMED, `${relPath}: sandbox.excludedCommands must be an array`);
|
|
413
|
+
}
|
|
331
414
|
return data;
|
|
332
415
|
};
|
|
333
416
|
|
|
@@ -464,13 +547,31 @@ const isKitToolShapedCommand = (entry) => {
|
|
|
464
547
|
return cmd !== undefined && tokenizeCommand(cmd)[0] === KIT_TOOL_INVOKER;
|
|
465
548
|
};
|
|
466
549
|
|
|
467
|
-
|
|
550
|
+
// A bridge-tier-SHAPED entry (a bare frozen-tier wrapper wildcard, or the quoted-grounding form)
|
|
551
|
+
// passes the shape screen — but only the entries the tier derives for THIS host (its PLACED
|
|
552
|
+
// bridges + this kit's grounding path) are audited; an absent-bridge or foreign-path spelling
|
|
553
|
+
// stays flagged for hand review (the same derived-set discipline as the kit-tools tier).
|
|
554
|
+
const isBridgeTierShapedCommand = (entry) => {
|
|
555
|
+
const cmd = getBashAllowCommand(entry);
|
|
556
|
+
if (cmd === undefined) return false;
|
|
557
|
+
const tokens = tokenizeCommand(cmd);
|
|
558
|
+
return (
|
|
559
|
+
BRIDGE_REVIEW_WRAPPERS.includes(tokens[0]) ||
|
|
560
|
+
(tokens[0] === KIT_TOOL_INVOKER && tokens.length === 2 && isQuotedGroundingToken(tokens[1]))
|
|
561
|
+
);
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const collectPreExistingNonReadonly = (sources, derivedTier = [], derivedBridgeAllow = []) =>
|
|
468
565
|
sources.flatMap(({ source, data }) =>
|
|
469
566
|
getAllowEntries(data)
|
|
470
567
|
.filter((entry) => typeof entry === 'string' && BASH_PERMISSION_PATTERN.test(entry))
|
|
471
568
|
.filter(
|
|
472
569
|
(entry) =>
|
|
473
|
-
!screenAllowlistEntry(entry) ||
|
|
570
|
+
!screenAllowlistEntry(entry) ||
|
|
571
|
+
// The quoted grounding rule is node-shaped AND bridge-tier-shaped: membership in EITHER
|
|
572
|
+
// derived set exempts it — the advisory never flags an entry a tier itself seeds.
|
|
573
|
+
(isKitToolShapedCommand(entry) && !derivedTier.includes(entry) && !derivedBridgeAllow.includes(entry)) ||
|
|
574
|
+
(isBridgeTierShapedCommand(entry) && !derivedBridgeAllow.includes(entry)),
|
|
474
575
|
)
|
|
475
576
|
.map((entry) => ({ source, entry })),
|
|
476
577
|
);
|
|
@@ -488,7 +589,7 @@ const assertTargetWritable = (absPath, deps = {}) => {
|
|
|
488
589
|
|
|
489
590
|
const formatJson = (data, eol) => `${JSON.stringify(data, null, SETTINGS_JSON_INDENT).replace(JSON_NEWLINE_PATTERN, eol)}${eol}`;
|
|
490
591
|
|
|
491
|
-
const mergeProjectSettings = (projectData, toAdd, acceptEdits) => {
|
|
592
|
+
const mergeProjectSettings = (projectData, toAdd, acceptEdits, excludedToAdd = []) => {
|
|
492
593
|
const base = projectData ?? {};
|
|
493
594
|
const permissions = getPermissions(base);
|
|
494
595
|
const allow = getAllowEntries(base);
|
|
@@ -498,7 +599,16 @@ const mergeProjectSettings = (projectData, toAdd, acceptEdits) => {
|
|
|
498
599
|
allow: mergedAllow,
|
|
499
600
|
...(acceptEdits === true ? { defaultMode: ACCEPT_EDITS_MODE } : {}),
|
|
500
601
|
};
|
|
501
|
-
|
|
602
|
+
// The bridge tier's second surface: sandbox.excludedCommands (merge-don't-clobber — foreign
|
|
603
|
+
// entries and sandbox sub-keys preserved; only the tier's wrapper names append, deduped). The
|
|
604
|
+
// flagless/kit-tools paths pass no entries, so the sandbox block stays untouched for them.
|
|
605
|
+
const existingSandbox = isJsonObject(base.sandbox) ? base.sandbox : {};
|
|
606
|
+
const existingExcluded = Array.isArray(existingSandbox.excludedCommands) ? existingSandbox.excludedCommands : [];
|
|
607
|
+
const newExcluded = excludedToAdd.filter((cmd) => !existingExcluded.includes(cmd));
|
|
608
|
+
const sandboxBlock = newExcluded.length
|
|
609
|
+
? { sandbox: { ...existingSandbox, excludedCommands: [...existingExcluded, ...newExcluded] } }
|
|
610
|
+
: {};
|
|
611
|
+
return { ...base, permissions: mergedPermissions, ...sandboxBlock };
|
|
502
612
|
};
|
|
503
613
|
|
|
504
614
|
const formatEntryList = (entries) => (entries.length ? entries.map((entry) => ` - ${entry}`) : [' - (none)']);
|
|
@@ -524,6 +634,25 @@ const formatKitTier = (result) =>
|
|
|
524
634
|
]
|
|
525
635
|
: [];
|
|
526
636
|
|
|
637
|
+
// The bridge tier's honest posture, printed on EVERY --bridge-tier run (test-pinned — the codex-R2
|
|
638
|
+
// informed-consent resolution): the exfiltration surface is stated, never pretended away.
|
|
639
|
+
export const KIT_BRIDGE_TIER_NOTICE =
|
|
640
|
+
'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): the harness runs an excluded command OUTSIDE the sandbox (the wrappers need network), so a plain allowlisted invocation triggers no sandbox-bypass approval. 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).';
|
|
641
|
+
|
|
642
|
+
const formatBridgeTier = (result) =>
|
|
643
|
+
result.bridgeTier
|
|
644
|
+
? [
|
|
645
|
+
`${result.wrote ? 'added' : 'would add'} bridge-wrappers tier allow entries: ${result.bridgeToAdd.length}`,
|
|
646
|
+
...formatEntryList(result.bridgeToAdd),
|
|
647
|
+
`already present (bridge tier): ${result.bridgeAlreadyPresent.length}`,
|
|
648
|
+
`${result.wrote ? 'added' : 'would add'} sandbox.excludedCommands entries: ${result.excludedToAdd.length}`,
|
|
649
|
+
...formatEntryList(result.excludedToAdd),
|
|
650
|
+
`already present (excludedCommands): ${result.excludedAlreadyPresent.length}`,
|
|
651
|
+
...result.bridgeSkips.map((s) => ` skipped: ${s.reason}`),
|
|
652
|
+
KIT_BRIDGE_TIER_NOTICE,
|
|
653
|
+
]
|
|
654
|
+
: [];
|
|
655
|
+
|
|
527
656
|
const formatGateAdvisory = (gateCandidates) => [
|
|
528
657
|
'gate advisory: candidates you may add BY HAND to .claude/settings.json or settings.local.json; this tool will NOT add them',
|
|
529
658
|
...(gateCandidates.length
|
|
@@ -549,6 +678,7 @@ const formatVelocityProfileResult = (result) =>
|
|
|
549
678
|
result.dryRun ? 'agent-workflow velocity profile - DRY RUN (no changes)' : 'agent-workflow velocity profile - APPLY',
|
|
550
679
|
...formatAllowlist(result),
|
|
551
680
|
...formatKitTier(result),
|
|
681
|
+
...formatBridgeTier(result),
|
|
552
682
|
formatDefaultMode(result),
|
|
553
683
|
...formatGateAdvisory(result.gateCandidates),
|
|
554
684
|
...formatPreExistingAdvisory(result.preExistingNonReadonly),
|
|
@@ -572,7 +702,15 @@ export const screenAllowlistEntry = (pattern) => {
|
|
|
572
702
|
if (tokens[0] === NPM_COMMAND) return NPM_READONLY_SUBCOMMANDS.includes(getSubcommand(tokens));
|
|
573
703
|
// Kit-tool wildcard class: `node <abs kit tool>` + args wildcard. run-gates is deliberately NOT
|
|
574
704
|
// here (Decision 3: only its exact root-pinned form below — a wildcard would cover `--cwd <any>`).
|
|
575
|
-
|
|
705
|
+
// The quoted-grounding form is the bridge tier's ONE quoted acceptance (never a general quote
|
|
706
|
+
// allowance): `node "<abs>/tools/grounding.mjs"` exactly.
|
|
707
|
+
if (tokens[0] === KIT_TOOL_INVOKER) {
|
|
708
|
+
return tokens.length === 2 && (isKitToolPathToken(tokens[1], KIT_WILDCARD_TOOLS) || isQuotedGroundingToken(tokens[1]));
|
|
709
|
+
}
|
|
710
|
+
// Bridge-review-wrapper class (AD-044 Plan 4): EXACTLY `<frozen-tier wrapper> code` + the args
|
|
711
|
+
// wildcard — never codex-exec/agy-run, never the bare or plan/diff spellings (those file-argument
|
|
712
|
+
// modes can read outside the repo; codex R2).
|
|
713
|
+
if (BRIDGE_REVIEW_WRAPPERS.includes(tokens[0])) return tokens.length === 2 && tokens[1] === BRIDGE_REVIEW_MODE;
|
|
576
714
|
return isSingleShellToken(wildcardCmd, tokens) && SHELL_READONLY.includes(wildcardCmd);
|
|
577
715
|
}
|
|
578
716
|
const exactCmd = getBashExactCommand(pattern);
|
|
@@ -591,7 +729,7 @@ export const screenAllowlistEntry = (pattern) => {
|
|
|
591
729
|
};
|
|
592
730
|
|
|
593
731
|
/**
|
|
594
|
-
* Derive the opt-in kit-tools tier for a project:
|
|
732
|
+
* Derive the opt-in kit-tools tier for a project: 9 wildcard entries (resolved-absolute script
|
|
595
733
|
* path + args wildcard), ONE exact run-gates entry pinned to the resolved project root, and the
|
|
596
734
|
* writer-preview exact dry-run byte-strings. Pure derivation — a stale path simply prompts again.
|
|
597
735
|
*/
|
|
@@ -721,12 +859,22 @@ export const preflightVelocityProfile = ({ cwd }, deps = {}) => {
|
|
|
721
859
|
return [];
|
|
722
860
|
}
|
|
723
861
|
})();
|
|
862
|
+
// Same defensive posture for the bridge tier: a probe failure falls back to flagging every
|
|
863
|
+
// bridge-shaped entry — over-flagging is the safe direction for a read-only advisory.
|
|
864
|
+
const derivedBridge = (() => {
|
|
865
|
+
try {
|
|
866
|
+
return deriveBridgeTierAllowlist({ findWrapper: deps.findWrapper }).allow;
|
|
867
|
+
} catch {
|
|
868
|
+
return [];
|
|
869
|
+
}
|
|
870
|
+
})();
|
|
724
871
|
const preExistingNonReadonly = collectPreExistingNonReadonly(
|
|
725
872
|
[
|
|
726
873
|
{ source: SETTINGS_FILE, data: projectSettings.data },
|
|
727
874
|
{ source: SETTINGS_LOCAL_FILE, data: localSettings.data },
|
|
728
875
|
],
|
|
729
876
|
derivedTier,
|
|
877
|
+
derivedBridge,
|
|
730
878
|
);
|
|
731
879
|
const gateCandidates = discoverGateCandidates(readPackageJson(projectDir, deps));
|
|
732
880
|
|
|
@@ -744,20 +892,40 @@ export const preflightVelocityProfile = ({ cwd }, deps = {}) => {
|
|
|
744
892
|
};
|
|
745
893
|
};
|
|
746
894
|
|
|
747
|
-
export const planVelocityProfile = (preflight, { acceptEdits, kitTools } = {}) => {
|
|
895
|
+
export const planVelocityProfile = (preflight, { acceptEdits, kitTools, bridgeTier, findWrapper } = {}) => {
|
|
748
896
|
const projectAllow = getAllowEntries(preflight.projectSettings?.data);
|
|
749
897
|
const toAdd = UNIVERSAL_READONLY_ALLOWLIST.filter((entry) => !projectAllow.includes(entry));
|
|
750
898
|
const alreadyPresent = UNIVERSAL_READONLY_ALLOWLIST.filter((entry) => projectAllow.includes(entry));
|
|
751
899
|
const tier = kitTools === true ? deriveKitToolsAllowlist({ projectDir: preflight.cwd }) : [];
|
|
752
900
|
const tierToAdd = tier.filter((entry) => !projectAllow.includes(entry));
|
|
753
901
|
const tierAlreadyPresent = tier.filter((entry) => projectAllow.includes(entry));
|
|
754
|
-
|
|
902
|
+
const bridge = bridgeTier === true ? deriveBridgeTierAllowlist({ findWrapper }) : { allow: [], excludedCommands: [], skips: [], placed: [] };
|
|
903
|
+
const bridgeToAdd = bridge.allow.filter((entry) => !projectAllow.includes(entry));
|
|
904
|
+
const bridgeAlreadyPresent = bridge.allow.filter((entry) => projectAllow.includes(entry));
|
|
905
|
+
const existingSandbox = isJsonObject(preflight.projectSettings?.data?.sandbox) ? preflight.projectSettings.data.sandbox : {};
|
|
906
|
+
const existingExcluded = Array.isArray(existingSandbox.excludedCommands) ? existingSandbox.excludedCommands : [];
|
|
907
|
+
const excludedToAdd = bridge.excludedCommands.filter((cmd) => !existingExcluded.includes(cmd));
|
|
908
|
+
const excludedAlreadyPresent = bridge.excludedCommands.filter((cmd) => existingExcluded.includes(cmd));
|
|
909
|
+
return {
|
|
910
|
+
toAdd,
|
|
911
|
+
alreadyPresent,
|
|
912
|
+
tierToAdd,
|
|
913
|
+
tierAlreadyPresent,
|
|
914
|
+
kitTools: kitTools === true,
|
|
915
|
+
bridgeTier: bridgeTier === true,
|
|
916
|
+
bridgeToAdd,
|
|
917
|
+
bridgeAlreadyPresent,
|
|
918
|
+
bridgeSkips: bridge.skips,
|
|
919
|
+
excludedToAdd,
|
|
920
|
+
excludedAlreadyPresent,
|
|
921
|
+
setsDefaultMode: acceptEdits === true,
|
|
922
|
+
};
|
|
755
923
|
};
|
|
756
924
|
|
|
757
|
-
export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true, kitTools = false } = {}, deps = {}) => {
|
|
925
|
+
export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true, kitTools = false, bridgeTier = false } = {}, deps = {}) => {
|
|
758
926
|
const projectDir = cwd ?? deps.cwd ?? process.cwd();
|
|
759
927
|
const preflight = preflightVelocityProfile({ cwd: projectDir }, deps);
|
|
760
|
-
const plan = planVelocityProfile(preflight, { acceptEdits, kitTools });
|
|
928
|
+
const plan = planVelocityProfile(preflight, { acceptEdits, kitTools, bridgeTier, findWrapper: deps.findWrapper });
|
|
761
929
|
// Drift guard runs on BOTH dry-run and apply (so a dry-run faithfully predicts the apply) and
|
|
762
930
|
// validates the FULL audited core, not just the to-add delta — a drifted core entry is caught even
|
|
763
931
|
// when it is already present in the user's allow list (and toAdd is a subset of the core anyway).
|
|
@@ -768,6 +936,13 @@ export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true,
|
|
|
768
936
|
const tier = deriveKitToolsAllowlist({ projectDir });
|
|
769
937
|
validateProfile(tier, [...UNIVERSAL_READONLY_ALLOWLIST, ...tier]);
|
|
770
938
|
}
|
|
939
|
+
// A --bridge-tier run validates the DERIVED bridge entries the same way — the third audit point
|
|
940
|
+
// (Decision 2): every seeded bridge entry passes the screen AND sits in the audited set, so the
|
|
941
|
+
// flagless advisory can never flag an entry the tier itself seeded.
|
|
942
|
+
if (bridgeTier === true) {
|
|
943
|
+
const bridge = deriveBridgeTierAllowlist({ findWrapper: deps.findWrapper });
|
|
944
|
+
validateProfile(bridge.allow, [...UNIVERSAL_READONLY_ALLOWLIST, ...bridge.allow]);
|
|
945
|
+
}
|
|
771
946
|
const resultBase = { ...preflight, ...plan };
|
|
772
947
|
if (dryRun) return { wrote: false, dryRun: true, ...resultBase };
|
|
773
948
|
|
|
@@ -781,7 +956,12 @@ export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true,
|
|
|
781
956
|
const fs = fsDeps(deps);
|
|
782
957
|
const settingsPath = join(projectDir, SETTINGS_FILE);
|
|
783
958
|
if (preflight.claudeDirAbsent) fs.mkdir(join(projectDir, CLAUDE_DIR), { recursive: true });
|
|
784
|
-
const merged = mergeProjectSettings(
|
|
959
|
+
const merged = mergeProjectSettings(
|
|
960
|
+
preflight.projectSettings.data,
|
|
961
|
+
[...plan.toAdd, ...plan.tierToAdd, ...plan.bridgeToAdd],
|
|
962
|
+
acceptEdits,
|
|
963
|
+
plan.excludedToAdd,
|
|
964
|
+
);
|
|
785
965
|
fs.writeFile(settingsPath, formatJson(merged, preflight.projectSettings.eol ?? LF), UTF8);
|
|
786
966
|
return { wrote: true, dryRun: false, settingsPath, ...resultBase };
|
|
787
967
|
};
|
|
@@ -1002,8 +1182,18 @@ const collectRedlineBypass = (sources) =>
|
|
|
1002
1182
|
// escape the sandbox entirely. The render owns only enabled/autoAllow and preserves other sandbox
|
|
1003
1183
|
// sub-keys (merge-don't-clobber, never a silent clobber of the user's sandbox tuning), so a pre-existing
|
|
1004
1184
|
// weakening sub-key is REPORTED loudly (remove it by hand) — never silently carried as security.
|
|
1005
|
-
const collectSandboxWeakenings = (sources) =>
|
|
1006
|
-
|
|
1185
|
+
const collectSandboxWeakenings = (sources) => {
|
|
1186
|
+
// Tier-known PROOF (codex R4): an excludedCommands entry is downgraded to a note ONLY when it is
|
|
1187
|
+
// demonstrably the consented tier's own output — it lives in the PROJECT settings.json (the file
|
|
1188
|
+
// the tier writes; a local-file exclusion is never tier output) AND the matching derived
|
|
1189
|
+
// code-mode allow rule is present there. A bare name match alone proves nothing.
|
|
1190
|
+
const project = sources.find((s) => s.source === SETTINGS_FILE);
|
|
1191
|
+
const projectAllow = getAllowEntries(project?.data);
|
|
1192
|
+
const isTierKnownExclusion = (source, cmd) =>
|
|
1193
|
+
source === SETTINGS_FILE &&
|
|
1194
|
+
BRIDGE_REVIEW_WRAPPERS.includes(cmd) &&
|
|
1195
|
+
projectAllow.includes(`Bash(${cmd} ${BRIDGE_REVIEW_MODE}:*)`);
|
|
1196
|
+
return sources.flatMap(({ source, data }) => {
|
|
1007
1197
|
const sb = isJsonObject(data?.[SANDBOX_KEY]) ? data[SANDBOX_KEY] : {};
|
|
1008
1198
|
const out = [];
|
|
1009
1199
|
const net = isJsonObject(sb.network) ? sb.network : {};
|
|
@@ -1018,10 +1208,22 @@ const collectSandboxWeakenings = (sources) =>
|
|
|
1018
1208
|
out.push({ source, key: `${SANDBOX_KEY}.allowUnsandboxedCommands`, weakens: 'every sandbox red-line', detail: 'commands may run unsandboxed' });
|
|
1019
1209
|
}
|
|
1020
1210
|
if (Array.isArray(sb.excludedCommands) && sb.excludedCommands.length) {
|
|
1021
|
-
|
|
1211
|
+
// The bridge tier's OWN wrapper names are tier-known ONLY with the proof above (the
|
|
1212
|
+
// --bridge-tier consent covers exactly this posture — the wrappers need network and
|
|
1213
|
+
// genuinely run unsandboxed); then they are an informational note, never a weakening flag
|
|
1214
|
+
// (the Decision-2 self-consistency bar). Every OTHER excluded command — hand-added names,
|
|
1215
|
+
// local-file exclusions, tier names without their allow rules — stays a loud weakening.
|
|
1216
|
+
const tierKnown = sb.excludedCommands.filter((c) => isTierKnownExclusion(source, c));
|
|
1217
|
+
const foreign = sb.excludedCommands.filter((c) => !tierKnown.includes(c));
|
|
1218
|
+
if (foreign.length) {
|
|
1219
|
+
out.push({ source, key: `${SANDBOX_KEY}.excludedCommands`, weakens: 'every sandbox red-line', detail: `${foreign.length} command(s) run UNSANDBOXED (network/fs confinement not applied to them)${tierKnown.length ? `; ${tierKnown.length} bridge-review wrapper exclusion(s) are tier-known and not flagged` : ''}` });
|
|
1220
|
+
} else if (tierKnown.length) {
|
|
1221
|
+
out.push({ source, key: `${SANDBOX_KEY}.excludedCommands`, weakens: null, tierKnown: true, detail: `${tierKnown.length} bridge-review wrapper exclusion(s) (${tierKnown.join(', ')}) — tier-known: the consented bridge-wrappers tier runs them outside the sandbox (network), and its allow rules are present in the project settings` });
|
|
1222
|
+
}
|
|
1022
1223
|
}
|
|
1023
1224
|
return out;
|
|
1024
1225
|
});
|
|
1226
|
+
};
|
|
1025
1227
|
|
|
1026
1228
|
// collectLocalMasks(localData, render) → the render-owned keys a settings.local.json value MASKS
|
|
1027
1229
|
// (local > project, so a differing local value defeats the rendered one silently). The render owns
|
|
@@ -1071,7 +1273,8 @@ export const formatAutonomyResult = (r) => {
|
|
|
1071
1273
|
lines.push(` ⚠ DEGRADE: ${b.source} has a pre-existing allow entry ${b.entry} that would BYPASS the rendered red-line(s) ${b.redlines.join('/')} (a matching allow rule AUTO-APPROVES the command, defeating ask/deny) — remove it by hand; this render never touches permissions.allow.`);
|
|
1072
1274
|
}
|
|
1073
1275
|
for (const w of r.sandboxWeakenings ?? []) {
|
|
1074
|
-
lines.push(`
|
|
1276
|
+
if (w.tierKnown) lines.push(` note: ${w.source} has ${w.key} (${w.detail}).`);
|
|
1277
|
+
else lines.push(` ⚠ DEGRADE: ${w.source} has ${w.key} (${w.detail}), which WEAKENS the rendered ${w.weakens} red-line — the render preserves your sandbox tuning (never clobbers it), so remove it by hand if you want the red-line fully enforced.`);
|
|
1075
1278
|
}
|
|
1076
1279
|
lines.push(AUTONOMY_RESIDUAL_NOTICE);
|
|
1077
1280
|
if (!r.wrote) lines.push(`re-run with ${FLAG_APPLY} to write .claude/settings.json (only the render-owned blocks change).`);
|
|
@@ -1204,6 +1407,7 @@ export const parseArgs = (argv) => {
|
|
|
1204
1407
|
if (arg === FLAG_APPLY) return { ...state, apply: true };
|
|
1205
1408
|
if (arg === FLAG_ACCEPT_EDITS) return { ...state, acceptEdits: true };
|
|
1206
1409
|
if (arg === FLAG_KIT_TOOLS) return { ...state, kitTools: true };
|
|
1410
|
+
if (arg === FLAG_BRIDGE_TIER) return { ...state, bridgeTier: true };
|
|
1207
1411
|
if (arg === FLAG_AUTONOMY) return { ...state, autonomy: true };
|
|
1208
1412
|
if (arg === FLAG_CHECK) return { ...state, check: true };
|
|
1209
1413
|
if (arg === FLAG_CWD) {
|
|
@@ -1214,7 +1418,7 @@ export const parseArgs = (argv) => {
|
|
|
1214
1418
|
if (arg.startsWith('-')) throw fail(EXIT_USAGE, `unknown flag: ${arg}`);
|
|
1215
1419
|
throw fail(EXIT_USAGE, `unexpected argument: ${arg}`);
|
|
1216
1420
|
},
|
|
1217
|
-
{ help: false, dryRunFlag: false, apply: false, acceptEdits: false, kitTools: false, autonomy: false, check: false, cwd: undefined, skipNext: false },
|
|
1421
|
+
{ help: false, dryRunFlag: false, apply: false, acceptEdits: false, kitTools: false, bridgeTier: false, autonomy: false, check: false, cwd: undefined, skipNext: false },
|
|
1218
1422
|
);
|
|
1219
1423
|
|
|
1220
1424
|
if (parsed.dryRunFlag && parsed.apply) throw fail(EXIT_USAGE, `${FLAG_DRY_RUN} and ${FLAG_APPLY} cannot be used together`);
|
|
@@ -1224,6 +1428,7 @@ export const parseArgs = (argv) => {
|
|
|
1224
1428
|
// Reject the mix LOUDLY rather than silently ignoring a flag.
|
|
1225
1429
|
if (parsed.autonomy && parsed.acceptEdits) throw fail(EXIT_USAGE, `${FLAG_AUTONOMY} sets ${DEFAULT_MODE_KEY} from the policy — ${FLAG_ACCEPT_EDITS} (an allowlist-mode flag) cannot be combined with it`);
|
|
1226
1430
|
if (parsed.autonomy && parsed.kitTools) throw fail(EXIT_USAGE, `${FLAG_KIT_TOOLS} is an allowlist-mode flag — it cannot be combined with ${FLAG_AUTONOMY} (a policy-only render)`);
|
|
1431
|
+
if (parsed.autonomy && parsed.bridgeTier) throw fail(EXIT_USAGE, `${FLAG_BRIDGE_TIER} is an allowlist-mode flag — it cannot be combined with ${FLAG_AUTONOMY} (a policy-only render)`);
|
|
1227
1432
|
if (parsed.check && !parsed.autonomy) throw fail(EXIT_USAGE, `${FLAG_CHECK} is only valid with ${FLAG_AUTONOMY}`);
|
|
1228
1433
|
if (parsed.check && parsed.apply) throw fail(EXIT_USAGE, `${FLAG_CHECK} is read-only — it cannot be combined with ${FLAG_APPLY}`);
|
|
1229
1434
|
return {
|
|
@@ -1232,6 +1437,7 @@ export const parseArgs = (argv) => {
|
|
|
1232
1437
|
apply: parsed.apply,
|
|
1233
1438
|
acceptEdits: parsed.acceptEdits,
|
|
1234
1439
|
kitTools: parsed.kitTools,
|
|
1440
|
+
bridgeTier: parsed.bridgeTier,
|
|
1235
1441
|
autonomy: parsed.autonomy,
|
|
1236
1442
|
check: parsed.check,
|
|
1237
1443
|
cwd: parsed.cwd,
|
|
@@ -1261,7 +1467,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
1261
1467
|
return EXIT_OK;
|
|
1262
1468
|
}
|
|
1263
1469
|
const result = writeVelocityProfile(
|
|
1264
|
-
{ cwd, acceptEdits: args.acceptEdits, dryRun: args.dryRun, kitTools: args.kitTools },
|
|
1470
|
+
{ cwd, acceptEdits: args.acceptEdits, dryRun: args.dryRun, kitTools: args.kitTools, bridgeTier: args.bridgeTier },
|
|
1265
1471
|
deps,
|
|
1266
1472
|
);
|
|
1267
1473
|
log(formatVelocityProfileResult(result));
|