@sema-agent/core 7.4.0 → 7.5.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/agents/cascade.d.ts +2 -1
  3. package/dist/agents/peer-notices.d.ts +11 -1
  4. package/dist/agents/peer-session-drain.js +2 -0
  5. package/dist/agents/verify.d.ts +2 -1
  6. package/dist/core/ask-origin.d.ts +130 -0
  7. package/dist/core/ask-origin.js +35 -0
  8. package/dist/core/auto-mode-arming.d.ts +40 -1
  9. package/dist/core/auto-mode-arming.js +51 -3
  10. package/dist/core/auto-mode.d.ts +87 -10
  11. package/dist/core/auto-mode.js +34 -6
  12. package/dist/core/checkpoint-store.d.ts +41 -6
  13. package/dist/core/checkpoint-store.js +8 -0
  14. package/dist/core/hooks.d.ts +63 -19
  15. package/dist/core/hooks.js +37 -16
  16. package/dist/core/mcp.d.ts +47 -55
  17. package/dist/core/mcp.js +89 -31
  18. package/dist/core/park-selfcheck.js +3 -0
  19. package/dist/core/permission-rule-consent.d.ts +2 -11
  20. package/dist/core/permission-rule-consent.js +13 -62
  21. package/dist/core/permission-rule-org.d.ts +70 -54
  22. package/dist/core/permission-rule-org.js +47 -61
  23. package/dist/core/permission-rule-provider.d.ts +162 -0
  24. package/dist/core/permission-rule-provider.js +148 -0
  25. package/dist/core/permission-rule-session.d.ts +23 -19
  26. package/dist/core/permission-rule-session.js +5 -5
  27. package/dist/core/permission-rule-store.d.ts +46 -21
  28. package/dist/core/permission-rule-store.js +13 -6
  29. package/dist/core/permission-rule-sync.d.ts +2 -1
  30. package/dist/core/permission-rule-sync.js +11 -0
  31. package/dist/core/runner/assemble-result.d.ts +3 -2
  32. package/dist/core/runner/checkpoint-scope.d.ts +32 -0
  33. package/dist/core/runner/checkpoint-scope.js +4 -0
  34. package/dist/core/runner/contracts.d.ts +1878 -0
  35. package/dist/core/runner/contracts.js +1 -0
  36. package/dist/core/runner/denial-limit-arms.d.ts +57 -31
  37. package/dist/core/runner/denial-limit-arms.js +42 -17
  38. package/dist/core/runner/derived-route-fallback.d.ts +34 -0
  39. package/dist/core/runner/derived-route-fallback.js +16 -0
  40. package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
  41. package/dist/core/runner/prepare-announce-once.d.ts +83 -0
  42. package/dist/core/runner/prepare-announce-once.js +105 -0
  43. package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
  44. package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
  45. package/dist/core/runner/prepare-config-doors.d.ts +2 -10
  46. package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
  47. package/dist/core/runner/prepare-defer-classify.js +107 -0
  48. package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
  49. package/dist/core/runner/prepare-delegation-surface.js +144 -0
  50. package/dist/core/runner/prepare-execution-env.d.ts +54 -0
  51. package/dist/core/runner/prepare-execution-env.js +86 -0
  52. package/dist/core/runner/prepare-file-history.d.ts +95 -0
  53. package/dist/core/runner/prepare-file-history.js +383 -0
  54. package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
  55. package/dist/core/runner/prepare-hands-readface.js +3 -3
  56. package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
  57. package/dist/core/runner/prepare-inherited-gate.js +266 -0
  58. package/dist/core/runner/prepare-listings.d.ts +77 -0
  59. package/dist/core/runner/prepare-listings.js +76 -0
  60. package/dist/core/runner/prepare-lsp.d.ts +55 -0
  61. package/dist/core/runner/prepare-lsp.js +27 -0
  62. package/dist/core/runner/prepare-memory.d.ts +1 -1
  63. package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
  64. package/dist/core/runner/prepare-offload-wrappers.js +45 -0
  65. package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
  66. package/dist/core/runner/prepare-permission-rules.js +140 -0
  67. package/dist/core/runner/prepare-project-context.d.ts +131 -0
  68. package/dist/core/runner/prepare-project-context.js +150 -0
  69. package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
  70. package/dist/core/runner/prepare-prompt-inputs.js +141 -0
  71. package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
  72. package/dist/core/runner/prepare-protocol-tools.js +182 -0
  73. package/dist/core/runner/prepare-question-face.d.ts +119 -0
  74. package/dist/core/runner/prepare-question-face.js +83 -0
  75. package/dist/core/runner/prepare-run-refs.d.ts +89 -0
  76. package/dist/core/runner/prepare-run-refs.js +39 -0
  77. package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
  78. package/dist/core/runner/prepare-task.d.ts +11 -1815
  79. package/dist/core/runner/prepare-task.js +138 -2542
  80. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
  81. package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
  82. package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
  83. package/dist/core/runner/prepare-wiring-manifest.js +240 -0
  84. package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
  85. package/dist/core/runner/prepare-workspace-restore.js +1 -22
  86. package/dist/core/runner/rollback-stack.d.ts +32 -0
  87. package/dist/core/runner/rollback-stack.js +30 -0
  88. package/dist/core/runner/runtask.d.ts +11 -2
  89. package/dist/core/runner/runtask.js +27 -9
  90. package/dist/core/runner/workspace-path.d.ts +33 -0
  91. package/dist/core/runner/workspace-path.js +22 -0
  92. package/dist/core/sensitive-path-policy.d.ts +16 -0
  93. package/dist/core/sensitive-path-policy.js +1 -1
  94. package/dist/core/tool-policy.d.ts +57 -9
  95. package/dist/core/tool-policy.js +11 -0
  96. package/dist/core/types.d.ts +63 -51
  97. package/dist/core/wiring-manifest.d.ts +40 -3
  98. package/dist/core/wiring-manifest.js +4 -3
  99. package/dist/core/write-protect.d.ts +13 -2
  100. package/dist/core/write-protect.js +58 -29
  101. package/dist/engine/harness/types.d.ts +38 -16
  102. package/dist/engine/harness/types.js +25 -1
  103. package/dist/engine/session/session.d.ts +3 -11
  104. package/dist/index.d.ts +10 -6
  105. package/dist/index.js +9 -5
  106. package/dist/internal/harness.d.ts +1 -0
  107. package/dist/stores/file/adoption/adopt.d.ts +1 -1
  108. package/dist/stores/file/adoption/marker.d.ts +1 -1
  109. package/dist/stores/file/permission-rule-adopt.js +4 -3
  110. package/dist/stores/file/permission-rule-store.d.ts +65 -25
  111. package/dist/stores/file/permission-rule-store.js +215 -37
  112. package/dist/stores/file/task-list-store.d.ts +1 -1
  113. package/dist/tools/fs/read-face.d.ts +1 -1
  114. package/package.json +8 -2
  115. package/test/export-surface.snapshot.json +76 -28
