@sema-agent/core 5.21.0 → 5.22.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 (73) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/agents/send-message-tool.js +6 -3
  3. package/dist/agents/subagent.d.ts +6 -0
  4. package/dist/agents/subagent.js +45 -4
  5. package/dist/brain/errors.d.ts +20 -0
  6. package/dist/brain/errors.js +40 -0
  7. package/dist/brain/retry.d.ts +16 -2
  8. package/dist/brain/retry.js +3 -2
  9. package/dist/brain/status-sink.d.ts +9 -2
  10. package/dist/brain/stream-engine.d.ts +22 -0
  11. package/dist/brain/stream-engine.js +41 -10
  12. package/dist/core/ask-class.d.ts +48 -0
  13. package/dist/core/ask-class.js +33 -0
  14. package/dist/core/checkpoint-store.d.ts +103 -10
  15. package/dist/core/checkpoint-store.js +3 -1
  16. package/dist/core/governance-codes.d.ts +38 -0
  17. package/dist/core/governance-codes.js +11 -0
  18. package/dist/core/hooks.d.ts +39 -0
  19. package/dist/core/hooks.js +26 -2
  20. package/dist/core/locked-config.d.ts +7 -1
  21. package/dist/core/locked-config.js +2 -1
  22. package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
  23. package/dist/core/memory-engine/delegation-provenance.js +26 -0
  24. package/dist/core/memory-engine/engine.d.ts +67 -1
  25. package/dist/core/memory-engine/engine.js +270 -12
  26. package/dist/core/memory-engine/header-hints.d.ts +30 -0
  27. package/dist/core/memory-engine/header-hints.js +41 -0
  28. package/dist/core/memory-engine/index.d.ts +3 -2
  29. package/dist/core/memory-engine/index.js +3 -2
  30. package/dist/core/memory-engine/layout.d.ts +166 -0
  31. package/dist/core/memory-engine/layout.js +399 -0
  32. package/dist/core/memory-engine/tools.d.ts +30 -0
  33. package/dist/core/memory-engine/tools.js +108 -17
  34. package/dist/core/permission-rule-consent.d.ts +25 -9
  35. package/dist/core/permission-rule-consent.js +91 -20
  36. package/dist/core/permission-rule-model.d.ts +9 -1
  37. package/dist/core/permission-rule-model.js +2 -2
  38. package/dist/core/permission-rule-org.d.ts +161 -0
  39. package/dist/core/permission-rule-org.js +211 -0
  40. package/dist/core/permission-rule-store.d.ts +249 -6
  41. package/dist/core/permission-rule-store.js +313 -3
  42. package/dist/core/permission-rule-sync.d.ts +131 -0
  43. package/dist/core/permission-rule-sync.js +314 -0
  44. package/dist/core/runner/prepare-memory.js +35 -8
  45. package/dist/core/runner/prepare-task.d.ts +54 -1
  46. package/dist/core/runner/prepare-task.js +246 -27
  47. package/dist/core/runner/runtask.js +147 -6
  48. package/dist/core/shared-memory/contract.js +19 -4
  49. package/dist/core/shared-memory/normalize.d.ts +3 -1
  50. package/dist/core/shared-memory/tools.js +73 -17
  51. package/dist/core/shared-memory/types.d.ts +27 -1
  52. package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
  53. package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
  54. package/dist/core/task-notification.d.ts +5 -2
  55. package/dist/core/task-registry-agent.d.ts +1 -1
  56. package/dist/core/task-registry-agent.js +6 -2
  57. package/dist/core/task-registry-shared.d.ts +9 -2
  58. package/dist/core/task-registry.d.ts +9 -3
  59. package/dist/core/task-registry.js +2 -0
  60. package/dist/core/tool-policy.d.ts +120 -2
  61. package/dist/core/tool-policy.js +116 -6
  62. package/dist/core/trace.d.ts +32 -1
  63. package/dist/core/types.d.ts +56 -3
  64. package/dist/index.d.ts +12 -7
  65. package/dist/index.js +10 -5
  66. package/dist/stores/file/checkpoint-store.d.ts +4 -0
  67. package/dist/stores/file/checkpoint-store.js +1 -0
  68. package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
  69. package/dist/stores/file/permission-rule-adopt.js +95 -0
  70. package/dist/stores/file/permission-rule-store.d.ts +80 -2
  71. package/dist/stores/file/permission-rule-store.js +189 -46
  72. package/dist/tools/fs/fs-search-tools.js +0 -1
  73. package/package.json +1 -1
