UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

851 lines (850 loc) • 42 kB
import { randomBytes } from "node:crypto"; import { canonicalize } from "./canonical-json.js"; import { escapeForDisclosure, isRuleBehavior, isRuleLive, parseRuleText } from "./permission-rule-model.js"; import { thrownText } from "./thrown-value.js"; export function sameRuleOwner(a, b) { return a.kind === "principal" ? b.kind === "principal" && a.principal === b.principal : b.kind === "local-owner"; } export function isRuleRevision(rev) { return typeof rev === "number" && Number.isSafeInteger(rev) && rev >= 0; } function outcomeWord(value) { if (value === undefined) return "no revision at all"; if (value === null) return "a null revision"; if (typeof value === "number") return `the number ${String(value)}, which is not a revision`; return `a ${typeof value} where a revision belongs`; } function isReadableDrop(drop) { if (drop === null || typeof drop !== "object") return false; const dot = drop.dot; if (dot === null || typeof dot !== "object") return false; return typeof dot.actor === "string" && typeof dot.counter === "number" && typeof drop.reason === "string"; } function readSyncReport(sync) { if (sync === undefined) return {}; if (sync === null || typeof sync !== "object") return { unreadable: `the landing carried a ${sync === null ? "null" : typeof sync} where its sync report belongs` }; for (const member of ["droppedInbound", "quarantined"]) { const value = sync[member]; if (value === undefined) continue; if (!Array.isArray(value)) return { unreadable: `the landing's sync report carried a ${typeof value} as \`${member}\`, which a reader cannot walk` }; const bad = value.findIndex((row) => !isReadableDrop(row)); if (bad !== -1) return { unreadable: `the landing's sync report carried a \`${member}\` entry (#${String(bad)}) that is not a drop record, so what was dropped cannot be read` }; } return { report: sync }; } export function readRuleWriteOutcome(res) { if (res === null || typeof res !== "object") { return { kind: "unreadable", why: `the store answered ${res === null ? "null" : `a ${typeof res}`} where a write outcome belongs` }; } const tagged = res; const refusal = tagged.refused; const conflict = tagged.conflict; if (refusal !== undefined && conflict !== undefined) { return { kind: "unreadable", why: "the store answered a refusal and a decline at once, and an answer that is two arms is not an answer about either" }; } if (refusal !== undefined) { return typeof refusal === "string" && refusal !== "" ? { kind: "refused", refused: refusal } : { kind: "unreadable", why: `the store tagged its answer as a refusal and named it with a ${refusal === null ? "null" : typeof refusal}, so which refusal it is cannot be read` }; } if (conflict !== undefined) { if (conflict !== true) return { kind: "unreadable", why: `the store tagged its answer as a decline with \`${String(conflict)}\`, which does not say a compare-and-set lost` }; return isRuleRevision(tagged.rev) ? { kind: "conflict", rev: tagged.rev } : { kind: "unreadable", why: `the store declined the write with ${outcomeWord(tagged.rev)}, so the revision to re-anchor the next attempt to is unknown` }; } if (!isRuleRevision(tagged.rev)) { return { kind: "unreadable", why: `the store answered neither a conflict nor a refusal, and carried ${outcomeWord(tagged.rev)} — a landing reports the revision it committed at, so this answer cannot say whether the write landed` }; } const { report, unreadable } = readSyncReport(tagged.sync); return { kind: "landed", rev: tagged.rev, ...(report !== undefined ? { sync: report } : {}), ...(unreadable !== undefined ? { reportUnreadable: unreadable } : {}) }; } export function unknownRuleWriteDeltaError(delta) { const kind = delta?.kind; return new Error(`the permission-rule store cannot apply a write delta of kind ${escapeForDisclosure(typeof kind === "string" ? kind : String(kind))} — the write face folds exactly four named arms (redemption-add, tighten-add, tighten-delete, sync-join), and an unnamed one is refused rather than read as any of them`); } export const PERMISSION_RULE_WRITER = "__semaPermissionRuleWriter"; export function writerOf(store) { const w = store[PERMISSION_RULE_WRITER]; return w !== undefined && typeof w.apply === "function" && typeof w.nextDot === "function" ? w : undefined; } export function sameScope(a, b) { if (a.kind === "global") return b.kind === "global"; if (a.kind === "session") return b.kind === "session" && a.sessionId === b.sessionId; return b.kind === "project" && a.root === b.root; } function sameDot(a, b) { return a.actor === b.actor && a.counter === b.counter; } export function sameRuleIdentity(a, b) { return a.behavior === b.behavior && a.rule === b.rule && sameScope(a.scope, b.scope); } export function applyTombstones(rules, tombstones) { return rules .map((r) => { const removed = tombstones.filter((t) => sameRuleIdentity(t, r)).flatMap((t) => t.removedDots); return removed.length === 0 ? r : { ...r, adds: r.adds.filter((a) => !removed.some((d) => sameDot(d, a.dot))) }; }) .filter(isRuleLive); } export function foldDelta(rules, delta) { const next = rules.map((r) => ({ ...r, adds: [...r.adds] })); const existing = next.find((r) => sameRuleIdentity(r, delta)); if (existing === undefined) { next.push({ behavior: delta.behavior, rule: delta.rule, tool: delta.tool, match: delta.match, command: delta.command, scope: delta.scope, adds: [delta.add] }); return next; } if (!existing.adds.some((a) => sameDot(a.dot, delta.add.dot))) existing.adds.push(delta.add); return next; } export function addDotsOf(rules) { return rules.flatMap((r) => r.adds.map((a) => a.dot)); } const ADD_ONLY_KEYS = ["add", "redemption", "rule", "command", "match", "tool", "behavior"]; export function assertDeleteDeltaCarriesNoAdd(delta) { const smuggled = ADD_ONLY_KEYS.filter((k) => Object.hasOwn(delta, k)); if (smuggled.length > 0) { throw new Error(`a tighten-delete may not introduce a new add (it carried: ${smuggled.join(", ")})`); } if (!Array.isArray(delta.tombstone?.removedDots) || delta.tombstone.removedDots.length === 0) { throw new Error("a tighten-delete must carry a tombstone naming at least one observed add dot"); } } export function assertWriteDeltaScopeDurable(delta) { const scope = delta.kind === "tighten-delete" ? delta.tombstone?.scope : delta.scope; if (scope?.kind === "session") { const e = new Error(delta.kind === "tighten-delete" ? "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" : `a session-scope rule cannot enter the persisted store — its home is the session's own overlay (design/382 §4.3); refusing the ${delta.kind}`); e.code = "unsupported.session_scope_store"; throw e; } if (!isValidDurableScope(scope)) { throw new Error(delta.kind === "tighten-delete" ? "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" : `a ${delta.kind} 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`); } } export function isValidConsentScope(scope) { const sc = scope; return (sc?.kind === "global" || (sc?.kind === "project" && typeof sc.root === "string" && sc.root !== "") || (sc?.kind === "session" && typeof sc.sessionId === "string" && sc.sessionId !== "")); } export function isValidDurableScope(scope) { const sc = scope; return sc?.kind === "global" || (sc?.kind === "project" && typeof sc.root === "string" && sc.root !== ""); } function isValidDot(dot) { const d = dot; return typeof d?.actor === "string" && d.actor !== "" && typeof d.counter === "number" && Number.isFinite(d.counter); } function scopeKey(scope) { return JSON.stringify(scope.kind === "global" ? ["g"] : scope.kind === "session" ? ["s", scope.sessionId] : ["p", scope.root]); } function dotKey(dot) { return JSON.stringify([dot.actor, dot.counter]); } function ruleIdentityKey(behavior, rule, scope) { return JSON.stringify([behavior, rule, scopeKey(scope)]); } function compareDots(a, b) { return a.actor < b.actor ? -1 : a.actor > b.actor ? 1 : a.counter - b.counter; } export function dotAtOrBelowFrontier(dot, frontier) { const cap = frontier[dot.actor]; return typeof cap === "number" && Number.isFinite(cap) && dot.counter <= cap; } export function ruleSyncVector(state) { const out = {}; const feed = (dot) => { const cur = out[dot.actor]; if (cur === undefined || dot.counter > cur) out[dot.actor] = dot.counter; }; for (const r of state.rules) for (const a of r.adds) feed(a.dot); for (const t of state.tombstones) { feed(t.deletedBy); for (const d of t.removedDots) feed(d); } return out; } export function joinFrontiers(a, b) { const out = { ...a }; for (const [actor, counter] of Object.entries(b)) { const cur = out[actor]; if (cur === undefined || counter > cur) out[actor] = counter; } return out; } export function normalizePersistedRule(r) { if (!isRuleBehavior(r.behavior)) return { reject: "invalid_rule_text" }; const parsed = parseRuleText(r.rule, r.behavior); if ("reject" in parsed) return { reject: "invalid_rule_text" }; const p = parsed.rule; if (p.rule !== r.rule) return { reject: "invalid_rule_text" }; if (r.tool !== p.tool || r.match !== p.match || r.command !== p.command) return { reject: "metadata_mismatch" }; return { rule: r }; } export function screenRuleSyncState(state) { const rejected = []; const rules = []; for (const r of state.rules) { if (!Array.isArray(r.adds) || r.adds.length === 0) continue; if (r.scope?.kind === "session") { for (const a of r.adds) rejected.push({ behavior: r.behavior, rule: r.rule, scope: r.scope, dot: a.dot, reason: "session_scope_not_durable" }); continue; } const n = normalizePersistedRule(r); if ("reject" in n) { for (const a of r.adds) rejected.push({ behavior: r.behavior, rule: r.rule, scope: r.scope, dot: a.dot, reason: n.reject }); continue; } rules.push(r); } const tombstones = []; for (const t of state.tombstones) { if (t?.scope?.kind === "session") { rejected.push({ behavior: t.behavior, rule: t.rule, scope: t.scope, dot: t.deletedBy, reason: "session_scope_not_durable" }); continue; } if (!isRuleBehavior(t.behavior)) { rejected.push({ behavior: t.behavior, rule: t.rule, scope: t.scope, dot: t.deletedBy, reason: "invalid_rule_text" }); continue; } tombstones.push(t); } return { state: { rules, tombstones }, rejected }; } export function joinRuleStates(a, b) { const sa = screenRuleSyncState(a).state; const sb = screenRuleSyncState(b).state; const ruleGroups = new Map(); for (const r of [...sa.rules, ...sb.rules]) { const key = ruleIdentityKey(r.behavior, r.rule, r.scope); const existing = ruleGroups.get(key); if (existing === undefined) { ruleGroups.set(key, { ...r, adds: [...r.adds] }); continue; } for (const add of r.adds) { const i = existing.adds.findIndex((x) => sameDot(x.dot, add.dot)); if (i === -1) existing.adds.push({ ...add }); else if (canonicalize(add) < canonicalize(existing.adds[i])) existing.adds[i] = { ...add }; } } const rules = [...ruleGroups.values()] .map((r) => ({ ...r, adds: [...r.adds].sort((x, y) => compareDots(x.dot, y.dot)) })) .sort((x, y) => compareIdentity(x, y)); const tombGroups = new Map(); for (const t of [...sa.tombstones, ...sb.tombstones]) { if (!Array.isArray(t.removedDots) || t.removedDots.length === 0) continue; const key = JSON.stringify([t.behavior, t.rule, scopeKey(t.scope), dotKey(t.deletedBy)]); const existing = tombGroups.get(key); if (existing === undefined) { tombGroups.set(key, { ...t, removedDots: [...t.removedDots] }); continue; } for (const d of t.removedDots) if (!existing.removedDots.some((x) => sameDot(x, d))) existing.removedDots.push(d); } const tombstones = [...tombGroups.values()] .map((t) => ({ ...t, removedDots: [...t.removedDots].sort(compareDots) })) .sort((x, y) => compareIdentity(x, y) || compareDots(x.deletedBy, y.deletedBy)); return { rules, tombstones }; } function compareIdentity(x, y) { if (x.rule !== y.rule) return x.rule < y.rule ? -1 : 1; const sx = scopeKey(x.scope); const sy = scopeKey(y.scope); if (sx !== sy) return sx < sy ? -1 : 1; return x.behavior === y.behavior ? 0 : x.behavior < y.behavior ? -1 : 1; } export function collectBelowFrontier(state, frontier) { const collectable = state.tombstones.filter((t) => dotAtOrBelowFrontier(t.deletedBy, frontier) && t.removedDots.every((d) => dotAtOrBelowFrontier(d, frontier))); if (collectable.length === 0) return { state, collectedTombstones: 0 }; const keep = state.tombstones.filter((t) => !collectable.includes(t)); const rules = state.rules .map((r) => { const covered = collectable.filter((t) => sameRuleIdentity(t, r)).flatMap((t) => t.removedDots); return covered.length === 0 ? r : { ...r, adds: r.adds.filter((a) => !covered.some((d) => sameDot(d, a.dot))) }; }) .filter((r) => r.adds.length > 0); return { state: { rules, tombstones: keep }, collectedTombstones: collectable.length }; } export function applySyncJoin(cur, delta, nowMs) { if (delta.kind !== "sync-join" || delta.inbound === null || typeof delta.inbound !== "object" || !Array.isArray(delta.inbound.rules) || !Array.isArray(delta.inbound.tombstones)) { throw new Error("a sync-join delta must carry an inbound state with rules and tombstones arrays"); } for (const t of delta.inbound.tombstones) { if (t?.scope?.kind === "session") continue; if (!Array.isArray(t?.removedDots) || t.removedDots.length === 0) { 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)"); } if (!isValidDurableScope(t.scope) || !isValidDot(t.deletedBy) || !t.removedDots.every(isValidDot)) { 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)"); } if (!isRuleBehavior(t.behavior)) { throw new Error("an inbound tombstone carries a behavior word outside deny/ask/allow — refusing the whole sync-join (a tombstone that cannot identity-match what it deletes would land its covered adds live)"); } const canonicalTomb = parseRuleText(t.rule, t.behavior); if ("reject" in canonicalTomb || canonicalTomb.rule.rule !== t.rule) { throw new Error(`an inbound tombstone names a non-canonical or invalid rule text ("${escapeForDisclosure(t.rule)}") — refusing the whole sync-join (the client's partitioning must withhold the round's adds alongside a malformed tombstone)`); } } const droppedInbound = []; const quarantinedNow = []; const screened = screenRuleSyncState(delta.inbound); droppedInbound.push(...screened.rejected); const forgedDot = (dot) => dot.actor === cur.actor && dot.counter > cur.counter; const dotIdentity = new Map(); for (const r of cur.rules) for (const a of r.adds) dotIdentity.set(dotKey(a.dot), ruleIdentityKey(r.behavior, r.rule, r.scope)); for (const q of cur.quarantined) dotIdentity.set(dotKey(q.add.dot), ruleIdentityKey(q.behavior, q.rule, q.scope)); const inboundClaims = new Map(); for (const r of screened.state.rules) { const identity = ruleIdentityKey(r.behavior, r.rule, r.scope); for (const a of r.adds) { const claims = inboundClaims.get(dotKey(a.dot)) ?? new Set(); claims.add(identity); inboundClaims.set(dotKey(a.dot), claims); } } const inboundTombstones = screened.state.tombstones.filter((t) => { const forged = forgedDot(t.deletedBy) ? t.deletedBy : t.removedDots.find(forgedDot); if (forged === undefined) return true; droppedInbound.push({ behavior: t.behavior, rule: t.rule, scope: t.scope, dot: forged, reason: "own_actor_forged" }); return false; }); const tombstoneRejected = inboundTombstones.length !== screened.state.tombstones.length; let withheldAdds = 0; const inboundRules = []; for (const r of screened.state.rules) { if (tombstoneRejected) { withheldAdds += r.adds.length; continue; } if (!isValidDurableScope(r.scope)) { for (const a of r.adds) droppedInbound.push({ behavior: r.behavior, rule: r.rule, scope: r.scope, dot: a.dot, reason: "metadata_mismatch" }); continue; } const identity = ruleIdentityKey(r.behavior, r.rule, r.scope); const kept = r.adds.filter((a) => { if (!isValidDot(a.dot)) { droppedInbound.push({ behavior: r.behavior, rule: r.rule, scope: r.scope, dot: a.dot, reason: "metadata_mismatch" }); return false; } if (forgedDot(a.dot)) { droppedInbound.push({ behavior: r.behavior, rule: r.rule, scope: r.scope, dot: a.dot, reason: "own_actor_forged" }); return false; } const known = dotIdentity.get(dotKey(a.dot)); if ((known !== undefined && known !== identity) || (inboundClaims.get(dotKey(a.dot))?.size ?? 0) > 1) { droppedInbound.push({ behavior: r.behavior, rule: r.rule, scope: r.scope, dot: a.dot, reason: "dot_identity_conflict" }); return false; } return true; }); if (kept.length > 0) inboundRules.push({ ...r, adds: kept }); } const inbound = { rules: inboundRules, tombstones: inboundTombstones }; const localScreen = screenRuleSyncState({ rules: cur.rules, tombstones: cur.tombstones }); const quarantined = [...cur.quarantined]; const quarantineKey = (behavior, rule, scope, dot) => JSON.stringify([behavior, rule, scopeKey(scope), dotKey(dot)]); const pushQuarantine = (behavior, rule, scope, add, reason) => { if (quarantined.some((q) => sameRuleIdentity(q, { behavior, rule, scope }) && sameDot(q.add.dot, add.dot))) return; quarantined.push({ behavior, rule, scope, add, reason, atMs: nowMs }); quarantinedNow.push({ behavior, rule, scope, dot: add.dot, reason }); }; for (const rej of localScreen.rejected) { const row = cur.rules.find((r) => sameRuleIdentity(r, rej)); const add = row?.adds.find((a) => sameDot(a.dot, rej.dot)); if (add !== undefined && (rej.reason === "invalid_rule_text" || rej.reason === "metadata_mismatch")) { pushQuarantine(rej.behavior, rej.rule, rej.scope, add, rej.reason); } } let joined = joinRuleStates(localScreen.state, inbound); let collectedTombstones = 0; if (delta.gcFrontier !== undefined) { const collected = collectBelowFrontier(joined, delta.gcFrontier); joined = collected.state; collectedTombstones = collected.collectedTombstones; const inboundDots = new Set(inbound.rules.flatMap((r) => r.adds.map((a) => dotKey(a.dot)))); const live = applyTombstones(joined.rules, joined.tombstones); for (const r of live) { for (const a of r.adds) { if (dotAtOrBelowFrontier(a.dot, delta.gcFrontier) && !inboundDots.has(dotKey(a.dot))) { pushQuarantine(r.behavior, r.rule, r.scope, a, "below_gc_frontier"); } } } } for (const q of delta.quarantine ?? []) { if (q.scope?.kind === "session") { 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"); e.code = "unsupported.session_scope_store"; throw e; } for (const dot of q.dots) { if (joined.tombstones.some((t) => sameDot(t.deletedBy, dot))) { throw new Error("a quarantine instruction may not touch a tombstone — it moves adds only"); } const row = joined.rules.find((r) => sameRuleIdentity(r, q)); const add = row?.adds.find((a) => sameDot(a.dot, dot)); if (add === undefined) continue; pushQuarantine(q.behavior, q.rule, q.scope, add, q.reason); } } const quarantinedRows = new Set(quarantined.map((q) => quarantineKey(q.behavior, q.rule, q.scope, q.add.dot))); const rules = joined.rules .map((r) => ({ ...r, adds: r.adds.filter((a) => !quarantinedRows.has(quarantineKey(r.behavior, r.rule, r.scope, a.dot))) })) .filter((r) => r.adds.length > 0); return { next: { actor: cur.actor, counter: cur.counter, rules, tombstones: joined.tombstones, quarantined, observedVector: delta.observedVector !== undefined ? joinFrontiers(cur.observedVector ?? {}, delta.observedVector) : cur.observedVector, }, report: structuredClone({ droppedInbound, quarantined: quarantinedNow, collectedTombstones, withheldAdds }), }; } export function assertRedemptionNotQuarantined(quarantined, delta) { const hit = quarantined.find((q) => sameDot(q.add.dot, delta.add.dot)); if (hit !== undefined) { throw new Error(`this approval's add (dot ${delta.add.dot.actor}#${delta.add.dot.counter}) was quarantined (${hit.reason}) — replaying the old record cannot revive it; a new consent (a new ask, a new record, a new dot) is required`); } } export async function ruleStoreChecksum(payload) { const { createHash } = await import("node:crypto"); return `sha256:${createHash("sha256").update(canonicalize(payload), "utf8").digest("hex")}`; } export const REMOVAL_LIVENESS_WORDS = Object.freeze(["yes", "no", "unknown"]); export const RULE_WRITE_COMMITTED_WORDS = Object.freeze(["no", "unknown"]); function resolveRuleWriter(store) { let w; try { w = writerOf(store); } catch (err) { return { unavailable: `the permission-rule store's write face could not be acquired (${errText(err)})`, committed: "unknown" }; } return w === undefined ? { unavailable: "the resolved permission-rule store has no write face", committed: "no" } : { writer: w }; } function ruleIdentityStands(rules, identity) { return rules.some((r) => sameRuleIdentity(r, identity) && isRuleLive(r)); } async function witnessRuleAdd(store, writer, delta) { const identity = { behavior: delta.behavior, rule: delta.rule, scope: delta.scope }; const dot = delta.add.dot; try { if (delta.scope.kind === "session") { const view = await store.effective({ sessionId: delta.scope.sessionId }); if (view.unreadable !== undefined) return { unreadable: view.unreadable }; return { seen: view.rules.some((r) => sameRuleIdentity(r, identity) && r.adds.some((a) => sameDot(a.dot, dot))), rev: view.rev }; } const raw = await writer.readRaw(); const inRows = raw.rules.some((r) => sameRuleIdentity(r, identity) && r.adds.some((a) => sameDot(a.dot, dot))); const inQuarantine = (raw.quarantined ?? []).some((q) => sameDot(q.add.dot, dot)); return { seen: inRows || inQuarantine, rev: raw.rev }; } catch (err) { return { unreadable: errText(err) }; } } async function readRuleLiveness(store, identity) { try { const after = await store.effective(); if (after.unreadable !== undefined) throw new Error(after.unreadable); return ruleIdentityStands(after.rules, identity) ? "yes" : "no"; } catch { return "unknown"; } } const REMOVE_MAX_ATTEMPTS = 8; export async function removePersistedRule(opts) { const behavior = opts.behavior; const rule = opts.rule; const scope = structuredClone(opts.scope); const target = opts.principal; if (!isRuleBehavior(behavior)) { return { status: "failed", error: `removePersistedRule: behavior ${escapeForDisclosure(behavior)} is not one of ${["deny", "ask", "allow"].join("/")} — refusing to name a rule identity nothing can carry`, committed: "no" }; } const identity = { behavior, rule, scope }; const owner = typeof target === "string" ? { kind: "principal", principal: target } : structuredClone(target); const provider = opts.provider; if (scope?.kind === "session") { return { status: "failed", 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)", committed: "no", }; } if (owner.kind === "local-owner" && provider.forLocalOwner === undefined) { return { status: "failed", error: "this provider has no local-owner bucket (forLocalOwner is not implemented) — a local-owner rule cannot be removed through it", committed: "no" }; } const store = owner.kind === "local-owner" ? provider.forLocalOwner() : provider.forPrincipal(owner.principal); const face = resolveRuleWriter(store); if ("unavailable" in face) { return { status: "failed", error: `${face.unavailable} — rules cannot be removed through it`, committed: face.committed }; } const writer = face.writer; let snapshot; try { snapshot = await store.effective(); } catch (err) { return { status: "failed", error: `could not read the permission-rule store: ${errText(err)}`, committed: "no" }; } if (snapshot.unreadable !== undefined) return { status: "failed", error: `could not read the permission-rule store: ${snapshot.unreadable}`, committed: "no" }; const live = snapshot.rules.filter((r) => sameRuleIdentity(r, identity)); const removedDots = live.flatMap((r) => r.adds.map((a) => a.dot)); if (removedDots.length === 0) return { status: "no-op", rev: snapshot.rev, stillLive: await readRuleLiveness(store, identity) }; let deletedBy; try { deletedBy = await writer.nextDot(); } catch (err) { return { status: "failed", error: `could not mint the removal's delete dot: ${errText(err)}`, committed: "no" }; } const tombstone = { behavior, rule, scope, removedDots, deletedBy }; let expectedRev = snapshot.rev; let lastError = ""; let settlement = "no"; for (let attempt = 0; attempt < REMOVE_MAX_ATTEMPTS; attempt++) { try { const reading = readRuleWriteOutcome(await writer.apply({ kind: "tighten-delete", tombstone }, { expectedRev })); if (reading.kind === "refused") { return { status: "failed", error: `the store refused the tombstone (${reading.refused}) — nothing was removed`, committed: "no" }; } if (reading.kind === "unreadable") { settlement = "unknown"; lastError = `the permission-rule store answered the write unreadably: ${reading.why}`; break; } if (reading.kind === "landed") { return { status: "removed", rev: reading.rev, stillLive: await readRuleLiveness(store, identity) }; } expectedRev = reading.rev; } catch (err) { settlement = "unknown"; lastError = errText(err); break; } } const reason = settlement === "unknown" ? lastError !== "" ? lastError : "the permission-rule store threw while applying the tombstone, with no message" : `optimistic-concurrency retries exhausted after ${REMOVE_MAX_ATTEMPTS} attempts`; const committed = settlement; try { const after = await writer.readRaw(); const landed = after.tombstones.some((t) => t.deletedBy.actor === tombstone.deletedBy.actor && t.deletedBy.counter === tombstone.deletedBy.counter); if (landed) { return { status: "removed", rev: after.rev, stillLive: ruleIdentityStands(applyTombstones(after.rules, after.tombstones), identity) ? "yes" : "no" }; } return { status: "failed", error: settlement === "unknown" ? `${reason}; the store retains no tombstone for this removal, which cannot tell a write that never landed from one whose evidence was collected — the outcome is indeterminate, reconcile with list()` : `${reason} (read-back confirms no tombstone was written)`, committed, }; } catch (err) { return { status: "failed", error: settlement === "unknown" ? `${reason}; the read-back also failed (${errText(err)}) so this outcome is indeterminate — reconcile with list()` : `${reason}; the read-back also failed (${errText(err)}), but every attempt was answered by the store, so nothing was written`, committed, }; } } const RULE_ADD_MAX_ATTEMPTS = 8; const SESSION_ENDED_REFUSAL = "the session this authorization targets has ended — a session rule dies with its session, and a late or replayed redemption cannot revive it (re-trigger the command in a live session for a fresh card)"; export async function landRuleAdd(args) { switch (args.delta.kind) { case "redemption-add": case "tighten-add": break; default: { const unreachable = args.delta; throw new Error(`landRuleAdd: unhandled add delta ${JSON.stringify(unreachable)}`); } } for (let attempt = 0; attempt < RULE_ADD_MAX_ATTEMPTS; attempt++) { let rev; try { const snapshot = await args.store.effective(); if (snapshot.unreadable !== undefined) throw new Error(snapshot.unreadable); rev = snapshot.rev; const refusal = await args.preflight?.(snapshot); if (refusal !== undefined) return { landed: false, reason: refusal, committed: "no" }; } catch (err) { return { landed: false, reason: `could not read the permission-rule store: ${errText(err)}`, committed: "no" }; } let res; try { res = await args.writer.apply(args.delta, { expectedRev: rev }); } catch (err) { return { landed: false, reason: `the permission-rule store refused the write: ${errText(err)}`, committed: "unknown" }; } const reading = readRuleWriteOutcome(res); if (reading.kind === "refused") return { landed: false, reason: SESSION_ENDED_REFUSAL, committed: "no" }; if (reading.kind === "unreadable") return { landed: false, reason: `the permission-rule store answered the write unreadably: ${reading.why}`, committed: "unknown" }; if (reading.kind === "landed") { const witness = await witnessRuleAdd(args.store, args.writer, args.delta); if ("unreadable" in witness) { return { landed: false, reason: `the permission-rule store reported a landing at revision ${String(reading.rev)}, and the read-back that would have witnessed it could not be taken (${witness.unreadable})`, committed: "unknown" }; } if (!witness.seen) { return { landed: false, reason: `the permission-rule store reported a landing at revision ${String(reading.rev)}, and a read-back of this add's own dot finds no trace of it — a revision says the ANSWER was well formed, not that a row exists, so nothing here can say the write landed`, committed: "unknown" }; } return { landed: true, rev: reading.rev }; } } return { landed: false, reason: `optimistic-concurrency retries exhausted after ${RULE_ADD_MAX_ATTEMPTS} attempts`, committed: "no" }; } export async function addPersistedRule(opts) { const behavior = opts.behavior; const ruleText = opts.rule; const scope = structuredClone(opts.scope); const target = opts.principal; if (!isRuleBehavior(behavior)) { return { status: "failed", error: `addPersistedRule: behavior ${escapeForDisclosure(behavior)} is not one of ${["deny", "ask", "allow"].join("/")} — refusing to name a rule identity nothing can carry`, committed: "no" }; } if (behavior === "allow") { return { status: "failed", error: "addPersistedRule: an allow cannot be written through this entry — a standing approval is a person's yes and takes a consent record (prepare → confirm → redeem); this entry writes the tightening direction (deny/ask) only", committed: "no", }; } if (!isValidConsentScope(scope)) { return { status: "failed", error: `addPersistedRule: ${escapeForDisclosure(JSON.stringify(scope))} is not a valid rule scope — refusing to write a row nothing can read back`, committed: "no" }; } const parsed = parseRuleText(ruleText, behavior); if ("reject" in parsed) return { status: "failed", error: `addPersistedRule: ${parsed.reject.code}: ${parsed.reject.message}`, committed: "no" }; const identity = { behavior, rule: parsed.rule.rule, scope }; const owner = typeof target === "string" ? { kind: "principal", principal: target } : structuredClone(target); const provider = opts.provider; if (owner.kind === "local-owner" && provider.forLocalOwner === undefined) { return { status: "failed", error: "this provider has no local-owner bucket (forLocalOwner is not implemented) — a local-owner rule cannot be added through it", committed: "no" }; } const store = owner.kind === "local-owner" ? provider.forLocalOwner() : provider.forPrincipal(owner.principal); const face = resolveRuleWriter(store); if ("unavailable" in face) { return { status: "failed", error: `${face.unavailable} — rules cannot be added through it`, committed: face.committed }; } const writer = face.writer; const query = scope.kind === "session" ? { sessionId: scope.sessionId } : undefined; let snapshot; try { snapshot = await store.effective(query); } catch (err) { return { status: "failed", error: `could not read the permission-rule store: ${errText(err)}`, committed: "no" }; } if (snapshot.unreadable !== undefined) return { status: "failed", error: `could not read the permission-rule store: ${snapshot.unreadable}`, committed: "no" }; if (snapshot.rules.some((r) => sameRuleIdentity(r, identity) && isRuleLive(r))) return { status: "no-op", rev: snapshot.rev }; let dot; try { dot = await writer.nextDot(); } catch (err) { return { status: "failed", error: `could not mint the add's dot: ${errText(err)}`, committed: "no" }; } const landing = await landRuleAdd({ store, writer, delta: { kind: "tighten-add", behavior, rule: parsed.rule.rule, scope, tool: parsed.rule.tool, match: parsed.rule.match, command: parsed.rule.command, add: { dot, origin: "user", createdAt: new Date().toISOString() }, }, }); if (landing.landed) return { status: "added", rev: landing.rev, dot }; if (landing.committed === "no") return { status: "failed", error: landing.reason, committed: landing.committed }; try { let rev; let rows; let quarantinedRows = []; if (scope.kind === "session") { const view = await store.effective(query); if (view.unreadable !== undefined) throw new Error(view.unreadable); ({ rev, rules: rows } = view); } else { const raw = await writer.readRaw(); ({ rev, rules: rows } = raw); quarantinedRows = raw.quarantined ?? []; } const stood = rows.some((r) => sameRuleIdentity(r, identity) && r.adds.some((a) => sameDot(a.dot, dot))) || quarantinedRows.some((q) => sameDot(q.add.dot, dot)); return stood ? { status: "added", rev, dot } : { status: "failed", error: `${landing.reason}; the store retains no trace of this add, which cannot tell a write that never landed from one whose evidence was collected — the outcome is indeterminate, reconcile with list()`, committed: landing.committed }; } catch (err) { return { status: "failed", error: `${landing.reason}; the read-back also failed (${errText(err)}) so this outcome is indeterminate — reconcile with list()`, committed: landing.committed }; } } export function errText(err) { return thrownText(err); } export const EMPTY_DURABLE_RULE_PARTITION = { list: async () => ({ rules: [], tombstones: [], rev: 0 }), durability: "process-local", }; export class InMemoryDurableRulePartition { actor; now; durability = "process-local"; fidelity = "structured-clone"; rules = []; tombstones = []; quarantinedRows = []; observedVector; rev = 0; counter = 0; constructor(actor = `mem-${randomBytes(6).toString("base64url")}`, now = Date.now) { this.actor = actor; this.now = now; } async list() { return { rules: structuredClone(applyTombstones(this.rules, this.tombstones)), tombstones: structuredClone(this.tombstones), rev: this.rev, }; } async quarantined() { return structuredClone(this.quarantinedRows); } [PERMISSION_RULE_WRITER] = { nextDot: async () => ({ actor: this.actor, counter: ++this.counter }), readRaw: async () => structuredClone({ actor: this.actor, counter: this.counter, rev: this.rev, rules: this.rules, tombstones: this.tombstones, ...(this.observedVector !== undefined ? { observedVector: this.observedVector } : {}), quarantined: this.quarantinedRows, }), apply: async (delta, opts) => { delta = structuredClone(delta); if (opts.expectedRev !== this.rev) return { conflict: true, rev: this.rev }; switch (delta.kind) { case "redemption-add": { assertWriteDeltaScopeDurable(delta); assertRedemptionNotQuarantined(this.quarantinedRows, delta); this.rules = foldDelta(this.rules, delta); break; } case "tighten-add": { assertWriteDeltaScopeDurable(delta); this.rules = foldDelta(this.rules, delta); break; } case "tighten-delete": { assertWriteDeltaScopeDurable(delta); assertDeleteDeltaCarriesNoAdd(delta); this.tombstones = [...this.tombstones, delta.tombstone]; break; } case "sync-join": { const { next, report } = applySyncJoin({ actor: this.actor, counter: this.counter, rules: this.rules, tombstones: this.tombstones, quarantined: this.quarantinedRows, ...(this.observedVector !== undefined ? { observedVector: this.observedVector } : {}), }, delta, this.now()); this.rules = next.rules; this.tombstones = next.tombstones; this.quarantinedRows = next.quarantined; this.observedVector = next.observedVector; this.rev += 1; return { rev: this.rev, sync: report }; } default: { const unhandled = delta; throw unknownRuleWriteDeltaError(unhandled); } } this.rev += 1; return { rev: this.rev }; }, }; }