@sema-agent/core 7.8.0 → 7.9.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 -1
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/core/ask-origin.d.ts +12 -1
- package/dist/core/ask-origin.js +5 -1
- package/dist/core/checkpoint-store.d.ts +34 -28
- package/dist/core/gate-lanes.js +49 -23
- package/dist/core/gate-outcome.d.ts +8 -4
- package/dist/core/gate-outcome.js +2 -1
- package/dist/core/hooks.d.ts +56 -31
- package/dist/core/permission-rule-consent.d.ts +29 -36
- package/dist/core/permission-rule-consent.js +110 -60
- package/dist/core/permission-rule-model.d.ts +254 -55
- package/dist/core/permission-rule-model.js +323 -43
- package/dist/core/permission-rule-org.d.ts +9 -8
- package/dist/core/permission-rule-org.js +6 -15
- package/dist/core/permission-rule-provider.d.ts +10 -6
- package/dist/core/permission-rule-provider.js +12 -8
- package/dist/core/permission-rule-session.d.ts +7 -6
- package/dist/core/permission-rule-session.js +29 -10
- package/dist/core/permission-rule-store.d.ts +40 -18
- package/dist/core/permission-rule-store.js +68 -42
- package/dist/core/permission-rule-sync.d.ts +9 -3
- package/dist/core/permission-rule-sync.js +29 -25
- package/dist/core/permission-rule-syntax.d.ts +30 -0
- package/dist/core/permission-rule-syntax.js +44 -0
- package/dist/core/permission-rules.d.ts +55 -33
- package/dist/core/permission-rules.js +65 -55
- package/dist/core/persisted-rule-arms.d.ts +56 -0
- package/dist/core/persisted-rule-arms.js +48 -0
- package/dist/core/runner/contracts.d.ts +21 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
- package/dist/core/runner/permission-rule-lanes.js +27 -21
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-safety-scan.js +8 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/resume-admission.d.ts +53 -0
- package/dist/core/runner/resume-admission.js +83 -0
- package/dist/core/runner/resume-apply.d.ts +50 -0
- package/dist/core/runner/resume-apply.js +184 -0
- package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
- package/dist/core/runner/resume-checkpoint-screen.js +108 -0
- package/dist/core/runner/resume-claim.d.ts +32 -0
- package/dist/core/runner/resume-claim.js +27 -0
- package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
- package/dist/core/runner/resume-internals-and-config.js +50 -0
- package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
- package/dist/core/runner/resume-policy-outcome.js +127 -0
- package/dist/core/runner/resume-preflight.d.ts +40 -0
- package/dist/core/runner/resume-preflight.js +122 -0
- package/dist/core/runner/resume-review-outcome.d.ts +30 -0
- package/dist/core/runner/resume-review-outcome.js +88 -0
- package/dist/core/runner/run-harness-handlers.js +1 -1
- package/dist/core/runner/runtask.d.ts +20 -0
- package/dist/core/runner/runtask.js +85 -734
- package/dist/core/runner/tool-end-body.d.ts +12 -5
- package/dist/core/runner/tool-end-body.js +5 -5
- package/dist/core/runner/tool-output-projection.d.ts +9 -6
- package/dist/core/runner/tool-output-projection.js +1 -18
- package/dist/core/runner/turn-attachments.d.ts +2 -2
- package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
- package/dist/core/tool-roster.d.ts +9 -0
- package/dist/core/tool-roster.js +15 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/stores/file/checkpoint-store.js +1 -1
- package/dist/stores/file/permission-rule-store.d.ts +28 -23
- package/dist/stores/file/permission-rule-store.js +64 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +43 -11
|
@@ -7,24 +7,31 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Projections over values the caller already holds, with ONE clock read: `toolResultMsg` stamps the message
|
|
9
9
|
* it mints with `Date.now()` (as it did inside the driver — the transcript's timestamp is minted where the
|
|
10
|
-
* message is).
|
|
10
|
+
* message is). The one callback here is the leg's card projector, handed in by every caller (it keeps the
|
|
11
|
+
* once-per-leg announce state; this module keeps none). Nothing here awaits.
|
|
11
12
|
* Layer-1 machinery, not a driven lane; the prefix rule it follows is written once, on initial-run-state.ts.
|
|
12
13
|
*/
|
|
13
14
|
import { type AgentMessage } from "../../internal/harness.js";
|
|
14
15
|
import type { GateOutcome } from "../gate-outcome.js";
|
|
15
16
|
import { type McpDelivered } from "../mcp-failure.js";
|
|
16
17
|
import type { RecoveredOrphan } from "../session-reconcile.js";
|
|
18
|
+
import { type StructuredProjector } from "./tool-output-projection.js";
|
|
17
19
|
import type { WriteFamily } from "./turn-attachments.js";
|
|
18
20
|
/** design/134 §3.1b — derive the thin `response` digest from a harness tool result: model-facing text
|
|
19
21
|
* content only (never `details` — the H4 thin-projection discipline), capped with a truncation note. */
|
|
20
22
|
export declare function batchResponseDigest(result: unknown): string | undefined;
|
|
21
23
|
/** The `tool_end` body fields projected from a harness tool result — output/truncated/totalChars via
|
|
22
|
-
* {@link toolOutputFrom} and the CC card via
|
|
24
|
+
* {@link toolOutputFrom} and the CC card via the LEG's projector (`Prepared.structuredProjector`: the engine card set
|
|
25
|
+
* ∪ the caller rows' declared `renderHints.resultCards`, an undeclared type dropped and announced once per leg —
|
|
26
|
+
* design/388 L4; handed in because the once-per-leg state lives on the leg). Single construction point for BOTH
|
|
23
27
|
* the live loop's frames and the resumed batch's frames (`resolvePendingCall` + the deferred-sibling
|
|
24
28
|
* close): the resumed frames used to carry only `isError`, so a client rendering tool output from frames
|
|
25
29
|
* showed an empty body for every durable-approved call. Same projection = same source as the transcript.
|
|
26
30
|
* Also the one place the gate outcome reaches a frame — see the parameter. */
|
|
27
31
|
export declare function toolEndBodyFrom(result: unknown, isError: boolean,
|
|
32
|
+
/** The leg's card projector — see the doc above. A parameter, never a module default: a body minted with the engine
|
|
33
|
+
* set alone would silently drop every declared caller card and never announce. */
|
|
34
|
+
structured: StructuredProjector,
|
|
28
35
|
/** The gate's record of the pass that admitted or refused this call, supplied by the CALLER of this
|
|
29
36
|
* projection — the live loop reads it off the gate's per-call sideband, the resumed leg off the decide's
|
|
30
37
|
* minted record. Deliberately a parameter and never derived from `result`: a tool's own `details`
|
|
@@ -58,10 +65,10 @@ gatedCallIdOfRun?: string): {
|
|
|
58
65
|
* disagreed. The projection goes through the same {@link toolEndBodyFrom} every live tool result uses, so
|
|
59
66
|
* `output` = the persisted model-facing text and `errorCode` = the persisted `details.errorKind`
|
|
60
67
|
* (`interrupted_never_started` / `interrupted_outcome_unknown`) — a consumer discriminates on the code
|
|
61
|
-
* instead of prose-matching. No `structured`: the reconcile mints no CC card (no `details.type`), which
|
|
62
|
-
*
|
|
68
|
+
* instead of prose-matching. No `structured`: the reconcile mints no CC card (no `details.type`), which the
|
|
69
|
+
* leg's projector already enforces.
|
|
63
70
|
*/
|
|
64
|
-
export declare function reconciledToolEndBody(orphan: Pick<RecoveredOrphan, "text" | "errorKind"
|
|
71
|
+
export declare function reconciledToolEndBody(orphan: Pick<RecoveredOrphan, "text" | "errorKind">, structured: StructuredProjector): ReturnType<typeof toolEndBodyFrom>;
|
|
65
72
|
/** task #51 F2/F3 — the CANONICAL write-tool → reminder-window mapping (CC qFm :480253-480254
|
|
66
73
|
* `a.name === bD || a.name === hF` = TaskCreate/TaskUpdate; BFm :480217-480219 = TodoWrite). Live
|
|
67
74
|
* deployment-alias resolution happens at the call sites (run-local `writeFamilyOf` over the mounted
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {} from "../../internal/harness.js";
|
|
2
2
|
import { MCP_DELIVERY_VERDICTS } from "../mcp-failure.js";
|
|
3
3
|
import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
|
|
4
|
-
import {
|
|
4
|
+
import { toolOutputFrom } from "./tool-output-projection.js";
|
|
5
5
|
import { TASK_CREATE_TOOL_NAME, TASK_UPDATE_TOOL_NAME, TODO_WRITE_TOOL_NAME } from "../tool-catalog-entries.js";
|
|
6
6
|
const BATCH_RESPONSE_MAX_CHARS = 500;
|
|
7
7
|
export function batchResponseDigest(result) {
|
|
@@ -32,9 +32,9 @@ export function batchResponseDigest(result) {
|
|
|
32
32
|
? `${text.slice(0, BATCH_RESPONSE_MAX_CHARS)}…[+${text.length - BATCH_RESPONSE_MAX_CHARS} chars truncated]`
|
|
33
33
|
: text;
|
|
34
34
|
}
|
|
35
|
-
export function toolEndBodyFrom(result, isError, gate, gatedCallIdOfRun) {
|
|
35
|
+
export function toolEndBodyFrom(result, isError, structured, gate, gatedCallIdOfRun) {
|
|
36
36
|
const o = toolOutputFrom(result);
|
|
37
|
-
const st =
|
|
37
|
+
const st = structured(result);
|
|
38
38
|
const det = isError ? result?.details : undefined;
|
|
39
39
|
const codeRaw = det?.code;
|
|
40
40
|
const kindRaw = det?.errorKind;
|
|
@@ -51,8 +51,8 @@ export function toolEndBodyFrom(result, isError, gate, gatedCallIdOfRun) {
|
|
|
51
51
|
...(gate !== undefined ? { gate } : {}),
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
-
export function reconciledToolEndBody(orphan) {
|
|
55
|
-
return toolEndBodyFrom({ content: orphan.text, details: { code: orphan.errorKind } }, true);
|
|
54
|
+
export function reconciledToolEndBody(orphan, structured) {
|
|
55
|
+
return toolEndBodyFrom({ content: orphan.text, details: { code: orphan.errorKind } }, true, structured);
|
|
56
56
|
}
|
|
57
57
|
export function writeFamilyOfCanonical(name) {
|
|
58
58
|
if (name === TASK_CREATE_TOOL_NAME || name === TASK_UPDATE_TOOL_NAME)
|
|
@@ -9,14 +9,17 @@ export declare const toolOutputFrom: (result: unknown) => {
|
|
|
9
9
|
truncated: boolean;
|
|
10
10
|
totalChars?: number;
|
|
11
11
|
} | undefined;
|
|
12
|
+
/** The `tool_end.structured` projection over a harness tool result: the CC card when `details.type` names a card
|
|
13
|
+
* the leg admits, else undefined. Minted per leg by {@link createStructuredProjector}; every body projection takes it
|
|
14
|
+
* as a parameter — there is no module-level card set to fall back to. */
|
|
15
|
+
export type StructuredProjector = (result: unknown) => unknown;
|
|
12
16
|
/**
|
|
13
17
|
* L4's run-time half (design/388 §2.4): a per-leg projector — the engine card set plus the leg's declared
|
|
14
|
-
* caller cards — that DROPS an undeclared `details.type`
|
|
15
|
-
*
|
|
16
|
-
*
|
|
18
|
+
* caller cards — that DROPS an undeclared `details.type` and announces it ONCE per leg
|
|
19
|
+
* (`config.tool_card_undeclared`, audience operator). With no options it is the engine set alone, unannounced —
|
|
20
|
+
* the shape a caller outside a leg (a test, a fixture) uses.
|
|
17
21
|
*/
|
|
18
|
-
export declare function createStructuredProjector(opts
|
|
22
|
+
export declare function createStructuredProjector(opts?: {
|
|
19
23
|
declaredCards?: Iterable<string>;
|
|
20
24
|
onUndeclared?: (type: string) => void;
|
|
21
|
-
}):
|
|
22
|
-
export declare const structuredFrom: (result: unknown) => unknown;
|
|
25
|
+
}): StructuredProjector;
|
|
@@ -58,7 +58,7 @@ export const toolOutputFrom = (result) => {
|
|
|
58
58
|
return { output: raw, truncated: true, totalChars };
|
|
59
59
|
};
|
|
60
60
|
const CC_DETAIL_TYPES = engineCardTypes();
|
|
61
|
-
export function createStructuredProjector(opts) {
|
|
61
|
+
export function createStructuredProjector(opts = {}) {
|
|
62
62
|
const allowed = new Set([...CC_DETAIL_TYPES, ...(opts.declaredCards ?? [])]);
|
|
63
63
|
const announced = new Set();
|
|
64
64
|
return (result) => {
|
|
@@ -89,20 +89,3 @@ function boundedCard(details) {
|
|
|
89
89
|
}
|
|
90
90
|
return details;
|
|
91
91
|
}
|
|
92
|
-
export const structuredFrom = (result) => {
|
|
93
|
-
const details = result !== null && typeof result === "object" ? result.details : undefined;
|
|
94
|
-
if (details === null || typeof details !== "object")
|
|
95
|
-
return undefined;
|
|
96
|
-
const t = details.type;
|
|
97
|
-
if (typeof t !== "string" || !CC_DETAIL_TYPES.has(t))
|
|
98
|
-
return undefined;
|
|
99
|
-
try {
|
|
100
|
-
const serialized = JSON.stringify(details) ?? "";
|
|
101
|
-
if (serialized.length > MAX_TOOL_END_OUTPUT_CHARS * 4)
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
return details;
|
|
108
|
-
};
|
|
@@ -110,7 +110,7 @@ export interface TurnAttachment {
|
|
|
110
110
|
source: AttachmentSource;
|
|
111
111
|
body: string;
|
|
112
112
|
}
|
|
113
|
-
/** H4 — the thin projection of a todo list kept run-local (NEVER the wire-capped
|
|
113
|
+
/** H4 — the thin projection of a todo list kept run-local (NEVER the wire-capped `tool_end.structured` card).
|
|
114
114
|
* Pure CONTENT memory: the write-window anchor lives on {@link AttachmentState.todoLastWriteTurn}
|
|
115
115
|
* (task #51 F3 — the window resets on tool-call INITIATION, which may precede/outlive any ingest). */
|
|
116
116
|
export interface ListProjection {
|
|
@@ -273,7 +273,7 @@ export declare const INSTRUCTIONS_CHANGE_BYTE_CAP = 512;
|
|
|
273
273
|
*/
|
|
274
274
|
export declare function createAttachmentState(): AttachmentState;
|
|
275
275
|
/**
|
|
276
|
-
* H4 — thin-projection reducer over a tool's RAW `result.details` (NOT the wire
|
|
276
|
+
* H4 — thin-projection reducer over a tool's RAW `result.details` (NOT the wire card projector
|
|
277
277
|
* projection and its 4×16K caps: a huge todo list must shrink into a bounded snapshot here, never
|
|
278
278
|
* disarm the reminder by being dropped wholesale). Only the first-party discriminants `todo` /
|
|
279
279
|
* `task` / `task-list` are recognized — every other card type (subagent `agent`/`fork`/`task-output`,
|
|
@@ -5,14 +5,15 @@ const GLOBAL = { kind: "global" };
|
|
|
5
5
|
function add(actor, counter, origin = "user") {
|
|
6
6
|
return { dot: { actor, counter }, origin, createdAt: "2026-01-01T00:00:00.000Z" };
|
|
7
7
|
}
|
|
8
|
-
function rule(text, adds, scope = GLOBAL) {
|
|
8
|
+
function rule(text, adds, scope = GLOBAL, behavior = "allow") {
|
|
9
9
|
const m = /^Bash\((.+?)(:\*)?\)$/.exec(text);
|
|
10
10
|
if (m === null || m[1] === undefined)
|
|
11
11
|
throw new Error(`contract fixture rule "${text}" is not a Bash(...) form`);
|
|
12
|
-
return { rule: text, tool: "Bash", match: m[2] !== undefined ? "prefix" : "exact", command: m[1], scope, adds };
|
|
12
|
+
return { behavior, rule: text, tool: "Bash", match: m[2] !== undefined ? "prefix" : "exact", command: m[1], scope, adds };
|
|
13
13
|
}
|
|
14
|
-
function tomb(text, removed, deletedBy, scope = GLOBAL) {
|
|
14
|
+
function tomb(text, removed, deletedBy, scope = GLOBAL, behavior = "allow") {
|
|
15
15
|
return {
|
|
16
|
+
behavior,
|
|
16
17
|
rule: text,
|
|
17
18
|
scope,
|
|
18
19
|
removedDots: removed.map(([actor, counter]) => ({ actor, counter })),
|
|
@@ -25,10 +26,10 @@ function state(rules = [], tombstones = []) {
|
|
|
25
26
|
function liveDots(s) {
|
|
26
27
|
const out = new Set();
|
|
27
28
|
for (const r of s.rules) {
|
|
28
|
-
const removed = s.tombstones.filter((t) => t.rule === r.rule && sameScope(t.scope, r.scope)).flatMap((t) => t.removedDots);
|
|
29
|
+
const removed = s.tombstones.filter((t) => t.behavior === r.behavior && t.rule === r.rule && sameScope(t.scope, r.scope)).flatMap((t) => t.removedDots);
|
|
29
30
|
for (const a of r.adds) {
|
|
30
31
|
if (!removed.some((d) => d.actor === a.dot.actor && d.counter === a.dot.counter))
|
|
31
|
-
out.add(`${r.rule}|${a.dot.actor}#${a.dot.counter}`);
|
|
32
|
+
out.add(`${r.behavior === "allow" ? "" : r.behavior + ":"}${r.rule}|${a.dot.actor}#${a.dot.counter}`);
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
return out;
|
|
@@ -41,7 +42,7 @@ function sameScope(a, b) {
|
|
|
41
42
|
return b.kind === "project" && a.root === b.root;
|
|
42
43
|
}
|
|
43
44
|
function allDots(s) {
|
|
44
|
-
return new Set(s.rules.flatMap((r) => r.adds.map((a) => `${r.rule}|${a.dot.actor}#${a.dot.counter}`)));
|
|
45
|
+
return new Set(s.rules.flatMap((r) => r.adds.map((a) => `${r.behavior === "allow" ? "" : r.behavior + ":"}${r.rule}|${a.dot.actor}#${a.dot.counter}`)));
|
|
45
46
|
}
|
|
46
47
|
export async function permissionRuleSyncContract(hooks = {}) {
|
|
47
48
|
const join = hooks.join ?? joinRuleStates;
|
|
@@ -141,8 +142,24 @@ export async function permissionRuleSyncContract(hooks = {}) {
|
|
|
141
142
|
assert.deepStrictEqual(new Set(ab.tombstones[0]?.removedDots.map((d) => `${d.actor}#${d.counter}`)), new Set(["a#1", "a#2"]), "the union — a one-sided pick can drop an observed removal");
|
|
142
143
|
assert.deepStrictEqual(ab, join(b, a));
|
|
143
144
|
});
|
|
145
|
+
run("behavior is an identity member: the same text as an allow and as a deny are two rows, each with its own adds and its own tombstone", async () => {
|
|
146
|
+
const both = state([rule("Bash(ls)", [add("a", 1)]), rule("Bash(ls)", [add("a", 2)], GLOBAL, "deny")], [tomb("Bash(ls)", [["a", 1]], ["a", 3])]);
|
|
147
|
+
const merged = join(both, state());
|
|
148
|
+
assert.strictEqual(merged.rules.length, 2, "one text, two behaviors — two identity groups, never folded");
|
|
149
|
+
assert.deepStrictEqual(liveDots(merged), new Set(["deny:Bash(ls)|a#2"]), "the allow tombstone must not reach the deny's add");
|
|
150
|
+
assert.deepStrictEqual(join(state(), both), merged, "commutative across the behavior axis");
|
|
151
|
+
const badWord = state([rule("Bash(pwd)", [add("b", 1)])], [Object.assign(tomb("Bash(pwd)", [["b", 1]], ["b", 2]), { behavior: "maybe" })]);
|
|
152
|
+
const screened = join(badWord, state());
|
|
153
|
+
assert.strictEqual(screened.tombstones.length, 0, "an unreadable behavior word on a tombstone must not land");
|
|
154
|
+
assert.ok(liveDots(screened).has("Bash(pwd)|b#1"), "…and must not cover anything");
|
|
155
|
+
});
|
|
156
|
+
run("closed-set identity: a row carrying a behavior word outside the closed set is refused like invalid text", async () => {
|
|
157
|
+
const forgedWord = Object.assign(rule("Bash(ls)", [add("z", 1)]), { behavior: "sometimes" });
|
|
158
|
+
const merged = join(state([forgedWord]), state());
|
|
159
|
+
assert.strictEqual(allDots(merged).size, 0, "a behavior this engine cannot read must not become an allow (or a deny) by falling through");
|
|
160
|
+
});
|
|
144
161
|
run("metadata re-projection (r2-F2): a record whose stored fields contradict its rule text is refused, both orders", async () => {
|
|
145
|
-
const forged = { rule: "Bash(ls)", tool: "Bash", match: "prefix", command: "rm -rf /", scope: GLOBAL, adds: [add("z", 1)] };
|
|
162
|
+
const forged = { behavior: "allow", rule: "Bash(ls)", tool: "Bash", match: "prefix", command: "rm -rf /", scope: GLOBAL, adds: [add("z", 1)] };
|
|
146
163
|
const clean = state([rule("Bash(git status)", [add("a", 1)])]);
|
|
147
164
|
const ab = join(state([forged]), clean);
|
|
148
165
|
const ba = join(clean, state([forged]));
|
|
@@ -150,9 +167,11 @@ export async function permissionRuleSyncContract(hooks = {}) {
|
|
|
150
167
|
assert.deepStrictEqual(ab, ba, "refusal must preserve commutativity");
|
|
151
168
|
assert.ok(allDots(ab).has("Bash(git status)|a#1"), "refusing one record must not disturb the rest");
|
|
152
169
|
});
|
|
153
|
-
run("single-validator door: an illegal
|
|
170
|
+
run("single-validator door: an illegal ALLOW shape (bare-interpreter prefix) cannot ride a join — while the same text as a DENY is a legitimate wide tightening and lands", async () => {
|
|
154
171
|
const merged = join(state([rule("Bash(node:*)", [add("z", 1)])]), state());
|
|
155
172
|
assert.strictEqual(allDots(merged).size, 0, "Bash(node:*) must be refused by the shared validator inside the join");
|
|
173
|
+
const denied = join(state([rule("Bash(node:*)", [add("z", 1)], GLOBAL, "deny")]), state());
|
|
174
|
+
assert.deepStrictEqual(allDots(denied), new Set(["deny:Bash(node:*)|z#1"]), "the tightening floor admits the interpreter-headed prefix");
|
|
156
175
|
});
|
|
157
176
|
run("durable two-member face (design/382 §4.3): a session-scope row or tombstone never survives a join, and never poisons the round", async () => {
|
|
158
177
|
const sessionScope = { kind: "session", sessionId: "sess-1" };
|
|
@@ -168,8 +187,8 @@ export async function permissionRuleSyncContract(hooks = {}) {
|
|
|
168
187
|
const scopeA = { kind: "project", root: "/x" };
|
|
169
188
|
const scopeB = { kind: "project", root: "/x a" };
|
|
170
189
|
const s = state([rule("Bash(ls)", [add("v", 1)], scopeA), rule("Bash(ls)", [add("w", 1)], scopeB)], [
|
|
171
|
-
{ rule: "Bash(ls)", scope: scopeA, removedDots: [{ actor: "v", counter: 1 }], deletedBy: { actor: "a 1", counter: 2 } },
|
|
172
|
-
{ rule: "Bash(ls)", scope: scopeB, removedDots: [{ actor: "w", counter: 1 }], deletedBy: { actor: "1", counter: 2 } },
|
|
190
|
+
{ behavior: "allow", rule: "Bash(ls)", scope: scopeA, removedDots: [{ actor: "v", counter: 1 }], deletedBy: { actor: "a 1", counter: 2 } },
|
|
191
|
+
{ behavior: "allow", rule: "Bash(ls)", scope: scopeB, removedDots: [{ actor: "w", counter: 1 }], deletedBy: { actor: "1", counter: 2 } },
|
|
173
192
|
]);
|
|
174
193
|
const merged = join(s, state());
|
|
175
194
|
assert.strictEqual(merged.tombstones.length, 2, "two distinct delete intents must both survive — a colliding key folds one away");
|
|
@@ -396,6 +396,13 @@ export declare class RosterBuilder {
|
|
|
396
396
|
/** Which source a `TaskSpec.tools` entry mounts as: a definition carrying a CORE contract is a built-in the host chose
|
|
397
397
|
* to mount; anything else is the caller's own tool. Identity follows the object, never the name. */
|
|
398
398
|
export declare function callerMountSource(tool: AgentTool): RosterSource;
|
|
399
|
+
/** The card namespace a roster row owns (design/388 §2.2 ④) — ONE mint, read by the row and by the prepare door that
|
|
400
|
+
* judges a caller's `renderHints.resultCards` against it: `builtin:<name>` for engine-owned definitions,
|
|
401
|
+
* `<mcp|a2a>:<peer>/<tool>` for protocol tools, `<caller|external>:<contractId>` for everything the caller mounts. */
|
|
402
|
+
export declare function rosterCardId(source: RosterSource, toolName: string, peer: string | null, protocolTool: string, contractId: string): string;
|
|
403
|
+
/** The card namespace a `TaskSpec.tools` entry will own once mounted — the same mint the row makes, so the prepare
|
|
404
|
+
* door and the roster row cannot disagree about which namespace a caller card must live under. */
|
|
405
|
+
export declare function callerCardIdOf(tool: Pick<AgentTool, "name" | "parameters">): string;
|
|
399
406
|
/** One row (design/388 §2.5). Every axis is read off the instance or the leg facts; nothing off a name table. */
|
|
400
407
|
export declare function mintRosterEntry(tool: AgentTool, meta: RosterMountMeta, facts: RosterSnapshotFacts): ToolRosterEntry;
|
|
401
408
|
/**
|
|
@@ -414,6 +421,8 @@ export declare class ToolRosterDeltaSeat {
|
|
|
414
421
|
republish(): ToolRosterDelta | undefined;
|
|
415
422
|
/** Install the stream sink; anything published before is flushed in order. */
|
|
416
423
|
subscribe(sink: (delta: ToolRosterDelta) => void): void;
|
|
424
|
+
/** The leg ended: drop the sink so a late republish never reaches a closed stream (it buffers, bounded, unread). */
|
|
425
|
+
unsubscribe(): void;
|
|
417
426
|
/** Take the buffered deltas (a consumer that polls instead of subscribing). */
|
|
418
427
|
drain(): ToolRosterDelta[];
|
|
419
428
|
}
|
package/dist/core/tool-roster.js
CHANGED
|
@@ -335,17 +335,24 @@ export class RosterBuilder {
|
|
|
335
335
|
export function callerMountSource(tool) {
|
|
336
336
|
return getToolContract(tool, "caller").origin === "core" ? "builtin" : "caller";
|
|
337
337
|
}
|
|
338
|
+
export function rosterCardId(source, toolName, peer, protocolTool, contractId) {
|
|
339
|
+
return source === "builtin" || source === "synthetic"
|
|
340
|
+
? `builtin:${toolName}`
|
|
341
|
+
: source === "mcp" || source === "a2a"
|
|
342
|
+
? `${source}:${peer}/${protocolTool}`
|
|
343
|
+
: `${source === "external" ? "external" : "caller"}:${contractId}`;
|
|
344
|
+
}
|
|
345
|
+
export function callerCardIdOf(tool) {
|
|
346
|
+
const t = tool;
|
|
347
|
+
return rosterCardId(callerMountSource(t), t.name, null, t.name, getToolContract(t, "caller").contractId);
|
|
348
|
+
}
|
|
338
349
|
export function mintRosterEntry(tool, meta, facts) {
|
|
339
350
|
const face = faceOf(tool);
|
|
340
351
|
const fallbackOrigin = meta.source === "builtin" ? "core" : meta.source === "synthetic" ? "synthetic" : meta.source === "mcp" || meta.source === "a2a" ? meta.source : "caller";
|
|
341
352
|
const d = getToolContract(tool, fallbackOrigin);
|
|
342
353
|
const peer = meta.origin?.peer ?? null;
|
|
343
354
|
const protocolTool = peer !== null ? protocolOf(tool.name)?.parse(tool.name)?.tool ?? tool.name : tool.name;
|
|
344
|
-
const cardId = meta.source
|
|
345
|
-
? `builtin:${tool.name}`
|
|
346
|
-
: meta.source === "mcp" || meta.source === "a2a"
|
|
347
|
-
? `${meta.source}:${peer}/${protocolTool}`
|
|
348
|
-
: `${meta.source === "external" ? "external" : "caller"}:${d.contractId}`;
|
|
355
|
+
const cardId = rosterCardId(meta.source, tool.name, peer, protocolTool, d.contractId);
|
|
349
356
|
const { keys: inputKeys } = inputKeysOf(tool.parameters);
|
|
350
357
|
const declaredEffect = face.effect;
|
|
351
358
|
const foldedEffect = facts.effects.get(tool.name);
|
|
@@ -431,6 +438,9 @@ export class ToolRosterDeltaSeat {
|
|
|
431
438
|
for (const d of pending)
|
|
432
439
|
sink(d);
|
|
433
440
|
}
|
|
441
|
+
unsubscribe() {
|
|
442
|
+
this.#sink = undefined;
|
|
443
|
+
}
|
|
434
444
|
drain() {
|
|
435
445
|
const pending = this.#pending;
|
|
436
446
|
this.#pending = [];
|
package/dist/index.d.ts
CHANGED
|
@@ -186,8 +186,8 @@ export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRul
|
|
|
186
186
|
* and nothing more. Removal is exported without ceremony, because narrowing on a user's behalf is
|
|
187
187
|
* allowed and widening is not.
|
|
188
188
|
*/
|
|
189
|
-
export {
|
|
190
|
-
export { removePersistedRule, applyTombstones, sameScope, isValidConsentScope, isValidDurableScope, InMemoryDurableRulePartition, EMPTY_DURABLE_RULE_PARTITION, type DurableRulePartition, type DurableRulePartitionProvider, type RuleWriteOutcome, type
|
|
189
|
+
export { RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, RULE_BEHAVIORS_BY_PRECEDENCE, isRuleBehavior, readRowBehavior, adjudicatePersistedRules, adjudicatePersistedPathRules, ruleReachesProgramRun, pathRuleReaches, ruleToolGrammarOf, COMMAND_RULE_TOOL, READ_RULE_TOOL, type PathRuleBases, type RuleBehavior, type PersistedRuleVerdict, parseRuleText, formatRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, renderUntrustedCommandText, stripFormatCharacters, UNCOVERED_SEGMENT_REASON_BASELINE, RULE_OFFERS_ABSENCE_BASELINE, type UncoveredSegmentDetail, type EditedRuleBreadthWarning, directoryRuleAdmits, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, type PersistedRule, type RuleTombstone, type RuleScope, type RuleDot, type RuleAdd, type RuleAddOrigin, type RuleOffer, type RuleOfferBatchMember, type SegmentRuleSuggestion, type SegmentCoverage, type RuleReject, type RuleRejectCode, type ParsedRule, type PersistedRuleTool, type PersistedRuleMatch, } from "./core/permission-rule-model.js";
|
|
190
|
+
export { removePersistedRule, applyTombstones, sameScope, sameRuleIdentity, isValidConsentScope, isValidDurableScope, InMemoryDurableRulePartition, EMPTY_DURABLE_RULE_PARTITION, type DurableRulePartition, type DurableRulePartitionProvider, type RuleWriteOutcome, type StoredRules, type RemoveResult, type PutResult, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, type RuleSyncState, type RuleSyncFrontier, type RuleSyncDrop, type RuleSyncLandingReport, type RuleOwner, type QuarantinedRuleAdd, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, type PermissionRuleWriter, type WritableDurableRulePartition, type RuleWriteDelta, type RuleAddDelta, type RuleDeleteDelta, type RuleSyncJoinDelta, type RawRuleSyncState, type RedemptionAuthorization, } from "./core/permission-rule-store.js";
|
|
191
191
|
export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, type PermissionRuleSyncTransport, type PermissionRuleSyncResult, type RuleSyncRequestBody, type RuleSyncResponseBody, } from "./core/permission-rule-sync.js";
|
|
192
192
|
export { createPermissionRuleStoreProvider, effectivePermissionRules, effectiveOrThrow, ruleSourceOf, type PermissionRuleStore, type PermissionRuleStoreProvider, type PermissionRuleStoreConfig, type EffectivePermissionRules, type EffectivePermissionRule, type RemovedPermissionRule, type RuleSource, } from "./core/permission-rule-provider.js";
|
|
193
193
|
export { InMemorySessionRulePartition, type SessionRulePartition, type SessionRuleAdd, type SessionRuleApplyResult, } from "./core/permission-rule-session.js";
|
package/dist/index.js
CHANGED
|
@@ -145,8 +145,8 @@ export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/au
|
|
|
145
145
|
export { AUTO_MODE_ARMING_RECIPE_VERSION, autoModeArmingRecipeOf, sanitizeAutoModeArmingRecipe, foldAutoModeArming, } from "./core/auto-mode-arming.js";
|
|
146
146
|
export { rebuildAutoModeDecider, } from "./core/auto-mode-rebuild.js";
|
|
147
147
|
export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, isNamespacedCoveringRuleName, namespacedRuleNameCovers, } from "./core/permission-rules.js";
|
|
148
|
-
export {
|
|
149
|
-
export { removePersistedRule, applyTombstones, sameScope, isValidConsentScope, isValidDurableScope, InMemoryDurableRulePartition, EMPTY_DURABLE_RULE_PARTITION, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, } from "./core/permission-rule-store.js";
|
|
148
|
+
export { RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, RULE_BEHAVIORS_BY_PRECEDENCE, isRuleBehavior, readRowBehavior, adjudicatePersistedRules, adjudicatePersistedPathRules, ruleReachesProgramRun, pathRuleReaches, ruleToolGrammarOf, COMMAND_RULE_TOOL, READ_RULE_TOOL, parseRuleText, formatRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, renderUntrustedCommandText, stripFormatCharacters, UNCOVERED_SEGMENT_REASON_BASELINE, RULE_OFFERS_ABSENCE_BASELINE, directoryRuleAdmits, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, } from "./core/permission-rule-model.js";
|
|
149
|
+
export { removePersistedRule, applyTombstones, sameScope, sameRuleIdentity, isValidConsentScope, isValidDurableScope, InMemoryDurableRulePartition, EMPTY_DURABLE_RULE_PARTITION, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, } from "./core/permission-rule-store.js";
|
|
150
150
|
export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, } from "./core/permission-rule-sync.js";
|
|
151
151
|
export { createPermissionRuleStoreProvider, effectivePermissionRules, effectiveOrThrow, ruleSourceOf, } from "./core/permission-rule-provider.js";
|
|
152
152
|
export { InMemorySessionRulePartition, } from "./core/permission-rule-session.js";
|
|
@@ -16,7 +16,7 @@ const KNOWN_CHECKPOINT_LEDGER_EVENTS = new Set(Object.keys(CHECKPOINT_LEDGER_EVE
|
|
|
16
16
|
function applyCheckpointEvent(cps, ev) {
|
|
17
17
|
if (!KNOWN_CHECKPOINT_LEDGER_EVENTS.has(ev.t)) {
|
|
18
18
|
throw new CheckpointError("checkpoint.unsupported_version", `file checkpoint ledger: unrecognised event kind "${String(ev.t)}" — ` +
|
|
19
|
-
`it was written by a newer worker; refusing the replay rather than serving an incomplete authority
|
|
19
|
+
`it was written by a newer worker; refusing the replay rather than serving an incomplete authority`, { reason: "ledger_event_unknown" });
|
|
20
20
|
}
|
|
21
21
|
if (ev.t === "put") {
|
|
22
22
|
cps.set(ev.token, ev.cp);
|
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
* design/179 §5/§8 / design/389 — the file backend of the permission-rule store's DURABLE partition (one
|
|
3
3
|
* bucket file per owner), plus the file home of the ORG partition's durable state (`org.json`).
|
|
4
4
|
*
|
|
5
|
-
* ## Schema
|
|
5
|
+
* ## Schema 4 and the one-time migration
|
|
6
6
|
*
|
|
7
7
|
* Pre-389 files (schema 1/2) kept the org last-known-good snapshot as an `org` block INSIDE each
|
|
8
|
-
* principal's bucket file — deployment-wide policy stored per person. Schema 3
|
|
9
|
-
* state lives in one `org.json` beside the buckets. `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* principal's bucket file — deployment-wide policy stored per person. Schema 3 dropped that block; the org
|
|
9
|
+
* state lives in one `org.json` beside the buckets. Schema 4 adds the `behavior` member to every rule row,
|
|
10
|
+
* tombstone and quarantine row (the rule identity is the (behavior, text, scope) triple): a schema-3 row has
|
|
11
|
+
* none, and every row a schema-3 engine wrote was an allow, so the migration stamps `"allow"` on each — the
|
|
12
|
+
* ONE reading of the pre-field row (`readRowBehavior`), applied once, here, never at a read face.
|
|
13
|
+
* `FileDurableRulePartitionProvider` migrates every legacy bucket file (1, 2 or 3) ONCE at construction
|
|
14
|
+
* (under the writer lock: the migration is a write, and a live older writer over the same directory is
|
|
15
|
+
* refused rather than raced), folding a 1/2 file's `org` block into `org.json` by the anti-rollback rule
|
|
16
|
+
* (the highest high-water wins; an equal revision with different content keeps what is already installed
|
|
17
|
+
* and discloses). After that the reader accepts schema 4 ONLY — a 1/2/3/5 file is refused whole, loudly
|
|
18
|
+
* (zero rules + disclosure). There is no second read path for the old shapes: the migrator is the only
|
|
19
|
+
* code that reads them, and it runs once.
|
|
16
20
|
*
|
|
17
21
|
* ## Why this store builds its own symlink refusal
|
|
18
22
|
*
|
|
@@ -55,23 +59,23 @@
|
|
|
55
59
|
* and is not: it detects corruption, and it does not detect an editor, because an editor recomputes it.
|
|
56
60
|
* For this backend the trust is host = user, exactly as for the settings file it mirrors.
|
|
57
61
|
*/
|
|
58
|
-
import type {
|
|
59
|
-
import type { DurableRulePartition, DurableRulePartitionProvider, PermissionRuleWriter, QuarantinedRuleAdd, RuleOwner, RuleSyncFrontier,
|
|
62
|
+
import type { PersistedRule, RuleTombstone } from "../../core/permission-rule-model.js";
|
|
63
|
+
import type { DurableRulePartition, DurableRulePartitionProvider, PermissionRuleWriter, QuarantinedRuleAdd, RuleOwner, RuleSyncFrontier, StoredRules, WritableDurableRulePartition } from "../../core/permission-rule-store.js";
|
|
60
64
|
import { PERMISSION_RULE_WRITER } from "../../core/permission-rule-store.js";
|
|
61
65
|
import type { OrgRuleStatePersistence, PersistedOrgRuleState } from "../../core/permission-rule-org.js";
|
|
62
66
|
import type { StoreDurability, StoreFidelity } from "../../core/checkpoint-store.js";
|
|
63
67
|
/**
|
|
64
68
|
* The on-disk shape of one bucket. `schemaVersion` is the only field a future reader may rely on before
|
|
65
|
-
* validating. Schema
|
|
66
|
-
*
|
|
67
|
-
* The org state is NOT here — see {@link OrgStateFile}.
|
|
69
|
+
* validating. Schema 4: the five core fields plus the OPTIONAL `sync` block (observation vector + round
|
|
70
|
+
* timestamp) and `quarantined` area (rows moved out of the live view, bytes preserved); every rule,
|
|
71
|
+
* tombstone and quarantine row carries its `behavior`. The org state is NOT here — see {@link OrgStateFile}.
|
|
68
72
|
*/
|
|
69
73
|
interface RuleFile {
|
|
70
|
-
schemaVersion:
|
|
74
|
+
schemaVersion: 4;
|
|
71
75
|
actor: string;
|
|
72
76
|
counter: number;
|
|
73
77
|
rev: number;
|
|
74
|
-
rules:
|
|
78
|
+
rules: PersistedRule[];
|
|
75
79
|
tombstones: RuleTombstone[];
|
|
76
80
|
sync?: {
|
|
77
81
|
observedVector?: RuleSyncFrontier;
|
|
@@ -88,10 +92,10 @@ interface OrgStateFile {
|
|
|
88
92
|
}
|
|
89
93
|
/** The org partition's durable home in a rule directory. */
|
|
90
94
|
export declare const ORG_STATE_FILE = "org.json";
|
|
91
|
-
/** Written once the directory is known to hold only schema-
|
|
92
|
-
* scan for good: a schema 1/2 bucket that appears AFTERWARDS (an older engine writing after the
|
|
95
|
+
/** Written once the directory is known to hold only schema-4 buckets. Its presence ends the legacy
|
|
96
|
+
* scan for good: a schema 1/2/3 bucket that appears AFTERWARDS (an older engine writing after the
|
|
93
97
|
* cutover) is refused by the reader on every later construction too, never re-admitted by a restart. */
|
|
94
|
-
export declare const
|
|
98
|
+
export declare const SCHEMA_MARKER_FILE = ".rule-schema-4";
|
|
95
99
|
/** The anti-rollback contract's DESIGNED refusal (a lower high-water mark, or different content under
|
|
96
100
|
* the same revision) — distinguished by class from every other install failure, because the two are
|
|
97
101
|
* handled in opposite directions by the migration: a refusal means the block is superseded and may be
|
|
@@ -148,7 +152,7 @@ declare class FileDurableRulePartition implements WritableDurableRulePartition {
|
|
|
148
152
|
*/
|
|
149
153
|
private screenDurableRows;
|
|
150
154
|
private write;
|
|
151
|
-
list(): Promise<
|
|
155
|
+
list(): Promise<StoredRules>;
|
|
152
156
|
/** The quarantine area (design/182 §5.2/§8.3): introspection only — never part of `list()`. */
|
|
153
157
|
quarantined(): Promise<QuarantinedRuleAdd[]>;
|
|
154
158
|
/**
|
|
@@ -209,7 +213,8 @@ export declare class FileDurableRulePartitionProvider implements DurableRulePart
|
|
|
209
213
|
dataRoot?: string;
|
|
210
214
|
});
|
|
211
215
|
/**
|
|
212
|
-
* Fold every legacy bucket file in `dir` into schema
|
|
216
|
+
* Fold every legacy bucket file in `dir` into schema 4 (stamping `behavior: "allow"` on every pre-field
|
|
217
|
+
* row — the one pre-field reading) and a 1/2 file's `org` block into `org.json`. A no-op on a
|
|
213
218
|
* directory with no legacy file (zero writes, the lock untouched — the second construction over a
|
|
214
219
|
* migrated directory is byte-silent). A legacy file whose checksum or shape does not hold is NOT
|
|
215
220
|
* migrated: its bytes stay where they are for a person to look at, and the reader keeps refusing it
|
|
@@ -220,10 +225,10 @@ export declare class FileDurableRulePartitionProvider implements DurableRulePart
|
|
|
220
225
|
* shape — the old engine's buckets are refused whole until they are migrated.
|
|
221
226
|
*/
|
|
222
227
|
private migrateLegacyBuckets;
|
|
223
|
-
/** Publish the schema-
|
|
228
|
+
/** Publish the schema-4 completion marker (idempotent, atomic, no lock: its content never changes and
|
|
224
229
|
* a concurrent writer of the same bytes is harmless). A failure to write it is disclosed, not thrown —
|
|
225
230
|
* the next construction simply scans again, which is the safe direction. */
|
|
226
|
-
private
|
|
231
|
+
private writeSchemaMarker;
|
|
227
232
|
/** The org partition's durable home in this directory (design/182 §7.4 contract; `org.json`). Shares
|
|
228
233
|
* this provider's writer lock — an install is a write. */
|
|
229
234
|
orgStatePersistence(): OrgRuleStatePersistence;
|