@sema-agent/core
Version:
Stateless, task-oriented AI agent core
81 lines (80 loc) • 4.6 kB
JavaScript
import { effectivePathSpellingOf } from "../../core/effective-path-target.js";
import { pathTargetOf } from "../../core/tool-registry.js";
import { fitMiddle, READ_DENY_SENTENCE_PART_MAX } from "./read-deny.js";
import { resolveKey, violationDetails } from "./safety.js";
import { readStationMemberOf } from "./shell-read-check.js";
import { grantOfferFor, parentDirLexically } from "./shell-read-judge.js";
const ADMISSION_MAX = 1024;
const VOUCH = { disposition: "vouch", finding: "none" };
export function createFileReadStation(opts) {
const admissions = new Map();
const remember = (toolCallId, key) => {
if (admissions.size >= ADMISSION_MAX) {
const oldest = admissions.keys().next().value;
if (oldest !== undefined)
admissions.delete(oldest);
}
admissions.set(toolCallId, key);
};
const check = {
judge(call, args, cwd, signal, callOpts) {
const member = readStationMemberOf(call, opts.tools)?.name;
if (member === undefined)
return null;
const toolName = call.toolName;
const selection = effectivePathSpellingOf({ toolName, args }, pathTargetOf(member), { root: opts.rootCanonical, cwd });
if (selection === undefined || "refused" in selection || selection.spelled === undefined)
return null;
const spelled = selection.spelled;
return (async () => {
const extra = opts.roots();
const r = await resolveKey(opts.env, opts.rootCanonical, spelled, signal, selection.base, extra, undefined, opts.readDeny, opts.readFace);
if (r.ok || r.violation.code !== "path_not_in_root" || r.violation.target === undefined || opts.excluded?.(r.violation.target) === true || opts.exempt?.(r.violation.target) === true)
return VOUCH;
const target = r.violation.target;
const toolCallId = callOpts.toolCallId;
const roots = [opts.rootCanonical, ...extra];
const details = violationDetails(r.violation);
const allowed = fitMiddle(roots.join(", "), READ_DENY_SENTENCE_PART_MAX);
if (callOpts.seat === "rejudge") {
return { disposition: "refuse", finding: "outside_roots", sentence: `Error (${toolName}): the approved edit reads "${fitMiddle(target, READ_DENY_SENTENCE_PART_MAX)}", outside the directories allowed for this session (${allowed}) — re-submit it as a fresh call and answer its card.`, details };
}
const info = pathTargetOf(member)?.absent === "base" ? await opts.env.fileInfo(target, signal) : undefined;
const readsDirectory = info?.ok === true && info.value.kind === "directory";
const offer = grantOfferFor(readsDirectory ? target : parentDirLexically(target), target, { roots, ...(opts.homeDir !== undefined ? { homeDir: opts.homeDir } : {}) });
return {
disposition: "ordinary",
finding: "outside_roots",
sentence: `reads outside the allowed directories for this session: "${fitMiddle(target, READ_DENY_SENTENCE_PART_MAX)}" is not under ${allowed}`,
details,
...(offer !== undefined ? { readRootCandidate: { dir: offer.dir, ...(offer.covers !== undefined ? { covers: offer.covers } : {}), clearsThisAsk: true } } : {}),
...(toolCallId !== undefined ? { admit: () => remember(toolCallId, target) } : {}),
};
})();
},
unanswered: () => VOUCH,
};
const admitted = (toolCallId, key) => {
if (toolCallId === undefined || admissions.get(toolCallId) !== key)
return false;
admissions.delete(toolCallId);
return true;
};
return { check, tools: opts.tools, admitted };
}
export function composeReadChecks(stations) {
if (stations.length === 0)
return undefined;
if (stations.length === 1)
return stations[0].check;
const byName = new Map();
for (const s of stations)
for (const n of s.tools)
if (!byName.has(n))
byName.set(n, s);
const stationOf = (call) => readStationMemberOf(call, byName)?.value;
return {
judge: (call, args, cwd, signal, callOpts) => stationOf(call)?.check.judge(call, args, cwd, signal, callOpts) ?? null,
unanswered: (call, cause) => (stationOf(call) ?? stations[0]).check.unanswered(call, cause),
};
}