@sema-agent/core 7.8.0 → 7.9.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 +52 -1
  2. package/dist/agents/subagent.d.ts +1 -1
  3. package/dist/core/ask-origin.d.ts +12 -1
  4. package/dist/core/ask-origin.js +5 -1
  5. package/dist/core/checkpoint-store.d.ts +34 -28
  6. package/dist/core/gate-lanes.js +49 -23
  7. package/dist/core/gate-outcome.d.ts +8 -4
  8. package/dist/core/gate-outcome.js +2 -1
  9. package/dist/core/hooks.d.ts +56 -31
  10. package/dist/core/permission-rule-consent.d.ts +29 -36
  11. package/dist/core/permission-rule-consent.js +110 -60
  12. package/dist/core/permission-rule-model.d.ts +254 -55
  13. package/dist/core/permission-rule-model.js +323 -43
  14. package/dist/core/permission-rule-org.d.ts +9 -8
  15. package/dist/core/permission-rule-org.js +6 -15
  16. package/dist/core/permission-rule-provider.d.ts +10 -6
  17. package/dist/core/permission-rule-provider.js +12 -8
  18. package/dist/core/permission-rule-session.d.ts +7 -6
  19. package/dist/core/permission-rule-session.js +29 -10
  20. package/dist/core/permission-rule-store.d.ts +40 -18
  21. package/dist/core/permission-rule-store.js +68 -42
  22. package/dist/core/permission-rule-sync.d.ts +9 -3
  23. package/dist/core/permission-rule-sync.js +29 -25
  24. package/dist/core/permission-rule-syntax.d.ts +30 -0
  25. package/dist/core/permission-rule-syntax.js +44 -0
  26. package/dist/core/permission-rules.d.ts +55 -33
  27. package/dist/core/permission-rules.js +65 -55
  28. package/dist/core/persisted-rule-arms.d.ts +56 -0
  29. package/dist/core/persisted-rule-arms.js +48 -0
  30. package/dist/core/runner/contracts.d.ts +21 -2
  31. package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
  32. package/dist/core/runner/permission-rule-lanes.js +27 -21
  33. package/dist/core/runner/prepare-gate-stations.js +1 -1
  34. package/dist/core/runner/prepare-safety-scan.js +8 -2
  35. package/dist/core/runner/prepare-task.js +1 -1
  36. package/dist/core/runner/resume-admission.d.ts +53 -0
  37. package/dist/core/runner/resume-admission.js +83 -0
  38. package/dist/core/runner/resume-apply.d.ts +50 -0
  39. package/dist/core/runner/resume-apply.js +184 -0
  40. package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
  41. package/dist/core/runner/resume-checkpoint-screen.js +108 -0
  42. package/dist/core/runner/resume-claim.d.ts +32 -0
  43. package/dist/core/runner/resume-claim.js +27 -0
  44. package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
  45. package/dist/core/runner/resume-internals-and-config.js +50 -0
  46. package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
  47. package/dist/core/runner/resume-policy-outcome.js +127 -0
  48. package/dist/core/runner/resume-preflight.d.ts +40 -0
  49. package/dist/core/runner/resume-preflight.js +122 -0
  50. package/dist/core/runner/resume-review-outcome.d.ts +30 -0
  51. package/dist/core/runner/resume-review-outcome.js +88 -0
  52. package/dist/core/runner/run-harness-handlers.js +1 -1
  53. package/dist/core/runner/runtask.d.ts +20 -0
  54. package/dist/core/runner/runtask.js +85 -734
  55. package/dist/core/runner/tool-end-body.d.ts +12 -5
  56. package/dist/core/runner/tool-end-body.js +5 -5
  57. package/dist/core/runner/tool-output-projection.d.ts +9 -6
  58. package/dist/core/runner/tool-output-projection.js +1 -18
  59. package/dist/core/runner/turn-attachments.d.ts +2 -2
  60. package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
  61. package/dist/core/tool-roster.d.ts +9 -0
  62. package/dist/core/tool-roster.js +15 -5
  63. package/dist/index.d.ts +2 -2
  64. package/dist/index.js +2 -2
  65. package/dist/stores/file/checkpoint-store.js +1 -1
  66. package/dist/stores/file/permission-rule-store.d.ts +28 -23
  67. package/dist/stores/file/permission-rule-store.js +64 -16
  68. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  69. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  70. package/dist/tools/fs/notebook.d.ts +1 -1
  71. package/dist/tools/fs/safety.d.ts +1 -1
  72. package/package.json +1 -1
  73. package/test/export-surface.snapshot.json +43 -11
