@sema-agent/core 7.40.1 → 7.41.1
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 +208 -0
- package/dist/agents/agent-transcript-tool.d.ts +10 -2
- package/dist/agents/agent-transcript-tool.js +52 -26
- package/dist/agents/list-agents-tool.d.ts +17 -2
- package/dist/agents/list-agents-tool.js +28 -16
- package/dist/agents/peer-session-drain.d.ts +3 -1
- package/dist/agents/peer-session-drain.js +1 -0
- package/dist/agents/subagent-steps.d.ts +12 -2
- package/dist/agents/subagent-steps.js +8 -4
- package/dist/agents/subagent.d.ts +1 -17
- package/dist/agents/subagent.js +12 -24
- package/dist/core/agent-definition.d.ts +19 -40
- package/dist/core/agent-state.d.ts +91 -0
- package/dist/core/agent-state.js +79 -0
- package/dist/core/ask-origin.d.ts +28 -6
- package/dist/core/ask-origin.js +9 -5
- package/dist/core/ask-unresolvable-notice.d.ts +27 -4
- package/dist/core/ask-unresolvable-notice.js +8 -0
- package/dist/core/auto-promote.d.ts +14 -27
- package/dist/core/auto-promote.js +5 -5
- package/dist/core/checkpoint-store.d.ts +11 -12
- package/dist/core/decision-receipt.d.ts +5 -2
- package/dist/core/decision-receipt.js +1 -1
- package/dist/core/engine-notice.d.ts +7 -1
- package/dist/core/gate-crash-trace.d.ts +1 -1
- package/dist/core/gate-fold.js +7 -3
- package/dist/core/gate-lanes.js +22 -3
- package/dist/core/governance-codes.d.ts +3 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +20 -6
- package/dist/core/hooks.js +28 -28
- package/dist/core/mcp-tool-meta.d.ts +24 -0
- package/dist/core/mcp-tool-meta.js +17 -0
- package/dist/core/mcp.d.ts +1 -6
- package/dist/core/mcp.js +6 -7
- package/dist/core/mode-policy.d.ts +25 -0
- package/dist/core/mode-policy.js +8 -0
- package/dist/core/protocol-table.d.ts +3 -0
- package/dist/core/protocol-table.js +3 -0
- package/dist/core/reminder-mint.d.ts +21 -1
- package/dist/core/reminder-mint.js +20 -0
- package/dist/core/retired-keys.d.ts +2 -1
- package/dist/core/retired-keys.js +13 -2
- package/dist/core/rule-denial-sentence.d.ts +2 -2
- package/dist/core/runner/call-hook-stations.d.ts +106 -0
- package/dist/core/runner/call-hook-stations.js +118 -0
- package/dist/core/runner/contracts.d.ts +35 -25
- package/dist/core/runner/gate-exit.d.ts +5 -1
- package/dist/core/runner/gate-exit.js +10 -6
- package/dist/core/runner/prepare-delegation-surface.d.ts +1 -0
- package/dist/core/runner/prepare-delegation-surface.js +4 -4
- package/dist/core/runner/prepare-gate-stations.d.ts +8 -3
- package/dist/core/runner/prepare-gate-stations.js +13 -70
- package/dist/core/runner/prepare-inherited-gate.d.ts +6 -3
- package/dist/core/runner/prepare-inherited-gate.js +5 -3
- package/dist/core/runner/prepare-park-ask.d.ts +4 -0
- package/dist/core/runner/prepare-park-ask.js +4 -1
- package/dist/core/runner/prepare-policy-chain.d.ts +6 -5
- package/dist/core/runner/prepare-policy-chain.js +14 -25
- package/dist/core/runner/prepare-project-context.d.ts +15 -16
- package/dist/core/runner/prepare-project-context.js +15 -11
- package/dist/core/runner/prepare-run-refs.d.ts +8 -8
- package/dist/core/runner/prepare-run-refs.js +3 -3
- package/dist/core/runner/prepare-safety-scan.js +3 -3
- package/dist/core/runner/prepare-suspend-saga.d.ts +2 -1
- package/dist/core/runner/prepare-suspend-saga.js +2 -1
- package/dist/core/runner/prepare-task.js +14 -12
- package/dist/core/runner/prepare-turn-wiring.d.ts +3 -1
- package/dist/core/runner/prepare-turn-wiring.js +16 -13
- package/dist/core/runner/resume-admission.js +2 -1
- package/dist/core/runner/resume-apply.js +12 -23
- package/dist/core/runner/resume-checkpoint-screen.js +3 -0
- package/dist/core/runner/run-leg.js +61 -47
- package/dist/core/runner/runtask.d.ts +1 -1
- package/dist/core/runner/session-rule-policy.d.ts +7 -19
- package/dist/core/runner/session-rule-policy.js +1 -2
- package/dist/core/runner/skill-base-dir.d.ts +10 -0
- package/dist/core/runner/skill-base-dir.js +4 -0
- package/dist/core/runner/synthetic-tools.d.ts +11 -17
- package/dist/core/runner/synthetic-tools.js +7 -8
- package/dist/core/safety-axis-vocab.d.ts +2 -2
- package/dist/core/safety-axis-vocab.js +2 -0
- package/dist/core/session-policy-store.d.ts +63 -11
- package/dist/core/session-policy-store.js +52 -6
- package/dist/core/skill-grant.d.ts +239 -0
- package/dist/core/skill-grant.js +313 -0
- package/dist/core/skills-directory.d.ts +5 -34
- package/dist/core/skills-directory.js +9 -58
- package/dist/core/task-registry-agent.d.ts +14 -2
- package/dist/core/task-registry-agent.js +23 -8
- package/dist/core/task-registry-shared.d.ts +6 -0
- package/dist/core/task-registry.d.ts +16 -0
- package/dist/core/task-registry.js +19 -6
- package/dist/core/task-spec.d.ts +10 -0
- package/dist/core/tool-face.d.ts +3 -2
- package/dist/core/tool-name-aliases.d.ts +38 -12
- package/dist/core/tool-name-aliases.js +16 -0
- package/dist/core/tool-name-readings.d.ts +6 -2
- package/dist/core/tool-name-readings.js +6 -4
- package/dist/core/tool-policy.d.ts +2 -2
- package/dist/core/tool-registry.d.ts +1 -4
- package/dist/core/tool-registry.js +0 -3
- package/dist/core/tool-spec.d.ts +11 -13
- package/dist/core/tools.d.ts +1 -1
- package/dist/core/trace.d.ts +15 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/engine/harness/agent-harness.d.ts +12 -1
- package/dist/engine/harness/agent-harness.js +17 -13
- package/dist/engine/loop/agent-loop.d.ts +16 -1
- package/dist/engine/loop/agent-loop.js +9 -15
- package/dist/engine/loop/types.d.ts +2 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +6 -2
- package/dist/stores/file/adoption/adopt.js +23 -16
- package/dist/stores/file/index.d.ts +8 -2
- package/dist/stores/file/index.js +2 -2
- package/dist/stores/file/mailbox-store.js +2 -2
- package/dist/stores/file/permission-rule-adopt.js +10 -1
- package/dist/stores/file/permission-rule-store.d.ts +6 -4
- package/dist/stores/file/permission-rule-store.js +127 -59
- package/dist/stores/file/session-policy-store.d.ts +86 -14
- package/dist/stores/file/session-policy-store.js +135 -118
- package/dist/stores/file/task-list-store.d.ts +5 -0
- package/dist/stores/file/task-list-store.js +18 -4
- package/dist/stores/file/usage-window-store.d.ts +9 -1
- package/dist/stores/file/usage-window-store.js +47 -33
- package/dist/tools/fs/safety.d.ts +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +55 -5
- package/wire-changes/core@7.41.0.json +1865 -0
- package/wire-changes/core@7.41.1.json +159 -0
- package/dist/core/runner/active-skill-scope.d.ts +0 -58
- package/dist/core/runner/active-skill-scope.js +0 -133
- package/dist/core/skill-tool-specifier.d.ts +0 -80
- package/dist/core/skill-tool-specifier.js +0 -57
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { loosenReasons, normalizeRules, stripRev, SessionPolicyError, } from "../../core/session-policy-store.js";
|
|
3
|
+
import { assertSessionRuleNamesCurrent, loosenReasons, normalizeRules, readMountedToolNames, stripRev, SessionPolicyError, } from "../../core/session-policy-store.js";
|
|
4
4
|
import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
|
|
5
5
|
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
6
6
|
import { assertAdoptionBootGate, readRootAdoptionFile } from "./adoption/marker.js";
|
|
@@ -13,27 +13,25 @@ export function readSessionPolicyMark(path) {
|
|
|
13
13
|
}
|
|
14
14
|
catch (err) {
|
|
15
15
|
if (err.code === "ENOENT")
|
|
16
|
-
return null;
|
|
16
|
+
return { mark: null };
|
|
17
17
|
throw err;
|
|
18
18
|
}
|
|
19
|
-
const
|
|
20
|
-
throw new SessionPolicyError("corrupt", `session rules high-water mark ${fault}: ${path} — refusing rather than restart this key's rev from zero. Deleting that file is the repair, and it gives up the number it was holding.`);
|
|
21
|
-
};
|
|
19
|
+
const corrupt = (reason) => ({ corrupt: { reason } });
|
|
22
20
|
let parsed;
|
|
23
21
|
try {
|
|
24
22
|
parsed = JSON.parse(raw);
|
|
25
23
|
}
|
|
26
24
|
catch {
|
|
27
|
-
return
|
|
25
|
+
return corrupt("is unparseable");
|
|
28
26
|
}
|
|
29
27
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
30
|
-
return
|
|
28
|
+
return corrupt("is not a JSON object");
|
|
31
29
|
const mark = parsed;
|
|
32
30
|
if (typeof mark.__hwm !== "number" || !Number.isFinite(mark.__hwm))
|
|
33
|
-
return
|
|
31
|
+
return corrupt("carries no numeric value");
|
|
34
32
|
if (mark.__gen !== undefined && (typeof mark.__gen !== "number" || !Number.isInteger(mark.__gen) || mark.__gen < 0))
|
|
35
|
-
return
|
|
36
|
-
return mark;
|
|
33
|
+
return corrupt("carries a lineage that is not a whole number");
|
|
34
|
+
return { mark };
|
|
37
35
|
}
|
|
38
36
|
export function judgeSessionPolicyRowObject(raw) {
|
|
39
37
|
let parsed;
|
|
@@ -63,12 +61,22 @@ export function judgeSessionPolicyRowShape(row) {
|
|
|
63
61
|
void _principal;
|
|
64
62
|
return { rules: clean };
|
|
65
63
|
}
|
|
64
|
+
export function attributeSessionPolicyRow(dir, file, row) {
|
|
65
|
+
const spell = (p) => (typeof p === "string" || p === null ? p : undefined);
|
|
66
|
+
if (row !== undefined && typeof row.__sid === "string")
|
|
67
|
+
return { owner: { sessionId: row.__sid, principal: spell(row.__principal) } };
|
|
68
|
+
const markRead = readSessionPolicyMark(join(dir, `${file.slice(0, -".json".length)}${MARK_SUFFIX}`));
|
|
69
|
+
const mark = "corrupt" in markRead ? null : markRead.mark;
|
|
70
|
+
return { owner: mark !== null && typeof mark.__sid === "string" ? { sessionId: mark.__sid, principal: spell(mark.__principal) } : undefined, markRead };
|
|
71
|
+
}
|
|
66
72
|
export class FileSessionPolicyStore {
|
|
67
73
|
dir;
|
|
68
74
|
onCorruptRead;
|
|
75
|
+
mountedToolNames;
|
|
69
76
|
anonymousAlias;
|
|
70
77
|
constructor(root, opts) {
|
|
71
78
|
assertAdoptionBootGate(root, "FileSessionPolicyStore");
|
|
79
|
+
this.mountedToolNames = readMountedToolNames(opts?.mountedToolNames);
|
|
72
80
|
this.dir = join(root, "session-policy");
|
|
73
81
|
ensureDir(this.dir);
|
|
74
82
|
this.onCorruptRead = guardCorruptSink(opts?.onCorruptRead);
|
|
@@ -79,11 +87,57 @@ export class FileSessionPolicyStore {
|
|
|
79
87
|
disclose(info) {
|
|
80
88
|
this.onCorruptRead?.({ kind: "file", ...info });
|
|
81
89
|
}
|
|
82
|
-
refuseDamaged(sessionId, principal, grade, reason) {
|
|
83
|
-
const path = this.pathFor(sessionId, principal);
|
|
90
|
+
refuseDamaged(sessionId, principal, path, grade, reason) {
|
|
84
91
|
this.disclose({ sessionId, ...(principal !== undefined ? { principal } : {}), path, reason });
|
|
85
92
|
throw new SessionPolicyError("corrupt", `session rules file for ${JSON.stringify(sessionId)} is ${grade} (${reason}) — refusing rather than read the rules it held as none (session rules only tighten). Repair it: fix the file, or delete it (deleting it = explicitly no rules, and gives up the rules it held). The file's location is reported to the store's corrupt-read disclosure (onCorruptRead), not in this message.`);
|
|
86
93
|
}
|
|
94
|
+
refuseDamagedMark(sessionId, principal, reason) {
|
|
95
|
+
this.disclose({ sessionId, ...(principal !== undefined ? { principal } : {}), path: this.markPathFor(sessionId, principal), reason: `high-water mark ${reason}` });
|
|
96
|
+
throw new SessionPolicyError("corrupt", `session rules high-water mark for ${JSON.stringify(sessionId)} ${reason} — refusing rather than restart this key's rev from zero. Repair it: fix the mark file, or delete it (deleting it gives up the number it was holding). The file's location is reported to the store's corrupt-read disclosure (onCorruptRead), not in this message.`);
|
|
97
|
+
}
|
|
98
|
+
refuseUnreadable(sessionId, path, what, err) {
|
|
99
|
+
const reason = `${what} could not be read (${err.code ?? err.name})`;
|
|
100
|
+
this.disclose({ sessionId, path, reason });
|
|
101
|
+
throw new SessionPolicyError("corrupt", `session rules for ${JSON.stringify(sessionId)}: ${reason} — refusing rather than guess whose rules it holds. Retry; if it persists, repair access to the file. The file's location is reported to the store's corrupt-read disclosure (onCorruptRead), not in this message.`);
|
|
102
|
+
}
|
|
103
|
+
ownerOf(sessionId, file, row) {
|
|
104
|
+
try {
|
|
105
|
+
return attributeSessionPolicyRow(this.dir, file, row);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
return this.refuseUnreadable(sessionId, join(this.dir, `${file.slice(0, -".json".length)}${MARK_SUFFIX}`), "a rules row's high-water mark", err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
sweepMark(sessionId, markPath) {
|
|
112
|
+
try {
|
|
113
|
+
return readSessionPolicyMark(markPath);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
return this.refuseUnreadable(sessionId, markPath, "a rules row's high-water mark", err);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
sweepEntries(sessionId) {
|
|
120
|
+
try {
|
|
121
|
+
return readdirSync(this.dir);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (err.code === "ENOENT")
|
|
125
|
+
return [];
|
|
126
|
+
return this.refuseUnreadable(sessionId, this.dir, "the session rules directory", err);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
readMark(sessionId, principal) {
|
|
130
|
+
let judged;
|
|
131
|
+
try {
|
|
132
|
+
judged = readSessionPolicyMark(this.markPathFor(sessionId, principal));
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
return this.refuseDamagedMark(sessionId, principal, `could not be read (${err.code ?? err.name})`);
|
|
136
|
+
}
|
|
137
|
+
if ("corrupt" in judged)
|
|
138
|
+
return this.refuseDamagedMark(sessionId, principal, judged.corrupt.reason);
|
|
139
|
+
return judged.mark;
|
|
140
|
+
}
|
|
87
141
|
pathFor(sessionId, principal) {
|
|
88
142
|
return join(this.dir, `${this.basenameFor(sessionId, principal)}.json`);
|
|
89
143
|
}
|
|
@@ -102,22 +156,28 @@ export class FileSessionPolicyStore {
|
|
|
102
156
|
atomicWriteFile(join(this.dir, "tmp"), path, JSON.stringify(mark));
|
|
103
157
|
}
|
|
104
158
|
read(sessionId, principal) {
|
|
159
|
+
const path = this.pathFor(sessionId, principal);
|
|
160
|
+
const judged = this.readRowFile(path);
|
|
161
|
+
if (judged === null)
|
|
162
|
+
return null;
|
|
163
|
+
if ("corrupt" in judged)
|
|
164
|
+
return this.refuseDamaged(sessionId, principal, path, judged.corrupt.grade, judged.corrupt.reason);
|
|
165
|
+
const shaped = judgeSessionPolicyRowShape(judged.row);
|
|
166
|
+
if ("corrupt" in shaped)
|
|
167
|
+
return this.refuseDamaged(sessionId, principal, path, shaped.corrupt.grade, shaped.corrupt.reason);
|
|
168
|
+
return shaped.rules;
|
|
169
|
+
}
|
|
170
|
+
readRowFile(path) {
|
|
105
171
|
let raw;
|
|
106
172
|
try {
|
|
107
|
-
raw = readFileSync(
|
|
173
|
+
raw = readFileSync(path, "utf8");
|
|
108
174
|
}
|
|
109
175
|
catch (err) {
|
|
110
176
|
if (err.code === "ENOENT")
|
|
111
177
|
return null;
|
|
112
|
-
return
|
|
178
|
+
return { corrupt: { grade: "unreadable", reason: `read failed (${err.code ?? err.name})` } };
|
|
113
179
|
}
|
|
114
|
-
|
|
115
|
-
if ("corrupt" in judged)
|
|
116
|
-
return this.refuseDamaged(sessionId, principal, judged.corrupt.grade, judged.corrupt.reason);
|
|
117
|
-
const shaped = judgeSessionPolicyRowShape(judged.row);
|
|
118
|
-
if ("corrupt" in shaped)
|
|
119
|
-
return this.refuseDamaged(sessionId, principal, shaped.corrupt.grade, shaped.corrupt.reason);
|
|
120
|
-
return shaped.rules;
|
|
180
|
+
return judgeSessionPolicyRowObject(raw);
|
|
121
181
|
}
|
|
122
182
|
async getRules(sessionId, principal) {
|
|
123
183
|
return this.read(sessionId, principal);
|
|
@@ -128,13 +188,14 @@ export class FileSessionPolicyStore {
|
|
|
128
188
|
throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${prior?.rev ?? 0})`);
|
|
129
189
|
}
|
|
130
190
|
const clean = normalizeRules(stripRev(rules));
|
|
191
|
+
assertSessionRuleNamesCurrent(clean, this.mountedToolNames);
|
|
131
192
|
if (prior && opts?.operator !== true) {
|
|
132
|
-
const loosens = loosenReasons(prior, clean);
|
|
193
|
+
const loosens = loosenReasons(prior, clean, this.mountedToolNames);
|
|
133
194
|
if (loosens.length > 0) {
|
|
134
195
|
throw new SessionPolicyError("loosen_forbidden", `a non-operator session-rule write may only tighten; refused: ${loosens.join("; ")}`);
|
|
135
196
|
}
|
|
136
197
|
}
|
|
137
|
-
const mark =
|
|
198
|
+
const mark = this.readMark(sessionId, principal);
|
|
138
199
|
if (mark?.__lineage === "lost") {
|
|
139
200
|
throw new SessionPolicyError("corrupt", `session rules for ${JSON.stringify(sessionId)} have no lineage evidence (the erase left a lost-lineage mark) — refusing to restart the key's lineage by guessing; delete the mark to restart it`);
|
|
140
201
|
}
|
|
@@ -147,15 +208,7 @@ export class FileSessionPolicyStore {
|
|
|
147
208
|
return next;
|
|
148
209
|
}
|
|
149
210
|
async listBySession(sessionId) {
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
files = readdirSync(this.dir);
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
if (err.code === "ENOENT")
|
|
156
|
-
return [];
|
|
157
|
-
throw err;
|
|
158
|
-
}
|
|
211
|
+
const files = this.sweepEntries(sessionId);
|
|
159
212
|
const out = [];
|
|
160
213
|
for (const file of files) {
|
|
161
214
|
if (file.endsWith(MARK_SUFFIX))
|
|
@@ -163,116 +216,80 @@ export class FileSessionPolicyStore {
|
|
|
163
216
|
if (!file.endsWith(".json"))
|
|
164
217
|
continue;
|
|
165
218
|
const full = join(this.dir, file);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
raw = readFileSync(full, "utf8");
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
if (err.code !== "ENOENT") {
|
|
172
|
-
this.disclose({ sessionId, path: full, reason: `read failed (non-ENOENT): ${err.message}` });
|
|
173
|
-
}
|
|
219
|
+
const judged = this.readRowFile(full);
|
|
220
|
+
if (judged === null)
|
|
174
221
|
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.disclose({ sessionId, path: full, reason: judged.corrupt.reason });
|
|
222
|
+
const { owner } = this.ownerOf(sessionId, file, "row" in judged ? judged.row : undefined);
|
|
223
|
+
if (owner === undefined) {
|
|
224
|
+
this.disclose({ sessionId, path: full, reason: "corrupt" in judged ? judged.corrupt.reason : "session metadata is not a string" });
|
|
179
225
|
continue;
|
|
180
226
|
}
|
|
181
|
-
if (
|
|
182
|
-
this.disclose({ sessionId, path: full, reason: "session metadata is not a string" });
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
if (judged.row.__sid !== sessionId)
|
|
227
|
+
if (owner.sessionId !== sessionId)
|
|
186
228
|
continue;
|
|
229
|
+
if ("corrupt" in judged)
|
|
230
|
+
this.refuseDamaged(sessionId, undefined, full, judged.corrupt.grade, judged.corrupt.reason);
|
|
231
|
+
if (typeof judged.row.__sid !== "string")
|
|
232
|
+
this.refuseDamaged(sessionId, undefined, full, "malformed", "session metadata is not a string");
|
|
187
233
|
const shaped = judgeSessionPolicyRowShape(judged.row);
|
|
188
|
-
if ("corrupt" in shaped)
|
|
189
|
-
this.
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
234
|
+
if ("corrupt" in shaped)
|
|
235
|
+
this.refuseDamaged(sessionId, undefined, full, shaped.corrupt.grade, shaped.corrupt.reason);
|
|
192
236
|
const attributed = judged.row.__principal;
|
|
193
237
|
if (attributed !== undefined && attributed !== null && typeof attributed !== "string") {
|
|
194
|
-
this.
|
|
195
|
-
continue;
|
|
238
|
+
this.refuseDamaged(sessionId, undefined, full, "malformed", "principal metadata is not a string");
|
|
196
239
|
}
|
|
197
240
|
out.push({ principal: attributed ?? undefined, rules: shaped.rules });
|
|
198
241
|
}
|
|
199
242
|
return out;
|
|
200
243
|
}
|
|
201
244
|
async deleteBySession(sessionId) {
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
files = readdirSync(this.dir);
|
|
205
|
-
}
|
|
206
|
-
catch (err) {
|
|
207
|
-
if (err.code === "ENOENT")
|
|
208
|
-
return;
|
|
209
|
-
throw err;
|
|
210
|
-
}
|
|
211
|
-
for (const file of files) {
|
|
245
|
+
for (const file of this.sweepEntries(sessionId)) {
|
|
212
246
|
if (file.endsWith(MARK_SUFFIX))
|
|
213
247
|
continue;
|
|
214
248
|
if (!file.endsWith(".json"))
|
|
215
249
|
continue;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
250
|
+
const full = join(this.dir, file);
|
|
251
|
+
const judged = this.readRowFile(full);
|
|
252
|
+
if (judged === null)
|
|
221
253
|
continue;
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
const tornMark = readSessionPolicyMark(tornMarkPath);
|
|
230
|
-
if (tornMark === null) {
|
|
231
|
-
this.writeMark(tornMarkPath, { __hwm: 0, __gen: 0, __lineage: "lost" });
|
|
232
|
-
this.disclose({ sessionId, path: join(this.dir, file), reason: "erase purged a torn row that had no high-water mark: the key's lineage is lost, writes are refused until the mark is deleted" });
|
|
233
|
-
}
|
|
234
|
-
else {
|
|
235
|
-
this.writeMark(tornMarkPath, { ...tornMark, __gen: (tornMark.__gen ?? 0) + 1 });
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
catch (err) {
|
|
239
|
-
if (!(err instanceof SessionPolicyError))
|
|
240
|
-
throw err;
|
|
241
|
-
this.disclose({ sessionId, path: join(this.dir, file), reason: `erase purged a torn row whose high-water mark is corrupt (${err.message}); the mark is left for the operator` });
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
unlinkSync(join(this.dir, file));
|
|
245
|
-
}
|
|
246
|
-
catch (err) {
|
|
247
|
-
if (err.code !== "ENOENT")
|
|
248
|
-
throw err;
|
|
249
|
-
}
|
|
254
|
+
const row = "row" in judged ? judged.row : undefined;
|
|
255
|
+
const markPath = join(this.dir, `${file.slice(0, -".json".length)}${MARK_SUFFIX}`);
|
|
256
|
+
const { owner, markRead } = this.ownerOf(sessionId, file, row);
|
|
257
|
+
if (owner !== undefined) {
|
|
258
|
+
if (owner.sessionId === sessionId)
|
|
259
|
+
this.eraseRow(full, markPath, sessionId, owner.principal ?? null, row, markRead);
|
|
250
260
|
continue;
|
|
251
261
|
}
|
|
252
|
-
|
|
253
|
-
if (row.__sid !== sessionId)
|
|
262
|
+
if (!("corrupt" in judged) || judged.corrupt.grade !== "unparseable")
|
|
254
263
|
continue;
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
mark = readSessionPolicyMark(markPath);
|
|
259
|
-
}
|
|
260
|
-
catch (err) {
|
|
261
|
-
if (!(err instanceof SessionPolicyError))
|
|
262
|
-
throw err;
|
|
263
|
-
mark = undefined;
|
|
264
|
+
const tornRead = markRead ?? this.sweepMark(sessionId, markPath);
|
|
265
|
+
if ("corrupt" in tornRead) {
|
|
266
|
+
this.disclose({ sessionId, path: markPath, reason: `erase purged a torn row whose high-water mark is corrupt (it ${tornRead.corrupt.reason}); the mark is left for the operator` });
|
|
264
267
|
}
|
|
265
|
-
if (mark
|
|
266
|
-
|
|
267
|
-
this.
|
|
268
|
+
else if (tornRead.mark === null) {
|
|
269
|
+
this.writeMark(markPath, { __hwm: 0, __gen: 0, __lineage: "lost" });
|
|
270
|
+
this.disclose({ sessionId, path: full, reason: "erase purged a torn row that had no high-water mark: the key's lineage is lost, writes are refused until the mark is deleted" });
|
|
268
271
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
catch (err) {
|
|
273
|
-
if (err.code !== "ENOENT")
|
|
274
|
-
throw err;
|
|
272
|
+
else {
|
|
273
|
+
this.writeMark(markPath, { ...tornRead.mark, __gen: (tornRead.mark.__gen ?? 0) + 1 });
|
|
275
274
|
}
|
|
275
|
+
unlinkIfPresent(full);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
eraseRow(full, markPath, sessionId, principal, row, observed) {
|
|
279
|
+
const markRead = observed ?? this.sweepMark(sessionId, markPath);
|
|
280
|
+
if (!("corrupt" in markRead)) {
|
|
281
|
+
const rowRev = typeof row?.rev === "number" && Number.isFinite(row.rev) ? row.rev : 0;
|
|
282
|
+
this.writeMarkFile(markPath, Math.max(markRead.mark?.__hwm ?? 0, rowRev), sessionId, principal, (markRead.mark?.__gen ?? 0) + 1);
|
|
276
283
|
}
|
|
284
|
+
unlinkIfPresent(full);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function unlinkIfPresent(path) {
|
|
288
|
+
try {
|
|
289
|
+
unlinkSync(path);
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
if (err.code !== "ENOENT")
|
|
293
|
+
throw err;
|
|
277
294
|
}
|
|
278
295
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TaskListStore } from "../../tools/task-list.js";
|
|
2
|
+
import { type CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
2
3
|
export declare function createFileTaskListStore(root: string, opts?: {
|
|
3
4
|
/**
|
|
4
5
|
* The DATA ROOT whose adoption marker gates construction (design/183 I6). Most of the file family
|
|
@@ -13,4 +14,8 @@ export declare function createFileTaskListStore(root: string, opts?: {
|
|
|
13
14
|
* on `adoptLocalDataRoot` lists that default among the seams it does not cover.
|
|
14
15
|
*/
|
|
15
16
|
dataRoot?: string;
|
|
17
|
+
/** Disclosure sink for a task-list document that is present but cannot be read as one: it fires WITH the
|
|
18
|
+
* refusal and is the only carrier of the file's location (the refusal's message names none). Never fires on
|
|
19
|
+
* plain absence. Swallow-guarded: a throwing sink cannot change the refusal. */
|
|
20
|
+
onCorruptRead?: (info: CorruptReadInfo) => void;
|
|
16
21
|
}): TaskListStore;
|
|
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { assertJsonMetadata, normalizeTaskShape } from "../../tools/task-list.js";
|
|
4
4
|
import { canonicalStoreKey, atomicWriteFile, ensureDir } from "./fs-atomic.js";
|
|
5
5
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
6
|
+
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
6
7
|
const dirLocks = new Map();
|
|
7
8
|
export function createFileTaskListStore(root, opts) {
|
|
8
9
|
const dir = resolve(root);
|
|
@@ -12,6 +13,11 @@ export function createFileTaskListStore(root, opts) {
|
|
|
12
13
|
ensureDir(dir);
|
|
13
14
|
const lockKey = canonicalStoreKey(dir);
|
|
14
15
|
ensureDir(tmpDir);
|
|
16
|
+
const disclose = guardCorruptSink(opts?.onCorruptRead);
|
|
17
|
+
const refuseDamaged = (fault) => {
|
|
18
|
+
disclose?.({ kind: "file", path, reason: fault });
|
|
19
|
+
throw new Error(`file task-list store: the task list document ${fault} — refusing rather than start an empty list, which would hand out again the task ids it reserved. Repair it: fix the file, or delete it (deleting it = an empty task list whose ids restart at 1). The file's location is reported to the store's corrupt-read disclosure (onCorruptRead), not in this message.`);
|
|
20
|
+
};
|
|
15
21
|
const load = () => {
|
|
16
22
|
let raw;
|
|
17
23
|
try {
|
|
@@ -20,14 +26,22 @@ export function createFileTaskListStore(root, opts) {
|
|
|
20
26
|
catch (err) {
|
|
21
27
|
if (err.code === "ENOENT")
|
|
22
28
|
return { version: 1, nextId: 1, tasks: {} };
|
|
23
|
-
|
|
29
|
+
return refuseDamaged(`could not be read (${err.code ?? err.name})`);
|
|
24
30
|
}
|
|
25
31
|
if (raw === "")
|
|
26
32
|
return { version: 1, nextId: 1, tasks: {} };
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(raw);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return refuseDamaged("is not valid JSON (torn write?)");
|
|
30
39
|
}
|
|
40
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
41
|
+
return refuseDamaged("is not a JSON object");
|
|
42
|
+
const doc = parsed;
|
|
43
|
+
if (typeof doc.nextId !== "number" || typeof doc.tasks !== "object" || doc.tasks === null)
|
|
44
|
+
return refuseDamaged("carries no numeric nextId or no tasks map");
|
|
31
45
|
return doc;
|
|
32
46
|
};
|
|
33
47
|
const save = (doc) => {
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type UsageWindow, type UsageWindowReading, type UsageWindowStore } from "../../core/usage-window-store.js";
|
|
2
|
+
import { type CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
3
|
+
export interface FileUsageWindowStoreOptions {
|
|
4
|
+
/** Disclosure sink for a ledger that is present but cannot be read as one: it fires WITH the refusal and is the
|
|
5
|
+
* only carrier of the file's location (the refusal's message, which reaches a run's failure text, names none).
|
|
6
|
+
* Never fires on plain absence. Swallow-guarded: a throwing sink cannot change the refusal. */
|
|
7
|
+
onCorruptRead?: (info: CorruptReadInfo) => void;
|
|
8
|
+
}
|
|
2
9
|
/**
|
|
3
10
|
* design/164 件五 — file-backed {@link UsageWindowStore}: ONE JSON record per governed key, semantics
|
|
4
11
|
* identical to `InMemoryUsageWindowStore` BY CONSTRUCTION (both are shells around the same pure
|
|
@@ -17,7 +24,8 @@ import { type UsageWindow, type UsageWindowReading, type UsageWindowStore } from
|
|
|
17
24
|
*/
|
|
18
25
|
export declare class FileUsageWindowStore implements UsageWindowStore {
|
|
19
26
|
private readonly dir;
|
|
20
|
-
|
|
27
|
+
private readonly onCorruptRead;
|
|
28
|
+
constructor(root: string, opts?: FileUsageWindowStoreOptions);
|
|
21
29
|
/** key → a safe, INJECTIVE filename (`sanitizeScope` appends the full sha256 of the raw key, so two
|
|
22
30
|
* distinct principals can never share a ledger file). */
|
|
23
31
|
private pathFor;
|
|
@@ -3,40 +3,30 @@ import { join } from "node:path";
|
|
|
3
3
|
import { chargeUsageRecord, readUsageRecord, EMPTY_USAGE_WINDOW_RECORD, } from "../../core/usage-window-store.js";
|
|
4
4
|
import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
|
|
5
5
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
6
|
+
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
6
7
|
export class FileUsageWindowStore {
|
|
7
8
|
dir;
|
|
8
|
-
|
|
9
|
+
onCorruptRead;
|
|
10
|
+
constructor(root, opts) {
|
|
9
11
|
assertAdoptionBootGate(root, "FileUsageWindowStore");
|
|
10
12
|
this.dir = join(root, "usage-windows");
|
|
11
13
|
ensureDir(this.dir);
|
|
14
|
+
this.onCorruptRead = guardCorruptSink(opts?.onCorruptRead);
|
|
12
15
|
}
|
|
13
16
|
pathFor(key) {
|
|
14
17
|
return join(this.dir, `${sanitizeScope(key)}.json`);
|
|
15
18
|
}
|
|
16
19
|
loadRecord(key) {
|
|
17
20
|
const path = this.pathFor(key);
|
|
18
|
-
let raw;
|
|
19
21
|
try {
|
|
20
|
-
|
|
22
|
+
return judgeRecord(path);
|
|
21
23
|
}
|
|
22
24
|
catch (err) {
|
|
23
|
-
if (err
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
if (!(err instanceof LedgerFault))
|
|
26
|
+
throw err;
|
|
27
|
+
this.onCorruptRead?.({ kind: "file", path, reason: err.message });
|
|
28
|
+
throw corrupt(key, err.message);
|
|
26
29
|
}
|
|
27
|
-
let parsed;
|
|
28
|
-
try {
|
|
29
|
-
parsed = JSON.parse(raw);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
throw corrupt(path, "unparseable JSON (torn/zero-byte write?)");
|
|
33
|
-
}
|
|
34
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
35
|
-
throw corrupt(path, "not a JSON object");
|
|
36
|
-
const rec = parsed;
|
|
37
|
-
const slots = validateSlots(rec.slots, path);
|
|
38
|
-
const buckets = validateBuckets(rec.buckets, path);
|
|
39
|
-
return { slots, buckets };
|
|
40
30
|
}
|
|
41
31
|
async charge(key, tokens, at, windows, costMicroUsd) {
|
|
42
32
|
const next = chargeUsageRecord(this.loadRecord(key), tokens, at, windows, costMicroUsd);
|
|
@@ -46,28 +36,52 @@ export class FileUsageWindowStore {
|
|
|
46
36
|
return readUsageRecord(this.loadRecord(key), windows, now);
|
|
47
37
|
}
|
|
48
38
|
}
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
class LedgerFault extends Error {
|
|
40
|
+
}
|
|
41
|
+
function judgeRecord(path) {
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = readFileSync(path, "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err.code === "ENOENT")
|
|
48
|
+
return EMPTY_USAGE_WINDOW_RECORD;
|
|
49
|
+
throw new LedgerFault(`read failed (${err.code ?? err.name})`);
|
|
50
|
+
}
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new LedgerFault("unparseable JSON (torn/zero-byte write?)");
|
|
57
|
+
}
|
|
58
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
59
|
+
throw new LedgerFault("not a JSON object");
|
|
60
|
+
const rec = parsed;
|
|
61
|
+
return { slots: validateSlots(rec.slots), buckets: validateBuckets(rec.buckets) };
|
|
62
|
+
}
|
|
63
|
+
function corrupt(key, reason) {
|
|
64
|
+
const e = new Error(`usage-window ledger for key ${JSON.stringify(key)} is unreadable (${reason}). Refused rather than treated as empty: an unreadable ceiling would grant the key an unlimited, silent allowance. Repair it: fix the file, or delete it (deleting it = no usage recorded for the key). The file's location is reported to the store's corrupt-read disclosure (onCorruptRead), not in this message.`);
|
|
51
65
|
e.code = "usage_window.store_corrupt";
|
|
52
66
|
return e;
|
|
53
67
|
}
|
|
54
|
-
function validateSlots(value
|
|
68
|
+
function validateSlots(value) {
|
|
55
69
|
if (value === undefined)
|
|
56
70
|
return [];
|
|
57
71
|
if (!Array.isArray(value))
|
|
58
|
-
throw
|
|
72
|
+
throw new LedgerFault("`slots` is not an array");
|
|
59
73
|
return value.map((s) => {
|
|
60
74
|
if (s === null || typeof s !== "object")
|
|
61
|
-
throw
|
|
75
|
+
throw new LedgerFault("`slots` entry is not an object");
|
|
62
76
|
const { at, tokens, costMicroUsd, costUnknown } = s;
|
|
63
77
|
if (typeof at !== "number" || !Number.isFinite(at) || typeof tokens !== "number" || !Number.isFinite(tokens)) {
|
|
64
|
-
throw
|
|
78
|
+
throw new LedgerFault("`slots` entry has a non-numeric `at`/`tokens`");
|
|
65
79
|
}
|
|
66
80
|
if (costMicroUsd !== undefined && (typeof costMicroUsd !== "number" || !Number.isFinite(costMicroUsd))) {
|
|
67
|
-
throw
|
|
81
|
+
throw new LedgerFault("`slots` entry has a non-numeric `costMicroUsd`");
|
|
68
82
|
}
|
|
69
83
|
if (costUnknown !== undefined && costUnknown !== true)
|
|
70
|
-
throw
|
|
84
|
+
throw new LedgerFault("`slots` entry has a `costUnknown` that is not `true`");
|
|
71
85
|
return {
|
|
72
86
|
at,
|
|
73
87
|
tokens,
|
|
@@ -76,14 +90,14 @@ function validateSlots(value, path) {
|
|
|
76
90
|
};
|
|
77
91
|
});
|
|
78
92
|
}
|
|
79
|
-
function validateBuckets(value
|
|
93
|
+
function validateBuckets(value) {
|
|
80
94
|
if (value === undefined)
|
|
81
95
|
return [];
|
|
82
96
|
if (!Array.isArray(value))
|
|
83
|
-
throw
|
|
97
|
+
throw new LedgerFault("`buckets` is not an array");
|
|
84
98
|
return value.map((b) => {
|
|
85
99
|
if (b === null || typeof b !== "object")
|
|
86
|
-
throw
|
|
100
|
+
throw new LedgerFault("`buckets` entry is not an object");
|
|
87
101
|
const { windowMs, openedAt, tokens, costMicroUsd, costUnknown } = b;
|
|
88
102
|
if (typeof windowMs !== "number" ||
|
|
89
103
|
!Number.isFinite(windowMs) ||
|
|
@@ -91,13 +105,13 @@ function validateBuckets(value, path) {
|
|
|
91
105
|
!Number.isFinite(openedAt) ||
|
|
92
106
|
typeof tokens !== "number" ||
|
|
93
107
|
!Number.isFinite(tokens)) {
|
|
94
|
-
throw
|
|
108
|
+
throw new LedgerFault("`buckets` entry has a non-numeric `windowMs`/`openedAt`/`tokens`");
|
|
95
109
|
}
|
|
96
110
|
if (costMicroUsd !== undefined && (typeof costMicroUsd !== "number" || !Number.isFinite(costMicroUsd))) {
|
|
97
|
-
throw
|
|
111
|
+
throw new LedgerFault("`buckets` entry has a non-numeric `costMicroUsd`");
|
|
98
112
|
}
|
|
99
113
|
if (costUnknown !== undefined && costUnknown !== true)
|
|
100
|
-
throw
|
|
114
|
+
throw new LedgerFault("`buckets` entry has a `costUnknown` that is not `true`");
|
|
101
115
|
return {
|
|
102
116
|
windowMs,
|
|
103
117
|
openedAt,
|
|
@@ -5,7 +5,7 @@ import { protectivePathTargetOf, type ToolCallLike } from "../../core/tool-regis
|
|
|
5
5
|
/**
|
|
6
6
|
* CC parity: Read/Edit/Write take the file path as `file_path`. Read the target the same way
|
|
7
7
|
* EVERYWHERE the path is consumed from a tool-call's args — not just the tool body but ALSO every gate that
|
|
8
|
-
* extracts the write target to confine it (sensitive-path,
|
|
8
|
+
* extracts the write target to confine it (sensitive-path, session allowDirs, the fs-write gate). A gate that only
|
|
9
9
|
* read `args.path` would see `undefined` for a `file_path` call and either fail-OPEN (circumvent the guard — a real
|
|
10
10
|
* hole) or fail-closed (deny a legitimate write). Single source.
|
|
11
11
|
*
|