@sema-agent/core
Version:
Stateless, task-oriented AI agent core
462 lines (461 loc) • 24.5 kB
JavaScript
import { anchorSpelling, canonicalizeTarget, isAbsoluteForFamily, isWinFormPath, writeTargetPath, isUncPath, pathFamilyOf } from "../tools/fs/safety.js";
import { judgePathRows } from "./path-guard-rows.js";
import { protectivePathTargetOf } from "./tool-registry.js";
import { pathTargetBaseOf } from "./effective-path-target.js";
import { canonicalDataRootKeys, dataRootSpellings, relativeUnderRoot } from "./sensitive-path-policy.js";
import { chainHopsOrNone, symlinkChainHops } from "./path-identity.js";
import { readDenyPatternRun } from "../tools/fs/read-deny.js";
import { agentConfigDirSegments, agentConfigDirTail } from "./agent-config-dir.js";
import { thrownText } from "./thrown-value.js";
import { fromSource, gateWaitFailureOf } from "./gate-bounded-await.js";
const freezeTable = (rows) => Object.freeze(rows.map((r) => Object.freeze(r)));
export const WRITE_PROTECTED_DEFAULT_TABLE = freezeTable([
{ name: ".gitconfig", kind: "basename" },
{ name: ".gitmodules", kind: "basename" },
{ name: ".bashrc", kind: "basename" },
{ name: ".bash_profile", kind: "basename" },
{ name: ".zshrc", kind: "basename" },
{ name: ".zprofile", kind: "basename" },
{ name: ".profile", kind: "basename" },
{ name: ".zshenv", kind: "basename" },
{ name: ".zlogin", kind: "basename" },
{ name: ".zlogout", kind: "basename" },
{ name: ".bash_login", kind: "basename" },
{ name: ".bash_aliases", kind: "basename" },
{ name: ".bash_logout", kind: "basename" },
{ name: ".envrc", kind: "basename" },
{ name: ".ripgreprc", kind: "basename" },
{ name: ".mcp.json", kind: "basename" },
{ name: ".claude.json", kind: "basename" },
{ name: ".npmrc", kind: "basename" },
{ name: ".yarnrc", kind: "basename" },
{ name: ".yarnrc.yml", kind: "basename" },
{ name: ".pnp.cjs", kind: "basename" },
{ name: ".pnp.loader.mjs", kind: "basename" },
{ name: ".pnpmfile.cjs", kind: "basename" },
{ name: "bunfig.toml", kind: "basename" },
{ name: ".bunfig.toml", kind: "basename" },
{ name: ".bazelrc", kind: "basename" },
{ name: ".bazelversion", kind: "basename" },
{ name: ".bazeliskrc", kind: "basename" },
{ name: ".pre-commit-config.yaml", kind: "basename" },
{ name: "lefthook.yml", kind: "basename" },
{ name: ".lefthook.yml", kind: "basename" },
{ name: "lefthook.yaml", kind: "basename" },
{ name: ".lefthook.yaml", kind: "basename" },
{ name: "gradle-wrapper.properties", kind: "basename" },
{ name: "maven-wrapper.properties", kind: "basename" },
{ name: ".devcontainer.json", kind: "basename" },
{ name: "pyrightconfig.json", kind: "basename" },
{ name: ".git", kind: "segment" },
{ name: ".vscode", kind: "segment" },
{ name: ".idea", kind: "segment" },
{ name: ".claude", kind: "segment" },
{ name: ".husky", kind: "segment" },
{ name: ".cargo", kind: "segment" },
{ name: ".devcontainer", kind: "segment" },
{ name: ".yarn", kind: "segment" },
{ name: ".mvn", kind: "segment" },
{ name: ".config/git", kind: "segment-run" },
{ name: ".ssh", kind: "segment" },
{ name: ".gnupg", kind: "segment" },
{ name: ".sema.json", kind: "basename" },
{ name: ".sema", kind: "segment" },
{ name: "credentials.env", kind: "basename" },
]);
export function foldWriteProtectSegment(s) {
const cased = s.toLowerCase().replace(/ı/g, "i").replace(/ſ/g, "s");
return stripInvisibleFormat(cased) || cased;
}
function stripInvisibleFormat(s) {
return s.replace(/[\u200c-\u200f\u202a-\u202e\u206a-\u206f\ufeff]/g, "");
}
function stripWin32Component(raw, normalization) {
const base = normalization.invisibleFirst ? stripInvisibleFormat(raw) : raw;
return (normalization.stream ? base.replace(/:.*$/, "") : base).replace(/[. ]+$/, "");
}
function foldWriteProtectName(name) {
return name.split("/").map(foldWriteProtectSegment).join("/");
}
const KINDS = ["basename", "segment", "segment-run"];
function describeEntryValue(v) {
try {
return JSON.stringify(v) ?? String(v);
}
catch {
return String(v);
}
}
function normalizeWriteProtectedEntry(entry, owner = "writeProtectedPaths") {
const shape = typeof entry === "string" ? { name: entry, kind: entry.includes("/") ? "segment-run" : "segment" } : entry;
if (typeof shape !== "object" || shape === null || typeof shape.name !== "string") {
throw new Error(`${owner}: entry ${describeEntryValue(entry)} is not a name string or { name, kind } row.`);
}
const name = shape.name;
if (!KINDS.includes(shape.kind)) {
throw new Error(`${owner}: entry ${describeEntryValue(entry)} has kind ${describeEntryValue(shape.kind)} — known kinds: ${KINDS.join(", ")}.`);
}
const kind = shape.kind;
const { homeAnchored, run } = readDenyPatternRun(name);
if (homeAnchored) {
throw new Error(`${owner}: entry ${describeEntryValue(name)} opens with the home word "~", which this table does not place — its names are matched wherever their segments appear. ` +
(run.length > 0 ? `Spell the name without it (${describeEntryValue(run)}), which covers the home's file and every same-named one.` : "The home directory itself is not a row; spell the name of what it holds."));
}
if (name.includes("*") || name.includes("?")) {
throw new Error(`${owner}: entry ${describeEntryValue(name)} contains a wildcard metacharacter — this table speaks LITERAL names only (a silently-literal "*" would guard less than it reads); pattern semantics live in createSensitivePathPolicy.`);
}
if (name.includes("\\")) {
throw new Error(`${owner}: entry ${describeEntryValue(name)} contains a backslash — names are "/"-separated (both path families are matched); spell the segments with "/".`);
}
const segments = name.split("/").filter((s) => s.length > 0);
if (segments.length === 0) {
throw new Error(`${owner}: entry ${describeEntryValue(name)} contains no path segments and would protect nothing — remove the entry or spell the name.`);
}
if (segments.some((s) => s === "." || s === "..")) {
throw new Error(`${owner}: entry ${describeEntryValue(name)} contains a "." or ".." segment — the judge folds dot segments lexically before matching, so such a row could never match anything; spell the real name.`);
}
if (segments.length > 1 && kind !== "segment-run") {
throw new Error(`${owner}: entry ${describeEntryValue(name)} spans ${segments.length} segments but declares kind "${kind}" — multi-segment names match as kind "segment-run".`);
}
return { name: segments.join("/"), kind };
}
export function writeProtectedEntryProblem(entry, owner) {
try {
normalizeWriteProtectedEntry(entry, owner);
return undefined;
}
catch (e) {
return e.message;
}
}
export function writeProtectedEntryIdentity(entry) {
const row = normalizeWriteProtectedEntry(entry);
return `${row.kind}:${foldWriteProtectName(row.name)}`;
}
export function resolveWriteProtectedTable(entries) {
if (entries === undefined)
return WRITE_PROTECTED_DEFAULT_TABLE;
if (!Array.isArray(entries)) {
throw new Error(`writeProtectedPaths: expected an array of entries (whole-table replacement; [] = no write-protection table), got ${describeEntryValue(entries)}.`);
}
const out = [];
const seen = new Set();
for (const entry of entries) {
const row = normalizeWriteProtectedEntry(entry);
const key = writeProtectedEntryIdentity(row);
if (seen.has(key))
continue;
seen.add(key);
out.push(row);
}
return freezeTable(out);
}
function pathViewSegments(path, normalization, seps = BOTH_SEPARATOR_FAMILIES, anchorFloor = 0) {
const segs = [];
const win32 = normalization !== "literal";
for (const raw of path.split(seps)) {
if (raw.length === 0)
continue;
const dot = !win32 || /^\.{1,2}$/.test(raw) ? raw : raw.replace(/ +$/, "");
if (dot === ".")
continue;
if (dot === "..") {
const last = segs[segs.length - 1];
if (segs.length > anchorFloor && last !== "..")
segs.pop();
else if (segs.length > anchorFloor || anchorFloor === 0)
segs.push("..");
continue;
}
const stripped = normalization === "literal" ? raw : stripWin32Component(raw, normalization);
segs.push(stripped.length > 0 ? stripped : raw);
}
return segs;
}
const WIN32_NORMALIZATIONS = [
{ invisibleFirst: false, stream: false },
{ invisibleFirst: false, stream: true },
{ invisibleFirst: true, stream: false },
{ invisibleFirst: true, stream: true },
];
const BOTH_SEPARATOR_FAMILIES = /[\\/]/;
const POSIX_SEPARATOR_FAMILY = /\//;
function uncAnchorSegments(path) {
const third = path[2];
return isUncPath(path) && third !== undefined && third !== "\\" && third !== "/" ? 2 : 0;
}
function relativeUnderDataRoot(path, roots) {
for (const root of roots) {
if (root.segments.length === 0)
continue;
if (root.win ? !isWinFormPath(path) : isWinFormPath(path) || !path.startsWith("/"))
continue;
const seps = root.win ? BOTH_SEPARATOR_FAMILIES : POSIX_SEPARATOR_FAMILY;
const segs = pathViewSegments(path, "literal", seps, uncAnchorSegments(path));
if (segs.length < root.segments.length)
continue;
if (root.segments.every((s, i) => segs[i] === s))
return segs.slice(root.segments.length).join("/");
}
return null;
}
export function spelledExemptionOf(dataRootPaths) {
const roots = dataRootPaths.map((r) => {
const win = isWinFormPath(r);
return { win, segments: pathViewSegments(r, "literal", win ? BOTH_SEPARATOR_FAMILIES : POSIX_SEPARATOR_FAMILY, uncAnchorSegments(r)) };
});
return (spelling) => relativeUnderDataRoot(spelling, roots);
}
export const WRITE_PROTECTION_TARGET_VIEWS = Object.freeze(["spelling-only", "target"]);
export function compileWriteProtection(entries, opts) {
const rows = resolveWriteProtectedTable(entries);
if (rows.length === 0)
return undefined;
const { dataRoot, ...seats } = opts ?? {};
return compileWriteJudgment(rows, entries === undefined ? "builtin" : "replacement", { ...seats, exemptRoots: dataRootSpellings(dataRoot) }).matcher;
}
export function compileLegWriteJudgment(entries, opts) {
const rows = resolveWriteProtectedTable(entries);
const compiled = compileWriteJudgment(rows, "replacement", opts);
const guardRows = opts.guardRows;
return {
matcher: rows.length === 0 ? undefined : compiled.matcher,
judgeWriteTarget: rows.length === 0 && guardRows === undefined
? undefined
: async (target, judgeOpts) => {
const hit = await compiled.judgment(target, { ...(judgeOpts.signal !== undefined ? { signal: judgeOpts.signal } : {}), ...(judgeOpts.canonical === true ? { canonical: true } : {}) }, judgeOpts.fs, guardRows);
return hit === undefined ? undefined : hit.unreadable === undefined && TABLE_ROWS.has(hit.row) ? "protected" : "guarded";
},
};
}
const TABLE_ROWS = new WeakSet();
function heldRows(cause) {
return { name: "the deployment's guarded write rows", grammar: { kind: "segment-glob", run: "" }, polarity: "deny", faces: ["write"], placement: "anywhere", fold: "exact", exemptible: false, origin: "replacement", reach: () => ({ reach: "unreadable", reason: `the rows did not come back: ${cause}` }) };
}
function compileWriteJudgment(rows, origin, opts) {
const dataRootPaths = opts.exemptRoots;
const spelledRemainder = spelledExemptionOf(dataRootPaths);
const readingMemo = new Map();
const readingsOf = (spelling) => {
const known = readingMemo.get(spelling);
if (known !== undefined)
return known;
const raw = [pathViewSegments(spelling, "literal", POSIX_SEPARATOR_FAMILY, uncAnchorSegments(spelling)), pathViewSegments(spelling, "literal"), ...WIN32_NORMALIZATIONS.map((n) => pathViewSegments(spelling, n))];
const folded = raw.map((segs) => segs.map(foldWriteProtectSegment));
if (readingMemo.size >= 64)
readingMemo.clear();
readingMemo.set(spelling, folded);
return folded;
};
const literalRow = (row, reach, placement) => {
const minted = { name: row.name, kind: row.kind, grammar: { kind: "literal-name", match: row.kind, name: row.name }, polarity: "ask", faces: ["write"], placement, fold: "protective", exemptible: true, origin, reach };
TABLE_ROWS.add(minted);
return minted;
};
const tableRows = [];
for (const kind of KINDS) {
for (const row of rows) {
if (row.kind !== kind)
continue;
const parts = foldWriteProtectName(row.name).split("/").filter((s) => s.length > 0);
tableRows.push(literalRow(row, (view) => (readingsOf(view.path).some((segs) => literalNameMeets(kind, parts, segs)) ? REACHED : NOT_REACHED), "anywhere"));
}
}
const configTails = [];
for (const row of rows) {
const tail = row.kind === "basename" ? undefined : agentConfigDirTail(row.name);
if (tail !== undefined && opts?.agentConfigDir !== undefined)
configTails.push({ row, tail: foldWriteProtectName(tail).split("/").filter((s) => s.length > 0) });
}
const configRootSpelled = opts?.agentConfigDir === undefined ? [] : [agentConfigDirSegments(opts.agentConfigDir)];
const configRowsAt = (canonicalRoots) => configTails.map(({ row, tail }) => literalRow(row, (view) => {
if (!isAbsoluteForFamily("own", view.path))
return NOT_REACHED;
const roots = view.kind === "canonical" ? [...configRootSpelled, ...canonicalRoots] : configRootSpelled;
return readingsOf(view.path).some((segs) => roots.some((root) => atConfigRoot(segs, root.map(foldWriteProtectSegment), tail))) ? REACHED : NOT_REACHED;
}, { below: opts?.agentConfigDir ?? "" }));
const tableAt = (canonicalRoots) => (configTails.length === 0 ? tableRows : [...tableRows, ...configRowsAt(canonicalRoots)]);
const spelledView = (spelled, kind) => {
const remainder = spelledRemainder(spelled);
return { path: spelled, kind, ...(remainder !== null ? { exemptRemainder: remainder } : {}) };
};
const anchoredSpelling = (path, cwd) => {
const base = typeof cwd === "string" && cwd.length > 0 ? cwd : undefined;
return base !== undefined ? anchorSpelling(pathFamilyOf({ cwd: base }), base, path) : path;
};
const rootPath = opts?.rootPath;
const judgment = async (path, ctx, env, extraSource) => {
let noted = false;
const noteAbsence = (cause, unresolvedSymlink) => {
if (noted)
return;
noted = true;
opts?.onViewAbsent?.({ cause, unresolvedSymlink });
};
const sourceFailed = (err, what) => {
if (gateWaitFailureOf(err) !== "aborted")
noteAbsence(what === undefined ? thrownText(err) : `${what}: ${thrownText(err)}`, false);
};
const extra = await (async () => {
if (extraSource === undefined)
return [];
const answer = await fromSource(() => extraSource({ ...(env !== undefined ? { env } : {}), ...(ctx.signal !== undefined ? { signal: ctx.signal } : {}) }), ctx.signal, "deploymentRows");
if (answer.ok)
return answer.value;
sourceFailed(answer.cause, "the deployment's guarded write rows did not come back, so the writes they could guard are not placed");
return [heldRows(thrownText(answer.cause))];
})();
const givenKey = ctx.canonical === true && env !== undefined;
const base = ctx.base ?? ctx.cwd ?? rootPath;
const lexical = givenKey ? undefined : spelledView(anchoredSpelling(path, ctx.base ?? ctx.cwd), "lexical");
const bySpelling = lexical === undefined ? undefined : judgePathRows([...tableAt([]), ...extra], { views: [lexical] }, "tighten");
if (bySpelling !== undefined && bySpelling.unreadable === undefined && TABLE_ROWS.has(bySpelling.row))
return bySpelling;
if (env === undefined)
return bySpelling;
const family = pathFamilyOf({ root: rootPath, cwd: ctx.cwd ?? env.cwd });
const rootLookupFailed = (err) => sourceFailed(err, "a root's key did not come back, so that root takes no part");
let canonKey = givenKey ? path : undefined;
let canonAliased = false;
let targetUnread;
let targetUnreadSymlink = false;
if (!givenKey) {
const answer = await fromSource(() => canonicalizeTarget(env, path, ctx.signal, family, base), ctx.signal, "canonicalTarget");
if (!answer.ok) {
sourceFailed(answer.cause);
targetUnread = thrownText(answer.cause);
}
else if (answer.value.ok) {
canonKey = answer.value.key;
canonAliased = answer.value.aliasResolved === true;
}
else {
noteAbsence(answer.value.message, answer.value.unresolvedSymlink === true);
targetUnread = answer.value.message;
targetUnreadSymlink = answer.value.unresolvedSymlink === true;
}
}
const canonicalRoots = canonKey === undefined || configTails.length === 0 || opts?.agentConfigDir === undefined ? [] : (await canonicalDataRootKeys(env, [opts.agentConfigDir], ctx.signal, rootLookupFailed)).map(agentConfigDirSegments);
const table = [...tableAt(canonicalRoots), ...extra];
const aliased = canonAliased ? { aliasResolved: true } : {};
const views = [...(lexical !== undefined ? [{ ...lexical, ...aliased }] : []), ...(canonKey !== undefined ? [{ path: canonKey, kind: "canonical", ...aliased }] : [])];
const identity = {};
const identityRemainder = () => (identity.asked ??= (async () => {
if (canonKey === undefined)
return undefined;
for (const rootKey of await canonicalDataRootKeys(env, dataRootPaths, ctx.signal, rootLookupFailed)) {
const relative = relativeUnderRoot(canonKey, rootKey);
if (relative !== null)
return relative;
}
return undefined;
})());
const judged = async () => {
const unread = targetUnread !== undefined ? { unreadable: targetUnread } : {};
const hit = judgePathRows(table, { views, ...unread }, "tighten");
if (hit === undefined)
return undefined;
const relative = await identityRemainder();
const final = relative === undefined ? hit : judgePathRows(table, { views: views.map((v) => (v.kind === "canonical" ? { ...v, exemptRemainder: relative } : v)), identityExempt: true, ...unread }, "tighten");
return final !== undefined && final.unreadable !== undefined && targetUnread !== undefined && targetUnreadSymlink ? { ...final, unresolvedSymlink: true } : final;
};
const byTarget = canonKey === undefined ? undefined : await judged();
if (byTarget !== undefined && byTarget.unreadable === undefined && TABLE_ROWS.has(byTarget.row))
return byTarget;
if (givenKey)
return byTarget;
if (identity.asked !== undefined && (await identity.asked) !== undefined && table.every((row) => row.exemptible))
return byTarget;
const chain = await fromSource(() => symlinkChainHops(env, path, ctx.signal, family, base), ctx.signal, "canonicalChain");
if (!chain.ok) {
sourceFailed(chain.cause);
targetUnread ??= thrownText(chain.cause);
}
else {
if (!chain.value.ok && chain.value.asked) {
noteAbsence(chain.value.cause, false);
targetUnread ??= chain.value.cause;
}
for (const hop of chainHopsOrNone(chain.value))
views.push(spelledView(hop, "hop"));
}
return judged();
};
const matcher = {
rows,
targetView: opts?.env === undefined ? "spelling-only" : "target",
matchPath(path, cwd) {
const hit = judgePathRows(tableAt([]), { views: [spelledView(anchoredSpelling(path, cwd), "lexical")] }, "tighten");
return hit === undefined ? null : { name: hit.row.name, kind: hit.row.kind };
},
async judge(path, ctx = {}) {
const hit = await judgment(path, ctx, opts?.env);
if (hit === undefined)
return { kind: "clear" };
if (hit.unreadable !== undefined)
return { kind: "unresolvable", cause: hit.unreadable, unresolvedSymlink: hit.unresolvedSymlink === true };
return verdictOf(hit);
},
};
return { matcher, judgment };
}
const REACHED = { reach: "reached" };
const NOT_REACHED = { reach: "not_reached" };
function literalNameMeets(kind, parts, segs) {
if (segs.length === 0)
return false;
if (kind === "basename")
return segs[segs.length - 1] === parts[0];
if (kind === "segment")
return segs.includes(parts[0]);
for (let i = 0; i + parts.length <= segs.length; i++)
if (parts.every((p, j) => segs[i + j] === p))
return true;
return false;
}
function atConfigRoot(segs, root, tail) {
return segs.length >= root.length + tail.length && root.every((a, i) => segs[i] === a) && tail.every((t, i) => segs[root.length + i] === t);
}
function verdictOf(hit) {
return { kind: "hit", hit: { name: hit.row.name, kind: hit.row.kind }, via: hit.viewKind === "canonical" ? "target" : hit.viewKind === "hop" ? "chain" : "spelling" };
}
export function writeProtectionTightenDecision(toolName, verdict) {
if (verdict.kind === "clear")
return undefined;
if (verdict.kind === "unresolvable") {
return {
action: "deny",
message: `tool "${toolName}" is refused (target_unresolved): where its write target leads on disk could not be determined${verdict.unresolvedSymlink ? " (a link on the way could not be followed)" : ""}, so the write-protected table cannot tell whether it lands on a protected path — ${verdict.cause}`,
decisionReason: "safety",
};
}
return {
action: "ask",
message: verdict.via === "target"
? `tool "${toolName}" writes to a write-protected path — its real target resolves onto table entry "${verdict.hit.name}" — explicit approval required`
: verdict.via === "chain"
? `tool "${toolName}" writes to a write-protected path — its symlink chain passes through table entry "${verdict.hit.name}" — explicit approval required`
: `tool "${toolName}" writes to a write-protected path (table entry "${verdict.hit.name}") — explicit approval required`,
decisionReason: "safety",
};
}
export function writeProtectionMandated(verdict) {
return verdict.kind === "hit";
}
export function createWriteProtectionCheck(entries, opts) {
const matcher = compileWriteProtection(entries, opts);
return matcher === undefined ? undefined : writeProtectionMountOver(matcher, opts?.rootPath);
}
export function writeProtectionMountOver(matcher, rootPath) {
const check = (toolName, args, face, cwd, signal) => {
const call = { toolName, args, ...(face !== undefined ? { face } : {}) };
const protective = protectivePathTargetOf(call);
if (protective === undefined || protective.access === "read")
return null;
const path = writeTargetPath(call, protective);
if (typeof path !== "string" || path.length === 0)
return null;
return matcher.judge(path, { ...(cwd !== undefined ? { cwd } : {}), ...(signal !== undefined ? { signal } : {}), ...(() => { const b = pathTargetBaseOf(protective, { root: rootPath, cwd }); return b !== undefined ? { base: b } : {}; })() });
};
return { check, targetView: matcher.targetView };
}