@@ -1,5 +1,5 @@
1
1
  import { escapeForDisclosure } from "./permission-rule-model.js";
2
- import { PERMISSION_RULE_WRITER, errText, normalizePersistedRule, sameScope, writerOf, } from "./permission-rule-store.js";
2
+ import { PERMISSION_RULE_WRITER, errText, normalizePersistedRule, sameRuleIdentity, writerOf, } from "./permission-rule-store.js";
3
3
  import { UNGOVERNED_ORG_RESOLUTION, compileOrgShadowPredicate, createOrgRulePartition } from "./permission-rule-org.js";
4
4
  export function ruleSourceOf(scope) {
5
5
  return scope.kind === "global" ? "user" : scope.kind;
@@ -61,6 +61,9 @@ class UnifiedPermissionRuleStore {
61
61
  this.readSessionRows(usableSessionId(query?.sessionId), disclosures),
62
62
  ]);
63
63
  const orgResolution = orgSettled;
64
+ if ("unreadable" in sessionRows) {
65
+ return { rules: [], removed: [], org: orgResolution, rev: 0, disclosures, unreadable: sessionRows.unreadable };
66
+ }
64
67
  if (!durableSettled.ok) {
65
68
  return { rules: [], removed: [], org: orgResolution, rev: 0, disclosures, unreadable: errText(durableSettled.e) };
66
69
  }
@@ -68,17 +71,17 @@ class UnifiedPermissionRuleStore {
68
71
  const shadowed = compileOrgShadowPredicate(orgResolution.rules);
69
72
  const rules = [];
70
73
  for (const r of [...sessionRows, ...durableRead.rules]) {
71
- rules.push({ ...r, source: ruleSourceOf(r.scope), status: shadowed(r) ? "shadowed-by-org" : "live" });
74
+ rules.push({ ...r, source: ruleSourceOf(r.scope), status: r.behavior === "allow" && shadowed(r) ? "shadowed-by-org" : "live" });
72
75
  }
73
76
  const removed = [];
74
77
  for (const t of durableRead.tombstones) {
75
- if (durableRead.rules.some((r) => r.rule === t.rule && sameScope(r.scope, t.scope)))
78
+ if (durableRead.rules.some((r) => sameRuleIdentity(r, t)))
76
79
  continue;
77
- if (removed.some((e) => e.rule === t.rule && sameScope(e.scope, t.scope)))
80
+ if (removed.some((e) => sameRuleIdentity(e, t)))
78
81
  continue;
79
82
  if (t.scope.kind === "session")
80
83
  continue;
81
- removed.push({ rule: t.rule, scope: t.scope, source: ruleSourceOf(t.scope), status: "removed" });
84
+ removed.push({ behavior: t.behavior, rule: t.rule, scope: t.scope, source: ruleSourceOf(t.scope), status: "removed" });
82
85
  }
83
86
  return { rules, removed, org: orgResolution, rev: durableRead.rev, disclosures };
84
87
  }
@@ -97,8 +100,9 @@ class UnifiedPermissionRuleStore {
97
100
  return rows;
98
101
  }
99
102
  catch (err) {
100
- disclosures.push(`the session rule partition could not be read: ${escapeForDisclosure(errText(err))}`);
101
- return [];
103
+ const why = `the session rule partition could not be read: ${escapeForDisclosure(errText(err))}`;
104
+ disclosures.push(why);
105
+ return { unreadable: why };
102
106
  }
103
107
  }
104
108
  }
@@ -115,7 +119,7 @@ function routingWriter(durable, session) {
115
119
  e.code = "unsupported.session_scope_store";
116
120
  throw e;
117
121
  }
118
- const outcome = await session.apply(scope.sessionId, { rule: delta.rule, tool: delta.tool, match: delta.match, command: delta.command, add: delta.add });
122
+ const outcome = await session.apply(scope.sessionId, { behavior: delta.behavior, rule: delta.rule, tool: delta.tool, match: delta.match, command: delta.command, add: delta.add });
119
123
  return "refused" in outcome ? { refused: "session_ended", rev: opts.expectedRev } : { rev: opts.expectedRev };
