@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.
Files changed (61) hide show
  1. package/dist/agents/send-message-tool.d.ts +4 -0
  2. package/dist/agents/send-message-tool.js +37 -24
  3. package/dist/agents/subagent.js +275 -127
  4. package/dist/brain/errors.d.ts +1 -0
  5. package/dist/brain/errors.js +14 -0
  6. package/dist/brain/stream-engine.js +3 -3
  7. package/dist/core/context-edit.js +2 -1
  8. package/dist/core/runner/prepare-task.js +20 -1
  9. package/dist/core/runner/tool-output-projection.js +2 -1
  10. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
  11. package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
  12. package/dist/core/store-contracts/contract-harness.d.ts +6 -0
  13. package/dist/core/store-contracts/contract-harness.js +16 -0
  14. package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
  15. package/dist/core/store-contracts/contract-kit-version.js +2 -0
  16. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
  17. package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
  18. package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
  19. package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
  20. package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
  21. package/dist/core/store-contracts/session-repo-contract.js +36 -0
  22. package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
  23. package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
  24. package/dist/core/task-notification.d.ts +2 -0
  25. package/dist/core/task-registry-agent.d.ts +4 -0
  26. package/dist/core/task-registry-agent.js +13 -0
  27. package/dist/core/task-registry-monitor.js +6 -6
  28. package/dist/core/task-registry-shared.d.ts +8 -2
  29. package/dist/core/task-registry-shared.js +1 -1
  30. package/dist/core/task-registry.d.ts +6 -0
  31. package/dist/core/task-registry.js +48 -4
  32. package/dist/core/tool-result-store.d.ts +3 -2
  33. package/dist/core/tool-result-store.js +12 -4
  34. package/dist/core/trace.d.ts +7 -0
  35. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  36. package/dist/engine/lsp/node-lsp-manager.js +16 -0
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +1 -0
  39. package/dist/orchestration/builtin-workflows.d.ts +1 -1
  40. package/dist/orchestration/builtin-workflows.js +11 -2
  41. package/dist/orchestration/workflow-governance.d.ts +6 -1
  42. package/dist/orchestration/workflow-governance.js +24 -4
  43. package/dist/orchestration/workflow-primitives.js +7 -1
  44. package/dist/orchestration/workflow.d.ts +1 -0
  45. package/dist/orchestration/workflow.js +31 -2
  46. package/dist/tools/fs/fs-bash.d.ts +7 -1
  47. package/dist/tools/fs/fs-bash.js +51 -20
  48. package/dist/tools/fs/fs-read.js +22 -11
  49. package/dist/tools/fs/fs-search-tools.js +3 -3
  50. package/dist/tools/fs/fs-shared.d.ts +20 -7
  51. package/dist/tools/fs/fs-shared.js +17 -3
  52. package/dist/tools/fs/fs-write.js +4 -4
  53. package/dist/tools/fs/index.d.ts +2 -0
  54. package/dist/tools/fs/index.js +7 -1
  55. package/dist/tools/fs/repo-map.js +2 -2
  56. package/dist/tools/fs/safety.d.ts +10 -0
  57. package/dist/tools/fs/safety.js +15 -1
  58. package/dist/tools/monitor.js +18 -4
  59. package/dist/tools/web.js +6 -2
  60. package/dist/tools/worktree.js +46 -25
  61. 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", netErr instanceof Error ? netErr.message : String(netErr));
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: ${e instanceof Error ? e.message : String(e)}`),
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) => `full text persisted; call ReadToolResult with ref "${ref}" to read it back`;
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,2 @@
1
+ import { engineVersion } from "../version.js";
2
+ export const CONTRACT_KIT_ENGINE_VERSION = engineVersion();
@@ -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>;