@sema-agent/core 2.3.0 → 2.4.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/dist/agents/send-message-tool.d.ts +4 -0
- package/dist/agents/send-message-tool.js +37 -24
- package/dist/agents/subagent.js +275 -127
- package/dist/brain/errors.d.ts +1 -0
- package/dist/brain/errors.js +14 -0
- package/dist/brain/stream-engine.js +3 -3
- package/dist/core/context-edit.js +2 -1
- package/dist/core/runner/prepare-task.js +20 -1
- package/dist/core/runner/tool-output-projection.js +2 -1
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
- package/dist/core/store-contracts/contract-harness.d.ts +6 -0
- package/dist/core/store-contracts/contract-harness.js +16 -0
- package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
- package/dist/core/store-contracts/contract-kit-version.js +2 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
- package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
- package/dist/core/store-contracts/session-repo-contract.js +36 -0
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
- package/dist/core/task-notification.d.ts +2 -0
- package/dist/core/task-registry-agent.d.ts +4 -0
- package/dist/core/task-registry-agent.js +13 -0
- package/dist/core/task-registry-monitor.js +6 -6
- package/dist/core/task-registry-shared.d.ts +8 -2
- package/dist/core/task-registry-shared.js +1 -1
- package/dist/core/task-registry.d.ts +6 -0
- package/dist/core/task-registry.js +48 -4
- package/dist/core/tool-result-store.d.ts +3 -2
- package/dist/core/tool-result-store.js +12 -4
- package/dist/core/trace.d.ts +7 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/orchestration/builtin-workflows.d.ts +1 -1
- package/dist/orchestration/builtin-workflows.js +11 -2
- package/dist/orchestration/workflow-governance.d.ts +6 -1
- package/dist/orchestration/workflow-governance.js +24 -4
- package/dist/orchestration/workflow-primitives.js +7 -1
- package/dist/orchestration/workflow.d.ts +1 -0
- package/dist/orchestration/workflow.js +31 -2
- package/dist/tools/fs/fs-bash.d.ts +7 -1
- package/dist/tools/fs/fs-bash.js +51 -20
- package/dist/tools/fs/fs-read.js +22 -11
- package/dist/tools/fs/fs-search-tools.js +3 -3
- package/dist/tools/fs/fs-shared.d.ts +20 -7
- package/dist/tools/fs/fs-shared.js +17 -3
- package/dist/tools/fs/fs-write.js +4 -4
- package/dist/tools/fs/index.d.ts +2 -0
- package/dist/tools/fs/index.js +7 -1
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +10 -0
- package/dist/tools/fs/safety.js +15 -1
- package/dist/tools/monitor.js +18 -4
- package/dist/tools/web.js +6 -2
- package/dist/tools/worktree.js +46 -25
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream, } from "../internal/llm.js";
|
|
2
|
-
import { BrainError, classifyHttp } from "./errors.js";
|
|
2
|
+
import { BrainError, classifyHttp, describeNetworkError } from "./errors.js";
|
|
3
3
|
import { retryBackoffMs } from "./retry.js";
|
|
4
4
|
import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
5
5
|
import { createConnectController } from "./timeout.js";
|
|
@@ -139,7 +139,7 @@ export function runStreamingBrain(args) {
|
|
|
139
139
|
continue;
|
|
140
140
|
}
|
|
141
141
|
if (netErr)
|
|
142
|
-
throw new BrainError("network",
|
|
142
|
+
throw new BrainError("network", describeNetworkError(netErr));
|
|
143
143
|
const detail = r ? await r.text().catch(() => "") : "";
|
|
144
144
|
const status = r?.status ?? 0;
|
|
145
145
|
throw new BrainError(classifyHttp(status), `${httpLabel} HTTP ${status || "ERR"}: ${detail.slice(0, 500)}`, status);
|
|
@@ -250,7 +250,7 @@ export function runStreamingBrain(args) {
|
|
|
250
250
|
throw e;
|
|
251
251
|
failure = {
|
|
252
252
|
kind: "connection",
|
|
253
|
-
err: new BrainError("network", `mid-stream read failed: ${
|
|
253
|
+
err: new BrainError("network", `mid-stream read failed: ${describeNetworkError(e)}`),
|
|
254
254
|
};
|
|
255
255
|
break readLoop;
|
|
256
256
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens, estimateTokens } from "../internal/harness.js";
|
|
2
2
|
import { isToolResult } from "./message-utils.js";
|
|
3
|
+
import { offloadPagebackHint } from "./tool-result-store.js";
|
|
3
4
|
const CLEARED_MARKER = "[tool result cleared to save context]";
|
|
4
|
-
const refNote = (ref) =>
|
|
5
|
+
const refNote = (ref) => offloadPagebackHint(ref, "cleared");
|
|
5
6
|
const mediaNote = (blocks) => {
|
|
6
7
|
const byType = new Map();
|
|
7
8
|
for (const b of blocks) {
|
|
@@ -435,10 +435,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
435
435
|
const offloadStore = rawOffloadStore instanceof RunnerSharedToolResultStore
|
|
436
436
|
? new ScopedToolResultStore(rawOffloadStore, taskScope)
|
|
437
437
|
: rawOffloadStore;
|
|
438
|
+
const offloadReachableToolsRef = {};
|
|
438
439
|
const maybeOffload = (tool, perTool) => {
|
|
439
440
|
if (!offloadStore || perTool?.offload === false)
|
|
440
441
|
return tool;
|
|
441
|
-
return withToolResultOffload(tool, offloadStore, perTool?.offloadThresholdChars ?? offloadThreshold, sessionId);
|
|
442
|
+
return withToolResultOffload(tool, offloadStore, perTool?.offloadThresholdChars ?? offloadThreshold, sessionId, () => offloadReachableToolsRef.current?.());
|
|
442
443
|
};
|
|
443
444
|
const firstPartyOffload = (tool) => {
|
|
444
445
|
const policy = firstPartyOffloadPolicy(tool.name);
|
|
@@ -1123,6 +1124,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1123
1124
|
phase: "config",
|
|
1124
1125
|
sessionId,
|
|
1125
1126
|
});
|
|
1127
|
+
emitTrace(deps.tracer, () => ({
|
|
1128
|
+
kind: "config.additional_directory_skipped",
|
|
1129
|
+
version: 1,
|
|
1130
|
+
taskId: hostTaskId,
|
|
1131
|
+
entry: dir,
|
|
1132
|
+
reason: `${c.error.code}: ${c.error.message}`,
|
|
1133
|
+
ts: Date.now(),
|
|
1134
|
+
}));
|
|
1126
1135
|
}
|
|
1127
1136
|
}
|
|
1128
1137
|
if (spec.envFacts?.scratchpadDir) {
|
|
@@ -1255,6 +1264,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1255
1264
|
...(internals?.parentNotify !== undefined ? { uplink: internals.parentNotify } : {}),
|
|
1256
1265
|
...(internals?.explicitAgentName !== undefined ? { senderName: internals.explicitAgentName } : {}),
|
|
1257
1266
|
...(internals?.parentRetainLedger !== undefined ? { siblingRetain: internals.parentRetainLedger } : {}),
|
|
1267
|
+
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
1268
|
+
...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
|
|
1258
1269
|
...(deps.rosterStore !== undefined ? { roster: deps.rosterStore } : {}),
|
|
1259
1270
|
...(deps.onBackgroundChildEvent ? { onBackgroundChildEvent: deps.onBackgroundChildEvent } : {}),
|
|
1260
1271
|
...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
|
|
@@ -1814,6 +1825,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1814
1825
|
if (deferred.has(n))
|
|
1815
1826
|
activeTools.add(n);
|
|
1816
1827
|
}
|
|
1828
|
+
offloadReachableToolsRef.current = () => {
|
|
1829
|
+
const s = new Set(tools.map((t) => t.name));
|
|
1830
|
+
for (const n of deferred)
|
|
1831
|
+
if (!activeTools.has(n))
|
|
1832
|
+
s.delete(n);
|
|
1833
|
+
s.add(TOOL_SEARCH_NAME);
|
|
1834
|
+
return s;
|
|
1835
|
+
};
|
|
1817
1836
|
let toolSearch;
|
|
1818
1837
|
const buildToolList = (active) => {
|
|
1819
1838
|
const list = tools.map((t) => (deferred.has(t.name) && !active.has(t.name) ? placeholders.get(t.name) : t));
|
|
@@ -57,10 +57,11 @@ export const toolOutputFrom = (result) => {
|
|
|
57
57
|
return { output: raw, truncated: true, totalChars };
|
|
58
58
|
};
|
|
59
59
|
const CC_DETAIL_TYPES = new Set([
|
|
60
|
-
"edit", "multiedit", "create", "update", "bash", "notebook-edit", "notebook", "text", "grep", "glob", "mcp",
|
|
60
|
+
"edit", "multiedit", "create", "update", "bash", "notebook-edit", "notebook", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
|
|
61
61
|
"agent", "task", "task-list", "task-output", "memory-saved", "workflow-run",
|
|
62
62
|
"web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
|
|
63
63
|
"task-stop", "tool-search", "memory-recall", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
|
|
64
|
+
"monitor-start", "path_not_in_root",
|
|
64
65
|
]);
|
|
65
66
|
export const structuredFrom = (result) => {
|
|
66
67
|
const details = result !== null && typeof result === "object" ? result.details : undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Checkpoint, type CheckpointStore, type ResumeOutcome } from "../checkpoint-store.js";
|
|
2
|
+
import { type ContractAssertionRunner } from "./contract-harness.js";
|
|
3
|
+
export declare function createCheckpointFixture(over?: Partial<Checkpoint>): Checkpoint;
|
|
4
|
+
export declare const ALLOW: Extract<ResumeOutcome, {
|
|
5
|
+
gate: "policy_ask";
|
|
6
|
+
}>;
|
|
7
|
+
export declare function checkpointStoreContract(make: () => CheckpointStore, runAssertion?: ContractAssertionRunner): Promise<void>;
|
|
8
|
+
export declare function checkpointListByScopeSummaries(store: CheckpointStore): Promise<import("../checkpoint-store.js").CheckpointSummary[]>;
|
|
9
|
+
export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
|
|
10
|
+
token: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
scope: string;
|
|
13
|
+
gateKind: string;
|
|
14
|
+
severity: number;
|
|
15
|
+
deadline: number;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
sourceTaskId: string;
|
|
18
|
+
principal: string;
|
|
19
|
+
toolCallId: string;
|
|
20
|
+
toolName: string;
|
|
21
|
+
toolInput: string;
|
|
22
|
+
spentMicroUsd?: undefined;
|
|
23
|
+
} | {
|
|
24
|
+
token: string;
|
|
25
|
+
sessionId: string;
|
|
26
|
+
scope: string;
|
|
27
|
+
gateKind: string;
|
|
28
|
+
spentMicroUsd: number;
|
|
29
|
+
createdAt: number;
|
|
30
|
+
severity?: undefined;
|
|
31
|
+
deadline?: undefined;
|
|
32
|
+
sourceTaskId?: undefined;
|
|
33
|
+
principal?: undefined;
|
|
34
|
+
toolCallId?: undefined;
|
|
35
|
+
toolName?: undefined;
|
|
36
|
+
toolInput?: undefined;
|
|
37
|
+
})[];
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { mintCheckpointToken, } from "../checkpoint-store.js";
|
|
3
|
+
import { beginContract } from "./contract-harness.js";
|
|
4
|
+
export function createCheckpointFixture(over = {}) {
|
|
5
|
+
const token = over.token ?? mintCheckpointToken();
|
|
6
|
+
return {
|
|
7
|
+
token,
|
|
8
|
+
scope: "tenant-a",
|
|
9
|
+
sessionId: "sess-1",
|
|
10
|
+
leafId: "leaf-1",
|
|
11
|
+
gate: { kind: "human", reason: "approve", toolName: "Write" },
|
|
12
|
+
pendingAction: {
|
|
13
|
+
kind: "tool_approval",
|
|
14
|
+
toolCallId: "call-3",
|
|
15
|
+
toolName: "Write",
|
|
16
|
+
args: { path: "/x", content: "y" },
|
|
17
|
+
boundInputHash: "h0",
|
|
18
|
+
batchToolCallIds: ["call-3"],
|
|
19
|
+
completedCallIds: [],
|
|
20
|
+
},
|
|
21
|
+
state: {
|
|
22
|
+
activeTools: [],
|
|
23
|
+
nestedStats: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0 },
|
|
24
|
+
},
|
|
25
|
+
status: "pending",
|
|
26
|
+
createdAt: 1_700_000_000_000,
|
|
27
|
+
...over,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export const ALLOW = {
|
|
31
|
+
gate: "policy_ask",
|
|
32
|
+
decision: "allow",
|
|
33
|
+
boundCallId: "call-3",
|
|
34
|
+
boundInputHash: "h0",
|
|
35
|
+
};
|
|
36
|
+
export async function checkpointStoreContract(make, runAssertion) {
|
|
37
|
+
const { run, settle } = beginContract(runAssertion);
|
|
38
|
+
run("kit prerequisites: reopen/setPendingSteer/listByScope are implemented (REQUIRED by this kit)", async () => {
|
|
39
|
+
const probe = make();
|
|
40
|
+
const missing = ["reopen", "setPendingSteer", "listByScope"].filter((m) => typeof probe[m] !== "function");
|
|
41
|
+
assert.equal(missing.length, 0, `backend does not implement ${missing.join(", ")} — required for the CheckpointStore contract kit`);
|
|
42
|
+
});
|
|
43
|
+
run("put create-once → already_exists", async () => {
|
|
44
|
+
const store = make();
|
|
45
|
+
const cp = createCheckpointFixture();
|
|
46
|
+
await store.put(cp.token, cp);
|
|
47
|
+
await assert.rejects(store.put(cp.token, cp), (e) => e.code === "checkpoint.already_exists");
|
|
48
|
+
});
|
|
49
|
+
run("resolve is an atomic CAS: first wins, double-resume false", async () => {
|
|
50
|
+
const store = make();
|
|
51
|
+
const cp = createCheckpointFixture();
|
|
52
|
+
await store.put(cp.token, cp);
|
|
53
|
+
assert.equal(await store.resolve(cp.token, cp.scope, ALLOW), true);
|
|
54
|
+
assert.equal(await store.resolve(cp.token, cp.scope, ALLOW), false);
|
|
55
|
+
assert.equal((await store.get(cp.token)).status, "resolved");
|
|
56
|
+
});
|
|
57
|
+
run("wrong-scope resolve never wins (multi-tenant isolation)", async () => {
|
|
58
|
+
const store = make();
|
|
59
|
+
const cp = createCheckpointFixture({ scope: "tenant-a" });
|
|
60
|
+
await store.put(cp.token, cp);
|
|
61
|
+
assert.equal(await store.resolve(cp.token, "tenant-b", ALLOW), false);
|
|
62
|
+
assert.equal((await store.get(cp.token)).status, "pending");
|
|
63
|
+
});
|
|
64
|
+
run("resolve records the winner; reopen(env_failed) preserves it + bumps rev", async () => {
|
|
65
|
+
const store = make();
|
|
66
|
+
const cp = createCheckpointFixture();
|
|
67
|
+
await store.put(cp.token, cp);
|
|
68
|
+
await store.resolve(cp.token, cp.scope, ALLOW);
|
|
69
|
+
const winner = (await store.get(cp.token)).resolvedOutcome;
|
|
70
|
+
assert.equal(winner.boundCallId, "call-3");
|
|
71
|
+
assert.equal(winner.decision, "allow");
|
|
72
|
+
assert.equal(await store.reopen(cp.token, cp.scope, "env_failed"), true);
|
|
73
|
+
const got = await store.get(cp.token);
|
|
74
|
+
assert.equal(got.status, "pending");
|
|
75
|
+
assert.equal(got.reopenReason, "env_failed");
|
|
76
|
+
assert.equal(got.resolvedOutcome.boundCallId, "call-3");
|
|
77
|
+
assert.equal(got.rev, 2);
|
|
78
|
+
});
|
|
79
|
+
run("rev OCC: a stale-rev resolve loses, the current-rev resolve wins", async () => {
|
|
80
|
+
const store = make();
|
|
81
|
+
const cp = createCheckpointFixture();
|
|
82
|
+
await store.put(cp.token, cp);
|
|
83
|
+
await store.resolve(cp.token, cp.scope, ALLOW);
|
|
84
|
+
await store.reopen(cp.token, cp.scope, "env_failed");
|
|
85
|
+
assert.equal(await store.resolve(cp.token, cp.scope, ALLOW, { rev: 0 }), false);
|
|
86
|
+
assert.equal(await store.resolve(cp.token, cp.scope, ALLOW, { rev: 2 }), true);
|
|
87
|
+
});
|
|
88
|
+
run("setPendingSteer rejects a </system-reminder> variant BEFORE mutation (byte-identical error)", async () => {
|
|
89
|
+
const store = make();
|
|
90
|
+
const cp = createCheckpointFixture();
|
|
91
|
+
await store.put(cp.token, cp);
|
|
92
|
+
await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "</SYSTEM-REMINDER>", trusted: true }), (e) => e.code === "steering.invalid_content");
|
|
93
|
+
assert.equal((await store.get(cp.token)).state.pendingSteer, undefined);
|
|
94
|
+
assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "go", trusted: false }), true);
|
|
95
|
+
assert.deepEqual((await store.get(cp.token)).state.pendingSteer, { text: "go", trusted: false });
|
|
96
|
+
});
|
|
97
|
+
run("expire vs resolve on the same row → exactly one wins; reap expires past-deadline pending in scope", async () => {
|
|
98
|
+
const store = make();
|
|
99
|
+
const a = createCheckpointFixture();
|
|
100
|
+
await store.put(a.token, a);
|
|
101
|
+
const [e, r] = await Promise.all([store.expire(a.token, a.scope), store.resolve(a.token, a.scope, ALLOW)]);
|
|
102
|
+
assert.equal([e, r].filter(Boolean).length, 1);
|
|
103
|
+
const b = createCheckpointFixture({ deadline: 1000 });
|
|
104
|
+
await store.put(b.token, b);
|
|
105
|
+
assert.equal(await store.reap(b.scope, 2000), 1);
|
|
106
|
+
assert.equal(await store.reap(b.scope, 2000), 0);
|
|
107
|
+
assert.equal((await store.get(b.token)).status, "expired");
|
|
108
|
+
});
|
|
109
|
+
run("listByScope (seam #1): only pending in-scope rows; resolved + other-scope excluded; empty → []", async () => {
|
|
110
|
+
const store = make();
|
|
111
|
+
assert.deepEqual(await store.listByScope("tenant-a"), []);
|
|
112
|
+
const pending = createCheckpointFixture({ scope: "tenant-a", sessionId: "p" });
|
|
113
|
+
const resolved = createCheckpointFixture({ scope: "tenant-a", sessionId: "r" });
|
|
114
|
+
const other = createCheckpointFixture({ scope: "tenant-b", sessionId: "o" });
|
|
115
|
+
for (const cp of [pending, resolved, other])
|
|
116
|
+
await store.put(cp.token, cp);
|
|
117
|
+
await store.resolve(resolved.token, resolved.scope, ALLOW);
|
|
118
|
+
const list = await store.listByScope("tenant-a");
|
|
119
|
+
assert.equal(list.length, 1);
|
|
120
|
+
assert.equal(list[0].sessionId, "p");
|
|
121
|
+
assert.equal(list[0].token, pending.token);
|
|
122
|
+
assert.equal((await store.get(pending.token)).status, "pending");
|
|
123
|
+
});
|
|
124
|
+
await settle();
|
|
125
|
+
}
|
|
126
|
+
const sortByToken = (a, b) => a.token.localeCompare(b.token);
|
|
127
|
+
export async function checkpointListByScopeSummaries(store) {
|
|
128
|
+
if (typeof store.listByScope !== "function") {
|
|
129
|
+
throw new Error("backend does not implement listByScope — required for checkpointListByScopeSummaries");
|
|
130
|
+
}
|
|
131
|
+
const escalation = createCheckpointFixture({
|
|
132
|
+
token: "tok-escalation",
|
|
133
|
+
scope: "tenant-a",
|
|
134
|
+
sessionId: "needs-approval",
|
|
135
|
+
deadline: 1_999_999_999_999,
|
|
136
|
+
createdAt: 1_700_000_000_000,
|
|
137
|
+
sourceTaskId: "needs-approval",
|
|
138
|
+
principal: "alice@corp",
|
|
139
|
+
gate: {
|
|
140
|
+
kind: "human",
|
|
141
|
+
reason: "approve",
|
|
142
|
+
toolName: "open_pr",
|
|
143
|
+
riskDescriptor: { severity: 5, axes: { egress: true, irreversible: true }, toolName: "open_pr" },
|
|
144
|
+
},
|
|
145
|
+
pendingAction: {
|
|
146
|
+
kind: "tool_approval",
|
|
147
|
+
toolCallId: "call-pr",
|
|
148
|
+
toolName: "open_pr",
|
|
149
|
+
args: { repo: "x" },
|
|
150
|
+
boundInputHash: "h0",
|
|
151
|
+
batchToolCallIds: ["call-pr"],
|
|
152
|
+
completedCallIds: [],
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const resource = createCheckpointFixture({
|
|
156
|
+
token: "tok-resource",
|
|
157
|
+
scope: "tenant-a",
|
|
158
|
+
sessionId: "out-of-budget",
|
|
159
|
+
createdAt: 1_700_000_000_001,
|
|
160
|
+
gate: { kind: "resource_limit", reason: "budget" },
|
|
161
|
+
pendingAction: { kind: "resource_limit", reason: "budget" },
|
|
162
|
+
resourceLedger: { totalBudgetMicroUsd: 10_000_000, spentMicroUsd: 4_200_000, spentTokens: 9, spentTurns: 3, sliceCount: 2 },
|
|
163
|
+
});
|
|
164
|
+
const resolved = createCheckpointFixture({ token: "tok-resolved", scope: "tenant-a", sessionId: "done" });
|
|
165
|
+
const other = createCheckpointFixture({ token: "tok-other", scope: "tenant-b", sessionId: "x" });
|
|
166
|
+
const fixture = [escalation, resource, resolved, other];
|
|
167
|
+
for (const cp of fixture)
|
|
168
|
+
await store.put(cp.token, cp);
|
|
169
|
+
await store.resolve(resolved.token, resolved.scope, ALLOW);
|
|
170
|
+
return (await store.listByScope("tenant-a")).sort(sortByToken);
|
|
171
|
+
}
|
|
172
|
+
export const EXPECTED_LISTBYSCOPE_SUMMARIES = [
|
|
173
|
+
{
|
|
174
|
+
token: "tok-escalation",
|
|
175
|
+
sessionId: "needs-approval",
|
|
176
|
+
scope: "tenant-a",
|
|
177
|
+
gateKind: "human",
|
|
178
|
+
severity: 5,
|
|
179
|
+
deadline: 1_999_999_999_999,
|
|
180
|
+
createdAt: 1_700_000_000_000,
|
|
181
|
+
sourceTaskId: "needs-approval",
|
|
182
|
+
principal: "alice@corp",
|
|
183
|
+
toolCallId: "call-pr",
|
|
184
|
+
toolName: "open_pr",
|
|
185
|
+
toolInput: '{"repo":"x"}',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
token: "tok-resource",
|
|
189
|
+
sessionId: "out-of-budget",
|
|
190
|
+
scope: "tenant-a",
|
|
191
|
+
gateKind: "resource_limit",
|
|
192
|
+
spentMicroUsd: 4_200_000,
|
|
193
|
+
createdAt: 1_700_000_000_001,
|
|
194
|
+
},
|
|
195
|
+
];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ContractAssertionRunner = (name: string, fn: () => Promise<void>) => void | Promise<void>;
|
|
2
|
+
export declare function defaultSequentialRunner(_name: string, fn: () => Promise<void>): Promise<void>;
|
|
3
|
+
export declare function beginContract(runAssertion?: ContractAssertionRunner): {
|
|
4
|
+
run: (name: string, fn: () => Promise<void>) => void;
|
|
5
|
+
settle: () => Promise<void>;
|
|
6
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function defaultSequentialRunner(_name, fn) {
|
|
2
|
+
return fn();
|
|
3
|
+
}
|
|
4
|
+
export function beginContract(runAssertion = defaultSequentialRunner) {
|
|
5
|
+
const pending = [];
|
|
6
|
+
return {
|
|
7
|
+
run: (name, fn) => {
|
|
8
|
+
const result = runAssertion(name, fn);
|
|
9
|
+
if (result instanceof Promise)
|
|
10
|
+
pending.push(result);
|
|
11
|
+
},
|
|
12
|
+
settle: async () => {
|
|
13
|
+
await Promise.all(pending);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CONTRACT_KIT_ENGINE_VERSION: string;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { FileSnapshotStore } from "../file-snapshot-store.js";
|
|
2
|
+
import { type ContractAssertionRunner } from "./contract-harness.js";
|
|
3
|
+
export declare function fileSnapshotStoreContract(make: () => FileSnapshotStore, runAssertion?: ContractAssertionRunner): Promise<void>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { beginContract } from "./contract-harness.js";
|
|
4
|
+
const bytes = (s) => new TextEncoder().encode(s);
|
|
5
|
+
const sha256 = (s) => createHash("sha256").update(bytes(s)).digest("hex");
|
|
6
|
+
const srcBlob = async (hash) => hash === sha256("alpha") ? bytes("alpha") : hash === sha256("beta") ? bytes("beta") : undefined;
|
|
7
|
+
export async function fileSnapshotStoreContract(make, runAssertion) {
|
|
8
|
+
const { run, settle } = beginContract(runAssertion);
|
|
9
|
+
run("kit prerequisites: exportManifest/getBlob/putBlob/importManifest are implemented (REQUIRED by this kit)", async () => {
|
|
10
|
+
const probe = make();
|
|
11
|
+
const missing = ["exportManifest", "getBlob", "putBlob", "importManifest"].filter((m) => typeof probe[m] !== "function");
|
|
12
|
+
assert.equal(missing.length, 0, `backend does not implement ${missing.join(", ")} — required for the FileSnapshotStore contract kit`);
|
|
13
|
+
});
|
|
14
|
+
run("absent everything: has=false, listKeys=[], exportManifest=null, getBlob=undefined", async () => {
|
|
15
|
+
const store = make();
|
|
16
|
+
assert.equal(await store.has("sc", "k1"), false);
|
|
17
|
+
assert.deepEqual(await store.listKeys("sc"), []);
|
|
18
|
+
assert.equal(await store.exportManifest("sc", "k1"), null);
|
|
19
|
+
assert.equal(await store.getBlob(sha256("alpha")), undefined);
|
|
20
|
+
});
|
|
21
|
+
run("putBlob verifies content-address integrity: mismatched bytes → read_failed, nothing stored", async () => {
|
|
22
|
+
const store = make();
|
|
23
|
+
const r = await store.putBlob(sha256("beta"), bytes("alpha"));
|
|
24
|
+
assert.equal(r.ok, false);
|
|
25
|
+
if (!r.ok)
|
|
26
|
+
assert.equal(r.error.code, "read_failed");
|
|
27
|
+
assert.equal(await store.getBlob(sha256("beta")), undefined);
|
|
28
|
+
});
|
|
29
|
+
run("putBlob ok; a repeat putBlob for the same hash is an idempotent no-op; getBlob round-trips the bytes", async () => {
|
|
30
|
+
const store = make();
|
|
31
|
+
assert.deepEqual(await store.putBlob(sha256("alpha"), bytes("alpha")), { ok: true });
|
|
32
|
+
assert.deepEqual(await store.putBlob(sha256("alpha"), bytes("alpha")), { ok: true });
|
|
33
|
+
assert.deepEqual([...(await store.getBlob(sha256("alpha")))], [...bytes("alpha")]);
|
|
34
|
+
});
|
|
35
|
+
run("importManifest commits only after every blob verifies: has=true, exportManifest round-trips", async () => {
|
|
36
|
+
const store = make();
|
|
37
|
+
const manifest = new Map([["a.txt", sha256("alpha")], ["b.txt", sha256("beta")]]);
|
|
38
|
+
assert.deepEqual(await store.importManifest("sc", "k1", manifest, srcBlob), { ok: true });
|
|
39
|
+
assert.equal(await store.has("sc", "k1"), true);
|
|
40
|
+
const exported = await store.exportManifest("sc", "k1");
|
|
41
|
+
assert.deepEqual([...exported.entries()].sort(), [...manifest.entries()].sort());
|
|
42
|
+
});
|
|
43
|
+
run("importManifest is create-once: a second import for an existing key is a no-op (ok, zero fetches, manifest unchanged)", async () => {
|
|
44
|
+
const store = make();
|
|
45
|
+
const manifest = new Map([["a.txt", sha256("alpha")]]);
|
|
46
|
+
assert.deepEqual(await store.importManifest("sc", "k1", manifest, srcBlob), { ok: true });
|
|
47
|
+
const refetched = [];
|
|
48
|
+
const second = await store.importManifest("sc", "k1", new Map([["z.txt", sha256("beta")]]), async (h) => {
|
|
49
|
+
refetched.push(h);
|
|
50
|
+
return srcBlob(h);
|
|
51
|
+
});
|
|
52
|
+
assert.deepEqual(second, { ok: true });
|
|
53
|
+
assert.deepEqual(refetched, []);
|
|
54
|
+
assert.deepEqual([...(await store.exportManifest("sc", "k1")).entries()], [...manifest.entries()]);
|
|
55
|
+
});
|
|
56
|
+
run("importManifest is FAIL-CLOSED: a missing or hash-mismatched source blob → read_failed, no manifest committed", async () => {
|
|
57
|
+
const store = make();
|
|
58
|
+
const manifest = new Map([["c.txt", "0".repeat(64)]]);
|
|
59
|
+
const miss = await store.importManifest("sc", "k2", manifest, async () => undefined);
|
|
60
|
+
assert.equal(miss.ok, false);
|
|
61
|
+
if (!miss.ok)
|
|
62
|
+
assert.equal(miss.error.code, "read_failed");
|
|
63
|
+
assert.equal(await store.has("sc", "k2"), false);
|
|
64
|
+
const mism = await store.importManifest("sc", "k2", manifest, async () => bytes("wrong-bytes"));
|
|
65
|
+
assert.equal(mism.ok, false);
|
|
66
|
+
if (!mism.ok)
|
|
67
|
+
assert.equal(mism.error.code, "read_failed");
|
|
68
|
+
assert.equal(await store.has("sc", "k2"), false);
|
|
69
|
+
});
|
|
70
|
+
run("RB-361 a throwing source fetch → read_failed NAMING the blob hash, byte-identical wording (since 2.2.0)", async () => {
|
|
71
|
+
const store = make();
|
|
72
|
+
const hash = sha256("x");
|
|
73
|
+
const r = await store.importManifest("sc", "k3", new Map([["d.txt", hash]]), () => Promise.reject(new Error("transport down")));
|
|
74
|
+
assert.equal(r.ok, false);
|
|
75
|
+
if (!r.ok) {
|
|
76
|
+
assert.equal(r.error.code, "read_failed");
|
|
77
|
+
assert.equal(r.error.message, `source blob ${hash} fetch failed: transport down`);
|
|
78
|
+
}
|
|
79
|
+
assert.equal(await store.has("sc", "k3"), false);
|
|
80
|
+
});
|
|
81
|
+
run("RB-361 a path-escaping hash is refused as UNSAFE before any fetch, byte-identical wording (since 2.2.0)", async () => {
|
|
82
|
+
const store = make();
|
|
83
|
+
const fetched = [];
|
|
84
|
+
const r = await store.importManifest("sc", "k4", new Map([["e.txt", "../../escape"]]), async (h) => {
|
|
85
|
+
fetched.push(h);
|
|
86
|
+
return undefined;
|
|
87
|
+
});
|
|
88
|
+
assert.equal(r.ok, false);
|
|
89
|
+
if (!r.ok) {
|
|
90
|
+
assert.equal(r.error.code, "read_failed");
|
|
91
|
+
assert.equal(r.error.message, `unsafe blob hash "../../escape"`);
|
|
92
|
+
}
|
|
93
|
+
assert.deepEqual(fetched, []);
|
|
94
|
+
assert.equal(await store.has("sc", "k4"), false);
|
|
95
|
+
});
|
|
96
|
+
run("restore of a missing key → not_found (never a throw out of the seam)", async () => {
|
|
97
|
+
const store = make();
|
|
98
|
+
const stubEnv = {};
|
|
99
|
+
const r = await store.restore("sc", "nope", stubEnv, "/nonexistent-root");
|
|
100
|
+
assert.equal(r.ok, false);
|
|
101
|
+
if (!r.ok)
|
|
102
|
+
assert.equal(r.error.code, "not_found");
|
|
103
|
+
});
|
|
104
|
+
run("listKeys reflects only committed keys and is scope-isolated", async () => {
|
|
105
|
+
const store = make();
|
|
106
|
+
await store.importManifest("sc", "k1", new Map([["a.txt", sha256("alpha")]]), srcBlob);
|
|
107
|
+
await store.importManifest("sc", "k2", new Map([["c.txt", "0".repeat(64)]]), async () => undefined);
|
|
108
|
+
assert.deepEqual(((await store.listKeys("sc")) ?? []).slice().sort(), ["k1"]);
|
|
109
|
+
assert.deepEqual(await store.listKeys("other"), []);
|
|
110
|
+
});
|
|
111
|
+
run("reap keeps listed keys and their blobs", async () => {
|
|
112
|
+
const store = make();
|
|
113
|
+
await store.importManifest("sc", "k1", new Map([["a.txt", sha256("alpha")]]), srcBlob);
|
|
114
|
+
assert.equal(await store.reap("sc", ["k1"]), 0);
|
|
115
|
+
assert.deepEqual(await store.listKeys("sc"), ["k1"]);
|
|
116
|
+
assert.deepEqual([...(await store.getBlob(sha256("alpha")))], [...bytes("alpha")]);
|
|
117
|
+
});
|
|
118
|
+
run("reap keep-nothing drops the key and GCs the now-unreferenced blobs", async () => {
|
|
119
|
+
const store = make();
|
|
120
|
+
await store.importManifest("sc", "k1", new Map([["a.txt", sha256("alpha")]]), srcBlob);
|
|
121
|
+
assert.equal(await store.reap("sc", []), 1);
|
|
122
|
+
assert.equal(await store.has("sc", "k1"), false);
|
|
123
|
+
assert.equal(await store.getBlob(sha256("alpha")), undefined);
|
|
124
|
+
});
|
|
125
|
+
await settle();
|
|
126
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MailboxStore } from "../mailbox-store.js";
|
|
2
|
+
import { type ContractAssertionRunner } from "./contract-harness.js";
|
|
3
|
+
export declare const MAILBOX_CONTRACT_SCOPE = "default";
|
|
4
|
+
export declare function mailboxStoreContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>;
|
|
5
|
+
export declare function mailboxAckOwnershipContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>;
|
|
6
|
+
export declare function mailboxBundledOnlyContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>;
|