@sema-agent/core
Version:
Stateless, task-oriented AI agent core
897 lines (896 loc) • 45.1 kB
JavaScript
import { randomBytes } from "node:crypto";
import { escapeForDisclosure, hasUnrenderableCharacters, parseRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, segmentCoverageOf, suggestRulesForCommand, RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, isRuleBehavior, } from "./permission-rule-model.js";
import { effectiveOrThrow } from "./permission-rule-provider.js";
import { errText, isValidConsentScope, landRuleAdd, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
export class InMemoryRuleApprovalRecordStore {
rows = new Map();
async get(id) {
const r = this.rows.get(id);
if (r === undefined)
return undefined;
if (r.schema !== 4) {
return {
staleSchema: true,
id: r.id,
...(typeof r.principal === "string" ? { principal: r.principal } : {}),
...(r.owner !== undefined ? { owner: structuredClone(r.owner) } : {}),
};
}
return structuredClone(r);
}
async create(record) {
if (this.rows.has(record.id))
throw new Error(`approval record ${record.id} already exists`);
this.rows.set(record.id, structuredClone(record));
}
async cas(id, expectRev, next) {
const cur = this.rows.get(id);
if (cur === undefined || cur.rev !== expectRev)
return false;
this.rows.set(id, structuredClone(next));
return true;
}
size() {
return this.rows.size;
}
}
function nowIso(deps) {
return (deps.now?.() ?? new Date()).toISOString();
}
function mintId(deps, prefix) {
if (deps.newId !== undefined)
return deps.newId();
return `${prefix}_${Date.now().toString(36)}_${randomBytes(9).toString("base64url")}`;
}
function requirePrincipal(principal, entry) {
if (typeof principal !== "string" || principal === "") {
throw new Error(`${entry} requires a verified principal — refusing to create an approval record without one`);
}
return principal;
}
function resolveCallerOwner(principal, owner, entry) {
if (owner !== undefined) {
if (owner.kind === "principal") {
const p = requirePrincipal(owner.principal, entry);
if (typeof principal === "string" && principal !== "" && principal !== p) {
throw new Error(`${entry}: contradictory identity — principal "${principal}" and owner principal "${p}" disagree`);
}
return { kind: "principal", principal: p };
}
if (typeof principal === "string" && principal !== "") {
throw new Error(`${entry}: contradictory identity — a principal and a local-owner were both given; a bucket has one owner`);
}
return { kind: "local-owner" };
}
return { kind: "principal", principal: requirePrincipal(principal, entry) };
}
function ownerOfRecord(rec) {
if (rec.owner !== undefined) {
if (rec.owner.kind === "principal") {
if (typeof rec.owner.principal !== "string" || rec.owner.principal === "")
return undefined;
if (typeof rec.principal === "string" && rec.principal !== "" && rec.principal !== rec.owner.principal)
return undefined;
return rec.owner;
}
if (typeof rec.principal === "string" && rec.principal !== "")
return undefined;
return { kind: "local-owner" };
}
return typeof rec.principal === "string" && rec.principal !== "" ? { kind: "principal", principal: rec.principal } : undefined;
}
function recordIdentityOf(owner) {
return owner.kind === "principal" ? { principal: owner.principal } : { owner: { kind: "local-owner" } };
}
function storeForOwner(provider, owner) {
if (owner.kind === "principal")
return provider.forPrincipal(owner.principal);
const store = provider.forLocalOwner?.();
if (store === undefined) {
throw new Error("this deployment's permission-rule provider has no local-owner bucket (forLocalOwner is not implemented)");
}
return store;
}
function isStale(rec) {
return rec.staleSchema === true;
}
const STALE_RECORD_MESSAGE = "this approval record predates the current record form, and the decision it carried is void — re-trigger the command to draw a fresh card";
function offerMembersOf(offer) {
return offer.kind === "single" ? [offer.candidate] : offer.candidates;
}
function approvalRecordDamageOf(rec) {
if (!Array.isArray(rec.candidates))
return "the record carries no candidate list";
if (!Array.isArray(rec.offers) || rec.offers.length === 0)
return "the record carries no offers";
const claimed = new Set();
for (const [at, offer] of rec.offers.entries()) {
if (typeof offer !== "object" || offer === null)
return `offer ${at} is not an offer object`;
if (offer.kind !== "single" && offer.kind !== "batch")
return `offer ${at} has an unknown kind`;
if (offer.kind === "batch" && (!Array.isArray(offer.candidates) || offer.candidates.length === 0)) {
return `batch offer ${at} has no members`;
}
const members = offerMembersOf(offer);
const seen = new Set();
for (const member of members) {
if (!Number.isInteger(member) || member < 0 || rec.candidates[member] === undefined) {
return `offer ${at} references candidate ${escapeForDisclosure(member)}, which the record does not carry`;
}
const cand = rec.candidates[member];
if (typeof cand !== "object" || cand === null || typeof cand.rule !== "string") {
return `offer ${at} references candidate ${member}, which is not a candidate row`;
}
if (!isRuleBehavior(cand.behavior)) {
return `offer ${at} references candidate ${member}, whose behavior is not one of ${RULE_BEHAVIORS.join("/")}`;
}
if (rec.kind === "card" && cand.behavior !== "allow") {
return `card offer ${at} references candidate ${member}, a ${cand.behavior} rule — a card mints allow candidates only`;
}
if (!isValidConsentScope(cand.scope)) {
return `offer ${at} references candidate ${member}, whose scope is not a consent-face scope (global, project, or session)`;
}
if (seen.has(member))
return `batch offer ${at} references candidate ${member} twice`;
seen.add(member);
if (claimed.has(member))
return `candidate ${member} belongs to more than one offer`;
claimed.add(member);
}
if (offer.kind === "batch") {
if (rec.kind === "card") {
if (!Array.isArray(offer.segments) || offer.segments.length !== offer.candidates.length || !offer.segments.every((s) => typeof s === "string")) {
return `card batch offer ${at} must carry exactly one source segment per member`;
}
if (!Number.isInteger(offer.uncoveredSegments) || offer.uncoveredSegments < 0) {
return `card batch offer ${at} must carry a non-negative uncovered-segment count`;
}
const detail = offer.uncoveredDetail;
if (!Array.isArray(detail))
return `card batch offer ${at} must carry its uncovered-segment reason rows`;
if (detail.length !== offer.uncoveredSegments) {
return `card batch offer ${at} carries ${detail.length} reason rows for an uncovered-segment count of ${offer.uncoveredSegments} — one fact, two seats, and they must agree`;
}
for (const [ri, row] of detail.entries()) {
if (typeof row !== "object" ||
row === null ||
typeof row.segment !== "string" ||
(row.reason !== "redirection" && row.reason !== "no_rule_form" && row.reason !== "cap_overflow")) {
return `card batch offer ${at} reason row ${ri} is not a segment/reason row from the closed reason set`;
}
}
if (detail.length > 0) {
const rows = detail;
const table = typeof rec.command === "string" ? segmentCoverageOf(rec.command, { persisted: [] }, { tool: CARD_RULE_TOOL, cwd: undefined }) : undefined;
if (table === undefined) {
return `card batch offer ${at} carries reason rows but the record's command cannot back them — a row without a readable source command is a row nobody can audit`;
}
let cursor = 0;
for (const [ri, row] of rows.entries()) {
let found = -1;
for (let j = cursor; j < table.length; j++) {
if (table[j]?.segment === row.segment) {
found = j;
break;
}
}
if (found === -1)
return `card batch offer ${at} reason row ${ri} names a segment the record's command does not carry at or after that position`;
cursor = found + 1;
}
}
}
else if (offer.segments !== undefined || offer.uncoveredSegments !== undefined || offer.uncoveredDetail !== undefined) {
return `${escapeForDisclosure(rec.kind)} batch offer ${at} must not carry segment metadata — there is no compound command behind it`;
}
}
}
for (const [at] of rec.candidates.entries()) {
if (!claimed.has(at))
return `candidate ${at} belongs to no offer — every candidate row is claimed by exactly one offer`;
}
if (rec.edited !== undefined) {
if (typeof rec.edited !== "object" || rec.edited === null)
return "the edited seat is not an object";
const w = rec.edited.warnings;
if (w !== undefined) {
if (!Array.isArray(w) || w.length !== 1 || (w[0] !== "broad_prefix" && w[0] !== "compound_prefix")) {
return "the edited seat's warnings are not a single-code array from the closed breadth-code set";
}
}
}
return undefined;
}
export function ruleOffersOfRecord(rec) {
const tripleOf = (index) => {
const cand = rec.candidates[index];
const text = cand?.rule;
if (cand === undefined || text === undefined)
throw new Error(`approval-record offer references candidate ${index}, which the record does not carry`);
const parsed = parseRuleText(text, cand.behavior);
if ("reject" in parsed) {
throw new Error(`approval-record candidate ${index} does not parse as a rule (${parsed.reject.code}) — refusing to render it as one`);
}
return { rule: parsed.rule.rule, match: parsed.rule.match, command: parsed.rule.command };
};
return rec.offers.map((offer) => {
if (offer.kind === "single") {
const t = tripleOf(offer.candidate);
if (t.match === "subpath") {
throw new Error("approval-record single offer references a directory rule — a shape this engine does not mint; refusing to render it");
}
return { kind: "single", ...t };
}
const segments = offer.segments;
if (segments === undefined || offer.uncoveredSegments === undefined || offer.uncoveredDetail === undefined) {
throw new Error("a batch offer without mint-time segment metadata has no card face to project — import/starter batches render as previews, not offer cards");
}
const rawDetail = offer.uncoveredDetail;
const count = offer.uncoveredSegments;
const detail = [];
const detailDamaged = (() => {
if (!Array.isArray(rawDetail) || !Number.isInteger(count) || count < 0 || rawDetail.length !== count)
return true;
for (let i = 0; i < rawDetail.length; i++) {
const row = rawDetail[i];
if (typeof row !== "object" || row === null)
return true;
const segment = row.segment;
const reason = row.reason;
if (typeof segment !== "string" || (reason !== "redirection" && reason !== "no_rule_form" && reason !== "cap_overflow"))
return true;
detail.push({ segment, reason });
}
return false;
})();
if (detailDamaged) {
throw new Error("a batch offer's uncovered-segment reason rows do not match their count or the closed reason set — refusing to render damaged mint-time facts as a card face");
}
return {
kind: "batch",
rules: offer.candidates.map((candidate, i) => {
const segment = segments[i];
if (segment === undefined)
throw new Error(`batch offer member ${i} has no source segment recorded`);
const t = tripleOf(candidate);
return t.match === "subpath"
? { kind: "directoryRead", rule: t.rule, directory: t.command, segment }
: { kind: "command", rule: t.rule, match: t.match, command: t.command, segment };
}),
uncoveredSegments: count,
uncoveredDetail: detail,
};
});
}
export function mintRuleTicket(recordId, candidateIndex) {
return `rt.${candidateIndex}.${recordId}`;
}
function parseRuleTicket(ticket) {
const m = /^rt\.(\d+)\.(.+)$/.exec(ticket);
if (m === null)
return undefined;
return { index: Number(m[1]), recordId: m[2] };
}
export async function prepareCardApproval(opts) {
const owner = resolveCallerOwner(opts.principal, opts.owner, "prepareCardApproval");
if ("candidates" in opts) {
const e = new Error("prepareCardApproval does not accept caller candidates — card options are minted by the engine from the adjudicated command");
e.code = "config.invalid_argument";
throw e;
}
if (opts.toolName !== CARD_RULE_TOOL)
return undefined;
const scope = (() => {
if (opts.scope !== undefined) {
if (!isValidConsentScope(opts.scope)) {
const e = new Error("prepareCardApproval got a scope that is not a consent-face scope (global, project with a non-empty root, or session with a non-empty sessionId) — refusing to guess where this consent should land");
e.code = "config.invalid_argument";
throw e;
}
return opts.scope;
}
if (opts.cwd !== undefined) {
if (typeof opts.cwd !== "string" || opts.cwd === "") {
const e = new Error("prepareCardApproval got a cwd that is not a usable path — it cannot anchor the derived project scope, and refusing beats minting a rule scoped to garbage");
e.code = "config.invalid_argument";
throw e;
}
return { kind: "project", root: opts.cwd };
}
const e = new Error("prepareCardApproval was given neither a scope nor a cwd — there is nowhere for this consent to land, and no direction of silence is honest (config.missing_scope): pass an explicit scope, or thread the adjudicated call's cwd");
e.code = "config.missing_scope";
throw e;
})();
let coverage;
try {
const view = await effectiveOrThrow(storeForOwner(opts.deps.provider, owner), opts.sessionId !== undefined ? { sessionId: opts.sessionId } : undefined);
const table = view.rules;
coverage = segmentCoverageOf(opts.command, { persisted: table }, { tool: CARD_RULE_TOOL, cwd: opts.cwd, sessionId: opts.sessionId, ...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}) });
}
catch {
coverage = undefined;
}
const suggested = suggestRulesForCommand(opts.command, {
...(coverage !== undefined ? { coverage } : {}),
scope,
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}),
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
...(opts.deps.deniesDirectoryRead !== undefined ? { deniesDirectoryRead: opts.deps.deniesDirectoryRead } : {}),
});
const candidates = [];
const claimedBy = new Map();
const claim = (text) => {
if (claimedBy.has(text))
throw new Error(`engine defect: rule "${text}" was minted into two offers on one card`);
const index = candidates.length;
candidates.push({ behavior: "allow", rule: text, scope });
claimedBy.set(text, index);
return index;
};
const offers = suggested.map((offer) => offer.kind === "single"
? { kind: "single", candidate: claim(offer.rule) }
: {
kind: "batch",
candidates: offer.rules.map((r) => claim(r.rule)),
segments: offer.rules.map((r) => r.segment),
uncoveredSegments: offer.uncoveredSegments,
uncoveredDetail: offer.uncoveredDetail ?? [],
});
if (candidates.length === 0)
return undefined;
const record = {
id: mintId(opts.deps, "rar"),
...recordIdentityOf(owner),
schema: 4,
kind: "card",
state: "pending",
rev: 0,
candidates,
offers,
command: opts.command,
createdAt: nowIso(opts.deps),
...(opts.toolCallId !== undefined ? { toolCallId: opts.toolCallId } : {}),
...(opts.boundInputHash !== undefined ? { boundInputHash: opts.boundInputHash } : {}),
};
await opts.deps.approvals.create(record);
return {
approvalId: record.id,
offers: ruleOffersOfRecord(record),
tickets: candidates.map((_, i) => mintRuleTicket(record.id, i)),
candidates,
};
}
const CARD_RULE_TOOL = "Bash";
export async function confirmRuleApproval(opts) {
const caller = resolveCallerOwner(opts.principal, opts.owner, "confirmRuleApproval");
const no = (reason, detail) => ({
ok: false,
reason,
...(detail !== undefined ? { detail } : {}),
});
if (opts.editedCandidate !== undefined && opts.selectedOffer !== undefined) {
const e = new Error("confirmRuleApproval takes selectedOffer OR editedCandidate, never both — one confirmation carries one choice");
e.code = "config.invalid_argument";
throw e;
}
if ("selectedCandidate" in opts) {
const e = new Error("confirmRuleApproval no longer takes selectedCandidate — selection is by OFFER index (selectedOffer) against the prepared record's offers; re-fetch the card and choose an offer");
e.code = "config.invalid_argument";
throw e;
}
const rec = await opts.deps.approvals.get(opts.approvalId);
const recOwner = rec === undefined ? undefined : ownerOfRecord(rec);
if (rec === undefined || recOwner === undefined || !sameRuleOwner(recOwner, caller))
return no("record_not_found");
if (isStale(rec))
return no("record_schema_stale", { message: STALE_RECORD_MESSAGE });
const damage = approvalRecordDamageOf(rec);
if (damage !== undefined)
return no("record_malformed", { message: damage });
if (opts.editedCandidate !== undefined)
return await confirmEditedCandidate(rec, opts.editedCandidate, opts.deps);
const chosen = opts.selectedOffer === undefined ? (rec.offers.length === 1 ? 0 : undefined) : opts.selectedOffer;
if (chosen === undefined)
return no("selection_missing");
if (!Number.isInteger(chosen) || rec.offers[chosen] === undefined)
return no("selection_invalid");
if (rec.state === "approved" || rec.state === "redeemed") {
const stored = rec.selectedOffer;
if (stored === undefined || !Number.isInteger(stored) || rec.offers[stored] === undefined) {
return no("record_malformed", { message: "the settled record does not name a valid chosen offer" });
}
return stored === chosen ? { ok: true } : no("selection_mismatch");
}
if (rec.state !== "pending")
return no("not_pending");
if (rec.selectedOffer !== undefined)
return no("record_malformed", { message: "a pending record must not already carry a chosen offer" });
const won = await opts.deps.approvals.cas(rec.id, rec.rev, { ...rec, rev: rec.rev + 1, state: "approved", selectedOffer: chosen });
if (won)
return { ok: true };
const again = await opts.deps.approvals.get(rec.id);
if (again === undefined)
return no("record_not_found");
if (isStale(again))
return no("record_schema_stale", { message: STALE_RECORD_MESSAGE });
if (again.state === "pending")
return no("conflict");
const settled = again.selectedOffer;
if (settled === undefined || !Number.isInteger(settled) || again.offers[settled] === undefined) {
return no("record_malformed", { message: "the settled record does not name a valid chosen offer" });
}
return settled === chosen ? { ok: true } : no("selection_mismatch");
}
function normalizeEditedSpelling(text) {
const m = /^([A-Za-z][A-Za-z0-9_]*)\((.+) \*\)$/.exec(text);
if (m === null)
return text;
const head = m[1];
const body = m[2];
if (head === undefined || body === undefined || body.includes("*"))
return text;
return `${head}(${body}:*)`;
}
function parseSubmittedSpelling(text) {
return parseRuleText(normalizeEditedSpelling(text), "allow");
}
function checkEditedRuleText(text, command) {
const parsed = parseSubmittedSpelling(text);
if ("reject" in parsed)
return { ok: false, code: parsed.reject.code, message: parsed.reject.message };
if (!ruleAdmitsCommand(parsed.rule, command)) {
return {
ok: false,
message: `the edited rule "${parsed.rule.rule}" does not admit the command that was decided ("${escapeForDisclosure(command)}") — a card's edit may widen how much the rule covers, never move it to a different grant`,
};
}
const warnings = ruleBreadthWarningsOf(parsed.rule);
return { ok: true, canonicalRule: parsed.rule.rule, ...(warnings.length > 0 ? { warnings } : {}) };
}
export function precheckEditedRuleText(text, command) {
if (typeof text !== "string") {
const e = new Error(`precheckEditedRuleText takes the edited rule text as a string (got ${typeof text}) — a non-string is a caller bug, not a rule a person could have typed`);
e.code = "config.invalid_argument";
throw e;
}
if (typeof command !== "string" || command === "") {
const e = new Error("precheckEditedRuleText takes the adjudicated command the card was drawn for — coverage cannot be judged without it, and reporting the absence as a rejected rule would blame the person's text");
e.code = "config.invalid_argument";
throw e;
}
if (hasUnrenderableCharacters(command)) {
const e = new Error("precheckEditedRuleText takes a command this lane can read — one carrying control or format characters is refused at this preflight face (likelier a caller wiring fault than a card's command; the in-engine confirmation arm still judges such a card, with the command escaped on its refusal line)");
e.code = "config.invalid_argument";
throw e;
}
return checkEditedRuleText(text, command);
}
async function confirmEditedCandidate(rec, edit, deps) {
const no = (reason, detail) => ({
ok: false,
reason,
...(detail !== undefined ? { detail } : {}),
});
if (rec.kind !== "card") {
return no("edit_rejected", { message: "a batch record takes no edited candidate — its confirmation covers the previewed list whole" });
}
if (typeof edit.boundInputHash !== "string" || edit.boundInputHash === "")
return no("edit_binding_mismatch");
if (rec.boundInputHash === undefined) {
return no("edit_binding_mismatch");
}
if (edit.boundInputHash !== rec.boundInputHash)
return no("edit_binding_mismatch");
if (rec.state === "approved" || rec.state === "redeemed") {
if (rec.edited === undefined)
return no("selection_mismatch");
const canonical = rec.candidates[rec.edited.index]?.rule;
if (canonical === undefined)
return no("selection_mismatch");
let hit = edit.text === rec.edited.text;
if (!hit) {
const reparsed = parseSubmittedSpelling(edit.text);
hit = "rule" in reparsed && reparsed.rule.rule === canonical;
}
if (!hit)
return no("selection_mismatch");
return { ok: true, mintedCandidate: { index: rec.edited.index, rule: canonical, ticket: mintRuleTicket(rec.id, rec.edited.index) } };
}
if (rec.state !== "pending")
return no("not_pending");
if (deps.cardEdits !== undefined && typeof deps.cardEdits !== "boolean") {
throw new Error(`RuleConsentDeps.cardEdits must be a boolean when present (got ${typeof deps.cardEdits}) — refusing to guess whether the card-edit face is enabled`);
}
if (deps.cardEdits !== true)
return no("edit_disabled");
const scope = rec.candidates[0]?.scope;
if (scope === undefined || !rec.candidates.every((c) => sameScope(c.scope, scope))) {
return no("edit_rejected", { message: "the record's candidates carry no single common scope — an edited candidate inherits the card's scope, and a record without one is malformed" });
}
if (rec.command === undefined) {
return no("edit_rejected", { message: "the record does not carry the adjudicated command (minted before card edits existed) — coverage cannot be verified, so the edit is refused" });
}
const checked = checkEditedRuleText(edit.text, rec.command);
if (!checked.ok) {
return no("edit_rejected", { ...(checked.code !== undefined ? { code: checked.code } : {}), message: checked.message });
}
const index = rec.candidates.length;
const next = {
...rec,
rev: rec.rev + 1,
state: "approved",
candidates: [...rec.candidates, { behavior: "allow", rule: checked.canonicalRule, scope }],
offers: [...rec.offers, { kind: "single", candidate: index }],
selectedOffer: rec.offers.length,
edited: { index, text: edit.text, at: nowIso(deps), ...(checked.warnings !== undefined ? { warnings: checked.warnings.map((w) => w.code) } : {}) },
};
const won = await deps.approvals.cas(rec.id, rec.rev, next);
if (!won) {
const again = await deps.approvals.get(rec.id);
if (again === undefined || isStale(again))
return no("record_not_found");
if (again.state === "pending")
return no("conflict");
return await confirmEditedCandidate(again, edit, deps);
}
return { ok: true, mintedCandidate: { index, rule: checked.canonicalRule, ticket: mintRuleTicket(rec.id, index) } };
}
export async function redeemRuleTicket(opts) {
const caller = resolveCallerOwner(opts.principal, opts.owner, "redeemRuleTicket");
const parsed = parseRuleTicket(opts.ticket);
if (parsed === undefined)
return { status: "refused", reason: "malformed ticket" };
const rec = await opts.deps.approvals.get(parsed.recordId);
if (rec === undefined)
return { status: "refused", reason: "no such approval record" };
const recOwner = ownerOfRecord(rec);
if (recOwner === undefined || !sameRuleOwner(recOwner, caller))
return { status: "refused", reason: "the ticket belongs to a different principal" };
if (isStale(rec))
return { status: "refused", reason: `record_schema_stale: ${STALE_RECORD_MESSAGE}` };
if (rec.state === "pending")
return { status: "refused", reason: "the approval record has not been confirmed" };
const damage = approvalRecordDamageOf(rec);
if (damage !== undefined)
return { status: "refused", reason: `the approval record is malformed: ${damage}` };
const candidate = rec.candidates[parsed.index];
if (candidate === undefined)
return { status: "refused", reason: "the ticket names no candidate on this record" };
const chosen = rec.selectedOffer;
const chosenOffer = chosen !== undefined && Number.isInteger(chosen) ? rec.offers[chosen] : undefined;
if (chosenOffer === undefined) {
return { status: "refused", reason: "the approval record is malformed: it is settled but names no valid chosen offer" };
}
if (!offerMembersOf(chosenOffer).includes(parsed.index)) {
return { status: "refused", reason: "the ticket names an option the person did not choose" };
}
const siblingsOf = candidate.behavior === "allow" ? { rec, offer: chosenOffer, index: parsed.index } : undefined;
const parsedRule = parseRuleText(candidate.rule, candidate.behavior);
if ("reject" in parsedRule)
return { status: "refused", reason: `${parsedRule.reject.code}: ${parsedRule.reject.message}` };
let store;
try {
store = storeForOwner(opts.deps.provider, caller);
}
catch (err) {
return { status: "refused", reason: errText(err) };
}
const writer = writerOf(store);
if (writer === undefined)
return { status: "refused", reason: "the resolved permission-rule store has no write face" };
const known = rec.redeemedDots?.[parsed.index];
const dot = known ?? (await writer.nextDot());
if (known === undefined) {
const next = {
...rec,
rev: rec.rev + 1,
state: "redeemed",
redeemedDots: { ...(rec.redeemedDots ?? {}), [parsed.index]: dot },
};
const ok = await opts.deps.approvals.cas(rec.id, rec.rev, next);
if (!ok) {
const again = await opts.deps.approvals.get(rec.id);
const theirs = again === undefined || isStale(again) ? undefined : again.redeemedDots?.[parsed.index];
if (theirs === undefined)
return { status: "refused", reason: "the approval record changed state concurrently" };
return await applyRedemption({ deps: opts.deps, owner: caller, candidate, parsedRule: parsedRule.rule, dot: theirs, recordId: rec.id, kind: rec.kind, replay: true, ...(siblingsOf !== undefined ? { siblingsOf } : {}) });
}
}
return await applyRedemption({ deps: opts.deps, owner: caller, candidate, parsedRule: parsedRule.rule, dot, recordId: rec.id, kind: rec.kind, replay: known !== undefined, ...(siblingsOf !== undefined ? { siblingsOf } : {}) });
}
async function tighteningSiblingsStand(rec, offer, index, store, durableSnapshot) {
for (const member of offerMembersOf(offer)) {
if (member === index)
continue;
const sibling = rec.candidates[member];
if (sibling === undefined || sibling.behavior === "allow")
continue;
const dot = rec.redeemedDots?.[member];
if (dot === undefined)
return false;
const view = sibling.scope.kind === "session" ? await effectiveOrThrow(store, { sessionId: sibling.scope.sessionId }) : durableSnapshot;
const live = view.rules.some((r) => r.behavior === sibling.behavior && r.rule === sibling.rule && sameScope(r.scope, sibling.scope) && r.adds.some((a) => a.dot.actor === dot.actor && a.dot.counter === dot.counter));
if (!live)
return false;
}
return true;
}
async function applyRedemption(args) {
let store;
try {
store = storeForOwner(args.deps.provider, args.owner);
}
catch (err) {
return { status: "refused", reason: errText(err) };
}
const writer = writerOf(store);
if (writer === undefined)
return { status: "refused", reason: "the resolved permission-rule store has no write face" };
const origin = originOfRecordKind(args.kind);
const siblings = args.siblingsOf;
const landing = await landRuleAdd({
store,
writer,
delta: {
kind: "redemption-add",
behavior: args.parsedRule.behavior,
rule: args.parsedRule.rule,
scope: args.candidate.scope,
tool: args.parsedRule.tool,
match: args.parsedRule.match,
command: args.parsedRule.command,
add: { dot: args.dot, origin, createdAt: nowIso(args.deps) },
redemption: {
recordId: args.recordId,
...(args.owner.kind === "principal" ? { principal: args.owner.principal } : { owner: args.owner }),
},
},
...(siblings !== undefined
? {
preflight: async (snapshot) => (await tighteningSiblingsStand(siblings.rec, siblings.offer, siblings.index, store, snapshot))
? undefined
: "withheld: this allow belongs to a batch whose deny/ask members do not all stand in the store — an allow must not stand wider than the list that was confirmed; redeem the batch (redeemRuleBatch) so the tightening members land first, or draw a fresh card",
}
: {}),
});
if (!landing.landed)
return { status: "refused", reason: landing.reason };
return { status: "redeemed", behavior: args.parsedRule.behavior, rule: args.parsedRule.rule, scope: args.candidate.scope, dot: args.dot, rev: landing.rev, alreadyRedeemed: args.replay };
}
function originOfRecordKind(kind) {
return kind === "import" ? "imported-cc" : kind === "starter" ? "starter" : "user";
}
const IMPORT_UNCOVERED_LAYERS = { flagSettings: "not-imported-v1", policySettings: "not-imported-v1" };
export async function prepareCcImport(opts) {
const owner = resolveCallerOwner(opts.principal, opts.owner, "prepareCcImport");
const candidates = [];
const skipped = [];
const translated = [];
const layers = [];
for (const layer of opts.layers) {
let raw;
try {
raw = await layer.readFile(layer.path);
}
catch {
raw = undefined;
}
if (raw === undefined) {
layers.push({ path: layer.path, layer: layer.layer, found: false });
continue;
}
layers.push({ path: layer.path, layer: layer.layer, found: true });
let settingsRoot;
try {
settingsRoot = JSON.parse(raw);
}
catch (err) {
skipped.push({ rule: layer.path, reason: `settings file is not valid JSON (${errText(err)})` });
continue;
}
let container;
if (settingsRoot === null)
container = undefined;
else if (typeof settingsRoot === "object" && !Array.isArray(settingsRoot))
container = settingsRoot.permissions;
else {
skipped.push({
rule: layer.path,
reason: `settings file is not a JSON object — no bucket in this layer could be read, so it is reported as unread rather than as empty (it stays in the settings file either way)`,
});
continue;
}
let permissions;
if (container === undefined || container === null)
permissions = undefined;
else if (typeof container === "object" && !Array.isArray(container))
permissions = container;
else {
skipped.push({
rule: layer.path,
reason: `settings "permissions" is not an object — no bucket in this layer could be read, so it is reported as unread rather than as empty (it stays in the settings file either way)`,
});
continue;
}
const scope = layer.layer === "userSettings" ? { kind: "global" } : { kind: "project", root: layer.root };
for (const behavior of RULE_BEHAVIORS) {
const entries = permissions?.[behavior];
if (entries === undefined || entries === null)
continue;
if (!Array.isArray(entries)) {
skipped.push({
rule: layer.path,
reason: `settings ${behavior} bucket is not an array — no entry in it could be read (it stays in the settings file either way)`,
});
continue;
}
for (const entry of entries) {
if (typeof entry !== "string") {
skipped.push({ rule: String(entry), reason: `settings ${behavior} entry is not a string` });
continue;
}
if (behavior === "allow" && entry.startsWith("Read(") && "reject" in parseRuleText(entry, behavior)) {
skipped.push({
rule: entry,
reason: "unsupported.form: only the Read(//abs-dir/**) directory form imports in v1 — this entry stays in the settings file, unimported",
});
continue;
}
if (!/^[A-Za-z][A-Za-z0-9_]*\(.*\)$/.test(entry)) {
const reason = /^[A-Za-z][A-Za-z0-9_-]*$/.test(entry)
? "unsupported.form: a bare tool-name entry is a name-set item, not a command rule — it stays in the settings file, unimported"
: "unsupported.form: not a command rule spelling this version imports — it stays in the settings file, unimported";
skipped.push({ rule: entry, reason });
continue;
}
const parsed = parseRuleText(entry, behavior);
if ("reject" in parsed) {
skipped.push({ rule: entry, reason: `${parsed.reject.code}: ${parsed.reject.message}` });
continue;
}
if (candidates.some((c) => c.behavior === behavior && c.rule === parsed.rule.rule && sameScope(c.scope, scope)))
continue;
candidates.push({ behavior, rule: parsed.rule.rule, scope });
}
}
}
const preview = { candidates, skipped, translated, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS } };
if (candidates.length === 0)
return { preview };
const record = {
id: mintId(opts.deps, "rar"),
...recordIdentityOf(owner),
schema: 4,
kind: "import",
state: "pending",
rev: 0,
candidates,
offers: [{ kind: "batch", candidates: candidates.map((_, i) => i) }],
createdAt: nowIso(opts.deps),
};
await opts.deps.approvals.create(record);
return { preview, approvalId: record.id };
}
export const STARTER_RULES = [
"Bash(ls)",
"Bash(ls -la)",
"Bash(pwd)",
"Bash(whoami)",
"Bash(uname -a)",
"Bash(date)",
"Bash(df -h)",
"Bash(node --version)",
"Bash(npm --version)",
];
export async function prepareStarterBatch(opts) {
const owner = resolveCallerOwner(opts.principal, opts.owner, "prepareStarterBatch");
const candidates = [];
for (const text of STARTER_RULES) {
const parsed = parseRuleText(text, "allow");
if ("reject" in parsed)
throw new Error(`starter rule "${text}" is not a valid rule: ${parsed.reject.message}`);
candidates.push({ behavior: "allow", rule: parsed.rule.rule, scope: { kind: "global" } });
}
const record = {
id: mintId(opts.deps, "rar"),
...recordIdentityOf(owner),
schema: 4,
kind: "starter",
state: "pending",
rev: 0,
candidates,
offers: [{ kind: "batch", candidates: candidates.map((_, i) => i) }],
createdAt: nowIso(opts.deps),
};
await opts.deps.approvals.create(record);
return { preview: candidates, approvalId: record.id };
}
export async function redeemRuleBatch(opts) {
const caller = resolveCallerOwner(opts.principal, opts.owner, "redeemRuleBatch");
const rec = await opts.deps.approvals.get(opts.approvalId);
if (rec === undefined)
return { status: "refused", reason: "no such approval record" };
const recOwner = ownerOfRecord(rec);
if (recOwner === undefined || !sameRuleOwner(recOwner, caller))
return { status: "refused", reason: "the record belongs to a different principal" };
if (isStale(rec))
return { status: "refused", reason: `record_schema_stale: ${STALE_RECORD_MESSAGE}` };
if (rec.state === "pending")
return { status: "refused", reason: "the approval record has not been confirmed" };
const damage = approvalRecordDamageOf(rec);
if (damage !== undefined)
return { status: "refused", reason: `the approval record is malformed: ${damage}` };
const chosen = rec.selectedOffer;
const chosenOffer = chosen !== undefined && Number.isInteger(chosen) ? rec.offers[chosen] : undefined;
if (chosenOffer === undefined) {
return { status: "refused", reason: "the approval record is malformed: it is settled but names no valid chosen offer" };
}
let store;
try {
store = storeForOwner(opts.deps.provider, caller);
}
catch (err) {
return { status: "refused", reason: errText(err) };
}
const members = [];
let rev = 0;
const ordered = [...offerMembersOf(chosenOffer)].sort((a, b) => {
const ra = RULE_BEHAVIOR_PRECEDENCE[rec.candidates[a].behavior];
const rb = RULE_BEHAVIOR_PRECEDENCE[rec.candidates[b].behavior];
return ra !== rb ? ra - rb : a - b;
});
let tighteningFailed;
for (const candidateIndex of ordered) {
const candidate = rec.candidates[candidateIndex];
const row = { candidateIndex, behavior: candidate.behavior, rule: candidate.rule, scope: candidate.scope };
if (candidate.behavior === "allow" && tighteningFailed !== undefined) {
members.push({ ...row, status: "refused", reason: `withheld: candidate ${tighteningFailed} (a deny/ask member of this batch) did not land, and an allow must not stand wider than the list that was confirmed — redeem the batch again once the store accepts the tightening member` });
continue;
}
let alreadyThere = false;
{
try {
const snap = await effectiveOrThrow(store, candidate.scope.kind === "session" ? { sessionId: candidate.scope.sessionId } : undefined);
rev = snap.rev;
alreadyThere = snap.rules.some((r) => r.behavior === candidate.behavior && r.rule === candidate.rule && sameScope(r.scope, candidate.scope));
}
catch (err) {
members.push({ ...row, status: "refused", reason: `could not read the permission-rule store: ${errText(err)}` });
if (candidate.behavior !== "allow")
tighteningFailed ??= candidateIndex;
continue;
}
}
const res = await redeemRuleTicket({
ticket: mintRuleTicket(rec.id, candidateIndex),
...(caller.kind === "principal" ? { principal: caller.principal } : { owner: caller }),
deps: opts.deps,
});
if (res.status === "refused") {
members.push({ ...row, status: "refused", reason: res.reason });
if (candidate.behavior !== "allow")
tighteningFailed ??= candidateIndex;
continue;
}
if (candidate.behavior !== "allow") {
let live = false;
try {
const snap = await effectiveOrThrow(store, candidate.scope.kind === "session" ? { sessionId: candidate.scope.sessionId } : undefined);
live = snap.rules.some((r) => r.behavior === candidate.behavior && r.rule === candidate.rule && sameScope(r.scope, candidate.scope) && r.adds.some((a) => a.dot.actor === res.dot.actor && a.dot.counter === res.dot.counter));
}
catch {
live = false;
}
if (!live) {
members.push({ ...row, status: "refused", reason: `this ${candidate.behavior} member's redeemed dot ${res.dot.actor}#${res.dot.counter} is not live in the store (removed since it was redeemed) — a removed rule is not re-landed by replaying its record; a new consent is required` });
tighteningFailed ??= candidateIndex;
continue;
}
}
rev = candidate.scope.kind === "session" ? Math.max(rev, res.rev) : res.rev;
members.push({ ...row, status: alreadyThere ? "deduped" : "persisted", alreadyRedeemed: res.alreadyRedeemed, dot: res.dot });
}
return { members, rev };
}