120
124
  },
121
125
  };
@@ -3,7 +3,7 @@
3
3
  * `{kind:"session"}` scoped allow rule lives.
4
4
  *
5
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
6
+ * rows are ordinary {@link PersistedRule}s (scope `session`), and the unified store's ONE read
7
7
  * query (`PermissionRuleStore.effective({ sessionId })`) splices this partition's rows IN FRONT of the
8
8
  * durable partition's (the §4.1 narrowest-first reporting order: session > project > global), so every
9
9
  * read-side consumer — the gate's conjunction arm, the coverage table, the consent prepare — takes them
@@ -40,12 +40,13 @@
40
40
  * (`permission-rule-store.ts`, the durable two-member face); this module is the OTHER side of that
41
41
  * wall, and the wall is the design.
42
42
  */
43
- import type { PersistedAllowRule, RuleAdd, PersistedRuleMatch, PersistedRuleTool } from "./permission-rule-model.js";
43
+ import type { PersistedRule, RuleAdd, RuleBehavior, PersistedRuleMatch, PersistedRuleTool } from "./permission-rule-model.js";
44
44
  /** One redeemed authorization landing in a session's overlay: the rule's canonical shape plus the
45
45
  * redemption's own add (dot + provenance) — the same fields a `redemption-add` delta carries, minus
46
46
  * the scope (the verb's `sessionId` IS the scope, constructed here so a caller cannot land a row
47
47
  * whose scope names a different session than the one it addressed). */
