UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

585 lines (584 loc) • 26.3 kB
import { closeSync, constants as FS, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { createHash, randomBytes } from "node:crypto"; import { isRuleBehavior } from "../../core/permission-rule-model.js"; import { posixModeBitsMeaningful } from "../../core/posix-mode-face.js"; import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, unknownRuleWriteDeltaError, } from "../../core/permission-rule-store.js"; import { sameOrgPolicyContent } from "../../core/permission-rule-org.js"; import { canonicalize } from "../../core/canonical-json.js"; import { BootLock, FileStoreLockError } from "./fs-atomic.js"; import { assertAdoptionBootGate } from "./adoption/marker.js"; import { thrownText } from "../../core/thrown-value.js"; function isRuleFile(v) { const p = v; return typeof p === "object" && p !== null && p.schemaVersion === 4 && Array.isArray(p.rules) && Array.isArray(p.tombstones) && typeof p.rev === "number" && (p.quarantined === undefined || Array.isArray(p.quarantined)); } function isOrgStateFile(v) { const p = v; return typeof p === "object" && p !== null && p.schemaVersion === 1 && typeof p.state === "object" && p.state !== null; } function schemaVersionWord(v) { return typeof v === "object" && v !== null ? v.schemaVersion : undefined; } function jsonForm(v) { return JSON.parse(JSON.stringify(v)); } export const ORG_STATE_FILE = "org.json"; export class OrgRevisionRefusal extends Error { } const LOCAL_OWNER_FILE = "local-owner.json"; const ADOPTION_FILE = "PERMISSION-RULES-ADOPTION.json"; function judgeRuleAdoptionFile(dir) { let raw; try { raw = readFileSync(join(dir, ADOPTION_FILE), "utf8"); } catch (err) { if (err?.code === "ENOENT") return { marker: undefined }; return { corrupt: `could not be read (${err?.code ?? err?.name ?? "unknown"})`, detail: thrownText(err) }; } let parsed; try { parsed = JSON.parse(raw); } catch (err) { return { corrupt: `is not valid JSON (${err.message})` }; } return isRuleAdoptionFileShape(parsed) ? { marker: parsed } : { corrupt: "does not carry a readable shape" }; } export function readRuleAdoptionFile(dir) { const judged = judgeRuleAdoptionFile(dir); if ("corrupt" in judged) { throw new Error(`the permission-rule adoption marker in ${dir} ${judged.corrupt}${judged.detail !== undefined ? `: ${judged.detail}` : ""} — refusing to resolve owners until it is repaired`); } return judged.marker; } function isRuleOwnerShape(v) { if (v === null || typeof v !== "object") return false; const owner = v; return owner.kind === "local-owner" || (owner.kind === "principal" && typeof owner.principal === "string"); } function isRuleAdoptionFileShape(v) { if (v === null || typeof v !== "object" || v.schemaVersion !== 1) return false; const hasMarker = "marker" in v; const hasAdopted = "adopted" in v; if (hasMarker === hasAdopted) return false; const rec = (hasMarker ? v.marker : v.adopted); if (rec === null || typeof rec !== "object") return false; if (!isRuleOwnerShape(rec.from) || typeof rec.toPrincipal !== "string" || rec.toPrincipal === "") return false; return hasMarker ? rec.phase === 2 || rec.phase === 3 || rec.phase === 4 || rec.phase === 5 : typeof rec.atMs === "number" && Number.isFinite(rec.atMs); } export function writeRuleAdoptionFile(dir, content) { assertSafeDir(dir); atomicPublish(dir, join(dir, ADOPTION_FILE), JSON.stringify(content, null, 2)); } function atomicPublish(dir, file, payload) { const tmp = join(dir, `.${randomBytes(8).toString("hex")}.tmp`); let fd; try { fd = openSync(tmp, FS.O_WRONLY | FS.O_CREAT | FS.O_EXCL | (FS.O_NOFOLLOW ?? 0), 0o600); writeFileSync(fd, payload); fsyncSync(fd); closeSync(fd); fd = undefined; renameSync(tmp, file); let dfd; try { dfd = openSync(dir, "r"); fsyncSync(dfd); } catch { } finally { if (dfd !== undefined) closeSync(dfd); } } catch (err) { if (fd !== undefined) { try { closeSync(fd); } catch { } } try { unlinkSync(tmp); } catch { } throw err; } } const EMPTY_READ = { rules: [], tombstones: [], rev: 0 }; function checksumOf(body) { return `sha256:${createHash("sha256").update(canonicalize(body), "utf8").digest("hex")}`; } function unsafeDirFault(dir, vetting) { mkdirSync(dir, { recursive: true, mode: 0o700 }); const st = lstatSync(dir); if (st.isSymbolicLink() || !st.isDirectory()) return "is a symlink or not a directory"; if (typeof process.getuid === "function" && st.uid !== process.getuid()) return "is owned by another user"; if (!posixModeBitsMeaningful(vetting?.platform)) { vetting?.modeBitsUnjudged?.(dir); return undefined; } if ((st.mode & 0o022) !== 0) return `is writable by group or others (mode ${(st.mode & 0o777).toString(8)}) — anyone who can write there can grant themselves permissions`; return undefined; } function assertSafeDir(dir) { const fault = unsafeDirFault(dir); if (fault !== undefined) throw new Error(`permission-rule store directory ${dir} ${fault}; refusing to use it`); } function refusalText(onError, sentence, location) { discloseTo(onError, `${sentence} (at ${location})`); return `${sentence}. The location is reported to the store's error disclosure (onError), not in this message.`; } function discloseTo(onError, message) { try { onError?.(message); } catch { } } function assertSafeDirAtRequest(dir, onError, act, vetting) { const fault = unsafeDirFault(dir, vetting); if (fault !== undefined) throw new Error(refusalText(onError, `the permission-rule store directory ${fault}; refusing to ${act} it`, dir)); } function writerLockRefusal(err, onError) { let sentence; switch (err.code) { case "store.dir_in_use": sentence = `another live process${err.ownerPid !== undefined ? ` (pid ${err.ownerPid})` : ""} holds the permission-rule store's writer lock — refusing to interleave writes with it; write through that process, or stop it`; break; case "store.dir_claiming": sentence = "another process is reclaiming the permission-rule store's writer lock from a crashed owner — retry"; break; case "store.lock_unreadable": sentence = "the permission-rule store's writer lock is in a state only a person should resolve — remove it by hand if no process holds it"; break; default: { const unhandled = err.code; sentence = `the permission-rule store's writer lock refused (${String(unhandled)})`; } } return new FileStoreLockError(err.code, refusalText(onError, sentence, `${err.lockPath}: ${err.message}`), err.lockPath, err.ownerPid); } class FileDurableRulePartition { dir; file; acquireWriteLock; onError; vetting; durability = "durable"; fidelity = "json"; constructor(dir, file, acquireWriteLock, onError, vetting) { this.dir = dir; this.file = file; this.acquireWriteLock = acquireWriteLock; this.onError = onError; this.vetting = vetting; } read() { const bucket = "this owner's permission-rule bucket"; const whole = "refusing the whole file and loading zero rules — repair the file, or remove it (removing it gives up every rule and tombstone it held)"; const unreadable = (sentence, location = this.file) => ({ unreadable: refusalText(this.onError, sentence, location) }); let raw; try { const dirFault = unsafeDirFault(this.dir, this.vetting); if (dirFault !== undefined) return unreadable(`the permission-rule store directory ${dirFault}; loading zero rules`, this.dir); const st = lstatSync(this.file); if (st.isSymbolicLink() || !st.isFile()) return unreadable(`${bucket} is a symlink or not a regular file; refusing to load any rule from it`); raw = readFileSync(this.file, "utf8"); } catch (err) { const e = err; if (e?.code === "ENOENT") return { absent: true }; return unreadable(`${bucket} could not be read (${e?.code ?? err?.name ?? "unknown"}); loading zero rules`, `${this.file}: ${thrownText(err)}`); } let parsed; try { parsed = JSON.parse(raw); } catch (err) { return unreadable(`${bucket} is not valid JSON (${err.message}); ${whole}`); } if (!isRuleFile(parsed)) { const sv = schemaVersionWord(parsed); return unreadable(typeof sv === "number" ? `${bucket} is a schema ${String(sv)} bucket; this engine reads schema 4 only and does not migrate an older engine's bucket — refusing the whole file and loading zero rules (the bytes are left for a person to look at)` : `${bucket} does not carry a readable rule-file shape; ${whole}`); } const { checksum, ...body } = parsed; if (checksum !== checksumOf(body)) return unreadable(`${bucket} failed its integrity check; ${whole}`); const rowKinds = [ ["rule", parsed.rules], ["tombstone", parsed.tombstones], ["quarantined", parsed.quarantined ?? []], ]; for (const [kind, rows] of rowKinds) { const bad = rows.findIndex((r) => !isRuleBehavior(r?.behavior)); if (bad !== -1) return unreadable(`${bucket} carries a ${kind} row (#${bad}) whose behavior word is outside deny/ask/allow — a schema 4 row must name its behavior; ${whole}`); } return { file: parsed }; } disclose(message) { discloseTo(this.onError, message); } screenDurableRows(file) { const isSession = (scope) => scope?.kind === "session"; const rules = file.rules.filter((r) => !isSession(r?.scope)); const tombstones = file.tombstones.filter((t) => !isSession(t?.scope)); const quarantined = (file.quarantined ?? []).filter((q) => !isSession(q?.scope)); const dropped = file.rules.length - rules.length + (file.tombstones.length - tombstones.length) + ((file.quarantined ?? []).length - quarantined.length); if (dropped > 0) { this.disclose(`${this.file} carries ${dropped} session-scope row(s) — a session authorization cannot live in the durable store (design/382 §4.3); those rows are ignored on every read face and never ride sync`); } return { rules, tombstones, quarantined }; } write(next) { assertSafeDirAtRequest(this.dir, this.onError, "write through", this.vetting); atomicPublish(this.dir, this.file, JSON.stringify({ ...next, checksum: checksumOf(next) }, null, 2)); } async list() { const r = this.read(); if ("unreadable" in r) throw new Error(r.unreadable); if (!("file" in r)) return { ...EMPTY_READ }; const screened = this.screenDurableRows(r.file); return { rules: applyTombstones(screened.rules, screened.tombstones), tombstones: screened.tombstones, rev: r.file.rev, checksum: r.file.checksum, }; } async quarantined() { const r = this.read(); if ("unreadable" in r) throw new Error(r.unreadable); return "file" in r ? this.screenDurableRows(r.file).quarantined : []; } current() { const r = this.read(); if ("unreadable" in r) { throw new Error(`refusing to write the permission-rule store while it cannot be read: ${r.unreadable}`); } if ("absent" in r) { return { schemaVersion: 4, actor: `file-${randomBytes(6).toString("hex")}`, counter: 0, rev: 0, rules: [], tombstones: [] }; } return { schemaVersion: r.file.schemaVersion, actor: r.file.actor, counter: r.file.counter, rev: r.file.rev, rules: r.file.rules, tombstones: r.file.tombstones, ...(r.file.sync !== undefined ? { sync: r.file.sync } : {}), ...(r.file.quarantined !== undefined ? { quarantined: r.file.quarantined } : {}), }; } writeAndVerify(next) { const expected = checksumOf(next); this.write(next); const after = this.read(); return "file" in after && after.file.checksum === expected; } writeChain = Promise.resolve(); serialize(fn) { const run = this.writeChain.then(fn, fn); this.writeChain = run.then(() => undefined, () => undefined); return run; } get [PERMISSION_RULE_WRITER]() { this.acquireWriteLock(); return this.writer; } writer = { nextDot: async () => this.serialize(() => { const cur = this.current(); const dot = { actor: cur.actor, counter: cur.counter + 1 }; if (!this.writeAndVerify({ ...cur, counter: dot.counter })) { throw new Error("the permission-rule store did not survive its own write; refusing to hand out an identity"); } return dot; }), readRaw: async () => this.serialize(() => { const cur = this.current(); const screened = this.screenDurableRows(cur); return { actor: cur.actor, counter: cur.counter, rev: cur.rev, rules: structuredClone(screened.rules), tombstones: structuredClone(screened.tombstones), ...(cur.sync?.observedVector !== undefined ? { observedVector: structuredClone(cur.sync.observedVector) } : {}), quarantined: structuredClone(screened.quarantined), }; }), apply: async (delta, opts) => this.serialize(() => { delta = structuredClone(delta); const cur = this.current(); if (cur.rev !== opts.expectedRev) return { conflict: true, rev: cur.rev }; let next; switch (delta.kind) { case "sync-join": { const { next: joined, report } = applySyncJoin({ actor: cur.actor, counter: cur.counter, rules: cur.rules, tombstones: cur.tombstones, quarantined: cur.quarantined ?? [], ...(cur.sync?.observedVector !== undefined ? { observedVector: cur.sync.observedVector } : {}), }, delta, Date.now()); const landed = { ...cur, rev: cur.rev + 1, rules: joined.rules, tombstones: joined.tombstones, sync: { ...(joined.observedVector !== undefined ? { observedVector: joined.observedVector } : {}), lastRoundAtMs: Date.now(), }, ...(joined.quarantined.length > 0 ? { quarantined: joined.quarantined } : {}), }; if (!this.writeAndVerify(landed)) { throw new Error("the permission-rule store did not read back as the version just written; whether the change reached the bytes on disk is unobservable from here — reconcile by reading the store"); } return { rev: cur.rev + 1, sync: report }; } case "redemption-add": { assertWriteDeltaScopeDurable(delta); assertRedemptionNotQuarantined(cur.quarantined ?? [], delta); next = { ...cur, rev: cur.rev + 1, rules: foldDelta(cur.rules, delta) }; break; } case "tighten-add": { assertWriteDeltaScopeDurable(delta); next = { ...cur, rev: cur.rev + 1, rules: foldDelta(cur.rules, delta) }; break; } case "tighten-delete": { assertWriteDeltaScopeDurable(delta); assertDeleteDeltaCarriesNoAdd(delta); next = { ...cur, rev: cur.rev + 1, tombstones: [...cur.tombstones, delta.tombstone] }; break; } default: { const unhandled = delta; throw unknownRuleWriteDeltaError(unhandled); } } if (!this.writeAndVerify(next)) { throw new Error("the permission-rule store did not read back as the version just written; whether the change reached the bytes on disk is unobservable from here — reconcile by reading the store"); } return { rev: cur.rev + 1 }; }), }; } export class FileDurableRulePartitionProvider { dir; onError; lock; orgPersistence; vetting; constructor(dir, onError, opts) { this.dir = dir; this.onError = onError; if (opts?.dataRoot !== undefined) assertAdoptionBootGate(opts.dataRoot, "FileDurableRulePartitionProvider"); let said = false; this.vetting = { platform: opts?.platform, modeBitsUnjudged: (where) => { if (said) return; said = true; discloseTo(this.onError, `the permission-rule store directory is not checked for being writable by other accounts: this platform reports no POSIX permission bits, so who else can write there is not verified here (at ${where})`); }, }; } adoptionMarker() { const judged = judgeRuleAdoptionFile(this.dir); if ("corrupt" in judged) { const location = `${join(this.dir, ADOPTION_FILE)}${judged.detail !== undefined ? `: ${judged.detail}` : ""}`; throw new Error(refusalText(this.onError, `the permission-rule store's adoption marker ${judged.corrupt}; refusing to resolve whose bucket this is until it is repaired`, location)); } return judged.marker; } orgStatePersistence() { return this.fileOrgPersistence(); } fileOrgPersistence() { this.orgPersistence ??= new FileOrgRuleStatePersistence(this.dir, this.acquireWriteLock, this.onError, this.vetting); return this.orgPersistence; } acquireWriteLock = () => { if (this.lock !== undefined) return; assertSafeDirAtRequest(this.dir, this.onError, "write through", this.vetting); const lock = new BootLock(join(this.dir, "PERMISSION-RULES-WRITER-LOCK")); try { lock.acquire(); } catch (err) { throw err instanceof FileStoreLockError ? writerLockRefusal(err, this.onError) : err; } this.lock = lock; }; forPrincipal(principal) { if (typeof principal !== "string" || principal === "") { return { list: async () => ({ ...EMPTY_READ }), durability: "process-local" }; } const adoption = this.adoptionMarker(); if (adoption !== undefined) { const rec = "adopted" in adoption ? adoption.adopted : adoption.marker; if (rec.from.kind === "principal" && rec.from.principal === principal) { return this.readOnlySourceView(principalRuleFileName(principal), principalRuleFileName(rec.toPrincipal), "adopted" in adoption || adoption.marker.phase >= 4); } } return new FileDurableRulePartition(this.dir, join(this.dir, principalRuleFileName(principal)), this.acquireWriteLock, this.onError, this.vetting); } readOnlySourceView(sourceName, targetName, flipped) { const pick = flipped ? targetName : existsSync(join(this.dir, sourceName)) ? sourceName : targetName; return readOnlyRuleStoreView(new FileDurableRulePartition(this.dir, join(this.dir, pick), this.acquireWriteLock, this.onError, this.vetting)); } forLocalOwner() { const adoption = this.adoptionMarker(); if (adoption !== undefined) { const rec = "adopted" in adoption ? adoption.adopted : adoption.marker; if (rec.from.kind === "local-owner") { return this.readOnlySourceView(LOCAL_OWNER_FILE, principalRuleFileName(rec.toPrincipal), "adopted" in adoption || adoption.marker.phase >= 4); } } return new FileDurableRulePartition(this.dir, join(this.dir, LOCAL_OWNER_FILE), this.acquireWriteLock, this.onError, this.vetting); } dispose() { this.lock?.release(); this.lock = undefined; } } class FileOrgRuleStatePersistence { dir; acquireWriteLock; onError; vetting; file; writeChain = Promise.resolve(); constructor(dir, acquireWriteLock, onError, vetting) { this.dir = dir; this.acquireWriteLock = acquireWriteLock; this.onError = onError; this.vetting = vetting; this.file = join(dir, ORG_STATE_FILE); } readSync() { const refuse = (sentence, location = this.file) => { throw new Error(refusalText(this.onError, `${sentence}; refusing it whole — repair the file, or remove it (removing it gives up the last-known-good org snapshot and its anti-rollback mark)`, location)); }; const ioFault = (err) => `could not be read (${err?.code ?? err?.name ?? "unknown"})`; let dirFault; try { dirFault = unsafeDirFault(this.dir, this.vetting); } catch (err) { return refuse(`the permission-rule store directory ${ioFault(err)}`, `${this.dir}: ${thrownText(err)}`); } if (dirFault !== undefined) return refuse(`the permission-rule store directory ${dirFault}`, this.dir); const unreadable = (err) => { if (err?.code === "ENOENT") return undefined; return refuse(`the org state file ${ioFault(err)}`, `${this.file}: ${thrownText(err)}`); }; let st; try { st = lstatSync(this.file); } catch (err) { return unreadable(err); } if (st.isSymbolicLink() || !st.isFile()) return refuse("the org state file is a symlink or not a regular file"); let raw; try { raw = readFileSync(this.file, "utf8"); } catch (err) { return unreadable(err); } let parsed; try { parsed = JSON.parse(raw); } catch (err) { return refuse(`the org state file is not valid JSON (${err.message})`); } if (!isOrgStateFile(parsed)) return refuse("the org state file does not carry a readable org-state shape"); const { checksum, ...body } = parsed; if (checksum !== checksumOf(body)) return refuse("the org state file failed its integrity check"); return parsed.state; } async readOrgState() { return this.readSync(); } installOrgStateSync(state) { this.acquireWriteLock(); assertSafeDirAtRequest(this.dir, this.onError, "write through", this.vetting); state = jsonForm(state); const cur = this.readSync(); if (cur !== undefined && state.revisionHighWater < cur.revisionHighWater) { throw new OrgRevisionRefusal(`refusing to lower the org revision high-water mark (${cur.revisionHighWater} → ${state.revisionHighWater}) — an interleaved install must re-read and re-compare`); } if (cur !== undefined && state.snapshot.revision === cur.snapshot.revision && !sameOrgPolicyContent(state.snapshot, cur.snapshot)) { throw new OrgRevisionRefusal(`refusing to replace org revision ${state.snapshot.revision} with DIFFERENT policy content under the same revision — content changes must bump the revision`); } if (cur !== undefined && state.snapshot.revision === cur.snapshot.revision && state.snapshot.fetchedAtMs <= cur.snapshot.fetchedAtMs) return; const body = { schemaVersion: 1, state }; atomicPublish(this.dir, this.file, JSON.stringify({ ...body, checksum: checksumOf(body) }, null, 2)); const after = this.readSync(); if (after === undefined || canonicalize(after) !== canonicalize(state)) { const why = "the org state file did not read back as the version just written; whether the snapshot reached the bytes on disk is unobservable from here — reconcile by reading the file"; this.onError?.(why); throw new Error(why); } } async installOrgState(state) { const run = this.writeChain.then(() => this.installOrgStateSync(state), () => this.installOrgStateSync(state)); this.writeChain = run.then(() => undefined, () => undefined); await run; } } function readOnlyRuleStoreView(store) { return { list: () => store.list(), ...(store.quarantined !== undefined ? { quarantined: () => store.quarantined() } : {}), ...(store.durability !== undefined ? { durability: store.durability } : {}), ...(store.fidelity !== undefined ? { fidelity: store.fidelity } : {}), }; } export function principalRuleFileName(principal) { return `${createHash("sha256").update(principal, "utf8").digest("hex")}.json`; } export { LOCAL_OWNER_FILE };