@@ -0,0 +1,95 @@
1
+ import { existsSync, renameSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { writerOf } from "../../core/permission-rule-store.js";
4
+ import { syncPermissionRules } from "../../core/permission-rule-sync.js";
5
+ import { FilePermissionRuleStoreProvider, LOCAL_OWNER_FILE, principalRuleFileName, readRuleAdoptionFile, writeRuleAdoptionFile, } from "./permission-rule-store.js";
6
+ export async function adoptFilePermissionRuleStore(opts) {
7
+ if (typeof opts.toPrincipal !== "string" || opts.toPrincipal === "") {
8
+ throw new Error("adoptFilePermissionRuleStore requires the adopting principal — adoption is the act of giving this bucket an identity");
9
+ }
10
+ if (opts.from.kind === "principal" && opts.from.principal === opts.toPrincipal) {
11
+ throw new Error(`the bucket already belongs to "${opts.toPrincipal}" — there is nothing to adopt`);
12
+ }
13
+ const sourceFile = join(opts.dir, opts.from.kind === "local-owner" ? LOCAL_OWNER_FILE : principalRuleFileName(opts.from.principal));
14
+ const targetFile = join(opts.dir, principalRuleFileName(opts.toPrincipal));
15
+ const existing = readRuleAdoptionFile(opts.dir);
16
+ if (existing !== undefined) {
17
+ if ("adopted" in existing) {
18
+ if (existing.adopted.toPrincipal === opts.toPrincipal && sameOwner(existing.adopted.from, opts.from))
19
+ return { status: "adopted" };
20
+ throw new Error(`this directory already carries a completed adoption (${ownerText(existing.adopted.from)} → "${existing.adopted.toPrincipal}") — a second adoption of the same directory is not a shape this helper supports`);
21
+ }
22
+ if (existing.marker.toPrincipal !== opts.toPrincipal || !sameOwner(existing.marker.from, opts.from)) {
23
+ throw new Error(`an adoption toward "${existing.marker.toPrincipal}" is already in flight in this directory — refusing to start a different one (resume the original, or repair the marker by hand)`);
24
+ }
25
+ }
26
+ else if (existsSync(targetFile)) {
27
+ throw new Error(`the target bucket for "${opts.toPrincipal}" already exists — merging two existing buckets goes through an explicit sync join, not a rename`);
28
+ }
29
+ const provider = new FilePermissionRuleStoreProvider(opts.dir, opts.onError);
30
+ try {
31
+ const lockProbe = writerOf(provider.forPrincipal(opts.toPrincipal));
32
+ if (lockProbe === undefined)
33
+ throw new Error("the target store refused a write face");
34
+ let phase = existing !== undefined && "marker" in existing ? existing.marker.phase : 2;
35
+ if (existing === undefined) {
36
+ writeRuleAdoptionFile(opts.dir, marker(opts.from, opts.toPrincipal, 2));
37
+ phase = 2;
38
+ }
39
+ if (phase < 3) {
40
+ const sourcePresent = existsSync(sourceFile);
41
+ const targetPresent = existsSync(targetFile);
42
+ if (sourcePresent && targetPresent) {
43
+ throw new Error(`both the source and the target bucket exist — merging two existing buckets goes through an explicit sync join, not a rename; the adoption marker is left in place for inspection`);
44
+ }
45
+ if (sourcePresent)
46
+ renameSync(sourceFile, targetFile);
47
+ writeRuleAdoptionFile(opts.dir, marker(opts.from, opts.toPrincipal, 3));
48
+ phase = 3;
49
+ }
50
+ if (phase < 4) {
51
+ writeRuleAdoptionFile(opts.dir, marker(opts.from, opts.toPrincipal, 4));
52
+ phase = 4;
53
+ }
54
+ let syncResult;
55
+ if (phase < 5) {
56
+ try {
57
+ syncResult = await syncPermissionRules({
58
+ provider,
59
+ principal: opts.toPrincipal,
60
+ transport: opts.transport,
61
+ ...(opts.now !== undefined ? { now: opts.now } : {}),
62
+ });
63
+ }
64
+ catch (err) {
65
+ return { status: "stalled", phase: 4, error: `the first sync round failed: ${err instanceof Error ? err.message : String(err)}` };
66
+ }
67
+ if (!syncResult.ok) {
68
+ return {
69
+ status: "stalled",
70
+ phase: 4,
71
+ error: `the first sync round did not land clean (${(syncResult.warnings ?? []).join("; ") || `${syncResult.dropped.length} drop(s)`}) — resume when the endpoint is healthy`,
72
+ };
73
+ }
74
+ writeRuleAdoptionFile(opts.dir, marker(opts.from, opts.toPrincipal, 5));
75
+ phase = 5;
76
+ }
77
+ writeRuleAdoptionFile(opts.dir, {
78
+ schemaVersion: 1,
79
+ adopted: { from: opts.from, toPrincipal: opts.toPrincipal, atMs: (opts.now ?? Date.now)() },
80
+ });
81
+ return { status: "adopted", ...(syncResult !== undefined ? { syncResult } : {}) };
82
+ }
83
+ finally {
84
+ provider.dispose();
85
+ }
86
+ }
87
+ function marker(from, toPrincipal, phase) {
88
+ return { schemaVersion: 1, marker: { from, toPrincipal, phase } };
89
+ }
90
+ function sameOwner(a, b) {
91
+ return a.kind === "principal" ? b.kind === "principal" && a.principal === b.principal : b.kind === "local-owner";
92
+ }
93
+ function ownerText(o) {
94
+ return o.kind === "principal" ? `principal "${o.principal}"` : "the local owner";
95
+ }
@@ -42,9 +42,63 @@
42
42
  * and is not: it detects corruption, and it does not detect an editor, because an editor recomputes it.
43
43
  * For this backend the trust is host = user, exactly as for the settings file it mirrors.
44
44
  */
45
- import type { PermissionRuleStore, PermissionRuleStoreProvider, PermissionRuleWriter, StoredAllowRules, WritablePermissionRuleStore } from "../../core/permission-rule-store.js";
45
+ import type { PersistedAllowRule, RuleTombstone } from "../../core/permission-rule-model.js";
46
+ import type { PermissionRuleStore, PermissionRuleStoreProvider, PermissionRuleWriter, QuarantinedRuleAdd, RuleOwner, RuleSyncFrontier, StoredAllowRules, WritablePermissionRuleStore } from "../../core/permission-rule-store.js";
46
47
  import { PERMISSION_RULE_WRITER } from "../../core/permission-rule-store.js";
48
+ import type { PersistedOrgRuleState } from "../../core/permission-rule-org.js";
47
49
  import type { StoreDurability, StoreFidelity } from "../../core/checkpoint-store.js";
50
+ /**
51
+ * The on-disk shape. `schemaVersion` is the only field a future reader may rely on before validating.
52
+ *
53
+ * v1 → v2 (design/182 §8.3): three OPTIONAL blocks — `sync` (observation vector + round timestamp),
54
+ * `org` (last-known-good org snapshot + anti-rollback high-water) and `quarantined` (rows moved out of
55
+ * the live view, bytes preserved). The five v1 fields are byte-identical in meaning. A file only ever
56
+ * ESCALATES to v2 when a write actually carries v2 content (a sync round, an org install, a
57
+ * quarantine): a deployment that never syncs keeps writing v1 files forever, and a v1 reader handed a
58
+ * v2 file refuses the whole file loudly (zero rules + disclosure — fail-closed, more asks, and the
59
+ * documented cost of rolling the engine back under an already-synced store).
60
+ */
61
+ interface RuleFile {
62
+ schemaVersion: 1 | 2;
63
+ actor: string;
64
+ counter: number;
65
+ rev: number;
66
+ rules: PersistedAllowRule[];
67
+ tombstones: RuleTombstone[];
68
+ sync?: {
69
+ observedVector?: RuleSyncFrontier;
70
+ lastRoundAtMs?: number;
71
+ };
72
+ org?: PersistedOrgRuleState;
73
+ quarantined?: QuarantinedRuleAdd[];
74
+ checksum: string;
75
+ }
76
+ /** The local-owner bucket's FIXED file name — never a principal-hash path (identity absence is a
77
+ * structural position, not a name; design/182 §4.5). Cannot collide with `forPrincipal` names, which
78
+ * are 64 hex characters. */
79
+ declare const LOCAL_OWNER_FILE = "local-owner.json";
80
+ /** The durable adoption marker (design/182 §4.5 — the recoverable six-phase state machine's truth). */
81
+ export type RuleAdoptionFile = {
82
+ schemaVersion: 1;
83
+ marker: {
84
+ from: RuleOwner;
85
+ toPrincipal: string;
86
+ phase: 2 | 3 | 4 | 5;
87
+ };
88
+ } | {
89
+ schemaVersion: 1;
90
+ adopted: {
91
+ from: RuleOwner;
92
+ toPrincipal: string;
93
+ atMs: number;
94
+ };
95
+ };
96
+ /** Read the adoption marker, `undefined` when none. A corrupt marker THROWS: after phase 4 the marker
97
+ * IS the owner-resolution truth, and guessing on a corrupt truth would either resurrect the retired
98
+ * bucket (write fork) or invent an adoption that never happened. */
99
+ export declare function readRuleAdoptionFile(dir: string): RuleAdoptionFile | undefined;
100
+ /** Atomically publish the adoption marker (same write-temp → fsync → rename discipline as the store). */
101
+ export declare function writeRuleAdoptionFile(dir: string, content: RuleAdoptionFile): void;
48
102
  /** One principal's rule file. */
49
103
  declare class FilePermissionRuleStore implements WritablePermissionRuleStore {
50
104
  private readonly dir;
@@ -59,6 +113,14 @@ declare class FilePermissionRuleStore implements WritablePermissionRuleStore {
59
113
  private disclose;
60
114
  private write;
61
115
  list(): Promise<StoredAllowRules>;
116
+ /** The quarantine area (design/182 §5.2/§8.3): introspection only — never part of `list()`. */
117
+ quarantined(): Promise<QuarantinedRuleAdd[]>;
118
+ /** design/182 §7.4 — the durable org block: last-known-good snapshot + anti-rollback high-water,
119
+ * installed in ONE atomic publish, surviving restarts. Reading takes no lock (the gate reads);
120
+ * installing takes the writer lock (it is a write) but does not move `rev` — nothing about the
121
+ * PERSONAL rule set changed, and an OCC holder must not be disturbed (the `nextDot` precedent). */
122
+ readOrgState(): Promise<PersistedOrgRuleState | undefined>;
123
+ installOrgState(state: PersistedOrgRuleState): Promise<void>;
62
124
  /**
63
125
  * The current file, for a WRITE.
64
126
  *
@@ -104,8 +166,24 @@ export declare class FilePermissionRuleStoreProvider implements PermissionRuleSt
104
166
  /** Take the writer lock, once, on first use of a write face. Reading never calls this. */
105
167
  private acquireWriteLock;
106
168
  forPrincipal(principal: string | undefined): PermissionRuleStore;
169
+ /** The read-only resolution of an adoption SOURCE: post-flip (phase ≥ 4 / terminal) always the
170
+ * target; pre-flip, whichever path currently holds the bytes (source first — it is the pre-flip
171
+ * truth when both somehow exist, which the resume path refuses loudly anyway). */
172
+ private readOnlySourceView;
173
+ /**
174
+ * design/182 §4.5 (F-011) — the identity-less LOCAL bucket: a fixed file name, never a principal-hash
175
+ * path. Once an adoption reached phase 4 (or its permanent terminal record), local-owner resolution
176
+ * points at the adopted principal's bucket FOREVER — the marker is the truth the host configuration
177
+ * reads, not the other way round, so a crash between marker and host-config change cannot fork writes
178
+ * into a retired bucket. A corrupt marker throws (fail-closed) rather than guessing an owner.
179
+ */
180
+ forLocalOwner(): PermissionRuleStore;
107
181
  /** Release the writer lock. A long-lived deployment holds it for its lifetime; a test or a short-lived
108
182
  * tool releases so the next holder is not told a live process owns the directory. */
109
183
  dispose(): void;
110
184
  }
111
- export type { FilePermissionRuleStore };
185
+ /** The hash-derived file name of one principal's bucket — shared with the adoption helper so the rename
186
+ * and the resolver can never disagree about where a principal lives. */
187
+ export declare function principalRuleFileName(principal: string): string;
188
+ export { LOCAL_OWNER_FILE };
189
+ export type { FilePermissionRuleStore, RuleFile };
@@ -1,9 +1,78 @@
1
- import { closeSync, constants as FS, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { closeSync, constants as FS, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
- import { PERMISSION_RULE_WRITER, applyTombstones, assertDeleteDeltaCarriesNoAdd, foldDelta } from "../../core/permission-rule-store.js";
4
+ import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, foldDelta, } from "../../core/permission-rule-store.js";
5
5
  import { canonicalize } from "../../core/canonical-json.js";
6
6
  import { BootLock } from "./fs-atomic.js";
7
+ const LOCAL_OWNER_FILE = "local-owner.json";
8
+ const ADOPTION_FILE = "PERMISSION-RULES-ADOPTION.json";
9
+ export function readRuleAdoptionFile(dir) {
10
+ let raw;
11
+ try {
12
+ raw = readFileSync(join(dir, ADOPTION_FILE), "utf8");
13
+ }
14
+ catch (err) {
15
+ if (err?.code === "ENOENT")
16
+ return undefined;
17
+ throw new Error(`could not read the permission-rule adoption marker in ${dir}: ${err.message}`);
18
+ }
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(raw);
22
+ }
23
+ catch (err) {
24
+ throw new Error(`the permission-rule adoption marker in ${dir} is not valid JSON (${err.message}) — refusing to resolve owners until it is repaired`);
25
+ }
26
+ if (!isRuleAdoptionFileShape(parsed)) {
27
+ throw new Error(`the permission-rule adoption marker in ${dir} does not carry a readable shape — refusing to resolve owners until it is repaired`);
28
+ }
29
+ return parsed;
30
+ }
31
+ function isRuleAdoptionFileShape(v) {
32
+ return typeof v === "object" && v !== null && v.schemaVersion === 1 && ("marker" in v || "adopted" in v);
33
+ }
34
+ export function writeRuleAdoptionFile(dir, content) {
35
+ assertSafeDir(dir);
36
+ atomicPublish(dir, join(dir, ADOPTION_FILE), JSON.stringify(content, null, 2));
37
+ }
38
+ function atomicPublish(dir, file, payload) {
39
+ const tmp = join(dir, `.${randomBytes(8).toString("hex")}.tmp`);
40
+ let fd;
41
+ try {
42
+ fd = openSync(tmp, FS.O_WRONLY | FS.O_CREAT | FS.O_EXCL | (FS.O_NOFOLLOW ?? 0), 0o600);
43
+ writeFileSync(fd, payload);
44
+ fsyncSync(fd);
45
+ closeSync(fd);
46
+ fd = undefined;
47
+ renameSync(tmp, file);
48
+ let dfd;
49
+ try {
50
+ dfd = openSync(dir, "r");
51
+ fsyncSync(dfd);
52
+ }
53
+ catch {
54
+ }
55
+ finally {
56
+ if (dfd !== undefined)
57
+ closeSync(dfd);
58
+ }
59
+ }
60
+ catch (err) {
61
+ if (fd !== undefined) {
62
+ try {
63
+ closeSync(fd);
64
+ }
65
+ catch {
66
+ }
67
+ }
68
+ try {
69
+ unlinkSync(tmp);
70
+ }
71
+ catch {
72
+ }
73
+ throw err;
74
+ }
75
+ }
7
76
  const EMPTY_READ = { rules: [], tombstones: [], rev: 0 };
8
77
  function checksumOf(body) {
9
78
  return `sha256:${createHash("sha256").update(canonicalize(body), "utf8").digest("hex")}`;
@@ -64,7 +133,11 @@ class FilePermissionRuleStore {
64
133
  this.disclose(why);
65
134
  return { unreadable: why };
66
135
  }
67
- if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.rules) || !Array.isArray(parsed.tombstones) || typeof parsed.rev !== "number") {
136
+ if ((parsed?.schemaVersion !== 1 && parsed?.schemaVersion !== 2) ||
137
+ !Array.isArray(parsed.rules) ||
138
+ !Array.isArray(parsed.tombstones) ||
139
+ typeof parsed.rev !== "number" ||
140
+ (parsed.quarantined !== undefined && !Array.isArray(parsed.quarantined))) {
68
141
  const why = `${this.file} does not carry a readable rule-file shape; refusing the whole file and loading zero rules`;
69
142
  this.disclose(why);
70
143
  return { unreadable: why };
@@ -86,43 +159,7 @@ class FilePermissionRuleStore {
86
159
  }
87
160
  write(next) {
88
161
  assertSafeDir(this.dir);
89
- const payload = JSON.stringify({ ...next, checksum: checksumOf(next) }, null, 2);
90
- const tmp = join(this.dir, `.${randomBytes(8).toString("hex")}.tmp`);
91
- let fd;
92
- try {
93
- fd = openSync(tmp, FS.O_WRONLY | FS.O_CREAT | FS.O_EXCL | (FS.O_NOFOLLOW ?? 0), 0o600);
94
- writeFileSync(fd, payload);
95
- fsyncSync(fd);
96
- closeSync(fd);
97
- fd = undefined;
98
- renameSync(tmp, this.file);
99
- let dfd;
100
- try {
101
- dfd = openSync(this.dir, "r");
102
- fsyncSync(dfd);
103
- }
104
- catch {
105
- }
106
- finally {
107
- if (dfd !== undefined)
108
- closeSync(dfd);
109
- }
110
- }
111
- catch (err) {
112
- if (fd !== undefined) {
113
- try {
114
- closeSync(fd);
115
- }
116
- catch {
117
- }
118
- }
119
- try {
120
- unlinkSync(tmp);
121
- }
122
- catch {
123
- }
124
- throw err;
125
- }
162
+ atomicPublish(this.dir, this.file, JSON.stringify({ ...next, checksum: checksumOf(next) }, null, 2));
126
163
  }
127
164
  async list() {
128
165
  const r = this.read();
@@ -135,14 +172,50 @@ class FilePermissionRuleStore {
135
172
  checksum: r.file.checksum,
136
173
  };
137
174
  }
175
+ async quarantined() {
176
+ const r = this.read();
177
+ return "file" in r ? (r.file.quarantined ?? []) : [];
178
+ }
179
+ async readOrgState() {
180
+ const r = this.read();
181
+ return "file" in r ? r.file.org : undefined;
182
+ }
183
+ async installOrgState(state) {
184
+ this.acquireWriteLock();
185
+ await this.serialize(() => {
186
+ const cur = this.current();
187
+ if (cur.org !== undefined && state.revisionHighWater < cur.org.revisionHighWater) {
188
+ throw new Error(`refusing to lower the org revision high-water mark (${cur.org.revisionHighWater} → ${state.revisionHighWater}) — an interleaved install must re-read and re-compare`);
189
+ }
190
+ if (cur.org !== undefined &&
191
+ state.snapshot.revision === cur.org.snapshot.revision &&
192
+ canonicalize(state.snapshot.rules) !== canonicalize(cur.org.snapshot.rules)) {
193
+ throw new Error(`refusing to replace org revision ${state.snapshot.revision} with DIFFERENT policy content under the same revision — content changes must bump the revision`);
194
+ }
195
+ if (!this.writeAndVerify({ ...cur, schemaVersion: 2, org: state })) {
196
+ throw new Error("the permission-rule store did not survive its own write; the org snapshot was not installed");
197
+ }
198
+ });
199
+ }
138
200
  current() {
139
201
  const r = this.read();
140
202
  if ("unreadable" in r) {
141
203
  throw new Error(`refusing to write the permission-rule store while it cannot be read: ${r.unreadable}`);
142
204
  }
143
- return "absent" in r
144
- ? { schemaVersion: 1, actor: `file-${randomBytes(6).toString("hex")}`, counter: 0, rev: 0, rules: [], tombstones: [] }
145
- : { schemaVersion: 1, actor: r.file.actor, counter: r.file.counter, rev: r.file.rev, rules: r.file.rules, tombstones: r.file.tombstones };
205
+ if ("absent" in r) {
206
+ return { schemaVersion: 1, actor: `file-${randomBytes(6).toString("hex")}`, counter: 0, rev: 0, rules: [], tombstones: [] };
207
+ }
208
+ return {
209
+ schemaVersion: r.file.schemaVersion,
210
+ actor: r.file.actor,
211
+ counter: r.file.counter,
212
+ rev: r.file.rev,
213
+ rules: r.file.rules,
214
+ tombstones: r.file.tombstones,
215
+ ...(r.file.sync !== undefined ? { sync: r.file.sync } : {}),
216
+ ...(r.file.org !== undefined ? { org: r.file.org } : {}),
217
+ ...(r.file.quarantined !== undefined ? { quarantined: r.file.quarantined } : {}),
218
+ };
146
219
  }
147
220
  writeAndVerify(next) {
148
221
  const expected = checksumOf(next);
@@ -169,12 +242,50 @@ class FilePermissionRuleStore {
169
242
  }
170
243
  return dot;
171
244
  }),
245
+ readRaw: async () => this.serialize(() => {
246
+ const cur = this.current();
247
+ return {
248
+ actor: cur.actor,
249
+ counter: cur.counter,
250
+ rev: cur.rev,
251
+ rules: structuredClone(cur.rules),
252
+ tombstones: structuredClone(cur.tombstones),
253
+ ...(cur.sync?.observedVector !== undefined ? { observedVector: structuredClone(cur.sync.observedVector) } : {}),
254
+ quarantined: structuredClone(cur.quarantined ?? []),
255
+ };
256
+ }),
172
257
  apply: async (delta, opts) => this.serialize(() => {
173
258
  const cur = this.current();
174
259
  if (cur.rev !== opts.expectedRev)
175
260
  return { conflict: true, rev: cur.rev };
261
+ if (delta.kind === "sync-join") {
262
+ const { next, report } = applySyncJoin({
263
+ actor: cur.actor,
264
+ counter: cur.counter,
265
+ rules: cur.rules,
266
+ tombstones: cur.tombstones,
267
+ quarantined: cur.quarantined ?? [],
268
+ ...(cur.sync?.observedVector !== undefined ? { observedVector: cur.sync.observedVector } : {}),
269
+ }, delta, Date.now());
270
+ const landed = {
271
+ ...cur,
272
+ schemaVersion: 2,
273
+ rev: cur.rev + 1,
274
+ rules: next.rules,
275
+ tombstones: next.tombstones,
276
+ sync: {
277
+ ...(next.observedVector !== undefined ? { observedVector: next.observedVector } : {}),
278
+ lastRoundAtMs: Date.now(),
279
+ },
280
+ ...(next.quarantined.length > 0 ? { quarantined: next.quarantined } : {}),
281
+ };
282
+ if (!this.writeAndVerify(landed)) {
283
+ throw new Error("the permission-rule store did not survive its own write; the change was not committed");
284
+ }
285
+ return { rev: cur.rev + 1, sync: report };
286
+ }
176
287
  const next = delta.kind === "redemption-add"
177
- ? { ...cur, rev: cur.rev + 1, rules: foldDelta(cur.rules, delta) }
288
+ ? (assertRedemptionNotQuarantined(cur.quarantined ?? [], delta), { ...cur, rev: cur.rev + 1, rules: foldDelta(cur.rules, delta) })
178
289
  : (assertDeleteDeltaCarriesNoAdd(delta), { ...cur, rev: cur.rev + 1, tombstones: [...cur.tombstones, delta.tombstone] });
179
290
  if (!this.writeAndVerify(next)) {
180
291
  throw new Error("the permission-rule store did not survive its own write; the change was not committed");
@@ -203,11 +314,43 @@ export class FilePermissionRuleStoreProvider {
203
314
  if (typeof principal !== "string" || principal === "") {
204
315
  return { list: async () => ({ ...EMPTY_READ }), durability: "process-local" };
205
316
  }
206
- const name = `${createHash("sha256").update(principal, "utf8").digest("hex")}.json`;
207
- return new FilePermissionRuleStore(this.dir, join(this.dir, name), this.acquireWriteLock, this.onError);
317
+ const adoption = readRuleAdoptionFile(this.dir);
318
+ if (adoption !== undefined) {
319
+ const rec = "adopted" in adoption ? adoption.adopted : adoption.marker;
320
+ if (rec.from.kind === "principal" && rec.from.principal === principal) {
321
+ return this.readOnlySourceView(principalRuleFileName(principal), principalRuleFileName(rec.toPrincipal), "adopted" in adoption || adoption.marker.phase >= 4);
322
+ }
323
+ }
324
+ return new FilePermissionRuleStore(this.dir, join(this.dir, principalRuleFileName(principal)), this.acquireWriteLock, this.onError);
325
+ }
326
+ readOnlySourceView(sourceName, targetName, flipped) {
327
+ const pick = flipped ? targetName : existsSync(join(this.dir, sourceName)) ? sourceName : targetName;
328
+ return readOnlyRuleStoreView(new FilePermissionRuleStore(this.dir, join(this.dir, pick), this.acquireWriteLock, this.onError));
329
+ }
330
+ forLocalOwner() {
331
+ const adoption = readRuleAdoptionFile(this.dir);
332
+ if (adoption !== undefined) {
333
+ const rec = "adopted" in adoption ? adoption.adopted : adoption.marker;
334
+ if (rec.from.kind === "local-owner") {
335
+ return this.readOnlySourceView(LOCAL_OWNER_FILE, principalRuleFileName(rec.toPrincipal), "adopted" in adoption || adoption.marker.phase >= 4);
336
+ }
337
+ }
338
+ return new FilePermissionRuleStore(this.dir, join(this.dir, LOCAL_OWNER_FILE), this.acquireWriteLock, this.onError);
208
339
  }
209
340
  dispose() {
210
341
  this.lock?.release();
211
342
  this.lock = undefined;
212
343
  }
213
344
  }
345
+ function readOnlyRuleStoreView(store) {
346
+ return {
347
+ list: () => store.list(),
348
+ ...(store.quarantined !== undefined ? { quarantined: () => store.quarantined() } : {}),
349
+ ...(store.durability !== undefined ? { durability: store.durability } : {}),
350
+ ...(store.fidelity !== undefined ? { fidelity: store.fidelity } : {}),
351
+ };
352
+ }
353
+ export function principalRuleFileName(principal) {
354
+ return `${createHash("sha256").update(principal, "utf8").digest("hex")}.json`;
355
+ }
356
+ export { LOCAL_OWNER_FILE };
@@ -206,7 +206,6 @@ export function createGlobTool(env, rootCanonical, additionalRoots) {
206
206
  export const HAND_TOOL_EFFECTS = {
207
207
  Read: "read",
208
208
  Edit: "write",
209
- MultiEdit: "write",
210
209
  Write: "write",
211
210
  NotebookEdit: "write",
212
211
  Grep: "read",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.21.0",
3
+ "version": "5.22.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",