@sema-agent/core 7.7.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.
- package/CHANGELOG.md +88 -0
- package/dist/agents/agent-transcript-tool.js +2 -2
- package/dist/agents/list-agents-tool.js +2 -3
- package/dist/agents/observer.js +2 -2
- package/dist/agents/send-message-tool.js +2 -1
- package/dist/agents/subagent-steps.js +5 -4
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/agents/subagent.js +2 -3
- package/dist/core/ask-origin.d.ts +12 -1
- package/dist/core/ask-origin.js +5 -1
- package/dist/core/ask-question.js +2 -2
- package/dist/core/checkpoint-store.d.ts +64 -24
- package/dist/core/context-edit.d.ts +0 -22
- package/dist/core/context-edit.js +2 -11
- package/dist/core/engine-notice.d.ts +6 -0
- package/dist/core/fs-write-gate-policy.d.ts +7 -1
- package/dist/core/fs-write-gate-policy.js +7 -7
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/gate-lanes.js +53 -26
- package/dist/core/gate-outcome.d.ts +8 -4
- package/dist/core/gate-outcome.js +2 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +6 -0
- package/dist/core/hooks.d.ts +64 -33
- package/dist/core/hooks.js +1 -1
- package/dist/core/lsp.js +2 -2
- package/dist/core/mcp-server-spec.d.ts +17 -0
- package/dist/core/mcp.js +3 -1
- package/dist/core/memory-engine/tools.js +4 -12
- package/dist/core/permission-rule-consent.d.ts +29 -36
- package/dist/core/permission-rule-consent.js +110 -60
- package/dist/core/permission-rule-model.d.ts +254 -55
- package/dist/core/permission-rule-model.js +323 -43
- package/dist/core/permission-rule-org.d.ts +9 -8
- package/dist/core/permission-rule-org.js +6 -15
- package/dist/core/permission-rule-provider.d.ts +10 -6
- package/dist/core/permission-rule-provider.js +12 -8
- package/dist/core/permission-rule-session.d.ts +7 -6
- package/dist/core/permission-rule-session.js +29 -10
- package/dist/core/permission-rule-store.d.ts +40 -18
- package/dist/core/permission-rule-store.js +68 -42
- package/dist/core/permission-rule-sync.d.ts +9 -3
- package/dist/core/permission-rule-sync.js +29 -25
- package/dist/core/permission-rule-syntax.d.ts +30 -0
- package/dist/core/permission-rule-syntax.js +44 -0
- package/dist/core/permission-rules.d.ts +85 -33
- package/dist/core/permission-rules.js +104 -88
- package/dist/core/persisted-rule-arms.d.ts +56 -0
- package/dist/core/persisted-rule-arms.js +48 -0
- package/dist/core/present-plan-tool.js +3 -4
- package/dist/core/runner/active-skill-scope.js +7 -4
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/contracts.d.ts +46 -3
- package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
- package/dist/core/runner/permission-rule-lanes.js +27 -21
- package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
- package/dist/core/runner/prepare-caps-and-workflow.js +16 -11
- package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
- package/dist/core/runner/prepare-defer-classify.js +10 -10
- package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
- package/dist/core/runner/prepare-delegation-surface.js +15 -17
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
- package/dist/core/runner/prepare-gate-stations.js +4 -2
- package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-lsp.d.ts +4 -3
- package/dist/core/runner/prepare-lsp.js +3 -2
- package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
- package/dist/core/runner/prepare-memory-engine-session.js +4 -2
- package/dist/core/runner/prepare-offload-wrappers.js +7 -5
- package/dist/core/runner/prepare-park-ask.d.ts +4 -0
- package/dist/core/runner/prepare-park-ask.js +4 -1
- package/dist/core/runner/prepare-project-context.d.ts +5 -23
- package/dist/core/runner/prepare-project-context.js +7 -5
- package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
- package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
- package/dist/core/runner/prepare-protocol-tools.js +38 -21
- package/dist/core/runner/prepare-question-face.d.ts +4 -3
- package/dist/core/runner/prepare-question-face.js +5 -3
- package/dist/core/runner/prepare-run-refs.d.ts +16 -0
- package/dist/core/runner/prepare-run-refs.js +6 -0
- package/dist/core/runner/prepare-safety-scan.js +48 -2
- package/dist/core/runner/prepare-task.js +37 -19
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
- package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
- package/dist/core/runner/prepare-wiring-manifest.js +3 -8
- package/dist/core/runner/resume-admission.d.ts +53 -0
- package/dist/core/runner/resume-admission.js +83 -0
- package/dist/core/runner/resume-apply.d.ts +50 -0
- package/dist/core/runner/resume-apply.js +184 -0
- package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
- package/dist/core/runner/resume-checkpoint-screen.js +108 -0
- package/dist/core/runner/resume-claim.d.ts +32 -0
- package/dist/core/runner/resume-claim.js +27 -0
- package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
- package/dist/core/runner/resume-internals-and-config.js +50 -0
- package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
- package/dist/core/runner/resume-policy-outcome.js +127 -0
- package/dist/core/runner/resume-preflight.d.ts +40 -0
- package/dist/core/runner/resume-preflight.js +122 -0
- package/dist/core/runner/resume-review-outcome.d.ts +30 -0
- package/dist/core/runner/resume-review-outcome.js +88 -0
- package/dist/core/runner/run-harness-handlers.d.ts +46 -0
- package/dist/core/runner/run-harness-handlers.js +345 -0
- package/dist/core/runner/run-turn-boundary.d.ts +69 -0
- package/dist/core/runner/run-turn-boundary.js +694 -0
- package/dist/core/runner/runtask.d.ts +20 -0
- package/dist/core/runner/runtask.js +129 -1774
- package/dist/core/runner/session-rule-policy.js +9 -4
- package/dist/core/runner/synthetic-tools.js +5 -8
- package/dist/core/runner/tool-disclosure.d.ts +4 -0
- package/dist/core/runner/tool-disclosure.js +9 -5
- package/dist/core/runner/tool-end-body.d.ts +12 -5
- package/dist/core/runner/tool-end-body.js +8 -7
- package/dist/core/runner/tool-face-overlay.d.ts +40 -0
- package/dist/core/runner/tool-face-overlay.js +125 -0
- package/dist/core/runner/tool-output-projection.d.ts +14 -1
- package/dist/core/runner/tool-output-projection.js +24 -18
- package/dist/core/runner/turn-attachments.d.ts +2 -2
- package/dist/core/sensitive-path-policy.d.ts +3 -0
- package/dist/core/sensitive-path-policy.js +6 -5
- package/dist/core/shared-memory/tools.js +3 -6
- package/dist/core/store-contracts/checkpoint-store-contract.js +23 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
- package/dist/core/task-event.d.ts +10 -0
- package/dist/core/task-registry.js +4 -5
- package/dist/core/task-spec.d.ts +8 -0
- package/dist/core/tool-catalog-entries.d.ts +48 -0
- package/dist/core/tool-catalog-entries.js +174 -0
- package/dist/core/tool-conformance.d.ts +22 -0
- package/dist/core/tool-conformance.js +37 -0
- package/dist/core/tool-face.d.ts +171 -0
- package/dist/core/tool-face.js +25 -0
- package/dist/core/tool-policy.d.ts +24 -0
- package/dist/core/tool-policy.js +4 -2
- package/dist/core/tool-registry.d.ts +114 -0
- package/dist/core/tool-registry.js +114 -0
- package/dist/core/tool-result-budget.d.ts +0 -3
- package/dist/core/tool-result-budget.js +4 -3
- package/dist/core/tool-result-store.d.ts +0 -18
- package/dist/core/tool-result-store.js +2 -18
- package/dist/core/tool-roster.d.ts +459 -0
- package/dist/core/tool-roster.js +507 -0
- package/dist/core/tool-spec.d.ts +54 -3
- package/dist/core/tools.js +7 -0
- package/dist/core/wiring-manifest.d.ts +13 -0
- package/dist/core/wiring-manifest.js +2 -1
- package/dist/core/write-protect.d.ts +2 -1
- package/dist/core/write-protect.js +5 -4
- package/dist/engine/compaction/utils.d.ts +6 -0
- package/dist/engine/compaction/utils.js +19 -19
- package/dist/engine/loop/types.d.ts +6 -0
- package/dist/index.d.ts +17 -6
- package/dist/index.js +17 -6
- package/dist/orchestration/goal.js +2 -1
- package/dist/orchestration/run-spec.js +3 -5
- package/dist/orchestration/run-workflow-tool.js +2 -2
- package/dist/prompt-assembly/event-registry.js +3 -3
- package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
- package/dist/stores/file/checkpoint-store.js +1 -1
- package/dist/stores/file/permission-rule-store.d.ts +28 -23
- package/dist/stores/file/permission-rule-store.js +64 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +7 -11
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +2 -2
- package/dist/tools/fs/fs-search-tools.d.ts +0 -7
- package/dist/tools/fs/fs-search-tools.js +3 -16
- package/dist/tools/fs/fs-write.js +4 -6
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +3 -2
- package/dist/tools/fs/safety.js +6 -5
- package/dist/tools/monitor.js +2 -2
- package/dist/tools/scheduler-tools.js +5 -8
- package/dist/tools/task-list.js +5 -8
- package/dist/tools/todo.js +2 -2
- package/dist/tools/web.js +3 -6
- package/dist/tools/worktree.js +3 -4
- package/package.json +4 -1
- package/test/export-surface.snapshot.json +189 -17
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { escapeForDisclosure } from "./permission-rule-model.js";
|
|
2
|
-
import { PERMISSION_RULE_WRITER, errText, normalizePersistedRule,
|
|
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) =>
|
|
78
|
+
if (durableRead.rules.some((r) => sameRuleIdentity(r, t)))
|
|
76
79
|
continue;
|
|
77
|
-
if (removed.some((e) =>
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
109
|
+
seenDots.set(key, identityOf(r));
|
|
105
110
|
const ownHolder = ownDotRule.get(key);
|
|
106
|
-
if (ownHolder !== undefined && ownHolder !== r
|
|
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
|
-
|
|
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 {
|
|
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
|
|
49
|
+
export interface StoredRules {
|
|
49
50
|
/** Rules with their FULL add sets — tombstoned dots already removed by the backend on read. */
|
|
50
|
-
rules:
|
|
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<
|
|
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:
|
|
145
|
-
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:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
388
|
-
rule:
|
|
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:
|
|
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<
|
|
566
|
+
list(): Promise<StoredRules>;
|
|
545
567
|
quarantined(): Promise<QuarantinedRuleAdd[]>;
|
|
546
568
|
readonly [PERMISSION_RULE_WRITER]: PermissionRuleWriter;
|
|
547
569
|
}
|