@@ -0,0 +1,148 @@
1
+ import { escapeForDisclosure } from "./permission-rule-model.js";
2
+ import { PERMISSION_RULE_WRITER, errText, normalizePersistedRule, sameScope, writerOf, } from "./permission-rule-store.js";
3
+ import { UNGOVERNED_ORG_RESOLUTION, compileOrgShadowPredicate, createOrgRulePartition } from "./permission-rule-org.js";
4
+ export function ruleSourceOf(scope) {
5
+ return scope.kind === "global" ? "user" : scope.kind;
6
+ }
7
+ function usableSessionId(sessionId) {
8
+ return typeof sessionId === "string" && sessionId !== "" ? sessionId : undefined;
9
+ }
10
+ const EMPTY_DURABLE = { rules: [], tombstones: [], rev: 0 };
11
+ export function createPermissionRuleStoreProvider(cfg) {
12
+ if (cfg === null || typeof cfg !== "object") {
13
+ throw new Error("createPermissionRuleStoreProvider takes a config object naming the partitions to compose");
14
+ }
15
+ const durable = cfg.durable;
16
+ const org = cfg.org !== undefined ? createOrgRulePartition(cfg.org) : undefined;
17
+ const session = cfg.session;
18
+ if (session !== undefined && (typeof session.read !== "function" || typeof session.apply !== "function")) {
19
+ throw new Error("the session partition must implement read(sessionId) and apply(sessionId, add) — refusing a partial face rather than serving a session source that cannot answer");
20
+ }
21
+ const partitions = Object.freeze({ durable: durable !== undefined, org: org !== undefined, session: session !== undefined });
22
+ const storeOver = (partition) => new UnifiedPermissionRuleStore(partition, org, session);
23
+ const provider = {
24
+ partitions,
25
+ forPrincipal: (principal) => storeOver(durable?.forPrincipal(principal)),
26
+ ...(durable?.forLocalOwner !== undefined ? { forLocalOwner: () => storeOver(durable.forLocalOwner()) } : {}),
27
+ };
28
+ return provider;
29
+ }
30
+ class UnifiedPermissionRuleStore {
31
+ durable;
32
+ org;
33
+ session;
34
+ durability;
35
+ fidelity;
36
+ constructor(durable, org, session) {
37
+ this.durable = durable;
38
+ this.org = org;
39
+ this.session = session;
40
+ this.durability = durable?.durability ?? "process-local";
41
+ this.fidelity = durable?.fidelity;
42
+ if (durable?.quarantined !== undefined) {
43
+ this.quarantined = () => durable.quarantined();
44
+ }
45
+ if (durable !== undefined) {
46
+ Object.defineProperty(this, PERMISSION_RULE_WRITER, {
47
+ get: () => {
48
+ const w = writerOf(durable);
49
+ return w === undefined ? undefined : routingWriter(w, session);
50
+ },
51
+ enumerable: false,
52
+ });
53
+ }
54
+ }
55
+ quarantined;
56
+ async effective(query) {
57
+ const disclosures = [];
58
+ const [orgSettled, durableSettled, sessionRows] = await Promise.all([
59
+ this.org !== undefined ? this.org.resolve() : Promise.resolve({ ...UNGOVERNED_ORG_RESOLUTION, rules: [], disclosures: [] }),
60
+ this.durable !== undefined ? this.durable.list().then((v) => ({ ok: true, v }), (e) => ({ ok: false, e })) : Promise.resolve({ ok: true, v: EMPTY_DURABLE }),
61
+ this.readSessionRows(usableSessionId(query?.sessionId), disclosures),
62
+ ]);
63
+ const orgResolution = orgSettled;
64
+ if (!durableSettled.ok) {
65
+ return { rules: [], removed: [], org: orgResolution, rev: 0, disclosures, unreadable: errText(durableSettled.e) };
66
+ }
67
+ const durableRead = durableSettled.v;
68
+ const shadowed = compileOrgShadowPredicate(orgResolution.rules);
69
+ const rules = [];
70
+ for (const r of [...sessionRows, ...durableRead.rules]) {
71
+ rules.push({ ...r, source: ruleSourceOf(r.scope), status: shadowed(r) ? "shadowed-by-org" : "live" });
72
+ }
73
+ const removed = [];
74
+ for (const t of durableRead.tombstones) {
75
+ if (durableRead.rules.some((r) => r.rule === t.rule && sameScope(r.scope, t.scope)))
76
+ continue;
77
+ if (removed.some((e) => e.rule === t.rule && sameScope(e.scope, t.scope)))
78
+ continue;
79
+ if (t.scope.kind === "session")
80
+ continue;
81
+ removed.push({ rule: t.rule, scope: t.scope, source: ruleSourceOf(t.scope), status: "removed" });
82
+ }
83
+ return { rules, removed, org: orgResolution, rev: durableRead.rev, disclosures };
84
+ }
85
+ async readSessionRows(sessionId, disclosures) {
86
+ if (this.session === undefined || sessionId === undefined)
87
+ return [];
88
+ try {
89
+ const served = structuredClone(await this.session.read(sessionId));
90
+ const rows = served.filter((r) => {
91
+ const scope = r?.scope;
92
+ return scope?.kind === "session" && scope.sessionId === sessionId && !("reject" in normalizePersistedRule(r));
93
+ });
94
+ if (rows.length < served.length) {
95
+ disclosures.push(`the session rule partition served ${served.length - rows.length} row(s) that are not canonical session rows of this session — dropped, not adjudicated`);
96
+ }
97
+ return rows;
98
+ }
99
+ catch (err) {
100
+ disclosures.push(`the session rule partition could not be read: ${escapeForDisclosure(errText(err))}`);
101
+ return [];
102
+ }
103
+ }
104
+ }
105
+ function routingWriter(durable, session) {
106
+ return {
107
+ nextDot: () => durable.nextDot(),
108
+ readRaw: () => durable.readRaw(),
109
+ apply: async (delta, opts) => {
110
+ const scope = delta.kind === "redemption-add" ? structuredClone(delta.scope) : undefined;
111
+ if (delta.kind !== "redemption-add" || scope?.kind !== "session")
112
+ return durable.apply(delta, opts);
113
+ if (session === undefined) {
114
+ const e = new Error("a session-scope rule has nowhere to land — this deployment configured no session rule partition (createPermissionRuleStoreProvider({ session })); refusing the redemption-add");
115
+ e.code = "unsupported.session_scope_store";
116
+ throw e;
117
+ }
118
+ const outcome = await session.apply(scope.sessionId, { rule: delta.rule, tool: delta.tool, match: delta.match, command: delta.command, add: delta.add });
119
+ return "refused" in outcome ? { refused: "session_ended", rev: opts.expectedRev } : { rev: opts.expectedRev };
120
+ },
121
+ };
122
+ }
123
+ export async function effectiveOrThrow(store, query) {
124
+ const view = await store.effective(query);
125
+ if (view.unreadable !== undefined)
126
+ throw new Error(view.unreadable);
127
+ return view;
128
+ }
129
+ export async function effectivePermissionRules(opts) {
130
+ return await resolveOwnerStore(opts).effective(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : undefined);
131
+ }
132
+ export function resolveOwnerStore(opts) {
133
+ if (opts.owner !== undefined && opts.owner.kind === "local-owner") {
134
+ if (opts.principal !== undefined) {
135
+ throw new Error("pass either a principal or a local-owner, not both — a bucket has one owner");
136
+ }
137
+ const store = opts.provider.forLocalOwner?.();
138
+ if (store === undefined) {
139
+ throw new Error("this provider has no local-owner bucket (forLocalOwner is not implemented)");
140
+ }
141
+ return store;
142
+ }
143
+ if (opts.owner !== undefined && opts.owner.kind === "principal" && opts.principal !== undefined && opts.owner.principal !== opts.principal) {
144
+ throw new Error(`contradictory identity: principal "${escapeForDisclosure(opts.principal)}" and owner principal "${escapeForDisclosure(opts.owner.principal)}" disagree`);
145
+ }
146
+ const principal = opts.owner?.kind === "principal" ? opts.owner.principal : opts.principal;
147
+ return opts.provider.forPrincipal(principal);
148
+ }
@@ -1,12 +1,14 @@
1
1
  /**
2
- * design/382 §4.3 — the SESSION-RULE OVERLAY: where a `{kind:"session"}` scoped allow rule lives.
2
+ * design/382 §4.3 / design/389 — the SESSION partition of the permission-rule store: where a
3
+ * `{kind:"session"}` scoped allow rule lives.
3
4
  *
4
- * A session authorization's home is the session's own state — never the persisted store. The rows are
5
- * ordinary {@link PersistedAllowRule}s (scope `session`), so every read-side consumer the gate's
6
- * conjunction arm, the coverage table, the consent prepare takes them through the ONE eligibility
7
- * predicate with zero second judges: the merge point splices overlay rows IN FRONT of the persisted
8
- * `list()` (the §4.1 narrowest-first reporting order: session > project > global), and everything
9
- * downstream is unchanged.
5
+ * A session authorization's home is the session's own lifetime — never the durable CRDT partition. The
6
+ * rows are ordinary {@link PersistedAllowRule}s (scope `session`), and the unified store's ONE read
7
+ * query (`PermissionRuleStore.effective({ sessionId })`) splices this partition's rows IN FRONT of the
8
+ * durable partition's (the §4.1 narrowest-first reporting order: session > project > global), so every
9
+ * read-side consumer — the gate's conjunction arm, the coverage table, the consent prepare takes them
10
+ * through the ONE eligibility predicate with zero second judges. The store's routing writer lands a
11
+ * session-scope `redemption-add` here; the durable partition never sees one.
10
12
  *
11
13
  * ## The contract (design/382 §8-Q6, settled here)
12
14
  *
@@ -20,7 +22,7 @@
20
22
  *
21
23
  * There is deliberately NO delete verb: a session row is not individually deletable — it dies with
22
24
  * its session (design/382 §10, ruled). Termination is the HOST's act on its own implementation (see
23
- * {@link InMemorySessionRuleOverlay.endSession}), not an engine verb.
25
+ * {@link InMemorySessionRulePartition.endSession}), not an engine verb.
24
26
  *
25
27
  * ## Lifecycle and durability
26
28
  *
@@ -32,7 +34,7 @@
32
34
  *
33
35
  * ## What this is NOT
34
36
  *
35
- * Not a store backend: no dots are minted here (the redemption leg mints the audit dot exactly as the
37
+ * Not a durable backend: no dots are minted here (the redemption leg mints the audit dot exactly as the
36
38
  * store leg does), no tombstones exist here, nothing here syncs. Every durable entrance — write
37
39
  * deltas, sync in both directions, at-rest bytes — refuses or drops a session row
38
40
  * (`permission-rule-store.ts`, the durable two-member face); this module is the OTHER side of that
@@ -43,7 +45,7 @@ import type { PersistedAllowRule, RuleAdd, PersistedRuleMatch, PersistedRuleTool
43
45
  * redemption's own add (dot + provenance) — the same fields a `redemption-add` delta carries, minus
44
46
  * the scope (the verb's `sessionId` IS the scope, constructed here so a caller cannot land a row
45
47
  * whose scope names a different session than the one it addressed). */
