@sema-agent/core 5.38.0 → 5.39.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 +84 -0
- package/dist/agents/send-message-tool.d.ts +8 -0
- package/dist/agents/send-message-tool.js +8 -0
- package/dist/agents/teacher.js +9 -3
- package/dist/agents/verify.js +9 -3
- package/dist/core/hooks.d.ts +23 -0
- package/dist/core/hooks.js +45 -4
- package/dist/core/mailbox-store.d.ts +39 -0
- package/dist/core/mailbox-store.js +9 -0
- package/dist/core/permission-rule-consent.d.ts +27 -4
- package/dist/core/permission-rule-consent.js +29 -4
- package/dist/core/permission-rule-model.d.ts +7 -1
- package/dist/core/runner/prepare-task.js +11 -3
- package/dist/core/sensitive-path-policy.js +3 -3
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +29 -1
- package/dist/core/store-contracts/mailbox-store-contract.js +78 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/write-protect.d.ts +93 -0
- package/dist/core/write-protect.js +194 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/orchestration/governance-baseline-validity.d.ts +44 -0
- package/dist/orchestration/governance-baseline-validity.js +55 -0
- package/dist/orchestration/run-workflow-tool.js +33 -8
- package/dist/orchestration/workflow-script-runner.js +9 -4
- package/dist/tools/fs/read-deny.d.ts +15 -5
- package/dist/tools/fs/read-deny.js +33 -12
- package/dist/tools/fs/safety.d.ts +4 -1
- package/dist/tools/fs/safety.js +4 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +13 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
|
+
import { MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../mailbox-store.js";
|
|
2
3
|
import { beginContract } from "./contract-harness.js";
|
|
3
4
|
export const MAILBOX_CONTRACT_SCOPE = "default";
|
|
4
5
|
const msg = (content, sentAt = 1000) => ({ content, sentAt });
|
|
@@ -191,3 +192,80 @@ export async function mailboxBundledOnlyContract(mk, runAssertion) {
|
|
|
191
192
|
});
|
|
192
193
|
await settle();
|
|
193
194
|
}
|
|
195
|
+
export async function mailboxTombstonedRecipientContract(mk, hooks) {
|
|
196
|
+
const { run: runRaw, settle } = beginContract(hooks.runAssertion);
|
|
197
|
+
const run = (name, fn) => runRaw(name, () => withStores(mk, fn));
|
|
198
|
+
const S = MAILBOX_CONTRACT_SCOPE;
|
|
199
|
+
const codeOf = (e) => e?.code;
|
|
200
|
+
run("预删除态的收件人:append 响亮拒且带码(既不静默收下级联即将删掉的消息,也不裸抛)", async (make) => {
|
|
201
|
+
const s = make();
|
|
202
|
+
await hooks.tombstone(s, S, "a1");
|
|
203
|
+
let thrown;
|
|
204
|
+
try {
|
|
205
|
+
await s.append(S, "a1", msg("m1"));
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
thrown = e;
|
|
209
|
+
}
|
|
210
|
+
assert.notEqual(thrown, undefined, "静默收下 = 给发件人一张级联马上撕掉的回执");
|
|
211
|
+
assert.equal(codeOf(thrown), MAILBOX_TOMBSTONED_RECIPIENT_CODE, "裸抛与拒同形:消费方要能按码分诊");
|
|
212
|
+
let again;
|
|
213
|
+
try {
|
|
214
|
+
await s.append(S, "a1", msg("m2"));
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
again = e;
|
|
218
|
+
}
|
|
219
|
+
assert.equal(codeOf(again), MAILBOX_TOMBSTONED_RECIPIENT_CODE);
|
|
220
|
+
});
|
|
221
|
+
run("拒的粒度是收件人,不是开关:同 scope 下另一个活收件人照常收信", async (make) => {
|
|
222
|
+
const s = make();
|
|
223
|
+
await hooks.tombstone(s, S, "a1");
|
|
224
|
+
assert.equal(await s.append(S, "a2", msg("still-live")), 1, "活收件人不受邻居的删除级联牵连");
|
|
225
|
+
assert.equal(await s.peekCount(S, "a2"), 1);
|
|
226
|
+
});
|
|
227
|
+
run("收件人身份是 (scope, handle) 整体:A 租户的删除级联不得拒掉 B 租户的同名收件人", async (make) => {
|
|
228
|
+
const s = make();
|
|
229
|
+
await hooks.tombstone(s, "tenant-a", "a1");
|
|
230
|
+
assert.equal(await s.append("tenant-b", "a1", msg("other-tenant")), 1, "同名 handle 在另一个租户下是另一个收件人");
|
|
231
|
+
assert.equal(await s.peekCount("tenant-b", "a1"), 1);
|
|
232
|
+
});
|
|
233
|
+
run("两件套还得是单射的:把 (scope, handle) 拼成一个字符串记态,不得让两个不同收件人撞成一个", async (make) => {
|
|
234
|
+
const s = make();
|
|
235
|
+
await hooks.tombstone(s, "tenant a", "b");
|
|
236
|
+
assert.equal(await s.append("tenant", "a b", msg("distinct-recipient")), 1, "拼键歧义不得把另一个收件人一起拒掉");
|
|
237
|
+
assert.equal(await s.peekCount("tenant", "a b"), 1);
|
|
238
|
+
});
|
|
239
|
+
run("副作用轴 · 活租约:被拒的 append 不得动别人手上的租约(单消费者围栏不因删除窗口松开)", async (make) => {
|
|
240
|
+
const s = make();
|
|
241
|
+
await s.append(S, "a1", msg("in-flight"));
|
|
242
|
+
const held = await s.claimLease(S, "a1", "consumer-A", 60_000, 10_000);
|
|
243
|
+
assert.equal(held?.messages.length, 1);
|
|
244
|
+
await hooks.tombstone(s, S, "a1");
|
|
245
|
+
await assert.rejects(s.append(S, "a1", msg("after-tombstone", 2_000)), "预删除态下的 append 必须拒");
|
|
246
|
+
assert.equal(await s.claimLease(S, "a1", "rival", 60_000, 11_000), null, "A 的活租约必须仍然 fence 住别人");
|
|
247
|
+
await s.ack(S, "a1", "consumer-A", held.maxSeq);
|
|
248
|
+
assert.equal(await s.peekCount(S, "a1"), 0);
|
|
249
|
+
});
|
|
250
|
+
run("拒的是入口,不是清仓:被拒的 append 零副作用,已入箱的消息按原 seq/内容原封不动", async (make) => {
|
|
251
|
+
const s = make();
|
|
252
|
+
const seq = await s.append(S, "a1", msg("parked-before"));
|
|
253
|
+
await hooks.tombstone(s, S, "a1");
|
|
254
|
+
let thrown;
|
|
255
|
+
let refused = false;
|
|
256
|
+
try {
|
|
257
|
+
await s.append(S, "a1", msg("after-tombstone", 2_000));
|
|
258
|
+
}
|
|
259
|
+
catch (e) {
|
|
260
|
+
refused = true;
|
|
261
|
+
thrown = e;
|
|
262
|
+
}
|
|
263
|
+
assert.equal(refused, true, "预删除态下的 append 必须拒");
|
|
264
|
+
assert.equal(codeOf(thrown), MAILBOX_TOMBSTONED_RECIPIENT_CODE, "箱非空时的拒同样要带码");
|
|
265
|
+
assert.equal(await s.peekCount(S, "a1"), 1, "被拒的 append 不得入箱(幽灵投递),也不得清箱");
|
|
266
|
+
const lease = await s.claimLease(S, "a1", "w1", 60_000, 10_000);
|
|
267
|
+
assert.deepEqual(lease?.messages.map((m) => m.seq), [seq]);
|
|
268
|
+
assert.deepEqual(lease?.messages.map((m) => m.content), ["parked-before"], "取到的仍是拒之前那一条");
|
|
269
|
+
});
|
|
270
|
+
await settle();
|
|
271
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4785,6 +4785,27 @@ export interface RunnerDeps {
|
|
|
4785
4785
|
* ignored). Same deployment-only seat, clamp argument and checkpoint posture as the tiers key.
|
|
4786
4786
|
*/
|
|
4787
4787
|
readDenyBuiltinExclude?: readonly string[];
|
|
4788
|
+
/**
|
|
4789
|
+
* backlog #286 (#279 — CC 2.1.233 `DANGEROUS_FILES`/`DANGEROUS_DIRECTORIES`/
|
|
4790
|
+
* `DANGEROUS_DIRECTORY_PATHS` parity): the WRITE-protection table. DEFAULT-ON: a path-confinable
|
|
4791
|
+
* write (Write/Edit/NotebookEdit) whose target lands on a table row has a surviving `allow`
|
|
4792
|
+
* demoted to `ask` at the tool gate (`decisionReason: "safety"`; a deny/ask verdict is untouched;
|
|
4793
|
+
* approval flows the ordinary ask-resolution chain — classifier, blanket `onAsk`, durable park —
|
|
4794
|
+
* with no `requiresRealApproval` mandate). Absent =
|
|
4795
|
+
* {@link import("./write-protect.js").WRITE_PROTECTED_DEFAULT_TABLE} (the CC triple verbatim +
|
|
4796
|
+
* the two argued sema rows). This key IS the whole-table escape hatch, deployment seat ONLY (no
|
|
4797
|
+
* TaskSpec twin, no governed-workflow channel): `[]` = no table (explicit and legal); a non-empty
|
|
4798
|
+
* list REPLACES the built-in table whole (compose additions as
|
|
4799
|
+
* `[...WRITE_PROTECTED_DEFAULT_TABLE, …]`; drop rows by filtering the exported table — the
|
|
4800
|
+
* visible/deletable admin face). Bad values refuse loudly at prepare (#123): garbage shapes,
|
|
4801
|
+
* glob metacharacters (the table speaks LITERAL names — glob semantics live in
|
|
4802
|
+
* `createSensitivePathPolicy`), unknown kinds, impossible kind/name combinations. Matching is
|
|
4803
|
+
* lexical over the spelled target with one case fold (ı/ſ included) — a symlink alias evades it
|
|
4804
|
+
* by construction; the canonicalizing opt-in deny policy remains the hard layer. Not frozen into
|
|
4805
|
+
* checkpoints: a resumed task follows the CURRENT deployment table (an approved parked call
|
|
4806
|
+
* bypasses the gate as always — the human already adjudicated it).
|
|
4807
|
+
*/
|
|
4808
|
+
writeProtectedPaths?: readonly import("./write-protect.js").WriteProtectedEntry[];
|
|
4788
4809
|
/**
|
|
4789
4810
|
* design/199 件A — the DEPLOYMENT's read-face declaration
|
|
4790
4811
|
* ({@link import("../tools/fs/read-face.js").ReadFace}; see {@link TaskSpec.readFace} for the
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How one table row matches the judged path:
|
|
3
|
+
* · `"basename"` — the target's last segment equals the row name (CC `DANGEROUS_FILES` form);
|
|
4
|
+
* · `"segment"` — ANY path segment equals the row name (CC `DANGEROUS_DIRECTORIES` form);
|
|
5
|
+
* · `"segment-run"` — a CONSECUTIVE run of segments equals the row's `/`-separated segments
|
|
6
|
+
* (CC `DANGEROUS_DIRECTORY_PATHS` form, e.g. `.config/git`).
|
|
7
|
+
*/
|
|
8
|
+
export type WriteProtectedKind = "basename" | "segment" | "segment-run";
|
|
9
|
+
/** One row of the write-protection table: a LITERAL name and how it matches. The `name` is also the
|
|
10
|
+
* row's stable identity — the string a refusal/ask message cites. */
|
|
11
|
+
export interface WriteProtectedRow {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly kind: WriteProtectedKind;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One deployment-authored entry for the replacement seat (`RunnerDeps.writeProtectedPaths`).
|
|
17
|
+
* String shorthand: a bare name ≡ `{ name, kind: "segment" }` (the WIDER single-segment kind —
|
|
18
|
+
* over-matching is the fail-safe direction for a tighten); a name containing `/` ≡
|
|
19
|
+
* `{ name, kind: "segment-run" }`. Spell `kind: "basename"` explicitly when last-segment-only
|
|
20
|
+
* matching is the intent.
|
|
21
|
+
*/
|
|
22
|
+
export type WriteProtectedEntry = string | WriteProtectedRow;
|
|
23
|
+
/** A table hit: which row matched (the row's canonical name + kind). */
|
|
24
|
+
export interface WriteProtectedHit {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly kind: WriteProtectedKind;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* WRITE_PROTECTED_DEFAULT_TABLE — the default-active table (the material basis of the deployment
|
|
30
|
+
* admin face: what an unconfigured deployment demotes to `ask`; visible here, deletable by
|
|
31
|
+
* replacing the seat with a filtered copy). CC 2.1.233 triple VERBATIM first, then the sema rows,
|
|
32
|
+
* each with its argument.
|
|
33
|
+
*
|
|
34
|
+
* NOT listed, deliberately (each a ruled-out candidate, recorded so the next reader does not
|
|
35
|
+
* re-litigate silently):
|
|
36
|
+
* · `.env` / `.env.*` — CC's own table excludes them too (only `.envrc`, the direnv AUTO-EXECUTION
|
|
37
|
+
* vector, is in): a plain `.env` is application config and routine workspace material for the
|
|
38
|
+
* tasks this engine runs (the read deny set rules it out on the same grounds); the opt-in write
|
|
39
|
+
* DENY (`RECOMMENDED_SENSITIVE_PATTERNS`) covers deployments that want it guarded.
|
|
40
|
+
* · key-material FILE patterns (`id_rsa*`, `*.pem`, …) — they are glob-shaped, and this table
|
|
41
|
+
* speaks literals; the opt-in deny policy owns that vocabulary.
|
|
42
|
+
* · cloud credential dirs (`.aws`, `.kube`, `.azure`, `.config/gcloud`) — writing cloud config IS
|
|
43
|
+
* the routine "configure this environment" action tasks are asked to perform, so a default ask
|
|
44
|
+
* on every such write is recurring friction without CC precedent; the opt-in deny policy covers
|
|
45
|
+
* them, and the READ side already default-refuses them (reading credentials exfiltrates; writing
|
|
46
|
+
* a fresh config file does not).
|
|
47
|
+
*/
|
|
48
|
+
export declare const WRITE_PROTECTED_DEFAULT_TABLE: readonly WriteProtectedRow[];
|
|
49
|
+
/**
|
|
50
|
+
* The ONE case fold of this module, applied to BOTH sides of every comparison (table names at
|
|
51
|
+
* compile, path segments at match) — `toLowerCase` plus the two confusable letters CC's fold maps
|
|
52
|
+
* (U+0131 dotless ı → i, U+017F long ſ → s). Deliberately NOT full Unicode confusable folding:
|
|
53
|
+
* that would be a different, wider claim than the one made (the read deny set's ASCII-contract doc
|
|
54
|
+
* states the same boundary for its own fold).
|
|
55
|
+
*/
|
|
56
|
+
export declare function foldWriteProtectCase(s: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the ACTIVE table under the deployment seat (validated, loud — #123's bad-value states all
|
|
59
|
+
* throw and name the knob). `undefined` = the default table; `[]` = NO table (an explicit, legal
|
|
60
|
+
* posture — the whole-table escape hatch's empty end); a non-empty list REPLACES the default table
|
|
61
|
+
* whole (compose additions as `[...WRITE_PROTECTED_DEFAULT_TABLE, …]`, drop rows by filtering the
|
|
62
|
+
* exported table — the visible/deletable admin face). Exact duplicates fold (idempotent, not one of
|
|
63
|
+
* #123's bad-value states). Exported so an admin face can preview the effective table under a
|
|
64
|
+
* candidate configuration with the engine's own rules.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveWriteProtectedTable(entries?: readonly WriteProtectedEntry[]): readonly WriteProtectedRow[];
|
|
67
|
+
/** The compiled judge over one resolved table. Pure and synchronous — literal folded comparisons,
|
|
68
|
+
* no filesystem access (see the module header for the declared lexical scope). */
|
|
69
|
+
export interface WriteProtectionMatcher {
|
|
70
|
+
/** The resolved rows this judge was compiled from (canonical names — the disclosure face). */
|
|
71
|
+
readonly rows: readonly WriteProtectedRow[];
|
|
72
|
+
/** Judge ONE path spelling. Returns the first matching row (basename rows first, then segment,
|
|
73
|
+
* then segment-run — deterministic), or null. */
|
|
74
|
+
matchPath(path: string): WriteProtectedHit | null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Compile the write-protection judge for a deployment configuration. Returns `undefined` when the
|
|
78
|
+
* resolved table is EMPTY (`[]` replacement) — the caller then mounts no tighten at all, keeping an
|
|
79
|
+
* opted-out deployment's decision path byte-identical to a build without this layer.
|
|
80
|
+
*/
|
|
81
|
+
export declare function compileWriteProtection(entries?: readonly WriteProtectedEntry[]): WriteProtectionMatcher | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Build the ENGINE-FILLED gate input (`ToolGateInput.writeProtectionCheck`): covered tools are the
|
|
84
|
+
* path-confinable write set (Write/Edit/NotebookEdit — the shared spelling), the judged target is
|
|
85
|
+
* the SAME string the tool itself will resolve (`writeTargetPath`, the shared single source with
|
|
86
|
+
* the sensitive-path guard and the fs-write gate), and the verdict is the table judge's. A covered
|
|
87
|
+
* write with NO resolvable target returns null here — this layer is ADDITIVE friction on a named
|
|
88
|
+
* set of targets, not a containment boundary (a target-less call cannot land on a named row, and
|
|
89
|
+
* the tool's own schema validation refuses it before any write); the fail-closed treatment of the
|
|
90
|
+
* unresolvable case belongs to the containment gates (fs-write-gate-policy documents that split).
|
|
91
|
+
* Returns `undefined` when the resolved table is empty — nothing to judge, mount nothing.
|
|
92
|
+
*/
|
|
93
|
+
export declare function createWriteProtectionCheck(entries?: readonly WriteProtectedEntry[]): ((toolName: string, args: unknown) => WriteProtectedHit | null) | undefined;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { writeTargetPath } from "../tools/fs/safety.js";
|
|
2
|
+
import { PATH_CONFINABLE_WRITE_TOOLS } from "./runner/session-rule-policy.js";
|
|
3
|
+
export const WRITE_PROTECTED_DEFAULT_TABLE = [
|
|
4
|
+
{ name: ".gitconfig", kind: "basename" },
|
|
5
|
+
{ name: ".gitmodules", kind: "basename" },
|
|
6
|
+
{ name: ".bashrc", kind: "basename" },
|
|
7
|
+
{ name: ".bash_profile", kind: "basename" },
|
|
8
|
+
{ name: ".zshrc", kind: "basename" },
|
|
9
|
+
{ name: ".zprofile", kind: "basename" },
|
|
10
|
+
{ name: ".profile", kind: "basename" },
|
|
11
|
+
{ name: ".zshenv", kind: "basename" },
|
|
12
|
+
{ name: ".zlogin", kind: "basename" },
|
|
13
|
+
{ name: ".zlogout", kind: "basename" },
|
|
14
|
+
{ name: ".bash_login", kind: "basename" },
|
|
15
|
+
{ name: ".bash_aliases", kind: "basename" },
|
|
16
|
+
{ name: ".bash_logout", kind: "basename" },
|
|
17
|
+
{ name: ".envrc", kind: "basename" },
|
|
18
|
+
{ name: ".ripgreprc", kind: "basename" },
|
|
19
|
+
{ name: ".mcp.json", kind: "basename" },
|
|
20
|
+
{ name: ".claude.json", kind: "basename" },
|
|
21
|
+
{ name: ".npmrc", kind: "basename" },
|
|
22
|
+
{ name: ".yarnrc", kind: "basename" },
|
|
23
|
+
{ name: ".yarnrc.yml", kind: "basename" },
|
|
24
|
+
{ name: ".pnp.cjs", kind: "basename" },
|
|
25
|
+
{ name: ".pnp.loader.mjs", kind: "basename" },
|
|
26
|
+
{ name: ".pnpmfile.cjs", kind: "basename" },
|
|
27
|
+
{ name: "bunfig.toml", kind: "basename" },
|
|
28
|
+
{ name: ".bunfig.toml", kind: "basename" },
|
|
29
|
+
{ name: ".bazelrc", kind: "basename" },
|
|
30
|
+
{ name: ".bazelversion", kind: "basename" },
|
|
31
|
+
{ name: ".bazeliskrc", kind: "basename" },
|
|
32
|
+
{ name: ".pre-commit-config.yaml", kind: "basename" },
|
|
33
|
+
{ name: "lefthook.yml", kind: "basename" },
|
|
34
|
+
{ name: ".lefthook.yml", kind: "basename" },
|
|
35
|
+
{ name: "lefthook.yaml", kind: "basename" },
|
|
36
|
+
{ name: ".lefthook.yaml", kind: "basename" },
|
|
37
|
+
{ name: "gradle-wrapper.properties", kind: "basename" },
|
|
38
|
+
{ name: "maven-wrapper.properties", kind: "basename" },
|
|
39
|
+
{ name: ".devcontainer.json", kind: "basename" },
|
|
40
|
+
{ name: "pyrightconfig.json", kind: "basename" },
|
|
41
|
+
{ name: ".git", kind: "segment" },
|
|
42
|
+
{ name: ".vscode", kind: "segment" },
|
|
43
|
+
{ name: ".idea", kind: "segment" },
|
|
44
|
+
{ name: ".claude", kind: "segment" },
|
|
45
|
+
{ name: ".husky", kind: "segment" },
|
|
46
|
+
{ name: ".cargo", kind: "segment" },
|
|
47
|
+
{ name: ".devcontainer", kind: "segment" },
|
|
48
|
+
{ name: ".yarn", kind: "segment" },
|
|
49
|
+
{ name: ".mvn", kind: "segment" },
|
|
50
|
+
{ name: ".config/git", kind: "segment-run" },
|
|
51
|
+
{ name: ".ssh", kind: "segment" },
|
|
52
|
+
{ name: ".gnupg", kind: "segment" },
|
|
53
|
+
];
|
|
54
|
+
export function foldWriteProtectCase(s) {
|
|
55
|
+
return s.toLowerCase().replace(/ı/g, "i").replace(/ſ/g, "s");
|
|
56
|
+
}
|
|
57
|
+
const KINDS = ["basename", "segment", "segment-run"];
|
|
58
|
+
function describeEntryValue(v) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.stringify(v) ?? String(v);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return String(v);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function normalizeWriteProtectedEntry(entry) {
|
|
67
|
+
const shape = typeof entry === "string" ? { name: entry, kind: entry.includes("/") ? "segment-run" : "segment" } : entry;
|
|
68
|
+
if (typeof shape !== "object" || shape === null || typeof shape.name !== "string") {
|
|
69
|
+
throw new Error(`writeProtectedPaths: entry ${describeEntryValue(entry)} is not a name string or { name, kind } row.`);
|
|
70
|
+
}
|
|
71
|
+
const name = shape.name;
|
|
72
|
+
if (!KINDS.includes(shape.kind)) {
|
|
73
|
+
throw new Error(`writeProtectedPaths: entry ${describeEntryValue(entry)} has kind ${describeEntryValue(shape.kind)} — known kinds: ${KINDS.join(", ")}.`);
|
|
74
|
+
}
|
|
75
|
+
const kind = shape.kind;
|
|
76
|
+
if (name.includes("*") || name.includes("?")) {
|
|
77
|
+
throw new Error(`writeProtectedPaths: entry ${describeEntryValue(name)} contains a wildcard metacharacter — this table speaks LITERAL names only (a silently-literal "*" would guard less than it reads); pattern semantics live in createSensitivePathPolicy.`);
|
|
78
|
+
}
|
|
79
|
+
if (name.includes("\\")) {
|
|
80
|
+
throw new Error(`writeProtectedPaths: entry ${describeEntryValue(name)} contains a backslash — names are "/"-separated (both path families are matched); spell the segments with "/".`);
|
|
81
|
+
}
|
|
82
|
+
const segments = name.split("/").filter((s) => s.length > 0);
|
|
83
|
+
if (segments.length === 0) {
|
|
84
|
+
throw new Error(`writeProtectedPaths: entry ${describeEntryValue(name)} contains no path segments and would protect nothing — remove the entry or spell the name.`);
|
|
85
|
+
}
|
|
86
|
+
if (segments.some((s) => s === "." || s === "..")) {
|
|
87
|
+
throw new Error(`writeProtectedPaths: entry ${describeEntryValue(name)} contains a "." or ".." segment — the judge folds dot segments lexically before matching, so such a row could never match anything; spell the real name.`);
|
|
88
|
+
}
|
|
89
|
+
if (segments.length > 1 && kind !== "segment-run") {
|
|
90
|
+
throw new Error(`writeProtectedPaths: entry ${describeEntryValue(name)} spans ${segments.length} segments but declares kind "${kind}" — multi-segment names match as kind "segment-run".`);
|
|
91
|
+
}
|
|
92
|
+
return { name: segments.join("/"), kind };
|
|
93
|
+
}
|
|
94
|
+
export function resolveWriteProtectedTable(entries) {
|
|
95
|
+
if (entries === undefined)
|
|
96
|
+
return WRITE_PROTECTED_DEFAULT_TABLE;
|
|
97
|
+
if (!Array.isArray(entries)) {
|
|
98
|
+
throw new Error(`writeProtectedPaths: expected an array of entries (whole-table replacement; [] = no write-protection table), got ${describeEntryValue(entries)}.`);
|
|
99
|
+
}
|
|
100
|
+
const out = [];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const row = normalizeWriteProtectedEntry(entry);
|
|
104
|
+
const key = `${row.kind}:${foldWriteProtectCase(row.name)}`;
|
|
105
|
+
if (seen.has(key))
|
|
106
|
+
continue;
|
|
107
|
+
seen.add(key);
|
|
108
|
+
out.push(row);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
export function compileWriteProtection(entries) {
|
|
113
|
+
const rows = resolveWriteProtectedTable(entries);
|
|
114
|
+
if (rows.length === 0)
|
|
115
|
+
return undefined;
|
|
116
|
+
const basenames = new Map();
|
|
117
|
+
const segments = new Map();
|
|
118
|
+
const runs = [];
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
const folded = foldWriteProtectCase(row.name);
|
|
121
|
+
if (row.kind === "basename") {
|
|
122
|
+
if (!basenames.has(folded))
|
|
123
|
+
basenames.set(folded, row.name);
|
|
124
|
+
}
|
|
125
|
+
else if (row.kind === "segment") {
|
|
126
|
+
if (!segments.has(folded))
|
|
127
|
+
segments.set(folded, row.name);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
runs.push({ name: row.name, parts: folded.split("/").filter((s) => s.length > 0) });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
rows,
|
|
135
|
+
matchPath(path) {
|
|
136
|
+
const viewOf = (win32) => {
|
|
137
|
+
const segs = [];
|
|
138
|
+
for (const raw of path.split(/[\\/]/)) {
|
|
139
|
+
if (raw.length === 0)
|
|
140
|
+
continue;
|
|
141
|
+
const dot = !win32 || /^\.{1,2}$/.test(raw) ? raw : raw.replace(/ +$/, "");
|
|
142
|
+
if (dot === ".")
|
|
143
|
+
continue;
|
|
144
|
+
if (dot === "..") {
|
|
145
|
+
const last = segs[segs.length - 1];
|
|
146
|
+
if (segs.length > 0 && last !== "..")
|
|
147
|
+
segs.pop();
|
|
148
|
+
else
|
|
149
|
+
segs.push("..");
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const stripped = win32 ? raw.replace(/[. ]+$/, "") : raw;
|
|
153
|
+
segs.push(foldWriteProtectCase(stripped.length > 0 ? stripped : raw));
|
|
154
|
+
}
|
|
155
|
+
return segs;
|
|
156
|
+
};
|
|
157
|
+
const judge = (segs) => {
|
|
158
|
+
if (segs.length === 0)
|
|
159
|
+
return null;
|
|
160
|
+
const base = basenames.get(segs[segs.length - 1] ?? "");
|
|
161
|
+
if (base !== undefined)
|
|
162
|
+
return { name: base, kind: "basename" };
|
|
163
|
+
for (const s of segs) {
|
|
164
|
+
const hit = segments.get(s);
|
|
165
|
+
if (hit !== undefined)
|
|
166
|
+
return { name: hit, kind: "segment" };
|
|
167
|
+
}
|
|
168
|
+
for (const run of runs) {
|
|
169
|
+
const n = run.parts.length;
|
|
170
|
+
for (let i = 0; i + n <= segs.length; i++) {
|
|
171
|
+
if (run.parts.every((p, j) => segs[i + j] === p))
|
|
172
|
+
return { name: run.name, kind: "segment-run" };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
};
|
|
177
|
+
const posix = viewOf(false);
|
|
178
|
+
return judge(posix) ?? judge(viewOf(true));
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function createWriteProtectionCheck(entries) {
|
|
183
|
+
const matcher = compileWriteProtection(entries);
|
|
184
|
+
if (matcher === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
return (toolName, args) => {
|
|
187
|
+
if (!PATH_CONFINABLE_WRITE_TOOLS.has(toolName))
|
|
188
|
+
return null;
|
|
189
|
+
const path = writeTargetPath(toolName, args);
|
|
190
|
+
if (typeof path !== "string" || path.length === 0)
|
|
191
|
+
return null;
|
|
192
|
+
return matcher.matchPath(path);
|
|
193
|
+
};
|
|
194
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -91,6 +91,7 @@ export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, ty
|
|
|
91
91
|
export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
|
|
92
92
|
export { READ_FACE_DEFAULT_DENY_ENTRIES, READ_FACE_BUILTIN_DENY_TABLE, READ_DENY_BUILTIN_TIERS, READ_DENY_DEFAULT_TIERS, resolveReadDenyBuiltins, compileReadDeny, type ReadDenyEntry, type ReadDenyMatcher, type NormalizedReadDenyEntry, type ReadDenyBuiltinTier, type ReadDenyBuiltinRow, type ReadDenyBuiltinConfig, } from "./tools/fs/index.js";
|
|
93
93
|
export { deploymentReadFaceClampNotice, resolveReadFace, type ReadFace, type ReadFaceInputs } from "./tools/fs/index.js";
|
|
94
|
+
export { WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, compileWriteProtection, type WriteProtectedEntry, type WriteProtectedRow, type WriteProtectedKind, type WriteProtectedHit, type WriteProtectionMatcher, } from "./core/write-protect.js";
|
|
94
95
|
export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, toolResultContentSegment, MAX_MINTED_TOOL_RESULT_REF_CHARS, type ToolResultProvenance, assertToolResultProvenanceMatch, normalizeToolResultProvenance, toolResultProvenanceOf, ToolResultRefConflictError, TOOL_RESULT_REF_CONFLICT_CODE, type ToolResultStore, type ToolResultSlice, type ToolResultDeletionReport, } from "./core/tool-result-store.js";
|
|
95
96
|
export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, type ActorAssertion, type PendingSteerEntry, type PendingSteerInput, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type ProbeCause, type ProbeCauseOperands, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type RealApprovalGateBit, type ResourceLedger, type ResourceLimitReason, type PlatformLimitReason, } from "./core/checkpoint-store.js";
|
|
96
97
|
export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, type UsageWindow, type UsageWindowStore, type UsageWindowReading, type UsageWindowRecord, type UsageSlot, type UsageBucketRow, } from "./core/usage-window-store.js";
|
|
@@ -114,7 +115,7 @@ export { type StoreDurability } from "./core/checkpoint-store.js";
|
|
|
114
115
|
export { type StoreFidelity } from "./core/checkpoint-store.js";
|
|
115
116
|
export { projectHumanInput, buildHumanInputEvent, type HumanInputSource, type HumanInputFrame, type HumanInputEvent, type HumanInputDelivery, } from "./core/human-input-projection.js";
|
|
116
117
|
export { TaskRegistry, defaultTaskRegistry, createTaskOutputTool, createTaskStopTool, type SemaTaskType, type SemaTaskStatus, type SemaTaskHandle, type ParkedClaimTicket, type TaskAccess, type UnifiedTaskOutput, type TaskRetrievalStatus, type StopSource, type RegisterMonitorInput, type MonitorTimers, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccessWorkflowRun, DURABLE_AGENT_HANDLE_RE, } from "./core/task-registry.js";
|
|
117
|
-
export { InMemoryMailboxStore, type MailboxStore, type MailboxMessage, type MailboxLease } from "./core/mailbox-store.js";
|
|
118
|
+
export { InMemoryMailboxStore, type MailboxStore, type MailboxMessage, type MailboxLease, MailboxStoreError, MAILBOX_TOMBSTONED_RECIPIENT_CODE, } from "./core/mailbox-store.js";
|
|
118
119
|
export { FileMailboxStore, type FileMailboxStoreOptions } from "./stores/file/mailbox-store.js";
|
|
119
120
|
export { createFileTaskListStore } from "./stores/file/task-list-store.js";
|
|
120
121
|
export { createCcFileTaskListStore } from "./stores/cc/task-list-store.js";
|
|
@@ -217,7 +218,7 @@ export { sessionRepoContract } from "./core/store-contracts/session-repo-contrac
|
|
|
217
218
|
export { toolResultStoreContract } from "./core/store-contracts/tool-result-store-contract.js";
|
|
218
219
|
export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
|
|
219
220
|
export { permissionRuleSyncContract, type PermissionRuleSyncContractHooks } from "./core/store-contracts/permission-rule-sync-contract.js";
|
|
220
|
-
export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
|
|
221
|
+
export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, mailboxTombstonedRecipientContract, type MailboxTombstonedRecipientContractHooks, } from "./core/store-contracts/mailbox-store-contract.js";
|
|
221
222
|
export { BACKGROUND_AGENT_CONTRACT_SCOPE, backgroundAgentStoreContract, backgroundAgentStoreScopesContract, } from "./core/store-contracts/background-agent-store-contract.js";
|
|
222
223
|
export { type BackgroundAgentStore, type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentUsage, canAccessAgentRecord, STALE_RUNNING_REAP_ATTRIBUTION, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, type BackgroundAgentQuery, } from "./core/background-agent-store.js";
|
|
223
224
|
export { serveDurableAgentRowLane, buildAgentPollDetails, type AgentPollDetailsInput, } from "./core/task-registry-agent.js";
|
package/dist/index.js
CHANGED
|
@@ -71,6 +71,7 @@ export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, }
|
|
|
71
71
|
export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
|
|
72
72
|
export { READ_FACE_DEFAULT_DENY_ENTRIES, READ_FACE_BUILTIN_DENY_TABLE, READ_DENY_BUILTIN_TIERS, READ_DENY_DEFAULT_TIERS, resolveReadDenyBuiltins, compileReadDeny, } from "./tools/fs/index.js";
|
|
73
73
|
export { deploymentReadFaceClampNotice, resolveReadFace } from "./tools/fs/index.js";
|
|
74
|
+
export { WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, compileWriteProtection, } from "./core/write-protect.js";
|
|
74
75
|
export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, toolResultContentSegment, MAX_MINTED_TOOL_RESULT_REF_CHARS, assertToolResultProvenanceMatch, normalizeToolResultProvenance, toolResultProvenanceOf, ToolResultRefConflictError, TOOL_RESULT_REF_CONFLICT_CODE, } from "./core/tool-result-store.js";
|
|
75
76
|
export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
|
|
76
77
|
export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, } from "./core/usage-window-store.js";
|
|
@@ -93,7 +94,7 @@ export {} from "./core/checkpoint-store.js";
|
|
|
93
94
|
export {} from "./core/checkpoint-store.js";
|
|
94
95
|
export { projectHumanInput, buildHumanInputEvent, } from "./core/human-input-projection.js";
|
|
95
96
|
export { TaskRegistry, defaultTaskRegistry, createTaskOutputTool, createTaskStopTool, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccessWorkflowRun, DURABLE_AGENT_HANDLE_RE, } from "./core/task-registry.js";
|
|
96
|
-
export { InMemoryMailboxStore } from "./core/mailbox-store.js";
|
|
97
|
+
export { InMemoryMailboxStore, MailboxStoreError, MAILBOX_TOMBSTONED_RECIPIENT_CODE, } from "./core/mailbox-store.js";
|
|
97
98
|
export { FileMailboxStore } from "./stores/file/mailbox-store.js";
|
|
98
99
|
export { createFileTaskListStore } from "./stores/file/task-list-store.js";
|
|
99
100
|
export { createCcFileTaskListStore } from "./stores/cc/task-list-store.js";
|
|
@@ -178,7 +179,7 @@ export { sessionRepoContract } from "./core/store-contracts/session-repo-contrac
|
|
|
178
179
|
export { toolResultStoreContract } from "./core/store-contracts/tool-result-store-contract.js";
|
|
179
180
|
export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
|
|
180
181
|
export { permissionRuleSyncContract } from "./core/store-contracts/permission-rule-sync-contract.js";
|
|
181
|
-
export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
|
|
182
|
+
export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, mailboxTombstonedRecipientContract, } from "./core/store-contracts/mailbox-store-contract.js";
|
|
182
183
|
export { BACKGROUND_AGENT_CONTRACT_SCOPE, backgroundAgentStoreContract, backgroundAgentStoreScopesContract, } from "./core/store-contracts/background-agent-store-contract.js";
|
|
183
184
|
export { canAccessAgentRecord, STALE_RUNNING_REAP_ATTRIBUTION, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, queryBackgroundAgents, } from "./core/background-agent-store.js";
|
|
184
185
|
export { serveDurableAgentRowLane, buildAgentPollDetails, } from "./core/task-registry-agent.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/98 §6.3 — the ONE reading of "is this governance baseline usable", shared by the deployment
|
|
3
|
+
* CAPABILITY predicate (`workflowsCapability`, which drives the surfaced `/workflows` affordance AND
|
|
4
|
+
* the orchestration-prompt injection) and the MOUNT-time validator (`createRunWorkflowTool`, which
|
|
5
|
+
* refuses loudly with a code).
|
|
6
|
+
*
|
|
7
|
+
* Why one function rather than two agreeing predicates: this seam split three times inside one
|
|
8
|
+
* release window, each time one level deeper. First the capability tested `!== undefined` while the
|
|
9
|
+
* mount tested truthiness (a JSON `null` announced workflows the tool never mounted). Then both
|
|
10
|
+
* tested the container while the mount refused an unusable `base` SLOT (`{ base: null }` announced
|
|
11
|
+
* workflows that hard-failed every opted-in task). Then the slot readings agreed but the mount
|
|
12
|
+
* additionally refused a malformed `worktreeBase` and malformed face lists — the same split again,
|
|
13
|
+
* one field further in. Two predicates that must "stay in agreement" are a standing invitation to
|
|
14
|
+
* drift; the only shape that cannot drift is a single function both call.
|
|
15
|
+
*
|
|
16
|
+
* The contract: {@link governanceBaselineProblem} returns `null` when the mount would SUCCEED, and
|
|
17
|
+
* a describable problem when the mount would REFUSE. Every mount-time refusal must be reachable
|
|
18
|
+
* from here, so a deployment is never told it has workflows it cannot actually run.
|
|
19
|
+
*/
|
|
20
|
+
export interface GovernanceBaselineProblem {
|
|
21
|
+
/** Which slot/field is wrong, in the spelling a deployment configures (`base.excludeTools`). */
|
|
22
|
+
readonly where: string;
|
|
23
|
+
/** What was found there, for the operator-facing message (`null`, `a string`, `an array`). */
|
|
24
|
+
readonly found: string;
|
|
25
|
+
/** The coded refusal the mount raises for this problem. */
|
|
26
|
+
readonly code: "config.invalid_governance_baseline" | "config.invalid_tool_name_set";
|
|
27
|
+
}
|
|
28
|
+
/** Name a bad value WITHOUT throwing on it (a bigint / cyclic object / poisoned `toJSON` must not
|
|
29
|
+
* crash the describer and cost the refusal its name). */
|
|
30
|
+
export declare function describeBaselineValue(v: unknown): string;
|
|
31
|
+
/**
|
|
32
|
+
* The single validity reading. `null` ⇒ the mount would succeed ⇒ the capability may be announced.
|
|
33
|
+
*
|
|
34
|
+
* `base` is the control plane every script-spawned child inherits, so a non-object there refuses:
|
|
35
|
+
* ungoverned is spelled by not configuring `workflowGovernanceBaseline` at all, never by an
|
|
36
|
+
* unusable anchor. `worktreeBase` is the optional overlay whose documented absence means "base
|
|
37
|
+
* governs every child" — `null`/absent is that absence, any other non-object is garbage and refuses
|
|
38
|
+
* rather than impersonating either absence or an overlay.
|
|
39
|
+
*/
|
|
40
|
+
export declare function governanceBaselineProblem(baseline: unknown): GovernanceBaselineProblem | null;
|
|
41
|
+
/** The mount's refusal, minted from the shared reading so the message and the code have one home. */
|
|
42
|
+
export declare function governanceBaselineError(p: GovernanceBaselineProblem): Error & {
|
|
43
|
+
code: string;
|
|
44
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const FACE_LIST_FIELDS = ["excludeTools", "deferTools", "alwaysLoadTools"];
|
|
2
|
+
export function describeBaselineValue(v) {
|
|
3
|
+
return v === null ? "null" : Array.isArray(v) ? "an array" : `a ${typeof v}`;
|
|
4
|
+
}
|
|
5
|
+
function isSpecObject(v) {
|
|
6
|
+
return v !== null && v !== undefined && typeof v === "object" && !Array.isArray(v);
|
|
7
|
+
}
|
|
8
|
+
function faceListProblem(slot, where) {
|
|
9
|
+
for (const field of FACE_LIST_FIELDS) {
|
|
10
|
+
const v = slot[field];
|
|
11
|
+
if (v === undefined || v === null)
|
|
12
|
+
continue;
|
|
13
|
+
if (!Array.isArray(v)) {
|
|
14
|
+
return { where: `${where}.${field}`, found: describeBaselineValue(v), code: "config.invalid_tool_name_set" };
|
|
15
|
+
}
|
|
16
|
+
for (const [i, entry] of v.entries()) {
|
|
17
|
+
if (typeof entry !== "string") {
|
|
18
|
+
return {
|
|
19
|
+
where: `${where}.${field}`,
|
|
20
|
+
found: `entry ${i} is ${describeBaselineValue(entry)}`,
|
|
21
|
+
code: "config.invalid_tool_name_set",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export function governanceBaselineProblem(baseline) {
|
|
29
|
+
if (!isSpecObject(baseline)) {
|
|
30
|
+
return { where: "workflowGovernanceBaseline", found: describeBaselineValue(baseline), code: "config.invalid_governance_baseline" };
|
|
31
|
+
}
|
|
32
|
+
const base = baseline.base;
|
|
33
|
+
if (!isSpecObject(base)) {
|
|
34
|
+
return { where: "base", found: describeBaselineValue(base), code: "config.invalid_governance_baseline" };
|
|
35
|
+
}
|
|
36
|
+
const baseFaces = faceListProblem(base, "base");
|
|
37
|
+
if (baseFaces !== null)
|
|
38
|
+
return baseFaces;
|
|
39
|
+
const wt = baseline.worktreeBase;
|
|
40
|
+
if (wt === null || wt === undefined)
|
|
41
|
+
return null;
|
|
42
|
+
if (!isSpecObject(wt)) {
|
|
43
|
+
return { where: "worktreeBase", found: describeBaselineValue(wt), code: "config.invalid_governance_baseline" };
|
|
44
|
+
}
|
|
45
|
+
return faceListProblem(wt, "worktreeBase");
|
|
46
|
+
}
|
|
47
|
+
export function governanceBaselineError(p) {
|
|
48
|
+
const detail = p.code === "config.invalid_tool_name_set"
|
|
49
|
+
? `must be an array of tool names (got ${p.found}) — refusing to mount rather than govern children with a roster nobody can read.`
|
|
50
|
+
: `is ${p.found}, not a spec object — refusing to mount rather than launch children under no governance ` +
|
|
51
|
+
`(to run ungoverned, leave workflowGovernanceBaseline unconfigured).`;
|
|
52
|
+
const e = new Error(`createRunWorkflowTool: the workflow governance baseline's ${p.where} ${detail}`);
|
|
53
|
+
e.code = p.code;
|
|
54
|
+
return e;
|
|
55
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool, errorResult } from "../core/tools.js";
|
|
3
|
+
import { governanceBaselineError, governanceBaselineProblem } from "./governance-baseline-validity.js";
|
|
3
4
|
import { redactSecrets, redactHostLeaks, boundedRedactedSummary } from "../core/untrusted-egress.js";
|
|
4
5
|
import { startWorkflow } from "./workflow.js";
|
|
5
6
|
import { buildWorkflowPrimitives } from "./workflow-primitives.js";
|
|
@@ -167,16 +168,40 @@ export async function createRunWorkflowTool(d) {
|
|
|
167
168
|
childMaxTokens: lim.childMaxTokens,
|
|
168
169
|
childMaxTurns: lim.childMaxTurns,
|
|
169
170
|
};
|
|
171
|
+
const problem = governanceBaselineProblem(d.governanceBaseline);
|
|
172
|
+
if (problem !== null)
|
|
173
|
+
throw governanceBaselineError(problem);
|
|
174
|
+
const FACE_LIST_FIELDS = ["excludeTools", "deferTools", "alwaysLoadTools"];
|
|
175
|
+
const dropNullFaces = (slot) => {
|
|
176
|
+
let out = slot;
|
|
177
|
+
for (const field of FACE_LIST_FIELDS) {
|
|
178
|
+
if (slot[field] !== null)
|
|
179
|
+
continue;
|
|
180
|
+
out = { ...out };
|
|
181
|
+
delete out[field];
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
};
|
|
185
|
+
const sanitizedBaseline = (() => {
|
|
186
|
+
const wt = d.governanceBaseline.worktreeBase;
|
|
187
|
+
const { worktreeBase: _absentOverlay, ...rest } = d.governanceBaseline;
|
|
188
|
+
return {
|
|
189
|
+
...rest,
|
|
190
|
+
base: dropNullFaces(d.governanceBaseline.base),
|
|
191
|
+
...(wt === null || wt === undefined ? {} : { worktreeBase: dropNullFaces(wt) }),
|
|
192
|
+
};
|
|
193
|
+
})();
|
|
194
|
+
const unionFaceList = (own, parent) => own === undefined ? [...new Set(parent)] : [...new Set([...own, ...parent])];
|
|
170
195
|
const withParentFace = (base, inherit) => ({
|
|
171
196
|
...base,
|
|
172
197
|
...(d.parentExcludeTools?.length
|
|
173
|
-
? { excludeTools:
|
|
198
|
+
? { excludeTools: unionFaceList(base.excludeTools ?? inherit?.excludeTools, d.parentExcludeTools) }
|
|
174
199
|
: {}),
|
|
175
200
|
...(d.parentDeferTools?.length
|
|
176
|
-
? { deferTools:
|
|
201
|
+
? { deferTools: unionFaceList(base.deferTools ?? inherit?.deferTools, d.parentDeferTools) }
|
|
177
202
|
: {}),
|
|
178
203
|
...(d.parentAlwaysLoadTools?.length
|
|
179
|
-
? { alwaysLoadTools:
|
|
204
|
+
? { alwaysLoadTools: unionFaceList(base.alwaysLoadTools ?? inherit?.alwaysLoadTools, d.parentAlwaysLoadTools) }
|
|
180
205
|
: {}),
|
|
181
206
|
...(() => {
|
|
182
207
|
const parent = d.parentRestoreGatedTools;
|
|
@@ -197,13 +222,13 @@ export async function createRunWorkflowTool(d) {
|
|
|
197
222
|
const withParentProfile = (base) => d.parentPromptProfile !== undefined && base.promptProfile === undefined ? { ...base, promptProfile: d.parentPromptProfile } : base;
|
|
198
223
|
const baselineWithParentFace = (d.parentExcludeTools?.length ?? 0) > 0 || (d.parentDeferTools?.length ?? 0) > 0 || (d.parentAlwaysLoadTools?.length ?? 0) > 0 || d.parentPromptProfile !== undefined || d.parentRestoreGatedTools !== undefined
|
|
199
224
|
? {
|
|
200
|
-
...
|
|
201
|
-
base: withParentProfile(withParentFace(
|
|
202
|
-
...(
|
|
203
|
-
? { worktreeBase: withParentFace(
|
|
225
|
+
...sanitizedBaseline,
|
|
226
|
+
base: withParentProfile(withParentFace(sanitizedBaseline.base)),
|
|
227
|
+
...(sanitizedBaseline.worktreeBase !== undefined
|
|
228
|
+
? { worktreeBase: withParentFace(sanitizedBaseline.worktreeBase, sanitizedBaseline.base) }
|
|
204
229
|
: {}),
|
|
205
230
|
}
|
|
206
|
-
:
|
|
231
|
+
: sanitizedBaseline;
|
|
207
232
|
const governance = { baseline: baselineWithParentFace, models: d.models, caps: childCaps, onNotice: d.onNotice };
|
|
208
233
|
const builtinsEnabled = d.builtinWorkflows !== false;
|
|
209
234
|
const namedWorkflowSection = renderNamedWorkflowListing(await collectNamedWorkflowListings(d.scriptStore, builtinsEnabled));
|