UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

220 lines (219 loc) • 15.9 kB
import assert from "node:assert"; import { collectBelowFrontier, joinRuleStates, ruleSyncVector } from "../permission-rule-store.js"; import { beginContract } from "./contract-harness.js"; const GLOBAL = { kind: "global" }; function add(actor, counter, origin = "user") { return { dot: { actor, counter }, origin, createdAt: "2026-01-01T00:00:00.000Z" }; } function rule(text, adds, scope = GLOBAL, behavior = "allow") { const m = /^Bash\((.+?)(:\*)?\)$/.exec(text); if (m === null || m[1] === undefined) throw new Error(`contract fixture rule "${text}" is not a Bash(...) form`); return { behavior, rule: text, tool: "Bash", match: m[2] !== undefined ? "prefix" : "exact", command: m[1], scope, adds }; } function tomb(text, removed, deletedBy, scope = GLOBAL, behavior = "allow") { return { behavior, rule: text, scope, removedDots: removed.map(([actor, counter]) => ({ actor, counter })), deletedBy: { actor: deletedBy[0], counter: deletedBy[1] }, }; } function state(rules = [], tombstones = []) { return { rules, tombstones }; } function liveDots(s) { const out = new Set(); for (const r of s.rules) { const removed = s.tombstones.filter((t) => t.behavior === r.behavior && t.rule === r.rule && sameScope(t.scope, r.scope)).flatMap((t) => t.removedDots); for (const a of r.adds) { if (!removed.some((d) => d.actor === a.dot.actor && d.counter === a.dot.counter)) out.add(`${r.behavior === "allow" ? "" : r.behavior + ":"}${r.rule}|${a.dot.actor}#${a.dot.counter}`); } } return out; } 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 allDots(s) { return new Set(s.rules.flatMap((r) => r.adds.map((a) => `${r.behavior === "allow" ? "" : r.behavior + ":"}${r.rule}|${a.dot.actor}#${a.dot.counter}`))); } export async function permissionRuleSyncContract(hooks = {}) { const join = hooks.join ?? joinRuleStates; const collect = hooks.collect ?? collectBelowFrontier; const vector = hooks.vector ?? ruleSyncVector; const { run, settle } = beginContract(hooks.runAssertion); const samples = () => [ state(), state([rule("Bash(ls)", [add("a", 1)])]), state([rule("Bash(ls)", [add("b", 1)]), rule("Bash(git status)", [add("b", 2)])]), state([rule("Bash(ls)", [add("a", 1)])], [tomb("Bash(ls)", [["a", 1]], ["a", 2])]), state([rule("Bash(pwd)", [add("c", 5)], { kind: "project", root: "/repo" })], [tomb("Bash(date)", [["d", 9]], ["c", 6])]), ]; run("join is idempotent: a ⊔ a = a (M11)", async () => { for (const s of samples()) assert.deepStrictEqual(join(s, s), join(s, join(s, s)), "a ⊔ a must equal (a ⊔ a) ⊔ a"); for (const s of samples()) assert.deepStrictEqual(liveDots(join(s, s)), liveDots(s), "a ⊔ a must not change the live view"); }); run("join is commutative: a ⊔ b = b ⊔ a", async () => { const pool = samples(); for (const a of pool) for (const b of pool) assert.deepStrictEqual(join(a, b), join(b, a)); }); run("join is associative and order/repetition independent (M12)", async () => { const [, a, b, c] = samples(); assert.ok(a && b && c); const one = join(join(a, b), c); const two = join(a, join(b, c)); const three = join(join(c, a), join(b, a)); assert.deepStrictEqual(one, two); assert.deepStrictEqual(one, three); }); run("M1: a single-sided add propagates", async () => { const merged = join(state([rule("Bash(ls)", [add("a", 1)])]), state()); assert.ok(liveDots(merged).has("Bash(ls)|a#1")); }); run("M2: a multi-dot delete covers every observed dot", async () => { const merged = join(state([rule("Bash(ls)", [add("a", 1), add("a", 2)])], [tomb("Bash(ls)", [["a", 1], ["a", 2]], ["a", 3])]), state()); assert.strictEqual(liveDots(merged).size, 0, "both observed dots must be dead"); assert.strictEqual(merged.tombstones.length, 1); assert.strictEqual(merged.tombstones[0]?.removedDots.length, 2, "the tombstone must carry BOTH observed dots"); }); run("M3: concurrent approvals of the same text coexist under their own dots", async () => { const merged = join(state([rule("Bash(ls)", [add("a", 1)])]), state([rule("Bash(ls)", [add("b", 1)])])); assert.deepStrictEqual(liveDots(merged), new Set(["Bash(ls)|a#1", "Bash(ls)|b#1"])); assert.strictEqual(merged.rules.length, 1, "one logical rule, two adds — not two rule rows"); }); run("M4: add-wins — an add the deleter never observed survives the tombstone", async () => { const merged = join(state([rule("Bash(ls)", [add("a", 1)])], [tomb("Bash(ls)", [["a", 1]], ["a", 2])]), state([rule("Bash(ls)", [add("b", 7)])])); assert.deepStrictEqual(liveDots(merged), new Set(["Bash(ls)|b#7"]), "dot a#1 dead, unobserved b#7 alive"); }); run("M5: deletes on both sides union to cover everything", async () => { const merged = join(state([rule("Bash(ls)", [add("a", 1)])], [tomb("Bash(ls)", [["a", 1]], ["a", 2])]), state([rule("Bash(ls)", [add("b", 7)])], [tomb("Bash(ls)", [["b", 7]], ["b", 8])])); assert.strictEqual(liveDots(merged).size, 0); }); run("M6: a deliberate re-add under a NEW dot survives the old tombstone", async () => { const merged = join(state([rule("Bash(ls)", [add("a", 1), add("a", 3)])], [tomb("Bash(ls)", [["a", 1]], ["a", 2])]), state()); assert.deepStrictEqual(liveDots(merged), new Set(["Bash(ls)|a#3"])); }); run("M7: a double delete is idempotent — distinct deletedBy tombstones both stand, semantics equal", async () => { const a = state([rule("Bash(ls)", [add("a", 1)])], [tomb("Bash(ls)", [["a", 1]], ["a", 2])]); const b = state([rule("Bash(ls)", [add("a", 1)])], [tomb("Bash(ls)", [["a", 1]], ["b", 5])]); const merged = join(a, b); assert.strictEqual(merged.tombstones.length, 2, "two delete intents, two identities — neither is dropped"); assert.strictEqual(liveDots(merged).size, 0); assert.deepStrictEqual(join(a, b), join(b, a)); }); run("M8: partially-observed deletes union to full coverage — each tombstone honest about its own view", async () => { const merged = join(state([rule("Bash(ls)", [add("a", 1), add("a", 2)])], [tomb("Bash(ls)", [["a", 1], ["a", 2]], ["a", 3])]), state([rule("Bash(ls)", [add("a", 1)])], [tomb("Bash(ls)", [["a", 1]], ["b", 1])])); assert.strictEqual(liveDots(merged).size, 0, "the union of observations must cover both dots"); }); run("M9: provenance rides each add and follows the surviving dot", async () => { const merged = join(state([rule("Bash(ls)", [add("a", 1, "imported-cc")])], [tomb("Bash(ls)", [["a", 1]], ["a", 2])]), state([rule("Bash(ls)", [add("b", 7, "user")])])); const survivor = merged.rules.find((r) => r.rule === "Bash(ls)"); assert.ok(survivor); const liveAdds = survivor.adds.filter((x) => !merged.tombstones.some((t) => t.removedDots.some((d) => d.actor === x.dot.actor && d.counter === x.dot.counter))); assert.strictEqual(liveAdds.length, 1); assert.strictEqual(liveAdds[0]?.origin, "user", "the display origin must be the SURVIVING add's own, not a folded one"); }); run("M10: replaying the same dot is deduplicated by identity", async () => { const merged = join(state([rule("Bash(ls)", [add("a", 1)])]), state([rule("Bash(ls)", [add("a", 1)])])); assert.strictEqual(merged.rules[0]?.adds.length, 1); }); run("M13: a tombstone whose (rule, scope) is locally absent is kept — arriving before its add is legal", async () => { const merged = join(state(), state([], [tomb("Bash(ls)", [["a", 1]], ["a", 2])])); assert.strictEqual(merged.tombstones.length, 1); const then = join(merged, state([rule("Bash(ls)", [add("a", 1), add("a", 4)])])); assert.deepStrictEqual(liveDots(then), new Set(["Bash(ls)|a#4"]), "the pre-arrived tombstone must cover a#1 when it lands"); }); run("tombstone-union (r1-F3): same identity, differing removedDots ⇒ the union, both orders", async () => { const a = state([], [tomb("Bash(ls)", [["a", 1]], ["a", 9])]); const b = state([], [tomb("Bash(ls)", [["a", 2]], ["a", 9])]); const ab = join(a, b); assert.strictEqual(ab.tombstones.length, 1, "same (rule, scope, deletedBy) is ONE identity"); assert.deepStrictEqual(new Set(ab.tombstones[0]?.removedDots.map((d) => `${d.actor}#${d.counter}`)), new Set(["a#1", "a#2"]), "the union — a one-sided pick can drop an observed removal"); assert.deepStrictEqual(ab, join(b, a)); }); run("behavior is an identity member: the same text as an allow and as a deny are two rows, each with its own adds and its own tombstone", async () => { const both = state([rule("Bash(ls)", [add("a", 1)]), rule("Bash(ls)", [add("a", 2)], GLOBAL, "deny")], [tomb("Bash(ls)", [["a", 1]], ["a", 3])]); const merged = join(both, state()); assert.strictEqual(merged.rules.length, 2, "one text, two behaviors — two identity groups, never folded"); assert.deepStrictEqual(liveDots(merged), new Set(["deny:Bash(ls)|a#2"]), "the allow tombstone must not reach the deny's add"); assert.deepStrictEqual(join(state(), both), merged, "commutative across the behavior axis"); const badWord = state([rule("Bash(pwd)", [add("b", 1)])], [Object.assign(tomb("Bash(pwd)", [["b", 1]], ["b", 2]), { behavior: "maybe" })]); const screened = join(badWord, state()); assert.strictEqual(screened.tombstones.length, 0, "an unreadable behavior word on a tombstone must not land"); assert.ok(liveDots(screened).has("Bash(pwd)|b#1"), "…and must not cover anything"); }); run("closed-set identity: a row carrying a behavior word outside the closed set is refused like invalid text", async () => { const forgedWord = Object.assign(rule("Bash(ls)", [add("z", 1)]), { behavior: "sometimes" }); const merged = join(state([forgedWord]), state()); assert.strictEqual(allDots(merged).size, 0, "a behavior this engine cannot read must not become an allow (or a deny) by falling through"); }); run("metadata re-projection (r2-F2): a record whose stored fields contradict its rule text is refused, both orders", async () => { const forged = { behavior: "allow", rule: "Bash(ls)", tool: "Bash", match: "prefix", command: "rm -rf /", scope: GLOBAL, adds: [add("z", 1)] }; const clean = state([rule("Bash(git status)", [add("a", 1)])]); const ab = join(state([forged]), clean); const ba = join(clean, state([forged])); assert.ok(!allDots(ab).has("Bash(ls)|z#1"), "the forged record must not survive the join"); assert.deepStrictEqual(ab, ba, "refusal must preserve commutativity"); assert.ok(allDots(ab).has("Bash(git status)|a#1"), "refusing one record must not disturb the rest"); }); run("single-validator door: an illegal ALLOW shape (bare-interpreter prefix) cannot ride a join — while the same text as a DENY is a legitimate wide tightening and lands", async () => { const merged = join(state([rule("Bash(node:*)", [add("z", 1)])]), state()); assert.strictEqual(allDots(merged).size, 0, "Bash(node:*) must be refused by the shared validator inside the join"); const denied = join(state([rule("Bash(node:*)", [add("z", 1)], GLOBAL, "deny")]), state()); assert.deepStrictEqual(allDots(denied), new Set(["deny:Bash(node:*)|z#1"]), "the tightening floor admits the interpreter-headed prefix"); }); 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 () => { const sessionScope = { kind: "session", sessionId: "sess-1" }; 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)]); const ab = join(dirty, state()); const ba = join(state(), dirty); assert.ok(!allDots(ab).has("Bash(ls)|z#1"), "a session-scope add must not survive the join — the durable face holds two members"); assert.strictEqual(ab.tombstones.length, 0, "a session-scope tombstone must not survive the join"); 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"); assert.deepStrictEqual(ab, ba, "the session-scope drop must preserve commutativity"); }); 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 () => { const scopeA = { kind: "project", root: "/x" }; const scopeB = { kind: "project", root: "/x a" }; const s = state([rule("Bash(ls)", [add("v", 1)], scopeA), rule("Bash(ls)", [add("w", 1)], scopeB)], [ { behavior: "allow", rule: "Bash(ls)", scope: scopeA, removedDots: [{ actor: "v", counter: 1 }], deletedBy: { actor: "a 1", counter: 2 } }, { behavior: "allow", rule: "Bash(ls)", scope: scopeB, removedDots: [{ actor: "w", counter: 1 }], deletedBy: { actor: "1", counter: 2 } }, ]); const merged = join(s, state()); assert.strictEqual(merged.tombstones.length, 2, "two distinct delete intents must both survive — a colliding key folds one away"); assert.strictEqual(liveDots(merged).size, 0, "both covered adds must stay dead"); assert.deepStrictEqual(join(merged, merged), merged, "idempotence must hold with space-carrying roots/actors"); }); run("state vector: max over add dots ∪ deletedBy ∪ removedDots (§5.1)", async () => { const v = vector(state([rule("Bash(ls)", [add("a", 3)])], [tomb("Bash(git status)", [["a", 7], ["b", 2]], ["c", 4])])); assert.deepStrictEqual(v, { a: 7, b: 2, c: 4 }); }); run("paired collection: a fully-below tombstone leaves WITH its covered adds; a partial one stays (§5.1)", async () => { const s = state([rule("Bash(ls)", [add("a", 1)]), rule("Bash(pwd)", [add("a", 2), add("b", 9)])], [tomb("Bash(ls)", [["a", 1]], ["a", 3]), tomb("Bash(pwd)", [["a", 2], ["b", 9]], ["a", 4])]); const { state: out, collectedTombstones } = collect(s, { a: 5 }); assert.strictEqual(collectedTombstones, 1, "only the fully-below pair is collectable (b#9 is above the frontier)"); assert.ok(!allDots(out).has("Bash(ls)|a#1"), "the covered add leaves WITH its tombstone — never one without the other"); assert.ok(allDots(out).has("Bash(pwd)|b#9"), "an above-frontier dot must never be collected"); assert.strictEqual(out.tombstones.length, 1, "the partially-below tombstone stays"); assert.deepStrictEqual(liveDots(out), liveDots(s)); }); run("collection + rejoin does not resurrect: the recycled pair stays gone through further joins", async () => { const s = state([rule("Bash(ls)", [add("a", 1)])], [tomb("Bash(ls)", [["a", 1]], ["a", 2])]); const { state: collected } = collect(s, { a: 2 }); const rejoined = join(collected, collected); assert.strictEqual(liveDots(rejoined).size, 0); assert.strictEqual(allDots(rejoined).size, 0, "neither the add nor the tombstone may reappear from a self-join"); }); await settle(); }