@sema-agent/core
Version:
Stateless, task-oriented AI agent core
279 lines (278 loc) • 14.3 kB
JavaScript
import { hasWin32AliasedComponent, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
import { globInWord } from "./bash-readonly-classifier.js";
import { readOfText } from "./shell-read-word.js";
import { passesUnasked } from "./shell-read-device.js";
import { skipsRootsContainment } from "./containment-face.js";
export const SHELL_READ_FINDINGS = Object.freeze(["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "unanswered", "none"]);
export const SHELL_READ_SEATS = Object.freeze(["full_shell", "readonly", "rejudge"]);
export const SHELL_READ_FINDING_RANK = {
identity_changed: 6,
deny_hit: 5,
unanswered: 4,
outside_roots: 3,
unestablishable: 2,
declined: 1,
none: 0,
};
export function dispositionOfCell(cell, context = {}) {
if (typeof cell === "string")
return cell;
if (context.redirectsOnly === true && cell.redirect !== undefined)
return cell.redirect;
return context.narrowedReadFace !== false ? cell.narrowed : cell.open;
}
export const SHELL_READ_DECISION = {
deny_hit: { full_shell: "refuse", readonly: "refuse", rejudge: "refuse" },
outside_roots: { full_shell: { narrowed: "mandated", open: "mandated", redirect: "ordinary" }, readonly: "refuse", rejudge: "refuse" },
unestablishable: { full_shell: { narrowed: "mandated", open: "ordinary" }, readonly: "refuse", rejudge: "refuse" },
declined: { full_shell: "ordinary", readonly: "refuse", rejudge: "refuse" },
identity_changed: { full_shell: "refuse", readonly: "refuse", rejudge: "refuse" },
unanswered: { full_shell: "mandated", readonly: "refuse", rejudge: "refuse" },
none: { full_shell: "vouch", readonly: "vouch", rejudge: "vouch" },
};
export function decideShellRead(finding, seat, context = {}) {
const row = Object.hasOwn(SHELL_READ_DECISION, finding) ? SHELL_READ_DECISION[finding] : undefined;
const cell = row !== undefined && Object.hasOwn(row, seat) ? row[seat] : undefined;
if (cell === undefined) {
throw new Error(`shell read decision: no cell for finding ${JSON.stringify(finding)} on seat ${JSON.stringify(seat)} — the seats are ${SHELL_READ_SEATS.map((w) => JSON.stringify(w)).join(", ")} (a seat names the face the command runs on)`);
}
return dispositionOfCell(cell, context);
}
export function dispositionsOfRow(finding) {
const row = Object.hasOwn(SHELL_READ_DECISION, finding) ? SHELL_READ_DECISION[finding] : undefined;
return new Set(Object.values(row ?? {}).flatMap((cell) => (typeof cell === "string" ? [cell] : [cell.narrowed, cell.open, ...(cell.redirect !== undefined ? [cell.redirect] : [])])));
}
export function judgeRead(read, boundary) {
const writeTarget = read.candidate.face === "write";
const deny = writeTarget ? boundary.writeDenyMatch : boundary.denyMatch;
if (deny !== undefined) {
const views = [...(read.target !== undefined ? [read.target] : []), ...read.spellings];
for (const view of views) {
const folded = normalizeAbsPathLexically(view);
const pattern = writeTarget ? boundary.writeDenyMatch(folded) : boundary.denyMatch(folded);
if (pattern !== null)
return { read, kind: "deny_hit", deny: { pattern, matchedView: folded, ...(writeTarget ? { face: "write" } : {}) }, containment: writeTarget ? writeContainmentOf(read, boundary) : containmentOf(read, boundary) };
}
if (!writeTarget && read.candidate.provenance === "traversal_root" && boundary.denyUnder !== undefined) {
let unplaced;
for (const view of views) {
const folded = normalizeAbsPathLexically(view);
const under = boundary.denyUnder(folded);
if (typeof under === "string")
return { read, kind: "deny_hit", deny: { pattern: under, matchedView: folded }, containment: containmentOf(read, boundary) };
if (under !== null && typeof under === "object")
unplaced ??= under.unplaced;
}
if (unplaced !== undefined) {
const cause = `the home-anchored deny row "${unplaced}" cannot be placed (no home directory is known for this environment), so "${read.target ?? read.candidate.spelled}" may hold it`;
return { read: { ...read, cause }, kind: "unanswered", containment: containmentOf(read, boundary) };
}
}
}
if (writeTarget)
return judgeWrite(read, boundary);
const containment = containmentOf(read, boundary);
if (read.candidate.denyOnly === true)
return { read, kind: "none", containment };
if (passesUnasked(read.candidate))
return { read, kind: "none", containment: "open" };
const provenance = read.candidate.provenance;
if (provenance === "env_assignment" || (provenance === "program_path" && read.candidate.expands !== "unresolvable"))
return { read, kind: "none", containment };
if (read.established === "unestablishable")
return { read, kind: "unestablishable", containment };
return { read, kind: containment === "outside" ? "outside_roots" : "none", containment };
}
export function denyRowAtOrBelow(read, boundary, carriesContent) {
if (boundary.writeDenyMatch !== undefined)
return true;
if (!carriesContent)
return false;
const atOrBelow = boundary.denyAtOrBelow;
if (atOrBelow === undefined)
return false;
return [...(read.target !== undefined ? [read.target] : []), ...read.spellings].some((view) => atOrBelow(normalizeAbsPathLexically(view)) !== null);
}
export function denyFloorShadowsOf(candidates) {
return candidates.flatMap((c) => (c.floorSpelled === undefined ? [] : [{ spelled: c.floorSpelled, raw: c.raw, provenance: c.provenance, expands: globInWord(readOfText(c.raw).word) ? "glob" : false, bases: [], denyOnly: true, ...(c.face !== undefined ? { face: c.face } : {}) }]));
}
function containmentOf(read, boundary) {
if (boundary.face === "open")
return "open";
if (read.target === undefined)
return "unknown";
return withinAnyRoot(boundary.roots, read.target) ? "inside" : "outside";
}
function writeContainmentOf(read, boundary) {
if (read.written === undefined || read.candidate.denyOnly === true || passesUnasked(read.candidate))
return "open";
const roots = boundary.writeRoots;
if (roots === undefined || read.target === undefined)
return "unknown";
return withinAnyRoot(roots, read.target) ? "inside" : "outside";
}
function judgeWrite(read, boundary) {
const containment = writeContainmentOf(read, boundary);
if (containment === "open")
return { read, kind: "none", containment };
if (read.established === "unestablishable" || boundary.writeRoots === undefined)
return { read, kind: "unestablishable", containment };
return { read, kind: containment === "outside" && !skipsRootsContainment(boundary.writeFace) ? "outside_roots" : "none", containment };
}
export function stationGuards(boundary) {
return boundary.face !== "open" || boundary.denyMatch !== undefined || boundary.unreadPartitions === true;
}
export function writeFaceGuards(boundary) {
return !skipsRootsContainment(boundary.writeFace) || boundary.writeDenyMatch !== undefined;
}
export function anyFaceGuards(boundary) {
return stationGuards(boundary) || writeFaceGuards(boundary);
}
export function foldShellReadFindings(judged) {
let best = { finding: "none" };
let unestablished = false;
let unplacedRead = false;
const raise = (next) => {
if (next.finding === "unestablishable" || next.finding === "declined" || next.finding === "unanswered") {
const face = next.witness?.read.candidate.face;
if (!(next.witness === undefined ? judged.guards || judged.writeGuards : face === "write" ? judged.writeGuards : judged.guards))
return;
unestablished = true;
if (next.finding === "unestablishable" && face !== "write")
unplacedRead = true;
}
const rank = SHELL_READ_FINDING_RANK[next.finding] - SHELL_READ_FINDING_RANK[best.finding];
if (rank > 0 || (rank === 0 && next.witness?.read.candidate.face === "write" && best.witness?.read.candidate.face !== "write"))
best = next;
};
const walk = (j, denyOnly) => {
if (j.plan.unreadable !== undefined && !denyOnly)
raise({ finding: "unestablishable" });
for (const s of j.segments) {
for (const f of s.findings) {
if (denyOnly && f.kind !== "deny_hit" && f.kind !== "unanswered")
continue;
if (f.kind !== "none")
raise({ finding: f.kind, witness: f });
}
if (!denyOnly) {
if (s.segment.declines.length > 0)
raise({ finding: "declined", declined: s.segment });
if (s.segment.reentry === "shell")
raise({ finding: "unestablishable", declined: s.segment });
if (s.segment.unreadProgramText !== undefined)
raise({ finding: "unestablishable", declined: s.segment });
const move = s.segment.baseMove;
if (move !== undefined && (typeof move.target === "string" || move.target.expands !== false))
raise({ finding: "unestablishable", declined: s.segment });
}
const nestedDenyOnly = denyOnly || s.segment.reentry === "shell";
for (const n of s.nested)
walk(n, nestedDenyOnly);
}
};
walk(judged, false);
const unfinished = judged.incomplete?.[0];
if (unfinished !== undefined && (judged.guards || judged.writeGuards) && SHELL_READ_FINDING_RANK.unanswered > SHELL_READ_FINDING_RANK[best.finding]) {
unestablished = true;
best = { finding: "unanswered", cause: unfinished.detail, ...(best.finding === "unestablishable" ? { ...(best.witness !== undefined ? { witness: best.witness } : {}), ...(best.declined !== undefined ? { declined: best.declined } : {}) } : {}) };
}
return { ...best, ...(unestablished ? { unestablished: true } : {}), ...(unplacedRead ? { unplacedRead: true } : {}) };
}
export function judgeEstablishedPlan(plan, established, boundary, nestedOf) {
return {
plan,
segments: plan.segments.map((segment) => ({
segment,
findings: (established.get(segment) ?? []).map((r) => judgeRead(r, boundary)),
nested: segment.nested.map(nestedOf),
})),
guards: stationGuards(boundary),
writeGuards: writeFaceGuards(boundary),
};
}
export function allFindings(judged) {
const out = [];
const walk = (j) => {
for (const s of j.segments) {
out.push(...s.findings);
for (const n of s.nested)
walk(n);
}
};
walk(judged);
return out;
}
export function parentDirLexically(p) {
return normalizeAbsPathLexically(`${p}/..`);
}
export function deepestCommonDir(a, b) {
if (a === b)
return a;
const shorter = Math.min(a.length, b.length);
let end = 0;
let i = 0;
while (i < shorter && a[i] === b[i]) {
if (a[i] === "/" || a[i] === "\\")
end = i;
i++;
}
if (i === shorter && (a[shorter] === "/" || a[shorter] === "\\" || b[shorter] === "/" || b[shorter] === "\\"))
end = shorter;
const cut = a.slice(0, end);
const candidate = cut.includes("/") || cut.includes("\\") ? cut : a.slice(0, end + 1);
if (candidate.length === 0 || !withinAnyRoot([candidate], a) || !withinAnyRoot([candidate], b))
return undefined;
return candidate;
}
export function readRootGrantCandidateOf(judged, boundary) {
const outside = allFindings(judged).filter((f) => f.kind === "outside_roots");
if (outside.length === 0)
return undefined;
const targets = [...new Set(outside.map((f) => f.read.target))];
const paths = targets.filter((p) => !targets.some((q) => q !== p && withinAnyRoot([p], q)));
if (outside.some((f) => f.read.candidate.spelled.split(/[\\/]/).includes("..")))
return undefined;
if (paths.some(hasWin32AliasedComponent))
return undefined;
const readsDirectory = new Set(outside
.filter((f) => (f.read.candidate.provenance === "traversal_root" && f.read.node !== "file") || f.read.candidate.provenance === "cd_target")
.map((f) => f.read.target));
const dirOf = (path) => (readsDirectory.has(path) ? path : parentDirLexically(path));
let candidate = dirOf(paths[0]);
for (const path of paths.slice(1)) {
if (candidate === undefined)
return undefined;
const d = dirOf(path);
if (d === undefined)
return undefined;
candidate = deepestCommonDir(candidate, d);
}
if (candidate === undefined)
return undefined;
const proposed = grantOfferFor(candidate, paths.length === 1 ? paths[0] : undefined, boundary);
if (proposed === undefined)
return undefined;
const widened = { ...boundary, roots: [...boundary.roots, proposed.dir] };
const replay = rejudge(judged, widened);
return foldShellReadFindings(replay).finding === "none" ? proposed : undefined;
}
export function grantOfferFor(candidate, single, bounds) {
const tooWide = parentDirLexically(candidate) === candidate ||
(bounds.homeDir !== undefined && withinAnyRoot([candidate], bounds.homeDir)) ||
bounds.roots.some((root) => withinAnyRoot([candidate], root));
return tooWide ? (single !== undefined ? { dir: single, covers: "exact" } : undefined) : { dir: candidate };
}
export function rejudge(judged, boundary) {
return {
plan: judged.plan,
segments: judged.segments.map((s) => ({
segment: s.segment,
findings: s.findings.map((f) => judgeRead(f.read, boundary)),
nested: s.nested.map((n) => rejudge(n, boundary)),
})),
guards: stationGuards(boundary),
writeGuards: writeFaceGuards(boundary),
...(judged.incomplete !== undefined ? { incomplete: judged.incomplete } : {}),
};
}