46
- export interface SessionRuleOverlayAdd {
48
+ export interface SessionRuleAdd {
47
49
  rule: string;
48
50
  tool: PersistedRuleTool;
49
51
  match: PersistedRuleMatch;
@@ -52,21 +54,23 @@ export interface SessionRuleOverlayAdd {
52
54
  }
53
55
  /** What one overlay apply answered. `session_ended` is the epoch fence speaking: the write arrived at
54
56
  * (or after) termination and landed nothing — the redemption reports the member `refused`. */
55
- export type SessionRuleOverlayApplyResult = {
57
+ export type SessionRuleApplyResult = {
56
58
  landed: true;
57
59
  } | {
58
60
  refused: "session_ended";
59
61
  };
60
62
  /**
61
- * The host-provided seam the consent lane and the gate's rule lane consume
62
- * (`RuleConsentDeps.sessionRules`, `RunnerDeps.sessionPermissionRules`). See the module doc for the
63
- * contract; {@link InMemorySessionRuleOverlay} is the reference implementation and the semantics.
63
+ * The session partition a deployment hands to `createPermissionRuleStoreProvider({ session })`. See the
64
+ * module doc for the contract; {@link InMemorySessionRulePartition} is the reference implementation and
65
+ * the semantics. A backend that keeps a session's rows beside its own session record (a host whose
66
+ * sessions are durable) may implement this over that storage — the law is LIFETIME-bound, not
67
+ * medium-bound: no tombstones, no sync, nothing outlives the session.
64
68
  */
65
- export interface SessionRuleOverlay {
69
+ export interface SessionRulePartition {
66
70
  /** The live session-scoped rows of ONE session. An ended (or never-written) session reads as `[]`. */
67
71
  read(sessionId: string): Promise<readonly PersistedAllowRule[]>;
68
72
  /** Land one redeemed authorization — idempotent by dot, refused once the session ended. */
69
- apply(sessionId: string, add: SessionRuleOverlayAdd): Promise<SessionRuleOverlayApplyResult>;
73
+ apply(sessionId: string, add: SessionRuleAdd): Promise<SessionRuleApplyResult>;
70
74
  }
71
75
  /**
72
76
  * The reference overlay — in-memory, per-process, the semantics every host implementation must keep:
@@ -85,7 +89,7 @@ export interface SessionRuleOverlay {
85
89
  * checkpoint is host-owned bytes, and a global row smuggled through this door would ride the
86
90
  * merge point straight into adjudication).
87
91
  */
88
- export declare class InMemorySessionRuleOverlay implements SessionRuleOverlay {
92
+ export declare class InMemorySessionRulePartition implements SessionRulePartition {
89
93
  private readonly rows;
90
94
  private readonly sealed;
91
95
  /** How many times one session's row set has CHANGED. Reading a caller-owned value runs caller code,
@@ -96,7 +100,7 @@ export declare class InMemorySessionRuleOverlay implements SessionRuleOverlay {
96
100
  private readonly epochs;
97
101
  private bumpEpoch;
98
102
  read(sessionId: string): Promise<readonly PersistedAllowRule[]>;
99
- apply(sessionId: string, add: SessionRuleOverlayAdd): Promise<SessionRuleOverlayApplyResult>;
103
+ apply(sessionId: string, add: SessionRuleAdd): Promise<SessionRuleApplyResult>;
100
104
  /**
101
105
  * The termination transfer: SEAL the lifetime epoch first (one atomic membership flip — after this
102
106
  * line every in-flight `apply` holding this id answers `session_ended`), THEN drop the rows. The
@@ -116,7 +120,7 @@ export declare class InMemorySessionRuleOverlay implements SessionRuleOverlay {
116
120
  * rows (rule text parses, canonical spelling, stored tool/match/command agree with the text): this
117
121
  * door takes host-owned checkpoint bytes with no redemption validator in front of them, and a row
118
122
  * the engine could not re-project would otherwise sit one merge away from adjudication. Rows this
119
- * overlay landed itself always pass — {@link InMemorySessionRuleOverlay.apply} runs the same screen,
123
+ * overlay landed itself always pass — {@link InMemorySessionRulePartition.apply} runs the same screen,
120
124
  * so the two doors agree on what a session row is and a snapshot of this overlay always restores —
121
125
  * which is what makes a refusal here foreign damage, refused whole.
122
126
  */
@@ -7,7 +7,7 @@ function isValidOverlayAdd(add) {
7
7
  return false;
8
8
  return typeof a.createdAt === "string";
9
9
  }
10
- export class InMemorySessionRuleOverlay {
10
+ export class InMemorySessionRulePartition {
11
11
  rows = new Map();
12
12
  sealed = new Set();
13
13
  epochs = new Map();
@@ -24,15 +24,15 @@ export class InMemorySessionRuleOverlay {
24
24
  if (this.sealed.has(sessionId))
25
25
  return { refused: "session_ended" };
26
26
  if (!isValidOverlayAdd(add?.add)) {
27
- throw new Error("SessionRuleOverlay.apply was handed a malformed add (dot/origin/createdAt) — evidence this shape cannot vouch for must not install");
27
+ throw new Error("SessionRulePartition.apply was handed a malformed add (dot/origin/createdAt) — evidence this shape cannot vouch for must not install");
28
28
  }
29
29
  const scope = { kind: "session", sessionId };
30
30
  if (!isValidConsentScope(scope)) {
31
- throw new Error("SessionRuleOverlay.apply was addressed with an unusable session id — a session nobody can name is not a scope a grant can live under");
31
+ throw new Error("SessionRulePartition.apply was addressed with an unusable session id — a session nobody can name is not a scope a grant can live under");
32
32
  }
33
33
  const projected = { rule: add.rule, tool: add.tool, match: add.match, command: add.command, scope, adds: [add.add] };
34
34
  if ("reject" in normalizePersistedRule(projected)) {
35
- throw new Error("SessionRuleOverlay.apply was handed a row whose text/metadata do not survive the canonical re-projection — a row the engine cannot re-project can never adjudicate, and would refuse its own restore");
35
+ throw new Error("SessionRulePartition.apply was handed a row whose text/metadata do not survive the canonical re-projection — a row the engine cannot re-project can never adjudicate, and would refuse its own restore");
36
36
  }
37
37
  const rows = this.rows.get(sessionId) ?? [];
38
38
  if (!this.rows.has(sessionId))
@@ -41,7 +41,7 @@ export class InMemorySessionRuleOverlay {
41
41
  for (const [heldSession, heldRows] of this.rows) {
42
42
  const holder = heldRows.find((r) => r.adds.some(sameDotAs));
43
43
  if (holder !== undefined && (heldSession !== sessionId || holder.rule !== add.rule)) {
44
- throw new Error(`SessionRuleOverlay.apply: dot ${add.add.dot.actor}#${add.add.dot.counter} already vouches for another grant — a dot identifies one add of one (rule, session), ever`);
44
+ throw new Error(`SessionRulePartition.apply: dot ${add.add.dot.actor}#${add.add.dot.counter} already vouches for another grant — a dot identifies one add of one (rule, session), ever`);
45
45
  }
46
46
  }
47
47
  const existing = rows.find((r) => r.rule === add.rule);
@@ -1,5 +1,13 @@
1
1
  /**
2
- * design/179 §8 — the persisted allow-rule store seam, its backend write face, and the removal entry.
2
+ * design/179 §8 / design/389 — the DURABLE partition of the permission-rule store (the OR-Set CRDT that
3
+ * holds the `user` and `project` sources), its backend write face, the pure fold/join law every backend
4
+ * shares, and the removal entry.
5
+ *
6
+ * The host-visible STORE is `permission-rule-provider.ts` (`PermissionRuleStore` — one read query over
7
+ * the four source partitions). What lives here is the partition a backend implements: `list()` over one
8
+ * owner's bucket, the writer handle, and the delta semantics. A backend author implements
9
+ * {@link DurableRulePartitionProvider}; the engine composes it into the store with
10
+ * `createPermissionRuleStoreProvider`.
3
11
  *
4
12
  * ## Two faces, deliberately unequal
5
13
  *
@@ -47,13 +55,15 @@ export interface StoredAllowRules {
47
55
  checksum?: string;
48
56
  }
49
57
  /**
50
- * The host-visible store face: read-only, already anchored to a single principal.
58
+ * The durable partition's read face: read-only, already anchored to a single owner. Rows carry the two
59
+ * durable scopes only (`global` = the `user` source, `project` = the `project` source); a session row
60
+ * never lives here (design/382 §4.3 — the durable two-member face).
51
61
  *
52
62
  * A backend that fails to read must not silently report an empty set as if it were an empty store — it
53
- * throws, and the lane's caller turns that into zero rules PLUS a loud disclosure. Both outcomes deny
63
+ * throws, and the store's caller turns that into zero rules PLUS a loud disclosure. Both outcomes deny
54
64
  * more than they allow; only one of them is honest about why.
55
65
  */
56
- export interface PermissionRuleStore {
66
+ export interface DurableRulePartition {
57
67
  list(): Promise<StoredAllowRules>;
58
68
  /**
59
69
  * design/182 §5.2/§8.3 — the quarantine area, for introspection. Rows a sync round moved out of the
@@ -83,16 +93,18 @@ export type RuleOwner = {
83
93
  };
84
94
  /** Do two owners name the same bucket? */
85
95
  export declare function sameRuleOwner(a: RuleOwner, b: RuleOwner): boolean;
86
- /** Resolves a store for one verified principal. `undefined` ⇒ a store that reports zero rules. */
87
- export interface PermissionRuleStoreProvider {
88
- forPrincipal(principal: string | undefined): PermissionRuleStore;
96
+ /** Resolves one owner's durable partition. `undefined` ⇒ a partition that reports zero rules. This is
97
+ * the BACKEND contract (file, in-memory, an out-of-repo SQL twin); the engine never consumes it
98
+ * directly — `createPermissionRuleStoreProvider({ durable })` composes it into the unified store. */
99
+ export interface DurableRulePartitionProvider {
100
+ forPrincipal(principal: string | undefined): DurableRulePartition;
89
101
  /**
90
102
  * design/182 §4.5 (F-011) — the identity-less LOCAL bucket, for a deployment that explicitly declared
91
103
  * local-owner rules. Optional and additive: a provider without it simply has no local-owner form.
92
104
  * A file backend resolves a FIXED file name (never a principal-hash path), and after an adoption
93
105
  * completed it resolves the ADOPTED principal's bucket forever — the retired bucket never revives.
94
106
  */
95
- forLocalOwner?(): PermissionRuleStore;
107
+ forLocalOwner?(): DurableRulePartition;
96
108
  }
97
109
  /** The outcome of one accepted write. `sync` is present only on a `sync-join` delta — the landing
98
110
  * report the disclosure layer reads (design/182 §8.1: quarantine/fence details ride the PutResult). */
@@ -100,6 +112,21 @@ export interface PutResult {
100
112
  rev: number;
101
113
  sync?: RuleSyncLandingReport;
102
114
  }
115
+ /**
116
+ * What one `apply` answers. `conflict` is the durable partition's compare-and-set losing; `refused` is
117
+ * the SESSION partition's epoch fence speaking (design/382 §4.3, carried through the unified store's
118
+ * routing writer): a `redemption-add` whose scope names a session that has ended lands nothing — the
119
+ * redemption reports the member `refused`, never a silent success and never a resurrected row. A
120
+ * durable backend never answers `refused` (it holds no session partition); the member exists on the
121
+ * union so the ONE write path the consent protocol takes has one result vocabulary.
122
+ */
123
+ export type RuleWriteOutcome = PutResult | {
124
+ conflict: true;
125
+ rev: number;
126
+ } | {
127
+ refused: "session_ended";
128
+ rev: number;
129
+ };
103
130
  /** Authorization accompanying an add: the redemption that produced it. Carrying the record id makes the
104
131
  * add's logical operation identity checkable at the backend, not just at the caller. A v1 caller
105
132
  * carries `principal`; the local-owner path (design/182 §4.5) carries `owner` instead — never both
@@ -179,10 +206,7 @@ export interface PermissionRuleWriter {
179
206
  nextDot(): Promise<RuleDot>;
180
207
  apply(delta: RuleWriteDelta, opts: {
181
208
  expectedRev: number;
182
- }): Promise<PutResult | {
183
- conflict: true;
184
- rev: number;
185
- }>;
209
+ }): Promise<RuleWriteOutcome>;
186
210
  /**
187
211
  * design/182 §4.2 — the RAW state a sync round exchanges: adds with tombstoned dots NOT pre-filtered
188
212
  * (list() filters; a join must not), plus the replica identity, the minted-counter high water and the
@@ -209,12 +233,14 @@ export interface RawRuleSyncState {
209
233
  * the consent boundary lives in the engine's wiring, not in this key's visibility).
210
234
  */
211
235
  export declare const PERMISSION_RULE_WRITER = "__semaPermissionRuleWriter";
212
- /** A store that also carries the backend write face. */
213
- export interface WritablePermissionRuleStore extends PermissionRuleStore {
236
+ /** A partition that also carries the backend write face. */
237
+ export interface WritableDurableRulePartition extends DurableRulePartition {
214
238
  readonly [PERMISSION_RULE_WRITER]: PermissionRuleWriter;
215
239
  }
216
- /** The writer of a store, or `undefined` when the backend is read-only from the engine's side. */
217
- export declare function writerOf(store: PermissionRuleStore): PermissionRuleWriter | undefined;
240
+ /** The writer of a store or partition, or `undefined` when the backend is read-only from the engine's
241
+ * side. Duck-typed on the handle, so it answers for the unified `PermissionRuleStore` (whose writer
242
+ * routes by scope) exactly as for a bare durable partition. */
243
+ export declare function writerOf(store: DurableRulePartition | import("./permission-rule-provider.js").PermissionRuleStore): PermissionRuleWriter | undefined;
218
244
  /** Do two scopes name the same place? Three members (design/382 §4.1): global, project-by-root,
219
245
  * session-by-sessionId. */
220
246
  export declare function sameScope(a: RuleScope, b: RuleScope): boolean;
@@ -494,17 +520,16 @@ export declare function removePersistedRule(opts: {
494
520
  * `forLocalOwner()` face existed, but removal could not name it, so a local-owner rule was
495
521
  * unrevokable through this entry). Same observed-remove/add-wins/stillLive semantics either way. */
496
522
  principal: string | RuleOwner;
497
- provider: PermissionRuleStoreProvider;
523
+ provider: import("./permission-rule-provider.js").PermissionRuleStoreProvider;
498
524
  }): Promise<RemoveResult>;
499
525
  export declare function errText(err: unknown): string;
500
- /** A store that always reports zero rules — what an unauthenticated principal resolves to, and the shape a
501
- * failed read degrades to at the consumption site. */
502
- export declare const EMPTY_RULE_STORE: PermissionRuleStore;
526
+ /** A durable partition that always reports zero rules — what an unauthenticated principal resolves to. */
527
+ export declare const EMPTY_DURABLE_RULE_PARTITION: DurableRulePartition;
503
528
  /**
504
529
  * In-memory backend — the reference implementation of the delta semantics, and the one the tests drive.
505
530
  * `process-local` by declaration: it makes no durability promise it cannot keep.
506
531
  */
507
- export declare class InMemoryPermissionRuleStore implements WritablePermissionRuleStore {
532
+ export declare class InMemoryDurableRulePartition implements WritableDurableRulePartition {
508
533
  private readonly actor;
509
534
  private readonly now;
510
535
  readonly durability: StoreDurability;
@@ -394,11 +394,13 @@ export async function removePersistedRule(opts) {
394
394
  }
395
395
  let snapshot;
396
396
  try {
397
- snapshot = await store.list();
397
+ snapshot = await store.effective();
398
398
  }
399
399
  catch (err) {
400
400
  return { status: "failed", error: `could not read the permission-rule store: ${errText(err)}` };
401
401
  }
402
+ if (snapshot.unreadable !== undefined)
403
+ return { status: "failed", error: `could not read the permission-rule store: ${snapshot.unreadable}` };
402
404
  const live = snapshot.rules.filter((r) => r.rule === rule && sameScope(r.scope, scope));
403
405
  const removedDots = live.flatMap((r) => r.adds.map((a) => a.dot));
404
406
  if (removedDots.length === 0)
@@ -409,10 +411,15 @@ export async function removePersistedRule(opts) {
409
411
  for (let attempt = 0; attempt < REMOVE_MAX_ATTEMPTS; attempt++) {
410
412
  try {
411
413
  const res = await writer.apply({ kind: "tighten-delete", tombstone }, { expectedRev });
414
+ if ("refused" in res) {
415
+ return { status: "failed", error: `the store refused the tombstone (${res.refused}) — nothing was removed` };
416
+ }
412
417
  if (!("conflict" in res)) {
413
418
  let stillLive = false;
414
419
  try {
415
- const after = await store.list();
420
+ const after = await store.effective();
421
+ if (after.unreadable !== undefined)
422
+ throw new Error(after.unreadable);
416
423
  stillLive = after.rules.some((r) => r.rule === rule && sameScope(r.scope, scope) && isRuleLive(r));
417
424
  }
418
425
  catch {
@@ -429,10 +436,10 @@ export async function removePersistedRule(opts) {
429
436
  }
430
437
  const reason = lastError !== "" ? lastError : `optimistic-concurrency retries exhausted after ${REMOVE_MAX_ATTEMPTS} attempts`;
431
438
  try {
432
- const after = await store.list();
439
+ const after = await writer.readRaw();
433
440
  const landed = after.tombstones.some((t) => t.deletedBy.actor === tombstone.deletedBy.actor && t.deletedBy.counter === tombstone.deletedBy.counter);
434
441
  return landed
435
- ? { status: "removed", rev: after.rev, stillLive: after.rules.some((r) => r.rule === rule && sameScope(r.scope, scope) && isRuleLive(r)) }
442
+ ? { status: "removed", rev: after.rev, stillLive: applyTombstones(after.rules, after.tombstones).some((r) => r.rule === rule && sameScope(r.scope, scope) && isRuleLive(r)) }
436
443
  : { status: "failed", error: `${reason} (read-back confirms no tombstone was written)` };
437
444
  }
438
445
  catch (err) {
@@ -442,11 +449,11 @@ export async function removePersistedRule(opts) {
442
449
  export function errText(err) {
443
450
  return err instanceof Error ? err.message : String(err);
444
451
  }
445
- export const EMPTY_RULE_STORE = {
452
+ export const EMPTY_DURABLE_RULE_PARTITION = {
446
453
  list: async () => ({ rules: [], tombstones: [], rev: 0 }),
447
454
  durability: "process-local",
448
455
  };
449
- export class InMemoryPermissionRuleStore {
456
+ export class InMemoryDurableRulePartition {
450
457
  actor;
451
458
  now;
452
459
  durability = "process-local";
@@ -26,7 +26,8 @@
26
26
  * level they are buying.
27
27
  */
28
28
  import type { PersistedAllowRule, RuleDot, RuleRejectCode, RuleScope, RuleTombstone } from "./permission-rule-model.js";
29
- import type { PermissionRuleStoreProvider, RuleOwner, RuleSyncDrop, RuleSyncFrontier, RuleSyncState } from "./permission-rule-store.js";
29
+ import type { PermissionRuleStoreProvider } from "./permission-rule-provider.js";
30
+ import type { RuleOwner, RuleSyncDrop, RuleSyncFrontier, RuleSyncState } from "./permission-rule-store.js";
30
31
  import { type TracerHook } from "./trace.js";
31
32
  /** The injected HTTP seam — core never bundles a fetch. The deployment owns base URL, auth, TLS and
32
33
  * retries; a non-2xx / network failure should THROW (the round then aborts with zero local effects —
@@ -178,6 +178,17 @@ export async function syncPermissionRules(opts) {
178
178
  current = await writer.readRaw();
179
179
  continue;
180
180
  }
181
+ if ("refused" in res) {
182
+ return disclose({
183
+ ok: false,
184
+ pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
185
+ landed: { newAdds: 0, newTombstones: 0 },
186
+ resurrected: [],
187
+ dropped,
188
+ rev: current.rev,
189
+ warnings: [...warnings, `the store refused the sync landing (${res.refused}) — nothing landed, the local state is unchanged`],
190
+ });
191
+ }
181
192
  const landedRaw = await writer.readRaw();
182
193
  const postLive = applyTombstones(landedRaw.rules, landedRaw.tombstones);
183
194
  const resurrected = [];
@@ -83,8 +83,9 @@ export interface Stats {
83
83
  llmRootMicroUsd: number;
84
84
  /** Delegated sub-agent (nested) LLM cost (micro-USD) = `stats.nested?.costMicroUsd ?? 0`. */
85
85
  nestedSubagentMicroUsd: number;
86
- /** Post-task memory-consolidation LLM cost (micro-USD) = `stats.memory?.costMicroUsd ?? 0`. Filled in
87
- * runtask AFTER the (async) consolidation pass sets `stats.memory`; 0 at assembleResult time. */
86
+ /** Post-task memory-consolidation LLM cost (micro-USD) = `stats.memory?.costMicroUsd ?? 0`. Permanently
87
+ * 0: the runner-integrated consolidation pass is retired (the public `TaskStats` doc is the one home
88
+ * for what would fill it again). */
88
89
  memoryConsolidationMicroUsd: number;
89
90
  /** Within-task compaction LLM cost (micro-USD) = the running `compactionMicroUsd` accumulator (the
90
91
  * category Framing-4 missed). This IS part of `costMicroUsd`, so it is subtracted from `llmRoot`. */
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The durable-suspend checkpoint SCOPE vocabulary — the default isolation scope and the ONE derivation of a row's scope —
3
+ * moved whole out of the orchestrator so the wiring-manifest phase (the synthesized durableApproval) and the driver's
4
+ * park-lane mint sites reach DOWN for the same two names. Pure: a constant and a string function over two spec fields.
5
+ */
6
+ /** design/77 §4.4: the multi-tenant scope used when a durable suspend fires for an IRREVERSIBLE tool in an
7
+ * unattended deployment that did NOT opt into `durableApproval` (so there is no caller-supplied scope). The
8
+ * checkpoint carries this scope; resume reads it back from the checkpoint (`cp.scope`), so it is
9
+ * self-consistent without needing the original `TaskSpec`. Kept distinct from any tenant key to make an
10
+ * unattended irreversible suspend auditable as such. */
11
+ export declare const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
12
+ /**
13
+ * design/153 §7.1 — the SINGLE derivation of a durable-suspend checkpoint's scope (both gate mint
14
+ * sites + the bg-watcher park lane's expire compensation/arbitration consume THIS; a re-derived copy
15
+ * that drifted was the exact bug the park tests caught — expire against the wrong scope silently
16
+ * no-ops). `|| DEFAULT` (not `??`): an EMPTY-string principal must not become a shared scope bucket.
17
+ *
18
+ * RB-75 (2026-07-25, red probe): that rule is stated twice in this file and was enforced on ONE of the two
19
+ * inputs — the principal half used `||`, the `durableApproval.scope` half used `??`, so an empty string
20
+ * arriving through the scope field (a config-centre / env lookup that came back blank is the realistic
21
+ * source) sailed through as the literal scope `""`. Two different tenants both landed in that one bucket,
22
+ * where `listByScope("")` reads BOTH their pending approvals — including each summary's `toolInput`
23
+ * preview of the raw arguments — and `reap("")` / `resolve(token, "")` act across both. `durableApproval.
24
+ * scope` is documented as "Multi-tenant isolation key … Required"; a blank string is the absence of an
25
+ * isolation key wearing its clothes, so both halves now fall through to the default isolation scope.
26
+ */
27
+ export declare function checkpointScopeOf(spec: {
28
+ durableApproval?: {
29
+ scope?: string;
30
+ };
31
+ principal?: string;
32
+ }): string;
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
2
+ export function checkpointScopeOf(spec) {
3
+ return spec.durableApproval?.scope || spec.principal || DEFAULT_IRREVERSIBLE_SCOPE;
4
+ }