48
48
  export interface SessionRuleAdd {
49
+ behavior: RuleBehavior;
49
50
  rule: string;
50
51
  tool: PersistedRuleTool;
51
52
  match: PersistedRuleMatch;
@@ -68,7 +69,7 @@ export type SessionRuleApplyResult = {
68
69
  */
69
70
  export interface SessionRulePartition {
70
71
  /** The live session-scoped rows of ONE session. An ended (or never-written) session reads as `[]`. */
71
- read(sessionId: string): Promise<readonly PersistedAllowRule[]>;
72
+ read(sessionId: string): Promise<readonly PersistedRule[]>;
72
73
  /** Land one redeemed authorization — idempotent by dot, refused once the session ended. */
73
74
  apply(sessionId: string, add: SessionRuleAdd): Promise<SessionRuleApplyResult>;
74
75
  }
@@ -99,7 +100,7 @@ export declare class InMemorySessionRulePartition implements SessionRulePartitio
99
100
  * termination; read across `restoreSession`'s snapshot. */
100
101
  private readonly epochs;
101
102
  private bumpEpoch;
102
- read(sessionId: string): Promise<readonly PersistedAllowRule[]>;
103
+ read(sessionId: string): Promise<readonly PersistedRule[]>;
103
104
  apply(sessionId: string, add: SessionRuleAdd): Promise<SessionRuleApplyResult>;
104
105
  /**
105
106
  * The termination transfer: SEAL the lifetime epoch first (one atomic membership flip — after this
@@ -109,7 +110,7 @@ export declare class InMemorySessionRulePartition implements SessionRulePartitio
109
110
  */
110
111
  endSession(sessionId: string): void;
111
112
  /** The park half of the checkpoint carriage: this session's rows, serializable as-is. */
112
- snapshotSession(sessionId: string): Promise<readonly PersistedAllowRule[]>;
113
+ snapshotSession(sessionId: string): Promise<readonly PersistedRule[]>;
113
114
  /**
114
115
  * The resume half. Fail-closed on every axis: a sealed session never rehydrates (termination is
115
116
  * one-way), and every row must be a well-formed SESSION row of exactly this session — anything else
@@ -124,5 +125,5 @@ export declare class InMemorySessionRulePartition implements SessionRulePartitio
124
125
  * so the two doors agree on what a session row is and a snapshot of this overlay always restores —
125
126
  * which is what makes a refusal here foreign damage, refused whole.
126
127
  */
127
- restoreSession(sessionId: string, rows: readonly PersistedAllowRule[]): Promise<void>;
128
+ restoreSession(sessionId: string, rows: readonly PersistedRule[]): Promise<void>;
128
129
  }
@@ -1,3 +1,4 @@
1
+ import { readRowBehavior } from "./permission-rule-model.js";
1
2
  import { isValidConsentScope, normalizePersistedRule } from "./permission-rule-store.js";
2
3
  function isValidOverlayAdd(add) {
3
4
  const a = add;
@@ -30,7 +31,7 @@ export class InMemorySessionRulePartition {
30
31
  if (!isValidConsentScope(scope)) {
31
32
  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
33
  }
33
- const projected = { rule: add.rule, tool: add.tool, match: add.match, command: add.command, scope, adds: [add.add] };
34
+ const projected = { behavior: add.behavior, rule: add.rule, tool: add.tool, match: add.match, command: add.command, scope, adds: [add.add] };
34
35
  if ("reject" in normalizePersistedRule(projected)) {
35
36
  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
37
  }
@@ -40,13 +41,13 @@ export class InMemorySessionRulePartition {
40
41
  const sameDotAs = (a) => a.dot.actor === add.add.dot.actor && a.dot.counter === add.add.dot.counter;
41
42
  for (const [heldSession, heldRows] of this.rows) {
42
43
  const holder = heldRows.find((r) => r.adds.some(sameDotAs));
43
- if (holder !== undefined && (heldSession !== sessionId || holder.rule !== add.rule)) {
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`);
44
+ if (holder !== undefined && (heldSession !== sessionId || holder.rule !== add.rule || holder.behavior !== add.behavior)) {
45
+ 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 (behavior, rule, session), ever`);
45
46
  }
46
47
  }
47
- const existing = rows.find((r) => r.rule === add.rule);
48
+ const existing = rows.find((r) => r.rule === add.rule && r.behavior === add.behavior);
48
49
  if (existing === undefined) {
49
- rows.push({ rule: add.rule, tool: add.tool, match: add.match, command: add.command, scope, adds: [structuredClone(add.add)] });
50
+ rows.push({ behavior: add.behavior, rule: add.rule, tool: add.tool, match: add.match, command: add.command, scope, adds: [structuredClone(add.add)] });
50
51
  this.bumpEpoch(sessionId);
51
52
  return { landed: true };
52
53
  }
@@ -77,6 +78,9 @@ export class InMemorySessionRulePartition {
77
78
  throw new Error(`restoreSession("${sessionId}") raced a landing on the same session — refusing the whole restore (this door replaces the session's rows, and a grant that already answered landed must not be erased by a resume)`);
78
79
  }
79
80
  for (const r of rows) {
81
+ const behavior = readRowBehavior(r?.behavior);
82
+ if (behavior !== undefined && r !== null && typeof r === "object")
83
+ r.behavior = behavior;
80
84
  const scope = r?.scope;
81
85
  if (scope?.kind !== "session" || scope.sessionId !== sessionId || !isValidConsentScope(scope)) {
82
86
  throw new Error(`restoreSession("${sessionId}") was handed a row that is not a session row of that session — refusing the whole restore (a foreign-scope row through this door would reach adjudication)`);
@@ -85,10 +89,11 @@ export class InMemorySessionRulePartition {
85
89
  throw new Error(`restoreSession("${sessionId}") was handed a row whose text/metadata do not survive the canonical re-projection — refusing the whole restore (a row the engine cannot re-project must not reach adjudication)`);
86
90
  }
87
91
  }
92
+ const identityOf = (r) => `${r.behavior}|${r.rule}`;
88
93
  const ownDotRule = new Map();
89
94
  for (const held of this.rows.get(sessionId) ?? []) {
90
95
  for (const h of held.adds)
91
- ownDotRule.set(`${h.dot.actor}#${h.dot.counter}`, held.rule);
96
+ ownDotRule.set(`${h.dot.actor}#${h.dot.counter}`, identityOf(held));
92
97
  }
93
98
  const seenDots = new Map();
94
99
  for (const r of rows) {
@@ -98,12 +103,12 @@ export class InMemorySessionRulePartition {
98
103
  for (const a of r.adds) {
99
104
  const key = `${a.dot.actor}#${a.dot.counter}`;
100
105
  const holder = seenDots.get(key);
101
- if (holder !== undefined && holder !== r.rule) {
106
+ if (holder !== undefined && holder !== identityOf(r)) {
102
107
  throw new Error(`restoreSession("${sessionId}"): dot ${key} vouches for two different rules — refusing the whole restore`);
103
108
  }
104
- seenDots.set(key, r.rule);
109
+ seenDots.set(key, identityOf(r));
105
110
  const ownHolder = ownDotRule.get(key);
106
- if (ownHolder !== undefined && ownHolder !== r.rule) {
111
+ if (ownHolder !== undefined && ownHolder !== identityOf(r)) {
107
112
  throw new Error(`restoreSession("${sessionId}"): dot ${key} is live in this session under a different rule — a restore may replay a grant, never reassign its dot; refusing the whole restore`);
108
113
  }
109
114
  for (const [heldSession, heldRows] of this.rows) {
@@ -115,7 +120,21 @@ export class InMemorySessionRulePartition {
115
120
  }
116
121
  }
117
122
  }
118
- this.rows.set(sessionId, rows);
123
+ const restoredDots = new Set(rows.flatMap((r) => r.adds.map((a) => `${a.dot.actor}#${a.dot.counter}`)));
124
+ const carried = [];
125
+ for (const held of this.rows.get(sessionId) ?? []) {
126
+ if (held.behavior === "allow")
127
+ continue;
128
+ const adds = held.adds.filter((a) => !restoredDots.has(`${a.dot.actor}#${a.dot.counter}`));
129
+ if (adds.length === 0)
130
+ continue;
131
+ const same = rows.find((r) => r.behavior === held.behavior && r.rule === held.rule);
132
+ if (same !== undefined)
133
+ same.adds.push(...structuredClone(adds));
134
+ else
135
+ carried.push({ ...structuredClone(held), adds: structuredClone(adds) });
136
+ }
137
+ this.rows.set(sessionId, [...rows, ...carried]);
119
138
  this.bumpEpoch(sessionId);
120
139
  }
121
140
  }
@@ -41,13 +41,14 @@
41
41
  * restart is a NEW delete intent (a new observation, a new tombstone) — which is the correct reading of a
42
42
  * person pressing delete a second time.
43
43
  */
44
- import type { PersistedAllowRule, RuleAdd, RuleDot, RuleScope, RuleTombstone } from "./permission-rule-model.js";
44
+ import type { PersistedRule, RuleAdd, RuleDot, RuleScope, RuleTombstone } from "./permission-rule-model.js";
45
+ import { type RuleBehavior } from "./permission-rule-model.js";
45
46
  import type { RuleSyncDropReason, RuleQuarantineReason } from "./governance-codes.js";
46
47
  import type { StoreDurability, StoreFidelity } from "./checkpoint-store.js";
47
48
  /** One store read: the rules as persisted, their tombstones, and the revision they were read at. */
48
- export interface StoredAllowRules {
49
+ export interface StoredRules {
49
50
  /** Rules with their FULL add sets — tombstoned dots already removed by the backend on read. */
50
- rules: PersistedAllowRule[];
51
+ rules: PersistedRule[];
51
52
  tombstones: RuleTombstone[];
52
53
  /** Monotonic revision of this store, for optimistic concurrency on every write. */
53
54
  rev: number;
@@ -64,7 +65,7 @@ export interface StoredAllowRules {
64
65
  * more than they allow; only one of them is honest about why.
65
66
  */
66
67
  export interface DurableRulePartition {
67
- list(): Promise<StoredAllowRules>;
68
+ list(): Promise<StoredRules>;
68
69
  /**
69
70
  * design/182 §5.2/§8.3 — the quarantine area, for introspection. Rows a sync round moved out of the
70
71
  * live view (fence arm, server rejection, validator rejection): bytes kept, never silently deleted,
@@ -139,10 +140,12 @@ export interface RedemptionAuthorization {
139
140
  /** The add half of the write union: one redeemed approval becoming one dot on one (rule, scope). */
140
141
  export interface RuleAddDelta {
141
142
  kind: "redemption-add";
143
+ /** The identity's behavior member (a redemption lands one dot on one (behavior, text, scope)). */
144
+ behavior: RuleBehavior;
142
145
  rule: string;
143
146
  scope: RuleScope;
144
- tool: PersistedAllowRule["tool"];
145
- match: PersistedAllowRule["match"];
147
+ tool: PersistedRule["tool"];
148
+ match: PersistedRule["match"];
146
149
  command: string;
147
150
  add: RuleAdd;
148
151
  redemption: RedemptionAuthorization;
@@ -176,6 +179,7 @@ export interface RuleSyncJoinDelta {
176
179
  * disclosed — they can re-enter only through a NEW consent (a new ask, a new record, a new dot).
177
180
  */
178
181
  quarantine?: Array<{
182
+ behavior: RuleBehavior;
179
183
  rule: string;
180
184
  scope: RuleScope;
181
185
  dots: RuleDot[];
@@ -222,7 +226,7 @@ export interface RawRuleSyncState {
222
226
  /** The highest counter this replica has minted — the own-actor forgery fence's local truth. */
223
227
  counter: number;
224
228
  rev: number;
225
- rules: PersistedAllowRule[];
229
+ rules: PersistedRule[];
226
230
  tombstones: RuleTombstone[];
227
231
  /** The persisted observation vector (absent = never completed a clean round). */
228
232
  observedVector?: RuleSyncFrontier;
@@ -244,18 +248,30 @@ export declare function writerOf(store: DurableRulePartition | import("./permiss
244
248
  /** Do two scopes name the same place? Three members (design/382 §4.1): global, project-by-root,
245
249
  * session-by-sessionId. */
246
250
  export declare function sameScope(a: RuleScope, b: RuleScope): boolean;
251
+ /** Do two rows/tombstones/quarantine rows name the SAME logical rule — the (behavior, text, scope)
252
+ * triple? Every identity comparison in this module goes through this one predicate: a comparison
253
+ * that forgot the behavior member would let a delete aimed at an allow take the same-text deny. */
254
+ export declare function sameRuleIdentity(a: {
255
+ behavior: RuleBehavior;
256
+ rule: string;
257
+ scope: RuleScope;
258
+ }, b: {
259
+ behavior: RuleBehavior;
260
+ rule: string;
261
+ scope: RuleScope;
262
+ }): boolean;
247
263
  /**
248
264
  * Apply tombstones to a raw persisted set: an add survives iff its dot appears in no tombstone for the
249
- * same (rule, scope). The criterion is dot IDENTITY — single dots across replicas carry no order to
265
+ * same (behavior, rule, scope). The criterion is dot IDENTITY — single dots across replicas carry no order to
250
266
  * compare — and an add the deleting replica never observed survives (add-wins), because behind every add
251
267
  * there is one real human approval.
252
268
  */
253
- export declare function applyTombstones(rules: readonly PersistedAllowRule[], tombstones: readonly RuleTombstone[]): PersistedAllowRule[];
269
+ export declare function applyTombstones(rules: readonly PersistedRule[], tombstones: readonly RuleTombstone[]): PersistedRule[];
254
270
  /** Fold one delta into a raw rule set, returning the new set. Pure — the backends share it so the file and
255
271
  * in-memory forms cannot drift in what a delta MEANS. */
256
- export declare function foldDelta(rules: readonly PersistedAllowRule[], delta: RuleAddDelta): PersistedAllowRule[];
272
+ export declare function foldDelta(rules: readonly PersistedRule[], delta: RuleAddDelta): PersistedRule[];
257
273
  /** Every add dot present in a rule set. */
258
- export declare function addDotsOf(rules: readonly PersistedAllowRule[]): RuleDot[];
274
+ export declare function addDotsOf(rules: readonly PersistedRule[]): RuleDot[];
259
275
  /**
260
276
  * The runtime half of the delete-arm authorization, shared by every backend so the two cannot drift on
261
277
  * what a delta MEANS.
@@ -299,7 +315,7 @@ export declare function assertWriteDeltaScopeDurable(delta: RuleAddDelta | RuleD
299
315
  * `permissionRuleSyncContract` against its own reimplementation).
300
316
  */
301
317
  export interface RuleSyncState {
302
- rules: PersistedAllowRule[];
318
+ rules: PersistedRule[];
303
319
  tombstones: RuleTombstone[];
304
320
  }
305
321
  /**
@@ -319,6 +335,7 @@ export type RuleSyncFrontier = Record<string, number>;
319
335
  * as a JSON blob; `rule`/`scope`/`reason`/`atMs` are query axes and belong in structured columns
320
336
  * (`at_ms` on the SQL side). */
321
337
  export interface QuarantinedRuleAdd {
338
+ behavior: RuleBehavior;
322
339
  rule: string;
323
340
  scope: RuleScope;
324
341
  add: RuleAdd;
@@ -328,6 +345,7 @@ export interface QuarantinedRuleAdd {
328
345
  /** One dropped/quarantined disclosure line. `reason` is a closed, typed code (RULE_SYNC_DROP_CODES in
329
346
  * governance-codes.ts) — "silently dropped" must be machine-checkable, so free text is not a member. */
330
347
  export interface RuleSyncDrop {
348
+ behavior: RuleBehavior;
331
349
  rule: string;
332
350
  scope: RuleScope;
333
351
  dot: RuleDot;
@@ -384,8 +402,8 @@ export declare function joinFrontiers(a: RuleSyncFrontier, b: RuleSyncFrontier):
384
402
  * would break the join's commutativity, and "take the wider" would widen a matching surface nobody
385
403
  * approved. Returns the normalized record or the refusal reason.
386
404
  */
387
- export declare function normalizePersistedRule(r: PersistedAllowRule): {
388
- rule: PersistedAllowRule;
405
+ export declare function normalizePersistedRule(r: PersistedRule): {
406
+ rule: PersistedRule;
389
407
  } | {
390
408
  reject: Extract<RuleSyncDropReason, "invalid_rule_text" | "metadata_mismatch">;
391
409
  };
@@ -413,10 +431,10 @@ export declare function screenRuleSyncState(state: RuleSyncState): {
413
431
  * (a join only ever unions, it never deletes). The output is CANONICALLY ORDERED so equal states are
414
432
  * deep-equal, which is what lets the contract suite assert the three laws structurally.
415
433
  *
416
- * - rules: grouped by (rule, scope); a group's adds are the dot-identity union (provenance rides each
434
+ * - rules: grouped by (behavior, rule, scope); a group's adds are the dot-identity union (provenance rides each
417
435
  * add; two payloads under ONE dot — unreachable in a healthy protocol — resolve by canonical-json
418
436
  * byte order, determinism only, detection belongs to the screening layer);
419
- * - tombstones: grouped by (rule, scope, deletedBy); same identity with differing `removedDots` takes
437
+ * - tombstones: grouped by (behavior, rule, scope, deletedBy); same identity with differing `removedDots` takes
420
438
  * the UNION (any one-sided pick breaks commutativity and can drop an observed removal — the union is
421
439
  * the only convergent answer, and it only ever tightens);
422
440
  * - both inputs pass {@link screenRuleSyncState} first (refused records are dropped deterministically;
@@ -437,7 +455,7 @@ export declare function collectBelowFrontier(state: RuleSyncState, frontier: Rul
437
455
  export interface RuleSyncJoinBase {
438
456
  actor: string;
439
457
  counter: number;
440
- rules: PersistedAllowRule[];
458
+ rules: PersistedRule[];
441
459
  tombstones: RuleTombstone[];
442
460
  quarantined: QuarantinedRuleAdd[];
443
461
  observedVector?: RuleSyncFrontier;
@@ -513,6 +531,10 @@ export type RemoveResult =
513
531
  * does not.
514
532
  */
515
533
  export declare function removePersistedRule(opts: {
534
+ /** The identity's behavior member — `removePersistedRule({ behavior: "allow", … })` for a standing
535
+ * approval, `"deny"`/`"ask"` for a standing refusal/question. Required: the same text can stand in
536
+ * more than one behavior, and a removal that named only the text would delete one nobody asked about. */
537
+ behavior: RuleBehavior;
516
538
  rule: string;
517
539
  scope: RuleScope;
518
540
  /** Whose bucket. A bare string stays the principal shorthand (unchanged callers); the structural
@@ -541,7 +563,7 @@ export declare class InMemoryDurableRulePartition implements WritableDurableRule
541
563
  private rev;
542
564
  private counter;
543
565
  constructor(actor?: string, now?: () => number);
544
- list(): Promise<StoredAllowRules>;
566
+ list(): Promise<StoredRules>;
545
567
  quarantined(): Promise<QuarantinedRuleAdd[]>;
546
568
  readonly [PERMISSION_RULE_WRITER]: PermissionRuleWriter;
547
569
  }