@sema-agent/core 7.40.0 → 7.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +218 -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 +19 -20
- 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 +15 -26
- package/dist/core/runner/resume-checkpoint-screen.js +3 -0
- package/dist/core/runner/resume-preflight.js +3 -3
- 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/runner-deps.d.ts +3 -1
- 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 +9 -4
- package/dist/index.js +4 -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 +45 -5
- package/wire-changes/core@7.40.1.json +39 -0
- package/wire-changes/core@7.41.0.json +1865 -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
|
@@ -4,7 +4,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
4
4
|
import { isRuleBehavior } from "../../core/permission-rule-model.js";
|
|
5
5
|
import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, unknownRuleWriteDeltaError, } from "../../core/permission-rule-store.js";
|
|
6
6
|
import { canonicalize } from "../../core/canonical-json.js";
|
|
7
|
-
import { BootLock } from "./fs-atomic.js";
|
|
7
|
+
import { BootLock, FileStoreLockError } from "./fs-atomic.js";
|
|
8
8
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
9
9
|
import { thrownText } from "../../core/thrown-value.js";
|
|
10
10
|
function isRuleFile(v) {
|
|
@@ -26,27 +26,31 @@ export class OrgRevisionRefusal extends Error {
|
|
|
26
26
|
}
|
|
27
27
|
const LOCAL_OWNER_FILE = "local-owner.json";
|
|
28
28
|
const ADOPTION_FILE = "PERMISSION-RULES-ADOPTION.json";
|
|
29
|
-
|
|
29
|
+
function judgeRuleAdoptionFile(dir) {
|
|
30
30
|
let raw;
|
|
31
31
|
try {
|
|
32
32
|
raw = readFileSync(join(dir, ADOPTION_FILE), "utf8");
|
|
33
33
|
}
|
|
34
34
|
catch (err) {
|
|
35
35
|
if (err?.code === "ENOENT")
|
|
36
|
-
return undefined;
|
|
37
|
-
|
|
36
|
+
return { marker: undefined };
|
|
37
|
+
return { corrupt: `could not be read (${err?.code ?? err?.name ?? "unknown"})`, detail: thrownText(err) };
|
|
38
38
|
}
|
|
39
39
|
let parsed;
|
|
40
40
|
try {
|
|
41
41
|
parsed = JSON.parse(raw);
|
|
42
42
|
}
|
|
43
43
|
catch (err) {
|
|
44
|
-
|
|
44
|
+
return { corrupt: `is not valid JSON (${err.message})` };
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
return isRuleAdoptionFileShape(parsed) ? { marker: parsed } : { corrupt: "does not carry a readable shape" };
|
|
47
|
+
}
|
|
48
|
+
export function readRuleAdoptionFile(dir) {
|
|
49
|
+
const judged = judgeRuleAdoptionFile(dir);
|
|
50
|
+
if ("corrupt" in judged) {
|
|
51
|
+
throw new Error(`the permission-rule adoption marker in ${dir} ${judged.corrupt}${judged.detail !== undefined ? `: ${judged.detail}` : ""} — refusing to resolve owners until it is repaired`);
|
|
48
52
|
}
|
|
49
|
-
return
|
|
53
|
+
return judged.marker;
|
|
50
54
|
}
|
|
51
55
|
function isRuleOwnerShape(v) {
|
|
52
56
|
if (v === null || typeof v !== "object")
|
|
@@ -114,19 +118,53 @@ const EMPTY_READ = { rules: [], tombstones: [], rev: 0 };
|
|
|
114
118
|
function checksumOf(body) {
|
|
115
119
|
return `sha256:${createHash("sha256").update(canonicalize(body), "utf8").digest("hex")}`;
|
|
116
120
|
}
|
|
117
|
-
function
|
|
121
|
+
function unsafeDirFault(dir) {
|
|
118
122
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
119
123
|
const st = lstatSync(dir);
|
|
120
|
-
if (st.isSymbolicLink() || !st.isDirectory())
|
|
121
|
-
|
|
124
|
+
if (st.isSymbolicLink() || !st.isDirectory())
|
|
125
|
+
return "is a symlink or not a directory";
|
|
126
|
+
if (typeof process.getuid === "function" && st.uid !== process.getuid())
|
|
127
|
+
return "is owned by another user";
|
|
128
|
+
if ((st.mode & 0o022) !== 0)
|
|
129
|
+
return `is writable by group or others (mode ${(st.mode & 0o777).toString(8)}) — anyone who can write there can grant themselves permissions`;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
function assertSafeDir(dir) {
|
|
133
|
+
const fault = unsafeDirFault(dir);
|
|
134
|
+
if (fault !== undefined)
|
|
135
|
+
throw new Error(`permission-rule store directory ${dir} ${fault}; refusing to use it`);
|
|
136
|
+
}
|
|
137
|
+
function refusalText(onError, sentence, location) {
|
|
138
|
+
try {
|
|
139
|
+
onError?.(`${sentence} (at ${location})`);
|
|
122
140
|
}
|
|
123
|
-
|
|
124
|
-
throw new Error(`permission-rule store directory ${dir} is owned by another user; refusing to use it`);
|
|
141
|
+
catch {
|
|
125
142
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
return `${sentence}. The location is reported to the store's error disclosure (onError), not in this message.`;
|
|
144
|
+
}
|
|
145
|
+
function assertSafeDirAtRequest(dir, onError, act) {
|
|
146
|
+
const fault = unsafeDirFault(dir);
|
|
147
|
+
if (fault !== undefined)
|
|
148
|
+
throw new Error(refusalText(onError, `the permission-rule store directory ${fault}; refusing to ${act} it`, dir));
|
|
149
|
+
}
|
|
150
|
+
function writerLockRefusal(err, onError) {
|
|
151
|
+
let sentence;
|
|
152
|
+
switch (err.code) {
|
|
153
|
+
case "store.dir_in_use":
|
|
154
|
+
sentence = `another live process${err.ownerPid !== undefined ? ` (pid ${err.ownerPid})` : ""} holds the permission-rule store's writer lock — refusing to interleave writes with it; write through that process, or stop it`;
|
|
155
|
+
break;
|
|
156
|
+
case "store.dir_claiming":
|
|
157
|
+
sentence = "another process is reclaiming the permission-rule store's writer lock from a crashed owner — retry";
|
|
158
|
+
break;
|
|
159
|
+
case "store.lock_unreadable":
|
|
160
|
+
sentence = "the permission-rule store's writer lock is in a state only a person should resolve — remove it by hand if no process holds it";
|
|
161
|
+
break;
|
|
162
|
+
default: {
|
|
163
|
+
const unhandled = err.code;
|
|
164
|
+
sentence = `the permission-rule store's writer lock refused (${String(unhandled)})`;
|
|
165
|
+
}
|
|
129
166
|
}
|
|
167
|
+
return new FileStoreLockError(err.code, refusalText(onError, sentence, `${err.lockPath}: ${err.message}`), err.lockPath, err.ownerPid);
|
|
130
168
|
}
|
|
131
169
|
class FileDurableRulePartition {
|
|
132
170
|
dir;
|
|
@@ -142,48 +180,41 @@ class FileDurableRulePartition {
|
|
|
142
180
|
this.onError = onError;
|
|
143
181
|
}
|
|
144
182
|
read() {
|
|
183
|
+
const bucket = "this owner's permission-rule bucket";
|
|
184
|
+
const whole = "refusing the whole file and loading zero rules — repair the file, or remove it (removing it gives up every rule and tombstone it held)";
|
|
185
|
+
const unreadable = (sentence, location = this.file) => ({ unreadable: refusalText(this.onError, sentence, location) });
|
|
145
186
|
let raw;
|
|
146
187
|
try {
|
|
147
|
-
|
|
188
|
+
const dirFault = unsafeDirFault(this.dir);
|
|
189
|
+
if (dirFault !== undefined)
|
|
190
|
+
return unreadable(`the permission-rule store directory ${dirFault}; loading zero rules`, this.dir);
|
|
148
191
|
const st = lstatSync(this.file);
|
|
149
|
-
if (st.isSymbolicLink() || !st.isFile())
|
|
150
|
-
|
|
151
|
-
this.disclose(why);
|
|
152
|
-
return { unreadable: why };
|
|
153
|
-
}
|
|
192
|
+
if (st.isSymbolicLink() || !st.isFile())
|
|
193
|
+
return unreadable(`${bucket} is a symlink or not a regular file; refusing to load any rule from it`);
|
|
154
194
|
raw = readFileSync(this.file, "utf8");
|
|
155
195
|
}
|
|
156
196
|
catch (err) {
|
|
157
197
|
const e = err;
|
|
158
198
|
if (e?.code === "ENOENT")
|
|
159
199
|
return { absent: true };
|
|
160
|
-
|
|
161
|
-
this.disclose(why);
|
|
162
|
-
return { unreadable: why };
|
|
200
|
+
return unreadable(`${bucket} could not be read (${e?.code ?? err?.name ?? "unknown"}); loading zero rules`, `${this.file}: ${thrownText(err)}`);
|
|
163
201
|
}
|
|
164
202
|
let parsed;
|
|
165
203
|
try {
|
|
166
204
|
parsed = JSON.parse(raw);
|
|
167
205
|
}
|
|
168
206
|
catch (err) {
|
|
169
|
-
|
|
170
|
-
this.disclose(why);
|
|
171
|
-
return { unreadable: why };
|
|
207
|
+
return unreadable(`${bucket} is not valid JSON (${err.message}); ${whole}`);
|
|
172
208
|
}
|
|
173
209
|
if (!isRuleFile(parsed)) {
|
|
174
210
|
const sv = schemaVersionWord(parsed);
|
|
175
|
-
|
|
176
|
-
? `${
|
|
177
|
-
: `${
|
|
178
|
-
this.disclose(why);
|
|
179
|
-
return { unreadable: why };
|
|
211
|
+
return unreadable(typeof sv === "number"
|
|
212
|
+
? `${bucket} is a schema ${String(sv)} bucket; this engine reads schema 4 only and does not migrate an older engine's bucket — refusing the whole file and loading zero rules (the bytes are left for a person to look at)`
|
|
213
|
+
: `${bucket} does not carry a readable rule-file shape; ${whole}`);
|
|
180
214
|
}
|
|
181
215
|
const { checksum, ...body } = parsed;
|
|
182
|
-
if (checksum !== checksumOf(body))
|
|
183
|
-
|
|
184
|
-
this.disclose(why);
|
|
185
|
-
return { unreadable: why };
|
|
186
|
-
}
|
|
216
|
+
if (checksum !== checksumOf(body))
|
|
217
|
+
return unreadable(`${bucket} failed its integrity check; ${whole}`);
|
|
187
218
|
const rowKinds = [
|
|
188
219
|
["rule", parsed.rules],
|
|
189
220
|
["tombstone", parsed.tombstones],
|
|
@@ -191,11 +222,8 @@ class FileDurableRulePartition {
|
|
|
191
222
|
];
|
|
192
223
|
for (const [kind, rows] of rowKinds) {
|
|
193
224
|
const bad = rows.findIndex((r) => !isRuleBehavior(r?.behavior));
|
|
194
|
-
if (bad !== -1)
|
|
195
|
-
|
|
196
|
-
this.disclose(why);
|
|
197
|
-
return { unreadable: why };
|
|
198
|
-
}
|
|
225
|
+
if (bad !== -1)
|
|
226
|
+
return unreadable(`${bucket} carries a ${kind} row (#${bad}) whose behavior word is outside deny/ask/allow — a schema 4 row must name its behavior; ${whole}`);
|
|
199
227
|
}
|
|
200
228
|
return { file: parsed };
|
|
201
229
|
}
|
|
@@ -218,7 +246,7 @@ class FileDurableRulePartition {
|
|
|
218
246
|
return { rules, tombstones, quarantined };
|
|
219
247
|
}
|
|
220
248
|
write(next) {
|
|
221
|
-
|
|
249
|
+
assertSafeDirAtRequest(this.dir, this.onError, "write through");
|
|
222
250
|
atomicPublish(this.dir, this.file, JSON.stringify({ ...next, checksum: checksumOf(next) }, null, 2));
|
|
223
251
|
}
|
|
224
252
|
async list() {
|
|
@@ -370,6 +398,14 @@ export class FileDurableRulePartitionProvider {
|
|
|
370
398
|
if (opts?.dataRoot !== undefined)
|
|
371
399
|
assertAdoptionBootGate(opts.dataRoot, "FileDurableRulePartitionProvider");
|
|
372
400
|
}
|
|
401
|
+
adoptionMarker() {
|
|
402
|
+
const judged = judgeRuleAdoptionFile(this.dir);
|
|
403
|
+
if ("corrupt" in judged) {
|
|
404
|
+
const location = `${join(this.dir, ADOPTION_FILE)}${judged.detail !== undefined ? `: ${judged.detail}` : ""}`;
|
|
405
|
+
throw new Error(refusalText(this.onError, `the permission-rule store's adoption marker ${judged.corrupt}; refusing to resolve whose bucket this is until it is repaired`, location));
|
|
406
|
+
}
|
|
407
|
+
return judged.marker;
|
|
408
|
+
}
|
|
373
409
|
orgStatePersistence() {
|
|
374
410
|
return this.fileOrgPersistence();
|
|
375
411
|
}
|
|
@@ -380,16 +416,21 @@ export class FileDurableRulePartitionProvider {
|
|
|
380
416
|
acquireWriteLock = () => {
|
|
381
417
|
if (this.lock !== undefined)
|
|
382
418
|
return;
|
|
383
|
-
|
|
419
|
+
assertSafeDirAtRequest(this.dir, this.onError, "write through");
|
|
384
420
|
const lock = new BootLock(join(this.dir, "PERMISSION-RULES-WRITER-LOCK"));
|
|
385
|
-
|
|
421
|
+
try {
|
|
422
|
+
lock.acquire();
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
throw err instanceof FileStoreLockError ? writerLockRefusal(err, this.onError) : err;
|
|
426
|
+
}
|
|
386
427
|
this.lock = lock;
|
|
387
428
|
};
|
|
388
429
|
forPrincipal(principal) {
|
|
389
430
|
if (typeof principal !== "string" || principal === "") {
|
|
390
431
|
return { list: async () => ({ ...EMPTY_READ }), durability: "process-local" };
|
|
391
432
|
}
|
|
392
|
-
const adoption =
|
|
433
|
+
const adoption = this.adoptionMarker();
|
|
393
434
|
if (adoption !== undefined) {
|
|
394
435
|
const rec = "adopted" in adoption ? adoption.adopted : adoption.marker;
|
|
395
436
|
if (rec.from.kind === "principal" && rec.from.principal === principal) {
|
|
@@ -403,7 +444,7 @@ export class FileDurableRulePartitionProvider {
|
|
|
403
444
|
return readOnlyRuleStoreView(new FileDurableRulePartition(this.dir, join(this.dir, pick), this.acquireWriteLock, this.onError));
|
|
404
445
|
}
|
|
405
446
|
forLocalOwner() {
|
|
406
|
-
const adoption =
|
|
447
|
+
const adoption = this.adoptionMarker();
|
|
407
448
|
if (adoption !== undefined) {
|
|
408
449
|
const rec = "adopted" in adoption ? adoption.adopted : adoption.marker;
|
|
409
450
|
if (rec.from.kind === "local-owner") {
|
|
@@ -430,25 +471,52 @@ class FileOrgRuleStatePersistence {
|
|
|
430
471
|
this.file = join(dir, ORG_STATE_FILE);
|
|
431
472
|
}
|
|
432
473
|
readSync() {
|
|
433
|
-
|
|
474
|
+
const refuse = (sentence, location = this.file) => {
|
|
475
|
+
throw new Error(refusalText(this.onError, `${sentence}; refusing it whole — repair the file, or remove it (removing it gives up the last-known-good org snapshot and its anti-rollback mark)`, location));
|
|
476
|
+
};
|
|
477
|
+
const ioFault = (err) => `could not be read (${err?.code ?? err?.name ?? "unknown"})`;
|
|
478
|
+
let dirFault;
|
|
434
479
|
try {
|
|
435
|
-
|
|
436
|
-
const st = lstatSync(this.file);
|
|
437
|
-
if (st.isSymbolicLink() || !st.isFile())
|
|
438
|
-
throw new Error(`${this.file} is a symlink or not a regular file; refusing to load the org state from it`);
|
|
439
|
-
raw = readFileSync(this.file, "utf8");
|
|
480
|
+
dirFault = unsafeDirFault(this.dir);
|
|
440
481
|
}
|
|
441
482
|
catch (err) {
|
|
483
|
+
return refuse(`the permission-rule store directory ${ioFault(err)}`, `${this.dir}: ${thrownText(err)}`);
|
|
484
|
+
}
|
|
485
|
+
if (dirFault !== undefined)
|
|
486
|
+
return refuse(`the permission-rule store directory ${dirFault}`, this.dir);
|
|
487
|
+
const unreadable = (err) => {
|
|
442
488
|
if (err?.code === "ENOENT")
|
|
443
489
|
return undefined;
|
|
444
|
-
|
|
490
|
+
return refuse(`the org state file ${ioFault(err)}`, `${this.file}: ${thrownText(err)}`);
|
|
491
|
+
};
|
|
492
|
+
let st;
|
|
493
|
+
try {
|
|
494
|
+
st = lstatSync(this.file);
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
return unreadable(err);
|
|
498
|
+
}
|
|
499
|
+
if (st.isSymbolicLink() || !st.isFile())
|
|
500
|
+
return refuse("the org state file is a symlink or not a regular file");
|
|
501
|
+
let raw;
|
|
502
|
+
try {
|
|
503
|
+
raw = readFileSync(this.file, "utf8");
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
return unreadable(err);
|
|
507
|
+
}
|
|
508
|
+
let parsed;
|
|
509
|
+
try {
|
|
510
|
+
parsed = JSON.parse(raw);
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
return refuse(`the org state file is not valid JSON (${err.message})`);
|
|
445
514
|
}
|
|
446
|
-
const parsed = JSON.parse(raw);
|
|
447
515
|
if (!isOrgStateFile(parsed))
|
|
448
|
-
|
|
516
|
+
return refuse("the org state file does not carry a readable org-state shape");
|
|
449
517
|
const { checksum, ...body } = parsed;
|
|
450
518
|
if (checksum !== checksumOf(body))
|
|
451
|
-
|
|
519
|
+
return refuse("the org state file failed its integrity check");
|
|
452
520
|
return parsed.state;
|
|
453
521
|
}
|
|
454
522
|
async readOrgState() {
|
|
@@ -456,7 +524,7 @@ class FileOrgRuleStatePersistence {
|
|
|
456
524
|
}
|
|
457
525
|
installOrgStateSync(state) {
|
|
458
526
|
this.acquireWriteLock();
|
|
459
|
-
|
|
527
|
+
assertSafeDirAtRequest(this.dir, this.onError, "write through");
|
|
460
528
|
state = jsonForm(state);
|
|
461
529
|
const cur = this.readSync();
|
|
462
530
|
if (cur !== undefined && state.revisionHighWater < cur.revisionHighWater) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PutRulesOptions, type SessionPermissionRules, type SessionPolicyStore, type SessionRulesRecord, type StoredSessionRules } from "../../core/session-policy-store.js";
|
|
1
|
+
import { type PutRulesOptions, type SessionPolicyStoreOptions, type SessionPermissionRules, type SessionPolicyStore, type SessionRulesRecord, type StoredSessionRules } from "../../core/session-policy-store.js";
|
|
2
2
|
/**
|
|
3
3
|
* design/99 §E6 — file-backed {@link SessionPolicyStore} for the local (TOC) backend: one JSON row file per
|
|
4
4
|
* `(sessionId, principal)`, plus — once that key has ever been stamped — a second, tiny file beside it holding
|
|
@@ -20,18 +20,23 @@ export interface SessionPolicyCorruptReadInfo {
|
|
|
20
20
|
/** Always a whole-file disclosure (the {@link import("./corrupt-read-sink.js").CorruptReadInfo} level). */
|
|
21
21
|
kind: "file";
|
|
22
22
|
/** The session whose read observed the corrupt row. On the enumeration face this is the session being
|
|
23
|
-
* ENUMERATED
|
|
23
|
+
* ENUMERATED: the row it refuses is that session's ({@link attributeSessionPolicyRow}), the row it omits is one
|
|
24
|
+
* nothing on disk attributes to anybody. */
|
|
24
25
|
sessionId: string;
|
|
25
26
|
/** Present only when the read was keyed by a principal (the enumeration face has none). */
|
|
26
27
|
principal?: string;
|
|
27
|
-
/** Absolute path of the offending file.
|
|
28
|
+
/** Absolute path of the offending file — a rules row, or a key's high-water mark (`*.hwm.json`). The refusals'
|
|
29
|
+
* messages never carry it: this is where a location is reported. */
|
|
28
30
|
path: string;
|
|
29
|
-
/** What was observed — the same words the refusal's message carries after its grade (unparseable / malformed / unreadable)
|
|
31
|
+
/** What was observed — the same words the refusal's message carries after its grade (unparseable / malformed / unreadable);
|
|
32
|
+
* for a high-water mark, `high-water mark ` + the reader's fault predicate (is unparseable / is not a JSON object / …). */
|
|
30
33
|
reason: string;
|
|
31
34
|
}
|
|
32
|
-
export interface FileSessionPolicyStoreOptions {
|
|
33
|
-
/** Disclosure sink for a damaged rules file (see {@link FileSessionPolicyStore.refuseDamaged}
|
|
34
|
-
* refusal on the keyed read and on
|
|
35
|
+
export interface FileSessionPolicyStoreOptions extends SessionPolicyStoreOptions {
|
|
36
|
+
/** Disclosure sink for a damaged rules file or high-water mark (see {@link FileSessionPolicyStore.refuseDamaged} and
|
|
37
|
+
* its mark sibling): it fires WITH the refusal on the keyed read / write and on the enumeration of the session that
|
|
38
|
+
* owns the row, and on its own where the enumeration omits a row nothing attributes to anybody or the erase sweep
|
|
39
|
+
* leaves a damaged mark. Never fires on plain ENOENT.
|
|
35
40
|
* Swallow-guarded: a throwing sink cannot change the refusal. */
|
|
36
41
|
onCorruptRead?: (info: SessionPolicyCorruptReadInfo) => void;
|
|
37
42
|
}
|
|
@@ -54,15 +59,27 @@ export interface StoredHighWaterMark {
|
|
|
54
59
|
/** The erase's tombstone (a torn row purged with no mark): writes are refused until an operator deletes this file. */
|
|
55
60
|
__lineage?: "lost";
|
|
56
61
|
}
|
|
62
|
+
/** The ONE reader's answer: the mark (`null` = no file), or the fault that makes the bytes not a mark. `reason` is a
|
|
63
|
+
* predicate ("is unparseable"), so every wrapper — the keyed store's refusal, the adoption leg's, the disclosure —
|
|
64
|
+
* says the same fault in the same words. */
|
|
65
|
+
export type SessionPolicyMarkRead = {
|
|
66
|
+
mark: StoredHighWaterMark | null;
|
|
67
|
+
} | {
|
|
68
|
+
corrupt: {
|
|
69
|
+
reason: string;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
57
72
|
/**
|
|
58
|
-
* The ONE reader of a mark file — the number it holds
|
|
59
|
-
*
|
|
60
|
-
* degrades to "absent" and costs nothing, while for a mark absence
|
|
61
|
-
* rollback the erase contract forbids.
|
|
62
|
-
* the
|
|
63
|
-
*
|
|
73
|
+
* The ONE reader of a mark file — the number it holds (`{ mark }`, `null` for no file: the ordinary state of a key
|
|
74
|
+
* nobody erased), or the fault that makes the bytes not a mark (`{ corrupt }`). Every outcome but those two is a
|
|
75
|
+
* refusal at the caller: for a ROW "unreadable" degrades to "absent" and costs nothing, while for a mark absence
|
|
76
|
+
* reads as ZERO, the rollback the erase contract forbids. It only JUDGES, carrying no sentence and no location,
|
|
77
|
+
* because the wording depends on the audience: a store method answers an end user (session, fault, repair; the
|
|
78
|
+
* location goes to the disclosure sink), the adoption leg answers an operator (it names the file). An IO failure
|
|
79
|
+
* propagates raw. Exported because adoption moves these files too; a second, more tolerant reader there is how the
|
|
80
|
+
* guarantee goes quietly missing.
|
|
64
81
|
*/
|
|
65
|
-
export declare function readSessionPolicyMark(path: string):
|
|
82
|
+
export declare function readSessionPolicyMark(path: string): SessionPolicyMarkRead;
|
|
66
83
|
/** Why one row file's bytes are not a row. `unparseable` is the ONE grade saying they are not JSON at all — the
|
|
67
84
|
* only evidence the erase sweep's crash-artifact ruling rests on. `reason` is the disclosure text, so two faces
|
|
68
85
|
* judging the SAME file name the same fault in the same words. */
|
|
@@ -91,9 +108,32 @@ export declare function judgeSessionPolicyRowShape(row: Record<string, unknown>)
|
|
|
91
108
|
} | {
|
|
92
109
|
corrupt: SessionPolicyRowFault;
|
|
93
110
|
};
|
|
111
|
+
/** Whose key one row file is, as the bytes on disk show it. */
|
|
112
|
+
export interface SessionPolicyRowOwner {
|
|
113
|
+
sessionId: string;
|
|
114
|
+
/** The principal the SAME evidence names: a string, `null` for the anonymous lane, `undefined` when it names none it can spell. */
|
|
115
|
+
principal: string | null | undefined;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The ONE answer to "whose row is this file?" for every face that sweeps the directory — the enumeration, the erase
|
|
119
|
+
* and the ownership rebind — so a damaged row is one session's fact on all three. The row's own `__sid` when its
|
|
120
|
+
* bytes are a JSON object carrying one; otherwise the `__sid` of its same-stem high-water mark, which every stamp
|
|
121
|
+
* writes BEFORE the row, so a torn, non-object or unreadable row still has it. `undefined` = nothing on disk names
|
|
122
|
+
* an owner (no mark, or one that is not a mark or carries no session): such a row can be judged by nobody's key, and
|
|
123
|
+
* each face keeps its own answer for it. A mark that cannot be READ is no evidence either way, so it THROWS (raw,
|
|
124
|
+
* from the reader) and the caller refuses rather than guess: guessing "nobody" would let an erase sweep another
|
|
125
|
+
* session's restriction, or a rebind strand it, on one transient read fault. When the answer rested on the mark,
|
|
126
|
+
* the read is handed back (`markRead`) so a caller that then acts on the mark acts on the SAME observation.
|
|
127
|
+
*/
|
|
128
|
+
export declare function attributeSessionPolicyRow(dir: string, file: string, row: Record<string, unknown> | undefined): {
|
|
129
|
+
owner: SessionPolicyRowOwner | undefined;
|
|
130
|
+
markRead?: SessionPolicyMarkRead;
|
|
131
|
+
};
|
|
94
132
|
export declare class FileSessionPolicyStore implements SessionPolicyStore {
|
|
95
133
|
private readonly dir;
|
|
96
134
|
private readonly onCorruptRead;
|
|
135
|
+
/** {@link SessionPolicyStoreOptions.mountedToolNames}, read once at construction. */
|
|
136
|
+
private readonly mountedToolNames;
|
|
97
137
|
/** #132 — the ADOPTED identity of a local-owner root's anonymous lane, or undefined on an
|
|
98
138
|
* unadopted (or principal-adopted) root. A local-owner adoption rebinds the anonymous
|
|
99
139
|
* `[sid,null]` estate rows to `toPrincipal`; until the deployment's principal wiring lands
|
|
@@ -116,6 +156,25 @@ export declare class FileSessionPolicyStore implements SessionPolicyStore {
|
|
|
116
156
|
* body, so it names no on-disk location. Two ways out, both the operator's: fix the file, or delete it — deleting is an explicit "no rules" and
|
|
117
157
|
* gives up the rules the file held. */
|
|
118
158
|
private refuseDamaged;
|
|
159
|
+
/** The sibling of {@link refuseDamaged} for the key's high-water MARK, and its only other voice: one disclosure
|
|
160
|
+
* shape, one code, a message naming the session, the fault and the repair but never a location (the path rides
|
|
161
|
+
* the sink, as the row's does). It refuses rather than read the mark as absent: that reads as ZERO and restarts
|
|
162
|
+
* the key's rev (see {@link readSessionPolicyMark}). Deleting the file gives up the number it held. */
|
|
163
|
+
private refuseDamagedMark;
|
|
164
|
+
/** The sweeps' (enumeration / erase) refusal when a file they must READ cannot be — the directory, or a mark that
|
|
165
|
+
* decides whose a row is or holds the number an erase must keep. Same voice as the two above: session, fault,
|
|
166
|
+
* repair, and the location only on the disclosure. Nothing is guessed and nothing is removed; a retry re-reads. */
|
|
167
|
+
private refuseUnreadable;
|
|
168
|
+
/** {@link attributeSessionPolicyRow} for a sweep: an unreadable mark becomes {@link refuseUnreadable}. */
|
|
169
|
+
private ownerOf;
|
|
170
|
+
/** A mark the erase must act on, read in the sweep's voice ({@link refuseUnreadable} when it cannot be read). */
|
|
171
|
+
private sweepMark;
|
|
172
|
+
/** The directory a sweep enumerates; absent = empty, unreadable = {@link refuseUnreadable}. */
|
|
173
|
+
private sweepEntries;
|
|
174
|
+
/** The keyed read of the mark for a write: the number, `null` for a key nobody stamped; a file that is there is a
|
|
175
|
+
* mark or a refusal ({@link refuseDamagedMark}), never "no mark". One that cannot be READ refuses in the same
|
|
176
|
+
* voice — the raw errno error names the file (EACCES does). */
|
|
177
|
+
private readMark;
|
|
119
178
|
/** `(sessionId, principal)` → a safe, INJECTIVE filename (sanitizeScope appends the full sha256 of the raw
|
|
120
179
|
* composite key, so distinct keys never collide on disk). On a local-owner-adopted root the
|
|
121
180
|
* anonymous lane resolves to the adopted principal's key (#132 — see {@link anonymousAlias}). */
|
|
@@ -130,8 +189,21 @@ export declare class FileSessionPolicyStore implements SessionPolicyStore {
|
|
|
130
189
|
/** The keyed read: `null` is ABSENCE only (no file); a file that is there is served as rules or refused
|
|
131
190
|
* ({@link refuseDamaged}) — it is never read as "no rules". */
|
|
132
191
|
private read;
|
|
192
|
+
/** One row file → its JSON object, the fault that makes it not one (the read failure included), or `null` for
|
|
193
|
+
* ABSENCE (ENOENT — also a row unlinked under a sweep, an honest absence). Every face reads a row through here. */
|
|
194
|
+
private readRowFile;
|
|
133
195
|
getRules(sessionId: string, principal?: string): Promise<StoredSessionRules | null>;
|
|
134
196
|
putRules(sessionId: string, principal: string | undefined, rules: SessionPermissionRules, opts?: PutRulesOptions): Promise<StoredSessionRules>;
|
|
135
197
|
listBySession(sessionId: string): Promise<SessionRulesRecord[]>;
|
|
136
198
|
deleteBySession(sessionId: string): Promise<void>;
|
|
199
|
+
/** Erase ONE row this session owns: the number must outlive it, so it is folded into the key's mark BEFORE the
|
|
200
|
+
* unlink, and the erase ends the row's lineage (`__gen` + 1) in that same single mark write — a crash in between
|
|
201
|
+
* leaves a mark one erase ahead of a row that still exists, which the next erase counts again (a skipped
|
|
202
|
+
* generation, and nothing else). Normally the mark is already ahead (every stamp writes it first); the fold earns
|
|
203
|
+
* its keep on rows that predate the mark. A row is released only once its number is provably safe, so exactly ONE
|
|
204
|
+
* fault may be absorbed: bytes that are not a mark. Those already block every future write to this key (`putRules`
|
|
205
|
+
* refuses on them), so they are left as they are rather than replaced by evidence this sweep cannot check. A mark
|
|
206
|
+
* that cannot be READ proves nothing: {@link refuseUnreadable}, and the erase is retried. `observed` = the read the
|
|
207
|
+
* owner judgment already took — acted on as observed, so a mark cannot vanish between the judgment and the erase. */
|
|
208
|
+
private eraseRow;
|
|
137
209
|
}
|