@sema-agent/core 6.0.0 → 7.0.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 +12 -0
- package/dist/core/governance-codes.d.ts +11 -2
- package/dist/core/governance-codes.js +1 -0
- package/dist/core/permission-rule-consent.d.ts +42 -2
- package/dist/core/permission-rule-consent.js +93 -11
- package/dist/core/permission-rule-model.d.ts +51 -9
- package/dist/core/permission-rule-model.js +4 -2
- package/dist/core/permission-rule-session.d.ts +124 -0
- package/dist/core/permission-rule-session.js +121 -0
- package/dist/core/permission-rule-store.d.ts +65 -2
- package/dist/core/permission-rule-store.js +60 -6
- package/dist/core/permission-rule-sync.d.ts +9 -0
- package/dist/core/permission-rule-sync.js +37 -8
- package/dist/core/runner/prepare-task.js +35 -2
- package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
- package/dist/core/trace.d.ts +7 -2
- package/dist/core/types.d.ts +11 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/stores/file/permission-rule-store.d.ts +11 -0
- package/dist/stores/file/permission-rule-store.js +22 -9
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +15 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { isValidConsentScope, normalizePersistedRule } from "./permission-rule-store.js";
|
|
2
|
+
function isValidOverlayAdd(add) {
|
|
3
|
+
const a = add;
|
|
4
|
+
if (typeof a?.dot?.actor !== "string" || a.dot.actor === "" || typeof a.dot.counter !== "number" || !Number.isFinite(a.dot.counter))
|
|
5
|
+
return false;
|
|
6
|
+
if (a.origin !== "user" && a.origin !== "imported-cc" && a.origin !== "starter")
|
|
7
|
+
return false;
|
|
8
|
+
return typeof a.createdAt === "string";
|
|
9
|
+
}
|
|
10
|
+
export class InMemorySessionRuleOverlay {
|
|
11
|
+
rows = new Map();
|
|
12
|
+
sealed = new Set();
|
|
13
|
+
epochs = new Map();
|
|
14
|
+
bumpEpoch(sessionId) {
|
|
15
|
+
this.epochs.set(sessionId, (this.epochs.get(sessionId) ?? 0) + 1);
|
|
16
|
+
}
|
|
17
|
+
async read(sessionId) {
|
|
18
|
+
return structuredClone(this.rows.get(sessionId) ?? []);
|
|
19
|
+
}
|
|
20
|
+
async apply(sessionId, add) {
|
|
21
|
+
if (this.sealed.has(sessionId))
|
|
22
|
+
return { refused: "session_ended" };
|
|
23
|
+
add = structuredClone(add);
|
|
24
|
+
if (this.sealed.has(sessionId))
|
|
25
|
+
return { refused: "session_ended" };
|
|
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");
|
|
28
|
+
}
|
|
29
|
+
const scope = { kind: "session", sessionId };
|
|
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");
|
|
32
|
+
}
|
|
33
|
+
const projected = { rule: add.rule, tool: add.tool, match: add.match, command: add.command, scope, adds: [add.add] };
|
|
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");
|
|
36
|
+
}
|
|
37
|
+
const rows = this.rows.get(sessionId) ?? [];
|
|
38
|
+
if (!this.rows.has(sessionId))
|
|
39
|
+
this.rows.set(sessionId, rows);
|
|
40
|
+
const sameDotAs = (a) => a.dot.actor === add.add.dot.actor && a.dot.counter === add.add.dot.counter;
|
|
41
|
+
for (const [heldSession, heldRows] of this.rows) {
|
|
42
|
+
const holder = heldRows.find((r) => r.adds.some(sameDotAs));
|
|
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`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const existing = rows.find((r) => r.rule === add.rule);
|
|
48
|
+
if (existing === undefined) {
|
|
49
|
+
rows.push({ rule: add.rule, tool: add.tool, match: add.match, command: add.command, scope, adds: [structuredClone(add.add)] });
|
|
50
|
+
this.bumpEpoch(sessionId);
|
|
51
|
+
return { landed: true };
|
|
52
|
+
}
|
|
53
|
+
if (!existing.adds.some((a) => a.dot.actor === add.add.dot.actor && a.dot.counter === add.add.dot.counter)) {
|
|
54
|
+
existing.adds.push(structuredClone(add.add));
|
|
55
|
+
this.bumpEpoch(sessionId);
|
|
56
|
+
}
|
|
57
|
+
return { landed: true };
|
|
58
|
+
}
|
|
59
|
+
endSession(sessionId) {
|
|
60
|
+
this.sealed.add(sessionId);
|
|
61
|
+
this.rows.delete(sessionId);
|
|
62
|
+
this.bumpEpoch(sessionId);
|
|
63
|
+
}
|
|
64
|
+
async snapshotSession(sessionId) {
|
|
65
|
+
return this.read(sessionId);
|
|
66
|
+
}
|
|
67
|
+
async restoreSession(sessionId, rows) {
|
|
68
|
+
if (this.sealed.has(sessionId)) {
|
|
69
|
+
throw new Error(`session "${sessionId}" has ended — a sealed session's authorizations cannot be restored (termination is one-way)`);
|
|
70
|
+
}
|
|
71
|
+
const epochBeforeRead = this.epochs.get(sessionId) ?? 0;
|
|
72
|
+
rows = structuredClone(rows);
|
|
73
|
+
if (this.sealed.has(sessionId)) {
|
|
74
|
+
throw new Error(`session "${sessionId}" has ended — a sealed session's authorizations cannot be restored (termination is one-way)`);
|
|
75
|
+
}
|
|
76
|
+
if ((this.epochs.get(sessionId) ?? 0) !== epochBeforeRead) {
|
|
77
|
+
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
|
+
for (const r of rows) {
|
|
80
|
+
const scope = r?.scope;
|
|
81
|
+
if (scope?.kind !== "session" || scope.sessionId !== sessionId || !isValidConsentScope(scope)) {
|
|
82
|
+
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)`);
|
|
83
|
+
}
|
|
84
|
+
if ("reject" in normalizePersistedRule(r)) {
|
|
85
|
+
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
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const ownDotRule = new Map();
|
|
89
|
+
for (const held of this.rows.get(sessionId) ?? []) {
|
|
90
|
+
for (const h of held.adds)
|
|
91
|
+
ownDotRule.set(`${h.dot.actor}#${h.dot.counter}`, held.rule);
|
|
92
|
+
}
|
|
93
|
+
const seenDots = new Map();
|
|
94
|
+
for (const r of rows) {
|
|
95
|
+
if (!Array.isArray(r.adds) || r.adds.length === 0 || !r.adds.every(isValidOverlayAdd)) {
|
|
96
|
+
throw new Error(`restoreSession("${sessionId}") was handed a row with malformed add evidence — refusing the whole restore (an add whose dot/origin cannot be read is not a consent anyone can audit)`);
|
|
97
|
+
}
|
|
98
|
+
for (const a of r.adds) {
|
|
99
|
+
const key = `${a.dot.actor}#${a.dot.counter}`;
|
|
100
|
+
const holder = seenDots.get(key);
|
|
101
|
+
if (holder !== undefined && holder !== r.rule) {
|
|
102
|
+
throw new Error(`restoreSession("${sessionId}"): dot ${key} vouches for two different rules — refusing the whole restore`);
|
|
103
|
+
}
|
|
104
|
+
seenDots.set(key, r.rule);
|
|
105
|
+
const ownHolder = ownDotRule.get(key);
|
|
106
|
+
if (ownHolder !== undefined && ownHolder !== r.rule) {
|
|
107
|
+
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
|
+
}
|
|
109
|
+
for (const [heldSession, heldRows] of this.rows) {
|
|
110
|
+
if (heldSession === sessionId)
|
|
111
|
+
continue;
|
|
112
|
+
if (heldRows.some((held) => held.adds.some((h) => h.dot.actor === a.dot.actor && h.dot.counter === a.dot.counter))) {
|
|
113
|
+
throw new Error(`restoreSession("${sessionId}"): dot ${key} already vouches for another live session's grant — refusing the whole restore`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.rows.set(sessionId, rows);
|
|
119
|
+
this.bumpEpoch(sessionId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -215,7 +215,8 @@ export interface WritablePermissionRuleStore extends PermissionRuleStore {
|
|
|
215
215
|
}
|
|
216
216
|
/** The writer of a store, or `undefined` when the backend is read-only from the engine's side. */
|
|
217
217
|
export declare function writerOf(store: PermissionRuleStore): PermissionRuleWriter | undefined;
|
|
218
|
-
/** Do two scopes name the same place?
|
|
218
|
+
/** Do two scopes name the same place? Three members (design/382 §4.1): global, project-by-root,
|
|
219
|
+
* session-by-sessionId. */
|
|
219
220
|
export declare function sameScope(a: RuleScope, b: RuleScope): boolean;
|
|
220
221
|
/**
|
|
221
222
|
* Apply tombstones to a raw persisted set: an add survives iff its dot appears in no tombstone for the
|
|
@@ -238,6 +239,33 @@ export declare function addDotsOf(rules: readonly PersistedAllowRule[]): RuleDot
|
|
|
238
239
|
* Every backend calls this before appending a tombstone.
|
|
239
240
|
*/
|
|
240
241
|
export declare function assertDeleteDeltaCarriesNoAdd(delta: RuleDeleteDelta): void;
|
|
242
|
+
/**
|
|
243
|
+
* design/382 §4.3 — the DURABLE-scope gate of the two direct write arms, shared by every backend
|
|
244
|
+
* (the `assertDeleteDeltaCarriesNoAdd` doctrine: the check is executed, not merely intended, because a
|
|
245
|
+
* union does not constrain a JavaScript caller's payload).
|
|
246
|
+
*
|
|
247
|
+
* A `redemption-add` whose scope — or a `tighten-delete` whose tombstone's scope — is a session scope
|
|
248
|
+
* refuses LOUDLY with the typed code `unsupported.session_scope_store`: a session authorization's home
|
|
249
|
+
* is the session's own overlay, and a session TOMBSTONE is equally unspellable here — session rows are
|
|
250
|
+
* not individually deletable (they die with their session, design/382 §10), so a tombstone naming one
|
|
251
|
+
* is either a foreign writer's fabrication or a caller wiring fault, and both deserve the loud arm.
|
|
252
|
+
* The third arm (`sync-join`) is NOT gated here: its session rows are per-row DROPPED AND DISCLOSED by
|
|
253
|
+
* {@link screenRuleSyncState} (a peer's damage must not veto a whole round the way a local caller's
|
|
254
|
+
* bug must), and a session-scoped QUARANTINE instruction throws inside {@link applySyncJoin} itself.
|
|
255
|
+
*
|
|
256
|
+
* The session member is the LOUD, typed arm; the rest of the face is executed too. A scope that is
|
|
257
|
+
* neither member in well-formed shape — `{kind:"project", root:""}` above all, which `pathWithinRoot`
|
|
258
|
+
* turns into base `/` so the row crosses every project boundary — is a caller wiring fault, refused
|
|
259
|
+
* with the untyped loud arm the sibling {@link assertDeleteDeltaCarriesNoAdd} uses (no closed-set code
|
|
260
|
+
* names it: it is nobody's supported request). The sync arm already refuses the same shape inbound
|
|
261
|
+
* ({@link applySyncJoin} runs {@link isValidDurableScope} on every joined row), so this closes the
|
|
262
|
+
* matching direct-write door rather than inventing a new rule.
|
|
263
|
+
*
|
|
264
|
+
* Exported with the backend contract for the same OPEN-SET duty {@link isValidDurableScope} names:
|
|
265
|
+
* any lane — host-side import doors included — that writes rows into a persisted store carries the
|
|
266
|
+
* durable two-member face.
|
|
267
|
+
*/
|
|
268
|
+
export declare function assertWriteDeltaScopeDurable(delta: RuleAddDelta | RuleDeleteDelta): void;
|
|
241
269
|
/**
|
|
242
270
|
* The state one sync round exchanges: raw adds (tombstoned dots NOT pre-filtered) plus tombstones.
|
|
243
271
|
* The live view is always DERIVED (`applyTombstones`); the join below never deletes anything.
|
|
@@ -291,6 +319,27 @@ export interface RuleSyncLandingReport {
|
|
|
291
319
|
* a dropped tombstone may have covered them — they re-arrive once the peer's state is clean). */
|
|
292
320
|
withheldAdds: number;
|
|
293
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* design/382 §4.3 — the CONSENT face's scope validity: the union's full three members, their shapes
|
|
324
|
+
* executed (a project scope with an empty root would admit every cwd, wider than global, through
|
|
325
|
+
* `pathWithinRoot`'s base construction; an empty sessionId would be a session nobody can name).
|
|
326
|
+
* Read by the consent protocol's entries (prepare's explicit-scope gate, the record integrity walk),
|
|
327
|
+
* and exported for the server's consent-projection twin. NEVER by a durable entrance — that face is
|
|
328
|
+
* {@link isValidDurableScope}, and keeping the two as separate named tables is the design's point:
|
|
329
|
+
* a session row leaking toward the store must meet an explicit refusal at every door, not a shared
|
|
330
|
+
* table someone widened for the other face's sake.
|
|
331
|
+
*/
|
|
332
|
+
export declare function isValidConsentScope(scope: RuleScope): boolean;
|
|
333
|
+
/**
|
|
334
|
+
* design/382 §4.3 — the DURABLE face's scope validity: the two members a persisted row may carry,
|
|
335
|
+
* `{global, project}`. A session scope is a WELL-FORMED consent scope that is structurally not a
|
|
336
|
+
* durable one — session authorizations live in the session's own overlay and die with it; letting one
|
|
337
|
+
* into the store (or the sync wire, or the at-rest bytes) would give it exactly the afterlife the
|
|
338
|
+
* dimension is defined not to have. Exported for the server's rule-sync validator (same face, same
|
|
339
|
+
* two members) and for any host-side lane that writes rows into a persisted store — the OPEN-SET
|
|
340
|
+
* duty (design/382 §10): every such entrance, present or future, carries this face.
|
|
341
|
+
*/
|
|
342
|
+
export declare function isValidDurableScope(scope: RuleScope): boolean;
|
|
294
343
|
/** Is `dot` at or below the frontier? Absent actor ⇒ not below (a frontier never covers what it never saw). */
|
|
295
344
|
export declare function dotAtOrBelowFrontier(dot: RuleDot, frontier: RuleSyncFrontier): boolean;
|
|
296
345
|
/**
|
|
@@ -314,7 +363,21 @@ export declare function normalizePersistedRule(r: PersistedAllowRule): {
|
|
|
314
363
|
} | {
|
|
315
364
|
reject: Extract<RuleSyncDropReason, "invalid_rule_text" | "metadata_mismatch">;
|
|
316
365
|
};
|
|
317
|
-
/** The normalization half exposed for the DISCLOSURE layer: which records a join would refuse, and why.
|
|
366
|
+
/** The normalization half exposed for the DISCLOSURE layer: which records a join would refuse, and why.
|
|
367
|
+
*
|
|
368
|
+
* design/382 §4.3 — this screen carries the SESSION member of the durable face on every JOIN leg
|
|
369
|
+
* (inbound state, local state, both sides of the pure join): a rule or tombstone row carrying a
|
|
370
|
+
* session scope is dropped and reported (`session_scope_not_durable`), never landed and never allowed
|
|
371
|
+
* to veto the round the way a malformed tombstone does. Dropping a session TOMBSTONE cannot widen:
|
|
372
|
+
* tombstones remove adds only under the SAME (rule, scope), and every session-scope add is dropped by
|
|
373
|
+
* this same screen — the #176 "its cover is not load-bearing" argument, one scope over.
|
|
374
|
+
*
|
|
375
|
+
* The face's OTHER member — a durable scope that is not well-formed, `{kind:"project", root:""}`
|
|
376
|
+
* above all — is executed on the INBOUND legs by {@link applySyncJoin} (its tombstone pre-loop and its
|
|
377
|
+
* rule arm both run {@link isValidDurableScope}) and at the direct write arms by
|
|
378
|
+
* {@link assertWriteDeltaScopeDurable}, not here. Stated so the boundary is readable: a LOCAL row of
|
|
379
|
+
* that shape — reachable only by editing the at-rest bytes, or from a store written before the write
|
|
380
|
+
* arm executed that axis — passes this screen and stays live. */
|
|
318
381
|
export declare function screenRuleSyncState(state: RuleSyncState): {
|
|
319
382
|
state: RuleSyncState;
|
|
320
383
|
rejected: RuleSyncDrop[];
|
|
@@ -10,7 +10,11 @@ export function writerOf(store) {
|
|
|
10
10
|
return w !== undefined && typeof w.apply === "function" && typeof w.nextDot === "function" ? w : undefined;
|
|
11
11
|
}
|
|
12
12
|
export function sameScope(a, b) {
|
|
13
|
-
|
|
13
|
+
if (a.kind === "global")
|
|
14
|
+
return b.kind === "global";
|
|
15
|
+
if (a.kind === "session")
|
|
16
|
+
return b.kind === "session" && a.sessionId === b.sessionId;
|
|
17
|
+
return b.kind === "project" && a.root === b.root;
|
|
14
18
|
}
|
|
15
19
|
function sameDot(a, b) {
|
|
16
20
|
return a.actor === b.actor && a.counter === b.counter;
|
|
@@ -47,7 +51,29 @@ export function assertDeleteDeltaCarriesNoAdd(delta) {
|
|
|
47
51
|
throw new Error("a tighten-delete must carry a tombstone naming at least one observed add dot");
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
|
-
function
|
|
54
|
+
export function assertWriteDeltaScopeDurable(delta) {
|
|
55
|
+
const scope = delta.kind === "redemption-add" ? delta.scope : delta.tombstone?.scope;
|
|
56
|
+
if (scope?.kind === "session") {
|
|
57
|
+
const e = new Error(delta.kind === "redemption-add"
|
|
58
|
+
? "a session-scope rule cannot enter the persisted store — its home is the session's own overlay (design/382 §4.3); refusing the redemption-add"
|
|
59
|
+
: "a session-scope tombstone cannot enter the persisted store — session rows are not individually deletable and die with their session (design/382 §4.3/§10); refusing the tighten-delete");
|
|
60
|
+
e.code = "unsupported.session_scope_store";
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
if (!isValidDurableScope(scope)) {
|
|
64
|
+
throw new Error(delta.kind === "redemption-add"
|
|
65
|
+
? "a redemption-add whose scope is not a well-formed durable scope cannot enter the persisted store (design/382 §4.3: the face is {global, project-with-a-root}) — refusing the write"
|
|
66
|
+
:
|
|
67
|
+
"a tighten-delete must carry a tombstone whose scope is a well-formed durable scope (design/382 §4.3: the face is {global, project-with-a-root}) — refusing the write");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function isValidConsentScope(scope) {
|
|
71
|
+
const sc = scope;
|
|
72
|
+
return (sc?.kind === "global" ||
|
|
73
|
+
(sc?.kind === "project" && typeof sc.root === "string" && sc.root !== "") ||
|
|
74
|
+
(sc?.kind === "session" && typeof sc.sessionId === "string" && sc.sessionId !== ""));
|
|
75
|
+
}
|
|
76
|
+
export function isValidDurableScope(scope) {
|
|
51
77
|
const sc = scope;
|
|
52
78
|
return sc?.kind === "global" || (sc?.kind === "project" && typeof sc.root === "string" && sc.root !== "");
|
|
53
79
|
}
|
|
@@ -56,7 +82,7 @@ function isValidDot(dot) {
|
|
|
56
82
|
return typeof d?.actor === "string" && d.actor !== "" && typeof d.counter === "number" && Number.isFinite(d.counter);
|
|
57
83
|
}
|
|
58
84
|
function scopeKey(scope) {
|
|
59
|
-
return JSON.stringify(scope.kind === "global" ? ["g"] : ["p", scope.root]);
|
|
85
|
+
return JSON.stringify(scope.kind === "global" ? ["g"] : scope.kind === "session" ? ["s", scope.sessionId] : ["p", scope.root]);
|
|
60
86
|
}
|
|
61
87
|
function dotKey(dot) {
|
|
62
88
|
return JSON.stringify([dot.actor, dot.counter]);
|
|
@@ -114,6 +140,11 @@ export function screenRuleSyncState(state) {
|
|
|
114
140
|
for (const r of state.rules) {
|
|
115
141
|
if (!Array.isArray(r.adds) || r.adds.length === 0)
|
|
116
142
|
continue;
|
|
143
|
+
if (r.scope?.kind === "session") {
|
|
144
|
+
for (const a of r.adds)
|
|
145
|
+
rejected.push({ rule: r.rule, scope: r.scope, dot: a.dot, reason: "session_scope_not_durable" });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
117
148
|
const n = normalizePersistedRule(r);
|
|
118
149
|
if ("reject" in n) {
|
|
119
150
|
for (const a of r.adds)
|
|
@@ -122,7 +153,15 @@ export function screenRuleSyncState(state) {
|
|
|
122
153
|
}
|
|
123
154
|
rules.push(r);
|
|
124
155
|
}
|
|
125
|
-
|
|
156
|
+
const tombstones = [];
|
|
157
|
+
for (const t of state.tombstones) {
|
|
158
|
+
if (t?.scope?.kind === "session") {
|
|
159
|
+
rejected.push({ rule: t.rule, scope: t.scope, dot: t.deletedBy, reason: "session_scope_not_durable" });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
tombstones.push(t);
|
|
163
|
+
}
|
|
164
|
+
return { state: { rules, tombstones }, rejected };
|
|
126
165
|
}
|
|
127
166
|
export function joinRuleStates(a, b) {
|
|
128
167
|
const sa = screenRuleSyncState(a).state;
|
|
@@ -183,10 +222,12 @@ export function applySyncJoin(cur, delta, nowMs) {
|
|
|
183
222
|
throw new Error("a sync-join delta must carry an inbound state with rules and tombstones arrays");
|
|
184
223
|
}
|
|
185
224
|
for (const t of delta.inbound.tombstones) {
|
|
225
|
+
if (t?.scope?.kind === "session")
|
|
226
|
+
continue;
|
|
186
227
|
if (!Array.isArray(t?.removedDots) || t.removedDots.length === 0) {
|
|
187
228
|
throw new Error("an inbound tombstone must name at least one observed add dot — refusing the whole sync-join (the client's partitioning must withhold the round's adds alongside a malformed tombstone)");
|
|
188
229
|
}
|
|
189
|
-
if (!
|
|
230
|
+
if (!isValidDurableScope(t.scope) || !isValidDot(t.deletedBy) || !t.removedDots.every(isValidDot)) {
|
|
190
231
|
throw new Error("an inbound tombstone carries a malformed scope or dot — refusing the whole sync-join (a tombstone that cannot identity-match what it deletes would land its covered adds live)");
|
|
191
232
|
}
|
|
192
233
|
const canonicalTomb = parseAllowRuleText(t.rule);
|
|
@@ -229,7 +270,7 @@ export function applySyncJoin(cur, delta, nowMs) {
|
|
|
229
270
|
withheldAdds += r.adds.length;
|
|
230
271
|
continue;
|
|
231
272
|
}
|
|
232
|
-
if (!
|
|
273
|
+
if (!isValidDurableScope(r.scope)) {
|
|
233
274
|
for (const a of r.adds)
|
|
234
275
|
droppedInbound.push({ rule: r.rule, scope: r.scope, dot: a.dot, reason: "metadata_mismatch" });
|
|
235
276
|
continue;
|
|
@@ -288,6 +329,11 @@ export function applySyncJoin(cur, delta, nowMs) {
|
|
|
288
329
|
}
|
|
289
330
|
}
|
|
290
331
|
for (const q of delta.quarantine ?? []) {
|
|
332
|
+
if (q.scope?.kind === "session") {
|
|
333
|
+
const e = new Error("a quarantine instruction names a session scope — the persisted store never holds a session row (design/382 §4.3); refusing the instruction");
|
|
334
|
+
e.code = "unsupported.session_scope_store";
|
|
335
|
+
throw e;
|
|
336
|
+
}
|
|
291
337
|
for (const dot of q.dots) {
|
|
292
338
|
if (joined.tombstones.some((t) => sameDot(t.deletedBy, dot))) {
|
|
293
339
|
throw new Error("a quarantine instruction may not touch a tombstone — it moves adds only");
|
|
@@ -332,6 +378,12 @@ export async function removePersistedRule(opts) {
|
|
|
332
378
|
const target = opts.principal;
|
|
333
379
|
const owner = typeof target === "string" ? { kind: "principal", principal: target } : structuredClone(target);
|
|
334
380
|
const provider = opts.provider;
|
|
381
|
+
if (scope?.kind === "session") {
|
|
382
|
+
return {
|
|
383
|
+
status: "failed",
|
|
384
|
+
error: "unsupported.session_scope_store: a session-scope rule cannot be removed through the persisted-store entry — session authorizations live in the session's own overlay and end with the session (design/382 §4.3)",
|
|
385
|
+
};
|
|
386
|
+
}
|
|
335
387
|
if (owner.kind === "local-owner" && provider.forLocalOwner === undefined) {
|
|
336
388
|
return { status: "failed", error: "this provider has no local-owner bucket (forLocalOwner is not implemented) — a local-owner rule cannot be removed through it" };
|
|
337
389
|
}
|
|
@@ -435,10 +487,12 @@ export class InMemoryPermissionRuleStore {
|
|
|
435
487
|
if (opts.expectedRev !== this.rev)
|
|
436
488
|
return { conflict: true, rev: this.rev };
|
|
437
489
|
if (delta.kind === "redemption-add") {
|
|
490
|
+
assertWriteDeltaScopeDurable(delta);
|
|
438
491
|
assertRedemptionNotQuarantined(this.quarantinedRows, delta);
|
|
439
492
|
this.rules = foldDelta(this.rules, delta);
|
|
440
493
|
}
|
|
441
494
|
else if (delta.kind === "tighten-delete") {
|
|
495
|
+
assertWriteDeltaScopeDurable(delta);
|
|
442
496
|
assertDeleteDeltaCarriesNoAdd(delta);
|
|
443
497
|
this.tombstones = [...this.tombstones, delta.tombstone];
|
|
444
498
|
}
|
|
@@ -134,6 +134,15 @@ interface ParsedRuleSyncResponse {
|
|
|
134
134
|
dot: RuleDot;
|
|
135
135
|
code: RuleRejectCode;
|
|
136
136
|
}>;
|
|
137
|
+
/** design/382 §4.3 — SESSION-scope tombstones, dropped at the parse as per-row refusals (never
|
|
138
|
+
* `undefined`-malformed): a session row can never land regardless of its dots, so classifying a
|
|
139
|
+
* DAMAGED one as generic-malformed would withhold every valid inbound add on every round — one
|
|
140
|
+
* peer row poisoning sync forever, the exact shape the per-row drop arm exists to avoid. */
|
|
141
|
+
sessionScopeTombstones: Array<{
|
|
142
|
+
rule: string;
|
|
143
|
+
scope: RuleScope;
|
|
144
|
+
dot: RuleDot;
|
|
145
|
+
}>;
|
|
137
146
|
warnings: string[];
|
|
138
147
|
}
|
|
139
148
|
/**
|
|
@@ -91,6 +91,9 @@ export async function syncPermissionRules(opts) {
|
|
|
91
91
|
for (const t of response.textRefusedTombstones) {
|
|
92
92
|
dropped.push({ rule: t.rule, scope: t.scope, dot: t.dot, reason: "invalid_rule_text" });
|
|
93
93
|
}
|
|
94
|
+
for (const t of response.sessionScopeTombstones) {
|
|
95
|
+
dropped.push({ rule: t.rule, scope: t.scope, dot: t.dot, reason: "session_scope_not_durable" });
|
|
96
|
+
}
|
|
94
97
|
if (response.textRefusedTombstones.length > 0) {
|
|
95
98
|
const named = response.textRefusedTombstones
|
|
96
99
|
.slice(0, DISCLOSED_REFUSED_TOMBSTONES)
|
|
@@ -131,6 +134,10 @@ export async function syncPermissionRules(opts) {
|
|
|
131
134
|
}
|
|
132
135
|
const localDots = new Set(raw.rules.flatMap((r) => r.adds.map((a) => JSON.stringify([a.dot.actor, a.dot.counter]))));
|
|
133
136
|
for (const d of response.dropped) {
|
|
137
|
+
if (d.scope.kind === "session") {
|
|
138
|
+
warnings.push(`the server's dropped list names a session-scope row (${escapeForDisclosure(d.rule)}) — the durable store never holds one, so there is no local row it could mean; ignored`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
134
141
|
if (localDots.has(JSON.stringify([d.dot.actor, d.dot.counter]))) {
|
|
135
142
|
const reason = d.reason === "below_gc_frontier" ? "below_gc_frontier" : "server_rejected";
|
|
136
143
|
quarantine.push({ rule: d.rule, scope: d.scope, dots: [d.dot], reason });
|
|
@@ -190,7 +197,7 @@ export async function syncPermissionRules(opts) {
|
|
|
190
197
|
const dotKeyOf = (d) => JSON.stringify([d.actor, d.counter]);
|
|
191
198
|
const preDots = new Set(current.rules.flatMap((r) => r.adds.map((a) => dotKeyOf(a.dot))));
|
|
192
199
|
const newAdds = landedRaw.rules.flatMap((r) => r.adds).filter((a) => !preDots.has(dotKeyOf(a.dot))).length;
|
|
193
|
-
const tombKeyOf = (t) => JSON.stringify([t.rule, t.scope.kind === "
|
|
200
|
+
const tombKeyOf = (t) => JSON.stringify([t.rule, t.scope.kind, t.scope.kind === "project" ? t.scope.root : t.scope.kind === "session" ? t.scope.sessionId : null, dotKeyOf(t.deletedBy)]);
|
|
194
201
|
const preTombs = new Set(current.tombstones.map(tombKeyOf));
|
|
195
202
|
const newTombstones = landedRaw.tombstones.filter((t) => !preTombs.has(tombKeyOf(t))).length;
|
|
196
203
|
const landedReport = res.sync;
|
|
@@ -228,13 +235,23 @@ function pickDot(v) {
|
|
|
228
235
|
const counter = d?.counter;
|
|
229
236
|
return typeof actor === "string" && actor !== "" && typeof counter === "number" && Number.isFinite(counter) ? { actor, counter } : undefined;
|
|
230
237
|
}
|
|
231
|
-
function
|
|
238
|
+
function pickScopeCaptured(v) {
|
|
232
239
|
const s = v;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
const kind = s?.kind;
|
|
241
|
+
if (kind === "global")
|
|
242
|
+
return { kind, scope: { kind: "global" } };
|
|
243
|
+
if (kind === "project") {
|
|
244
|
+
const root = s?.root;
|
|
245
|
+
return { kind, scope: typeof root === "string" && root !== "" ? { kind: "project", root } : undefined };
|
|
246
|
+
}
|
|
247
|
+
if (kind === "session") {
|
|
248
|
+
const sessionId = s?.sessionId;
|
|
249
|
+
return { kind, scope: typeof sessionId === "string" && sessionId !== "" ? { kind: "session", sessionId } : undefined };
|
|
250
|
+
}
|
|
251
|
+
return { kind, scope: undefined };
|
|
252
|
+
}
|
|
253
|
+
function pickScope(v) {
|
|
254
|
+
return pickScopeCaptured(v).scope;
|
|
238
255
|
}
|
|
239
256
|
function pickAdd(v) {
|
|
240
257
|
const a = v;
|
|
@@ -325,9 +342,11 @@ export function parseRuleSyncResponse(raw, expectedPrincipal) {
|
|
|
325
342
|
}
|
|
326
343
|
const tombstones = [];
|
|
327
344
|
const textRefusedTombstones = [];
|
|
345
|
+
const sessionScopeTombstones = [];
|
|
328
346
|
for (const entry of mergedTombstones) {
|
|
329
347
|
const t = entry;
|
|
330
|
-
const
|
|
348
|
+
const pickedScope = pickScopeCaptured(t?.scope);
|
|
349
|
+
const scope = pickedScope.scope;
|
|
331
350
|
const ruleText = t?.rule;
|
|
332
351
|
const deletedBy = pickDot(t?.deletedBy);
|
|
333
352
|
const removedRaw = t?.removedDots;
|
|
@@ -342,6 +361,15 @@ export function parseRuleSyncResponse(raw, expectedPrincipal) {
|
|
|
342
361
|
removedDots.push(dot);
|
|
343
362
|
}
|
|
344
363
|
}
|
|
364
|
+
if (pickedScope.kind === "session") {
|
|
365
|
+
if (typeof ruleText === "string" && deletedBy !== undefined && scope !== undefined) {
|
|
366
|
+
sessionScopeTombstones.push({ rule: ruleText, scope, dot: deletedBy });
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
warnings.push("the response carried a session-scope tombstone whose identity could not be read — dropped per-row (a session row never lands here, and nothing is withheld for it)");
|
|
370
|
+
}
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
345
373
|
if (typeof ruleText !== "string" || scope === undefined || deletedBy === undefined || removedDots.length === 0) {
|
|
346
374
|
tombstones.push(undefined);
|
|
347
375
|
continue;
|
|
@@ -384,6 +412,7 @@ export function parseRuleSyncResponse(raw, expectedPrincipal) {
|
|
|
384
412
|
...(gcFrontier !== undefined ? { gcFrontier } : {}),
|
|
385
413
|
dropped,
|
|
386
414
|
textRefusedTombstones,
|
|
415
|
+
sessionScopeTombstones,
|
|
387
416
|
warnings,
|
|
388
417
|
};
|
|
389
418
|
}
|
|
@@ -24,6 +24,7 @@ import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
|
|
|
24
24
|
import { askApproverIdentity, carriesBidiControls, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOfLayer, isApprovalSettledBy, isAskDenyResolution, screenApproverAttribution, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
|
|
25
25
|
const PERSISTED_RULE_TOOL = "Bash";
|
|
26
26
|
import { findAdmittingRule, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
27
|
+
import { normalizePersistedRule } from "../permission-rule-store.js";
|
|
27
28
|
import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
|
|
28
29
|
import { CHANGED_FILES_MTIME_EPS_MS, fenceMcpServerInstructions, renderAgentListingDelta } from "./turn-attachments.js";
|
|
29
30
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
@@ -893,6 +894,37 @@ function explicitlyDeferredMemoryTrio(mounted, roster, deferNames) {
|
|
|
893
894
|
function memoryGroupRetractionSet(builtinDeferPairNames, engineTrioInPlay) {
|
|
894
895
|
return new Set([...builtinDeferPairNames, ...(engineTrioInPlay ? MEMORY_ENGINE_TOOL_NAMES : [])]);
|
|
895
896
|
}
|
|
897
|
+
async function spliceSessionOverlayRows(overlay, sessionId, persisted, tracer, hostTaskId) {
|
|
898
|
+
if (overlay === undefined)
|
|
899
|
+
return persisted;
|
|
900
|
+
try {
|
|
901
|
+
const served = structuredClone(await overlay.read(sessionId));
|
|
902
|
+
const sessionRows = served.filter((r) => {
|
|
903
|
+
const scope = r?.scope;
|
|
904
|
+
return scope?.kind === "session" && scope.sessionId === sessionId && !("reject" in normalizePersistedRule(r));
|
|
905
|
+
});
|
|
906
|
+
if (sessionRows.length < served.length) {
|
|
907
|
+
emitTrace(tracer, () => ({
|
|
908
|
+
kind: "permission.rule_store_unreadable",
|
|
909
|
+
version: 1,
|
|
910
|
+
taskId: hostTaskId,
|
|
911
|
+
message: `session-rule overlay served ${served.length - sessionRows.length} row(s) that are not canonical session rows of this session — dropped, not adjudicated`,
|
|
912
|
+
ts: Date.now(),
|
|
913
|
+
}));
|
|
914
|
+
}
|
|
915
|
+
return sessionRows.length > 0 ? [...sessionRows, ...persisted] : persisted;
|
|
916
|
+
}
|
|
917
|
+
catch (err) {
|
|
918
|
+
emitTrace(tracer, () => ({
|
|
919
|
+
kind: "permission.rule_store_unreadable",
|
|
920
|
+
version: 1,
|
|
921
|
+
taskId: hostTaskId,
|
|
922
|
+
message: `session-rule overlay: ${err instanceof Error ? err.message : String(err)}`,
|
|
923
|
+
ts: Date.now(),
|
|
924
|
+
}));
|
|
925
|
+
return persisted;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
896
928
|
function refuseRequireExistingWithoutSession(spec) {
|
|
897
929
|
if (spec.requireExistingSession && !spec.sessionId) {
|
|
898
930
|
const e = new Error(`requireExistingSession requires a sessionId — cannot require an existing session without one (design/114 Phase3)`);
|
|
@@ -3600,10 +3632,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3600
3632
|
}));
|
|
3601
3633
|
return { unreadable: true };
|
|
3602
3634
|
}
|
|
3603
|
-
const
|
|
3635
|
+
const table = await spliceSessionOverlayRows(deps.sessionPermissionRules, sessionId, listed.rules, deps.tracer, hostTaskId);
|
|
3636
|
+
const admitting = findAdmittingRule(table, { tool: req.toolName, command, cwd: root, sessionId });
|
|
3604
3637
|
if (admitting !== undefined)
|
|
3605
3638
|
return persistedRuleHitOf(admitting);
|
|
3606
|
-
const coverage = segmentCoverageOf(command, { persisted:
|
|
3639
|
+
const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: root, sessionId });
|
|
3607
3640
|
return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
|
|
3608
3641
|
},
|
|
3609
3642
|
};
|
|
@@ -34,7 +34,11 @@ function liveDots(s) {
|
|
|
34
34
|
return out;
|
|
35
35
|
}
|
|
36
36
|
function sameScope(a, b) {
|
|
37
|
-
|
|
37
|
+
if (a.kind === "global")
|
|
38
|
+
return b.kind === "global";
|
|
39
|
+
if (a.kind === "session")
|
|
40
|
+
return b.kind === "session" && a.sessionId === b.sessionId;
|
|
41
|
+
return b.kind === "project" && a.root === b.root;
|
|
38
42
|
}
|
|
39
43
|
function allDots(s) {
|
|
40
44
|
return new Set(s.rules.flatMap((r) => r.adds.map((a) => `${r.rule}|${a.dot.actor}#${a.dot.counter}`)));
|
|
@@ -150,6 +154,16 @@ export async function permissionRuleSyncContract(hooks = {}) {
|
|
|
150
154
|
const merged = join(state([rule("Bash(node:*)", [add("z", 1)])]), state());
|
|
151
155
|
assert.strictEqual(allDots(merged).size, 0, "Bash(node:*) must be refused by the shared validator inside the join");
|
|
152
156
|
});
|
|
157
|
+
run("durable two-member face (design/382 §4.3): a session-scope row or tombstone never survives a join, and never poisons the round", async () => {
|
|
158
|
+
const sessionScope = { kind: "session", sessionId: "sess-1" };
|
|
159
|
+
const dirty = state([rule("Bash(ls)", [add("z", 1)], sessionScope), rule("Bash(git status)", [add("a", 1)])], [tomb("Bash(pwd)", [["z", 2]], ["z", 3], sessionScope)]);
|
|
160
|
+
const ab = join(dirty, state());
|
|
161
|
+
const ba = join(state(), dirty);
|
|
162
|
+
assert.ok(!allDots(ab).has("Bash(ls)|z#1"), "a session-scope add must not survive the join — the durable face holds two members");
|
|
163
|
+
assert.strictEqual(ab.tombstones.length, 0, "a session-scope tombstone must not survive the join");
|
|
164
|
+
assert.ok(allDots(ab).has("Bash(git status)|a#1"), "dropping the session row must not withhold or disturb the durable rest — per-row, never round-poisoning");
|
|
165
|
+
assert.deepStrictEqual(ab, ba, "the session-scope drop must preserve commutativity");
|
|
166
|
+
});
|
|
153
167
|
run("identity keys are collision-free: roots and actors containing spaces keep DISTINCT tombstone identities, and the covered add stays dead through an identity join", async () => {
|
|
154
168
|
const scopeA = { kind: "project", root: "/x" };
|
|
155
169
|
const scopeB = { kind: "project", root: "/x a" };
|
package/dist/core/trace.d.ts
CHANGED
|
@@ -449,7 +449,10 @@ export type TraceEvent = {
|
|
|
449
449
|
/** The bucket this round synced. A local-owner bucket cannot sync, so this is always a principal. */
|
|
450
450
|
principal: string;
|
|
451
451
|
rule: string;
|
|
452
|
-
|
|
452
|
+
/** `"session"` is unreachable here in a healthy fleet (design/382 §4.3: a session row never
|
|
453
|
+
* lands, so it can never resurrect) — declared because the sibling drop event names it and a
|
|
454
|
+
* lockstep union is what keeps the two from drifting. */
|
|
455
|
+
scopeKind: "global" | "project" | "session";
|
|
453
456
|
ts: number;
|
|
454
457
|
} | {
|
|
455
458
|
/**
|
|
@@ -462,7 +465,9 @@ export type TraceEvent = {
|
|
|
462
465
|
version: 1;
|
|
463
466
|
principal: string;
|
|
464
467
|
rule: string;
|
|
465
|
-
|
|
468
|
+
/** `"session"` names the design/382 §4.3 durable-face drop: a peer sent a session-scope row,
|
|
469
|
+
* refused per-row with `session_scope_not_durable`. */
|
|
470
|
+
scopeKind: "global" | "project" | "session";
|
|
466
471
|
/** Closed reason code ({@link import("./governance-codes.js").RuleSyncDropReason}). */
|
|
467
472
|
reason: string;
|
|
468
473
|
ts: number;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -6436,6 +6436,17 @@ export interface RunnerDeps {
|
|
|
6436
6436
|
* adopted principal's bucket forever.
|
|
6437
6437
|
*/
|
|
6438
6438
|
localOwnerRules?: boolean;
|
|
6439
|
+
/**
|
|
6440
|
+
* design/382 §4.3 — the SESSION-RULE OVERLAY: where `{kind:"session"}` scoped allow rules live
|
|
6441
|
+
* (the session's own state, never the persisted store). Wired, the gate's persisted-rule lane
|
|
6442
|
+
* splices the CURRENT session's overlay rows in front of the store's `list()` on every
|
|
6443
|
+
* adjudication — session > project > global, the narrowest-first reporting order — and threads the
|
|
6444
|
+
* session's identity as the eligibility context's third axis; a host lands rows into it through
|
|
6445
|
+
* the consent protocol (`RuleConsentDeps.sessionRules`, a session-scope candidate's redemption).
|
|
6446
|
+
* Omitted ⇒ the session dimension does not exist at this gate and the lane is byte-identical.
|
|
6447
|
+
* See `permission-rule-session.ts` for the contract and the reference implementation.
|
|
6448
|
+
*/
|
|
6449
|
+
sessionPermissionRules?: import("./permission-rule-session.js").SessionRuleOverlay;
|
|
6439
6450
|
/**
|
|
6440
6451
|
* design/182 §7 — the ORG rule overlay for an org-GOVERNED deployment. Constructed with
|
|
6441
6452
|
* `createOrgRuleOverlay` (that constructor is the boot gate: a governed declaration with no snapshot
|
package/dist/index.d.ts
CHANGED
|
@@ -167,8 +167,9 @@ export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRul
|
|
|
167
167
|
* allowed and widening is not.
|
|
168
168
|
*/
|
|
169
169
|
export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, renderUntrustedCommandText, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, type PersistedAllowRule, type RuleTombstone, type RuleScope, type RuleDot, type RuleAdd, type RuleAddOrigin, type RuleOffer, type SegmentRuleSuggestion, type SegmentCoverage, type RuleReject, type RuleRejectCode, type ParsedAllowRule, type PersistedRuleTool, type PersistedRuleMatch, } from "./core/permission-rule-model.js";
|
|
170
|
-
export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, type PermissionRuleStore, type PermissionRuleStoreProvider, type StoredAllowRules, type RemoveResult, type PutResult, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, type RuleSyncState, type RuleSyncFrontier, type RuleSyncDrop, type RuleSyncLandingReport, type RuleOwner, type QuarantinedRuleAdd, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, type PermissionRuleWriter, type WritablePermissionRuleStore, type RuleWriteDelta, type RuleAddDelta, type RuleDeleteDelta, type RuleSyncJoinDelta, type RawRuleSyncState, type RedemptionAuthorization, } from "./core/permission-rule-store.js";
|
|
170
|
+
export { removePersistedRule, applyTombstones, sameScope, isValidConsentScope, isValidDurableScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, type PermissionRuleStore, type PermissionRuleStoreProvider, type StoredAllowRules, type RemoveResult, type PutResult, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, type RuleSyncState, type RuleSyncFrontier, type RuleSyncDrop, type RuleSyncLandingReport, type RuleOwner, type QuarantinedRuleAdd, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, type PermissionRuleWriter, type WritablePermissionRuleStore, type RuleWriteDelta, type RuleAddDelta, type RuleDeleteDelta, type RuleSyncJoinDelta, type RawRuleSyncState, type RedemptionAuthorization, } from "./core/permission-rule-store.js";
|
|
171
171
|
export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, type PermissionRuleSyncTransport, type PermissionRuleSyncResult, type RuleSyncRequestBody, type RuleSyncResponseBody, } from "./core/permission-rule-sync.js";
|
|
172
|
+
export { InMemorySessionRuleOverlay, type SessionRuleOverlay, type SessionRuleOverlayAdd, type SessionRuleOverlayApplyResult, } from "./core/permission-rule-session.js";
|
|
172
173
|
export { createOrgRuleOverlay, orgRuleVerdictFor, effectivePermissionRules, orgRuleStatePersistenceOf, ORG_UNAVAILABLE_DECISION_REASON, ORG_RULE_DECISION_REASON, ORG_ADJUDICATION_TIMEOUT_MS, type OrgPermissionRule, type OrgRuleSnapshot, type OrgRuleSnapshotProvider, type OrgRuleStatePersistence, type PersistedOrgRuleState, type OrgRuleOverlay, type OrgOverlayResolution, type OrgOverlayStatus, type EffectivePermissionRule, } from "./core/permission-rule-org.js";
|
|
173
174
|
export { RULE_SYNC_DROP_CODES, type RuleSyncDropReason, type RuleQuarantineReason } from "./core/governance-codes.js";
|
|
174
175
|
export { prepareCardApproval, confirmRuleApproval, type ConfirmResult, type ConfirmRefusalReason, precheckEditedRuleText, type EditedRuleTextPrecheck, redeemRuleTicket, redeemRuleBatch, prepareCcImport, prepareStarterBatch, mintRuleTicket, STARTER_RULES, InMemoryRuleApprovalRecordStore, ruleOffersOfRecord, type RuleTicket, type RuleCandidate, type RuleApprovalKind, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleOffer2, type StaleRuleApprovalRecord, type RuleConsentDeps, type RedeemResult, type RedeemedBatchMember, type CcImportLayer, type ImportedSettingsLayer, type ImportPreview, } from "./core/permission-rule-consent.js";
|
package/dist/index.js
CHANGED
|
@@ -127,8 +127,9 @@ export { AUTO_MODE_ARMING_RECIPE_VERSION, autoModeArmingRecipeOf, sanitizeAutoMo
|
|
|
127
127
|
export { rebuildAutoModeDecider, } from "./core/auto-mode-rebuild.js";
|
|
128
128
|
export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, isNamespacedCoveringRuleName, namespacedRuleNameCovers, } from "./core/permission-rules.js";
|
|
129
129
|
export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, renderUntrustedCommandText, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, } from "./core/permission-rule-model.js";
|
|
130
|
-
export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, } from "./core/permission-rule-store.js";
|
|
130
|
+
export { removePersistedRule, applyTombstones, sameScope, isValidConsentScope, isValidDurableScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, } from "./core/permission-rule-store.js";
|
|
131
131
|
export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, } from "./core/permission-rule-sync.js";
|
|
132
|
+
export { InMemorySessionRuleOverlay, } from "./core/permission-rule-session.js";
|
|
132
133
|
export { createOrgRuleOverlay, orgRuleVerdictFor, effectivePermissionRules, orgRuleStatePersistenceOf, ORG_UNAVAILABLE_DECISION_REASON, ORG_RULE_DECISION_REASON, ORG_ADJUDICATION_TIMEOUT_MS, } from "./core/permission-rule-org.js";
|
|
133
134
|
export { RULE_SYNC_DROP_CODES } from "./core/governance-codes.js";
|
|
134
135
|
export { prepareCardApproval, confirmRuleApproval, precheckEditedRuleText, redeemRuleTicket, redeemRuleBatch, prepareCcImport, prepareStarterBatch, mintRuleTicket, STARTER_RULES, InMemoryRuleApprovalRecordStore, ruleOffersOfRecord, } from "./core/permission-rule-consent.js";
|