@sabaiway/agent-workflow-kit 14.26.0 → 14.27.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 +34 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +7 -7
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/recommendations.md +6 -6
- package/references/modes/velocity.md +4 -4
- package/tools/apply-danger-check.mjs +19 -14
- package/tools/bridge-sandbox-recipe.mjs +102 -15
- package/tools/bridge-state-dirs.mjs +94 -0
- package/tools/manifest/schema.md +8 -6
- package/tools/recommendations.mjs +27 -35
- package/tools/velocity-profile.mjs +174 -111
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// bridge-state-dirs.mjs — the one leaf that turns the bundled bridges' declared writable state dirs
|
|
2
|
+
// (`writableDirs`) into `sandbox.filesystem.allowWrite` entries (contract: bridges/velocity-profile).
|
|
3
|
+
// The bridge tier writes them, the advisor counts them, the danger check admits them, the autonomy
|
|
4
|
+
// preview names them: one resolution, one home-relative spelling, one merge, so the four cannot drift.
|
|
5
|
+
//
|
|
6
|
+
// A LEAF: imports declared-paths.mjs and node:path only. No side effects on import. Dependency-free,
|
|
7
|
+
// Node >= 22.
|
|
8
|
+
|
|
9
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
10
|
+
import { dirCovers, isResolvableDeclaredEntry, resolveDeclaredDir } from './declared-paths.mjs';
|
|
11
|
+
|
|
12
|
+
// D6 resolution, mirroring the wrappers' byte-semantics (`${VAR:-default}` + the exact case-arms:
|
|
13
|
+
// `~` / `~/…` / `/…` ride as-given; EVERY other form — including `~user/…`, which the wrappers
|
|
14
|
+
// never resolve as a home path — anchors like a relative path). Anchored to the TARGET PROJECT ROOT,
|
|
15
|
+
// matching what a wrapper invoked from the project root resolves.
|
|
16
|
+
export const resolveWritableDir = (entry, { env, root }) => {
|
|
17
|
+
const value = entry.env == null ? '' : (env[entry.env] ?? '');
|
|
18
|
+
if (value === '') return entry.default;
|
|
19
|
+
if (value === '~' || value.startsWith('~/') || value.startsWith('/')) return value;
|
|
20
|
+
return resolve(root, value);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// The HOME-SYMBOLIC spelling: tilde forms stay symbolic, an absolute dir under the home (by path
|
|
24
|
+
// segment, never a string prefix) folds back to `~/…`, anything outside the home stays absolute.
|
|
25
|
+
export const homeRelative = (dir, home) => {
|
|
26
|
+
if (dir === '~') return '~';
|
|
27
|
+
if (dir.startsWith('~/')) return `~/${dir.slice(2)}`;
|
|
28
|
+
const homeAbs = resolve(home);
|
|
29
|
+
const abs = resolve(dir);
|
|
30
|
+
if (abs === homeAbs) return '~';
|
|
31
|
+
return dirCovers(homeAbs, abs) ? `~/${relative(homeAbs, abs)}` : abs;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const ancestorsOf = (dir) => {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (let at = dirname(dir); at !== dir; dir = at, at = dirname(at)) out.push(at);
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// A grant on `/`, on the home, or on an ancestor of the home or of the root would hand every
|
|
41
|
+
// sandboxed command the user's whole tree — never a bridge's state dir. The override that produced
|
|
42
|
+
// it is named, so the user knows which variable to fix.
|
|
43
|
+
const dangerOf = (abs, { home, root }) => {
|
|
44
|
+
const homeAbs = resolve(home);
|
|
45
|
+
if (abs === '/') return 'the filesystem root';
|
|
46
|
+
if (abs === homeAbs) return 'the home directory';
|
|
47
|
+
if (ancestorsOf(homeAbs).includes(abs)) return 'an ancestor of the home directory';
|
|
48
|
+
if (ancestorsOf(resolve(root)).includes(abs)) return 'an ancestor of the project root';
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const stateDirsOf = (dirEntries, { env, root, home }) => {
|
|
53
|
+
const dirs = [];
|
|
54
|
+
for (const entry of dirEntries) {
|
|
55
|
+
const raw = resolveWritableDir(entry, { env, root });
|
|
56
|
+
const danger = dangerOf(resolveDeclaredDir(raw, { home, root }), { home, root });
|
|
57
|
+
if (danger !== null) {
|
|
58
|
+
const source = entry.env == null ? 'its declared default' : entry.env;
|
|
59
|
+
throw new Error(`writable state dir "${raw}" from ${source} resolves to ${danger} — refusing to grant it; point ${source} at the bridge's own state dir`);
|
|
60
|
+
}
|
|
61
|
+
const rendered = homeRelative(raw, home);
|
|
62
|
+
if (!dirs.includes(rendered)) dirs.push(rendered);
|
|
63
|
+
}
|
|
64
|
+
return dirs;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// The dirs no existing allowWrite entry covers (resolved both sides, containment by path segment).
|
|
68
|
+
export const missingStateDirs = (dirs, allowWrite, { home, root }) => {
|
|
69
|
+
const existing = (Array.isArray(allowWrite) ? allowWrite : [])
|
|
70
|
+
.filter(isResolvableDeclaredEntry)
|
|
71
|
+
.map((entry) => resolveDeclaredDir(entry, { home, root }));
|
|
72
|
+
return dirs.filter((dir) => {
|
|
73
|
+
const abs = resolveDeclaredDir(dir, { home, root });
|
|
74
|
+
return !existing.some((container) => dirCovers(container, abs));
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// The ONE writer of `sandbox.filesystem.allowWrite`: the other sandbox and filesystem sub-keys stay
|
|
79
|
+
// where they are, each dir not already present appends once. Pure: returns a new object, and with
|
|
80
|
+
// nothing to add returns the input unchanged (no sandbox key is created for nothing).
|
|
81
|
+
export const mergeAllowWrite = (settings, dirs) => {
|
|
82
|
+
const base = settings ?? {};
|
|
83
|
+
const existing = Array.isArray(base.sandbox?.filesystem?.allowWrite) ? base.sandbox.filesystem.allowWrite : [];
|
|
84
|
+
const toAppend = [];
|
|
85
|
+
for (const dir of dirs) if (!existing.includes(dir) && !toAppend.includes(dir)) toAppend.push(dir);
|
|
86
|
+
if (toAppend.length === 0) return base;
|
|
87
|
+
return {
|
|
88
|
+
...base,
|
|
89
|
+
sandbox: {
|
|
90
|
+
...base.sandbox,
|
|
91
|
+
filesystem: { ...base.sandbox?.filesystem, allowWrite: [...existing, ...toAppend] },
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
};
|
package/tools/manifest/schema.md
CHANGED
|
@@ -100,15 +100,16 @@ drift-guarded set-equal to this block by the bridge `bin/*.test.mjs` suites (hel
|
|
|
100
100
|
`networkHosts` declares the hosts the backend CLI is **observed** to contact (synthetic examples:
|
|
101
101
|
`*.api.backend.example`, `accounts.backend.example`; the real observed lists live in each bridge's
|
|
102
102
|
`capability.json`) — the **single source** of the hosts `velocity-profile --bridge-tier` seeds
|
|
103
|
-
into the project's `sandbox.network.allowedDomains` for every
|
|
103
|
+
into the project's `sandbox.network.allowedDomains` for every used bridge, and of a hand-applied
|
|
104
104
|
session sandbox allowlist. Rules:
|
|
105
105
|
|
|
106
106
|
- Each entry is a bare dotted hostname or a `*.family` wildcard — never a scheme/path/port.
|
|
107
107
|
Entries must be unique. A malformed list **fails** `--strict` (the entries are pasted verbatim
|
|
108
108
|
into allowlist lines by the Recommendations advisor).
|
|
109
|
-
- **The `--bridge-tier` seed is the primary lane** (AD-172):
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
- **The `--bridge-tier` seed is the primary lane** (AD-172): for every used bridge it merges
|
|
110
|
+
hosts into the project's `sandbox.network.allowedDomains` beside the wrapper's `<wrapper> *`
|
|
111
|
+
exclusion (`codex-exec *` for the execution wrapper). A settings-native harness that drops the
|
|
112
|
+
exclusion still admits them; that pre-allow widens egress
|
|
112
113
|
for **every** sandboxed command, which the tier's consent states. The **hand-apply** fallback
|
|
113
114
|
stays for a harness-managed sandbox that ignores the settings keys.
|
|
114
115
|
- Observed-minimal, honestly incomplete: a blocked host names itself at run time — extend the
|
|
@@ -132,8 +133,9 @@ declared path is only the **default under no override**. Rules:
|
|
|
132
133
|
relative path. The advisor anchors those to the **target project root** (its pinned `--cwd`),
|
|
133
134
|
matching what a wrapper invoked from the project root resolves — the documented dispatch form
|
|
134
135
|
(the wrapper itself anchors to its invocation `$PWD`); else the `default` applies.
|
|
135
|
-
-
|
|
136
|
-
`sandbox.filesystem.allowWrite`
|
|
136
|
+
- **The `--bridge-tier` seeds every used bridge's state dirs**, home-relative under the home
|
|
137
|
+
(absolute outside it), into `sandbox.filesystem.allowWrite` beside the wrapper's `<wrapper> *`
|
|
138
|
+
exclusion (`codex-exec *` for the execution wrapper) — the entries feed the
|
|
137
139
|
sandbox-lane discoverability item (session/host sandbox config is hand-applied territory).
|
|
138
140
|
|
|
139
141
|
## Mode catalog (BRIDGE-MODES-CATALOG)
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
// below) and VERDICT-FIRST (D1): every non-optimal state opens with ONE composed verdict line.
|
|
20
20
|
// Registry strings are frozen tool DATA, fact-true, one line under the shape cap (D2); posture/
|
|
21
21
|
// risk prose lives in the mode doc at the consent moment (D3). A probe failure is a stated
|
|
22
|
-
// skipped-item line — never a crash, never a fabricated item.
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
// skipped-item line — never a crash, never a fabricated item. The consented
|
|
23
|
+
// `velocity-profile --bridge-tier` seeds used bridges' allow rules, <wrapper> * exclusions
|
|
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
|
|
25
26
|
// fingerprint-bound acknowledgement recorded by the consent-gated ack writer into the family-owned
|
|
26
27
|
// docs/ai/acks.json (AD-055 relocated it off the host settings schema), never a security key (D4).
|
|
27
28
|
//
|
|
@@ -80,6 +81,7 @@ import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './decl
|
|
|
80
81
|
import { resolveGitHooksPath } from './commit-guard.mjs';
|
|
81
82
|
import { loadConfig } from './orchestration-config.mjs';
|
|
82
83
|
import { bundledSandboxRecipe } from './bridge-sandbox-recipe.mjs';
|
|
84
|
+
import { homeRelative, resolveWritableDir } from './bridge-state-dirs.mjs';
|
|
83
85
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
84
86
|
import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
|
|
85
87
|
import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
|
|
@@ -248,7 +250,7 @@ export const composeVerdict = ({ attention, optional, skipped }) => {
|
|
|
248
250
|
export const WHATS = Object.freeze({
|
|
249
251
|
'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
|
|
250
252
|
'kit-tools-tier': "the kit's own read-only tools still prompt — {n} kit-tools tier entr(ies) not seeded",
|
|
251
|
-
'bridge-tier': 'bridge-wrappers tier incomplete — {n} entr(ies) missing (allow rules,
|
|
253
|
+
'bridge-tier': 'bridge-wrappers tier incomplete — {n} entr(ies) missing (allow rules, exclusions, hosts or state dirs of used bridges)',
|
|
252
254
|
'autonomy-policy': 'no {path} — the computed defaults apply implicitly (red-lines ask/deny; every activity floors at prompt)',
|
|
253
255
|
'autonomy-render': 'the declared autonomy policy is not rendered into .claude/settings.json — drift: {drift}',
|
|
254
256
|
'sandbox-provision': 'the OS sandbox is unavailable: {reason}',
|
|
@@ -324,7 +326,7 @@ export const BENEFITS = Object.freeze({
|
|
|
324
326
|
'velocity-core': 'velocity — routine read-only commands stop prompting while the maintainer is away',
|
|
325
327
|
'kit-tools-tier': "velocity — the kit's own read-only tools run promptless (audited, resolved-absolute tier)",
|
|
326
328
|
'bridge-tier':
|
|
327
|
-
'velocity —
|
|
329
|
+
'velocity — used bridges get <wrapper> * exclusions (codex-exec * if delegated), hosts, state dirs (allowWrite); reviews: code-mode allows',
|
|
328
330
|
'autonomy-policy': 'clarity — the per-activity autonomy policy becomes an explicit, versioned declaration instead of implicit computed defaults',
|
|
329
331
|
'autonomy-render': `velocity — confined commands auto-allow per your declared policy; ${DUAL_SECURITY_BENEFIT}`,
|
|
330
332
|
'sandbox-provision': `velocity — confined ad-hoc commands stop prompting; ${DUAL_SECURITY_BENEFIT}`,
|
|
@@ -409,9 +411,9 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
409
411
|
// state that makes a configured subagent carrier run solo.
|
|
410
412
|
{ id: 'executor-vehicle', mode: 'agents', advisorKey: 'executor-vehicle' },
|
|
411
413
|
// Exempt, not un-audited. `acceptEdits` auto-applies Edit/Write and auto-runs mkdir/touch/mv/cp:
|
|
412
|
-
// a TRUST-POSTURE change.
|
|
413
|
-
//
|
|
414
|
-
//
|
|
414
|
+
// a TRUST-POSTURE change. Velocity presents its full posture at its own consent moment;
|
|
415
|
+
// the bridge tier separately seeds used bridges' allow rules, <wrapper> * exclusions
|
|
416
|
+
// (codex-exec * where delegated), hosts and state dirs (allowWrite).
|
|
415
417
|
{
|
|
416
418
|
id: 'accept-edits',
|
|
417
419
|
mode: 'velocity',
|
|
@@ -460,8 +462,12 @@ const probeVelocityItems = ({ root, deps, add, skip }) => {
|
|
|
460
462
|
skip('kit-tools-tier', err);
|
|
461
463
|
}
|
|
462
464
|
try {
|
|
463
|
-
const bt = planVelocityProfile(preflight, {
|
|
464
|
-
|
|
465
|
+
const bt = planVelocityProfile(preflight, {
|
|
466
|
+
bridgeTier: true, findWrapper: deps.findWrapper,
|
|
467
|
+
bundleRoot: deps.bundleRoot, readdir: deps.readdir, readFile: deps.readFile,
|
|
468
|
+
detect: deps.detect, env: deps.getenv ?? deps.env, home: deps.home,
|
|
469
|
+
});
|
|
470
|
+
const delta = bt.bridgeToAdd.length + bt.excludedToAdd.length + bt.hostsToAdd.length + bt.dirsToAdd.length;
|
|
465
471
|
if (delta > 0) {
|
|
466
472
|
add('bridge-tier', fillTemplate(WHATS['bridge-tier'], { n: delta }), applyLine(' --bridge-tier'));
|
|
467
473
|
}
|
|
@@ -1159,18 +1165,6 @@ const probeMasksItem = ({ root, deps, add, skip }) => {
|
|
|
1159
1165
|
// knob is retired in the wrappers (recognized, arms nothing) and an oversized code review is now a
|
|
1160
1166
|
// chunked feed with a per-part delivery proof, so there is nothing left to offer.
|
|
1161
1167
|
|
|
1162
|
-
// D6 resolution, mirroring the wrappers' byte-semantics (`${VAR:-default}` + the exact case-arms:
|
|
1163
|
-
// `~` / `~/…` / `/…` ride as-given; EVERY other form — including `~user/…`, which the wrappers
|
|
1164
|
-
// never resolve as a home path — anchors like a relative path). The advisor anchors to the TARGET
|
|
1165
|
-
// PROJECT ROOT (the pinned --cwd), matching what a wrapper invoked from the project root resolves
|
|
1166
|
-
// (the documented dispatch form; the wrapper itself anchors to its invocation $PWD).
|
|
1167
|
-
const resolveWritableDir = (entry, { env, root }) => {
|
|
1168
|
-
const value = entry.env == null ? '' : (env[entry.env] ?? '');
|
|
1169
|
-
if (value === '') return entry.default;
|
|
1170
|
-
if (value === '~' || value.startsWith('~/') || value.startsWith('/')) return value;
|
|
1171
|
-
return resolve(root, value);
|
|
1172
|
-
};
|
|
1173
|
-
|
|
1174
1168
|
// The NEUTRAL recipe fingerprint (D4): a hash over the resolved hosts ∪ dirs data — an
|
|
1175
1169
|
// acknowledgement token, never a security key. Canonical form is HOME-SYMBOLIC: an
|
|
1176
1170
|
// absolute dir under the resolved home canonicalizes BACK to its `~/…` form and tilde forms stay
|
|
@@ -1179,14 +1173,7 @@ const resolveWritableDir = (entry, { env, root }) => {
|
|
|
1179
1173
|
// churns between them); only a genuinely-outside-home absolute override stays absolute
|
|
1180
1174
|
// (machine-specific by nature). Any change to the recipe re-fires the item.
|
|
1181
1175
|
export const recipeFingerprint = ({ hosts, dirs, home }) => {
|
|
1182
|
-
const
|
|
1183
|
-
const norm = (d) => {
|
|
1184
|
-
if (d === '~') return '~';
|
|
1185
|
-
if (d.startsWith('~/')) return `~/${d.slice(2)}`;
|
|
1186
|
-
const abs = resolve(d);
|
|
1187
|
-
if (abs === homeAbs) return '~';
|
|
1188
|
-
return abs.startsWith(`${homeAbs}/`) ? `~/${abs.slice(homeAbs.length + 1)}` : abs;
|
|
1189
|
-
};
|
|
1176
|
+
const norm = (dir) => homeRelative(dir, home);
|
|
1190
1177
|
return factFingerprint(JSON.stringify({ hosts: [...hosts].sort(), dirs: [...new Set(dirs.map(norm))].sort() }));
|
|
1191
1178
|
};
|
|
1192
1179
|
|
|
@@ -1346,14 +1333,14 @@ const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
|
1346
1333
|
// Wired = the two-surface proof the tier wrote before AD-172 (the wrapper in the PROJECT
|
|
1347
1334
|
// excludedCommands + its code-mode allow rule, either scope) — surfacing the recipe before those
|
|
1348
1335
|
// exist would front-run the bridge-tier item (codex terminal). The hosts surface (AD-172) is
|
|
1349
|
-
// deliberately not required:
|
|
1350
|
-
// render beside a bridge-tier item offering the
|
|
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.
|
|
1351
1338
|
const allowRules = [
|
|
1352
1339
|
...(Array.isArray(settings.data?.permissions?.allow) ? settings.data.permissions.allow : []),
|
|
1353
1340
|
...(Array.isArray(localSettings.data?.permissions?.allow) ? localSettings.data.permissions.allow : []),
|
|
1354
1341
|
];
|
|
1355
1342
|
const wired = BRIDGE_REVIEW_WRAPPERS.filter(
|
|
1356
|
-
(w) => excluded.includes(w) && probePlaced(w) && allowRules.includes(`Bash(${w} ${BRIDGE_REVIEW_MODE}:*)`),
|
|
1343
|
+
(w) => (excluded.includes(w) || excluded.includes(`${w} *`)) && probePlaced(w) && allowRules.includes(`Bash(${w} ${BRIDGE_REVIEW_MODE}:*)`),
|
|
1357
1344
|
);
|
|
1358
1345
|
if (wired.length === 0) return; // the tier's rules and exclusions are not wired — the bridge-tier item covers first
|
|
1359
1346
|
const { hosts, dirEntries } = bundledSandboxRecipe(wired, deps);
|
|
@@ -1767,8 +1754,13 @@ section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a pro
|
|
|
1767
1754
|
stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
|
|
1768
1755
|
the doctor item pins via a cd prefix; the ONE exception is the set-autonomy item — a
|
|
1769
1756
|
conversational skill invocation labeled "run IN the target project") and preserve each writer's
|
|
1770
|
-
own consent semantics
|
|
1771
|
-
|
|
1757
|
+
own consent semantics. The consented velocity-profile --bridge-tier seeds used bridges' code-mode
|
|
1758
|
+
allow rules, <wrapper> * exclusions (codex-exec * where an execute slot delegates), hosts in
|
|
1759
|
+
sandbox.network.allowedDomains and state dirs in sandbox.filesystem.allowWrite. Where the host
|
|
1760
|
+
honors the settings sandbox keys, an excluded wrapper runs outside the sandbox, every sandboxed
|
|
1761
|
+
command of the project can reach each host, and every sandboxed command can write each state dir;
|
|
1762
|
+
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
|
|
1772
1764
|
the consent-gated ack writer into docs/ai/acks.json (never a security key).
|
|
1773
1765
|
|
|
1774
1766
|
Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
|