@sema-agent/core
Version:
Stateless, task-oriented AI agent core
91 lines (90 loc) • 6.68 kB
JavaScript
import assert from "node:assert";
import { PERMISSION_RULE_WRITER, isRuleRevision } from "../permission-rule-store.js";
import { beginContract } from "./contract-harness.js";
const GLOBAL = { kind: "global" };
function add(actor, counter) {
return { dot: { actor, counter }, origin: "user", createdAt: "2026-01-01T00:00:00.000Z" };
}
const IDENTITY = { behavior: "deny", rule: "Bash(curl:*)", scope: GLOBAL, tool: "Bash", match: "prefix", command: "curl" };
function tombstoneFor(removed, deletedBy) {
return { behavior: IDENTITY.behavior, rule: IDENTITY.rule, scope: GLOBAL, removedDots: removed.map((a) => a.dot), deletedBy };
}
function liveDots(rules) {
return new Set(rules.flatMap((r) => r.adds.map((a) => `${r.behavior}|${r.rule}|${a.dot.actor}#${a.dot.counter}`)));
}
async function rawShape(part) {
const raw = await part[PERMISSION_RULE_WRITER].readRaw();
const dots = [...liveDots(raw.rules)].sort();
const tombs = raw.tombstones.map((t) => `${t.behavior}|${t.rule}|${t.deletedBy.actor}#${t.deletedBy.counter}`).sort();
return JSON.stringify({ rev: raw.rev, dots, tombs });
}
function assertDecline(outcome, what) {
const answer = (outcome ?? {});
assert.equal(answer.conflict, true, `${what} must answer \`conflict: true\` — a decline tagged with anything else is an answer the engine reads as unreadable, so a kit accepting it certifies a backend the engine cannot use`);
assert.equal(answer.refused, undefined, `${what} must not wear the refusal tag as well — an answer that is two arms is an answer about neither`);
}
function assertRevision(outcome, what) {
const rev = outcome?.rev;
assert.ok(isRuleRevision(rev), `${what} must carry a revision — a non-negative safe integer — and this answer carried ${rev === undefined ? "none at all" : `a ${typeof rev}`}`);
}
export async function permissionRuleWriteContract(hooks) {
const { run, settle } = beginContract(hooks.runAssertion);
run("the named arms land, a landing ADVANCES the revision, and a spent revision is ANSWERED (positive control)", async () => {
const part = await hooks.make();
const writer = part[PERMISSION_RULE_WRITER];
const a1 = add("kit", 1);
const held = (await part.list()).rev;
const first = await writer.apply({ kind: "tighten-add", ...IDENTITY, add: a1 }, { expectedRev: held });
assert.ok(!("conflict" in first) && !("refused" in first), "a well-formed tighten-add must land");
assertRevision(first, "the tighten-add landing");
const afterAdd = await part.list();
assert.deepEqual(liveDots(afterAdd.rules), new Set([`deny|${IDENTITY.rule}|kit#1`]), "the added dot must be visible through list()");
assert.ok(afterAdd.rev > held, "a landed write must ADVANCE the revision — a constant one is not a fence, it is a number");
assert.equal((await part[PERMISSION_RULE_WRITER].readRaw()).rev, afterAdd.rev, "every face must report ONE revision");
const rawBefore = await rawShape(part);
const stale = await writer.apply({ kind: "tighten-add", ...IDENTITY, add: add("kit", 2) }, { expectedRev: held });
assertDecline(stale, "a spent expectedRev");
assertRevision(stale, "the declining conflict");
const staleDelete = await writer.apply({ kind: "tighten-delete", tombstone: tombstoneFor([a1], { actor: "kit", counter: 8 }) }, { expectedRev: held });
assertDecline(staleDelete, "a spent expectedRev on the delete arm");
assertRevision(staleDelete, "the declining conflict on the delete arm");
assert.equal(await rawShape(part), rawBefore, "a declined write must move nothing — rows, tombstones or revision");
const beforeDelete = (await part.list()).rev;
const removed = await writer.apply({ kind: "tighten-delete", tombstone: tombstoneFor([a1], { actor: "kit", counter: 9 }) }, { expectedRev: beforeDelete });
assert.ok(!("conflict" in removed) && !("refused" in removed), "a well-formed tighten-delete must land");
assertRevision(removed, "the tighten-delete landing");
assert.deepEqual(liveDots((await part.list()).rules), new Set(), "the tombstoned dot must leave the live view");
assert.ok((await part.list()).rev > beforeDelete, "a landed delete must advance the revision too");
const raw = await part[PERMISSION_RULE_WRITER].readRaw();
assert.ok(liveDots(raw.rules).has(`deny|${IDENTITY.rule}|kit#1`), "the raw face must RETAIN a tombstoned add (list() filters; the evidence may not be filtered too)");
const tomb = raw.tombstones.find((t) => t.deletedBy.actor === "kit" && t.deletedBy.counter === 9);
assert.ok(tomb !== undefined, "the raw face must retain the tombstone a landed delete wrote");
assert.deepEqual({ behavior: tomb.behavior, rule: tomb.rule, scope: tomb.scope, removedDots: tomb.removedDots }, { behavior: IDENTITY.behavior, rule: IDENTITY.rule, scope: GLOBAL, removedDots: [a1.dot] }, "the retained tombstone must be the WHOLE one — a partial one identifies nothing");
});
run("a delta whose kind is outside the union is REFUSED — not folded as the arm it resembles", async () => {
for (const [label, payload] of [
["① a legal tombstone under an unnamed kind", { kind: "tighten-purge", tombstone: tombstoneFor([add("kit", 1)], { actor: "kit", counter: 9 }) }],
["② a legal add payload under an unnamed kind", { kind: "import-add", ...IDENTITY, add: add("kit", 2) }],
]) {
const part = await hooks.make();
const writer = part[PERMISSION_RULE_WRITER];
const seeded = add("kit", 7);
await writer.apply({ kind: "tighten-add", ...IDENTITY, add: seeded }, { expectedRev: (await part.list()).rev });
const before = await part.list();
const rawShapeBefore = await rawShape(part);
let threw = false;
try {
await writer.apply(payload, { expectedRev: before.rev });
}
catch {
threw = true;
}
assert.equal(threw, true, `${label}: the write face must refuse an unnamed delta kind`);
const after = await part.list();
assert.deepEqual(liveDots(after.rules), liveDots(before.rules), `${label}: a refused delta must not change the rows`);
assert.equal(after.rev, before.rev, `${label}: a refused delta must not advance the revision`);
assert.equal(await rawShape(part), rawShapeBefore, `${label}: a refused delta must move nothing in the RAW state either`);
}
});
await settle();
}