UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

81 lines (80 loc) • 4.6 kB
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), }; }