UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

236 lines (235 loc) • 13.3 kB
import { anchorSpelling, canonicalizeTarget, familyFor, hasWin32AliasedComponent, isAbsoluteForFamily, normalizeAbsPathLexically, pathFamilyOf, pathIdentityOf, spellingOfIdentity, win32NamespaceScreen } from "../tools/fs/safety.js"; import { declaredPathTargetOf, pathTargetValue, protectivePathTargetOf } from "./tool-registry.js"; import { symlinkChainHops } from "./path-identity.js"; import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "./gate-bounded-await.js"; import { thrownText } from "./thrown-value.js"; import { canonicalize } from "./canonical-json.js"; export function pathTargetBaseOf(pt, bases) { return (pt?.base ?? "cwd") === "root" ? bases.root : (bases.cwd ?? bases.root); } export function splitAbsoluteGlobPattern(pattern, tree) { const family = tree ?? familyFor("own", pattern); const screened = win32NamespaceScreen(pattern, family); if (!screened.ok) return { refused: screened.refuse }; const spelled = screened.spelled; if (!isAbsoluteForFamily(family, spelled)) return undefined; const lastSep = (s) => (family === "win" ? Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\")) : s.lastIndexOf("/")); const m = /[*?[{]/.exec(spelled); const cut = m ? lastSep(spelled.slice(0, m.index)) : lastSep(spelled); if (cut <= 0 && (m === null || cut === -1)) return undefined; let baseDir = spelled.slice(0, cut); if (baseDir === "") baseDir = "/"; if (/^[A-Za-z]:$/.test(baseDir)) baseDir = baseDir + spelled[cut]; return { baseDir, relativePattern: spelled.slice(cut + 1) }; } export function effectivePathTargetOf(call, pt, bases) { const spelling = effectivePathSpellingOf(call, pt, bases); if (spelling === undefined) return undefined; if ("refused" in spelling) return { kind: "refused", reason: spelling.refused }; if (spelling.spelled !== undefined) return placeSpelling(spelling.spelled, spelling.base, bases); return spelling.base !== undefined && isAbsoluteForFamily(readingFamilyOf(bases, spelling.base), spelling.base) ? placeSpelling(spelling.base, undefined, bases) : unreadableBase(spelling.base); } export function effectivePathSpellingOf(call, pt, bases) { if (pt === undefined) return undefined; const declaredBase = pathTargetBaseOf(pt, bases); const spelled = pathTargetValue(call.args, pt); if (spelled !== undefined) return { spelled, base: declaredBase }; if ((pt.absent ?? "none") === "none") return undefined; if (pt.patternParam !== undefined) { const pattern = pathTargetValue(call.args, { param: pt.patternParam }); const split = pattern === undefined ? undefined : splitAbsoluteGlobPattern(pattern, pathFamilyOf(bases)); if (split !== undefined && "refused" in split) return { refused: split.refused }; if (split !== undefined) return { spelled: split.baseDir, base: declaredBase }; } return { base: declaredBase }; } export function placeSpelling(spelled, declaredBase, bases) { const family = readingFamilyOf(bases, spelled); const screened = win32NamespaceScreen(spelled, family); if (!screened.ok) return { kind: "refused", reason: screened.refuse }; const tree = bases.root ?? bases.cwd; if (isAbsoluteForFamily(family, screened.spelled)) return readingOf(pathIdentityOf(screened.spelled, family, tree), family); if (declaredBase === undefined || !isAbsoluteForFamily(family, declaredBase)) return unreadableBase(declaredBase); return readingOf(pathIdentityOf(anchorSpelling(family, declaredBase, screened.spelled), family, tree), family); } export function readingFamilyOf(bases, spelled) { return pathFamilyOf(bases) ?? familyFor("own", spelled); } function readingOf(r, family) { if (r.ok) return { kind: "path", identity: r.identity, family }; return r.reason === "relative" || r.reason === "unanchored" ? undefined : { kind: "refused", reason: r.reason }; } function unreadableBase(base) { return { kind: "unreadable", reason: base === undefined ? "the call's path is relative to a base that was not supplied" : `the base this call resolves against (${JSON.stringify(base)}) is not a fully-qualified path of this tree's family` }; } export const READ_ROOT_CANDIDATE_DIR_MAX = 1024; export function validatePath(value, contract) { if (typeof value !== "string" || !isAbsoluteForFamily("own", value)) return undefined; if (contract.intent === "validate-coordinate") return value; if (value.length > READ_ROOT_CANDIDATE_DIR_MAX) return undefined; if (/[\u0000-\u001f\u007f\u2028\u2029]/.test(value)) return undefined; const folded = normalizeAbsPathLexically(value); if (folded !== value || normalizeAbsPathLexically(`${value}/..`) === value) return undefined; if (hasWin32AliasedComponent(value)) return undefined; return win32NamespaceScreen(value, familyFor("own", value)).ok ? value : undefined; } export async function readPath(env, spelled, base, bases, contract, signal) { const lexical = placeSpelling(spelled, base, bases); const family = pathFamilyOf(bases); if (contract.views === "lexical" || env === undefined || family === undefined || lexical?.kind === "unreadable") return { lexical, hops: [] }; const canonical = await canonicalizeTarget(env, spelled, signal, family, base); const climbs = (s) => s !== undefined && (family === "win" ? /(^|[\\/])\.\.([\\/]|$)/ : /(^|\/)\.\.(\/|$)/).test(s); const followsALink = canonical.ok && (climbs(spelled) || climbs(base) || lexical?.kind !== "path" || absoluteTargetIdentityOf(canonical.key, bases) !== lexical.identity); const chain = contract.views === "chain" && followsALink ? await withGateTimeout(symlinkChainHops(env, spelled, signal, family, base), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "canonicalChain").catch((err) => ({ ok: false, cause: thrownText(err), asked: true })) : { ok: true, hops: [] }; return chain.ok ? { lexical, canonical, hops: chain.hops } : { lexical, canonical, hops: [], hopsUnread: true }; } export function absoluteTargetIdentityOf(spelled, bases) { const r = pathIdentityOf(spelled, pathFamilyOf(bases), bases.root ?? bases.cwd); return r.ok ? r.identity : undefined; } const READ_FOR = new WeakMap(); const REREAD = new WeakMap(); export async function readCallTarget(env, call, signal, leads) { const bases = { ...(call.root !== undefined ? { root: call.root } : {}), ...(call.cwd !== undefined ? { cwd: call.cwd } : {}) }; const views = new Map(); const viewOf = (pt) => { const sel = effectivePathSpellingOf(call, pt, bases); if (sel === undefined) return Promise.resolve(undefined); if ("refused" in sel) return Promise.resolve({ kind: "refused", reason: sel.refused }); const target = sel.spelled !== undefined ? { spelled: sel.spelled, base: sel.base } : sel.base !== undefined && isAbsoluteForFamily(readingFamilyOf(bases, sel.base), sel.base) ? { spelled: sel.base, base: undefined } : undefined; if (target === undefined) return Promise.resolve(unreadableBase(sel.base)); const { spelled, base } = target; const key = JSON.stringify([spelled, base ?? null]); const prior = views.get(key); if (prior !== undefined) return prior; const read = readPath(env, spelled, base, bases, { intent: "resolve", views: "chain" }, signal) .catch((err) => ({ lexical: placeSpelling(spelled, base, bases), canonical: { ok: false, message: `the environment threw resolving the target: ${thrownText(err)}` }, hops: [] })) .then(viewFrom); views.set(key, read); return read; }; const protective = await viewOf(protectivePathTargetOf(call)); const declared = call.face === undefined ? undefined : await viewOf(declaredPathTargetOf(call)); const led = leads === undefined ? undefined : [...leads].filter((e) => e[1] !== undefined && e[1] !== e[0]); const reading = Object.freeze({ ...(protective !== undefined ? { protective } : {}), ...(declared !== undefined ? { declared } : {}), ...(led !== undefined && led.length > 0 ? { leads: new Map(led) } : {}) }); if (typeof call.args === "object" && call.args !== null) READ_FOR.set(reading, call.args); REREAD.set(reading, (args, rereadSignal) => readCallTarget(env, { ...call, args }, rereadSignal, leads)); return reading; } const ARGS_READING = new WeakMap(); export function stampArgsReading(face, read) { ARGS_READING.set(face, read); } export function argsAsTheToolReads(req, args) { const read = req.face === undefined ? undefined : ARGS_READING.get(req.face); return read === undefined ? args : argsReadBy(read, args); } export function argsReadBy(read, args) { const out = read(args); if (out === args || canonicalize(out) === canonicalize(args)) return args; if (typeof out === "object" && out !== null) READ_FROM.set(out, args); return out; } const READ_FROM = new WeakMap(); export function argsReadFrom(args) { return typeof args === "object" && args !== null && READ_FROM.has(args) ? READ_FROM.get(args) : args; } export async function withArgsReading(req, args, signal) { const read = argsAsTheToolReads(req, args); const reading = callTargetReadingOf(req); const reread = reading === undefined ? undefined : REREAD.get(reading); return reread === undefined ? { ...req, args: read } : { ...req, args: read, targetReading: await reread(read, signal) }; } function viewFrom(v) { if (v.lexical === undefined) return undefined; if (v.lexical.kind !== "path") return Object.freeze({ ...v.lexical }); const canonical = v.canonical?.ok === true ? v.canonical.key : undefined; const unread = (v.canonical !== undefined && !v.canonical.ok) || v.hopsUnread === true; return Object.freeze({ kind: "path", lexical: spellingOfIdentity(v.lexical.identity, v.lexical.family), ...(canonical !== undefined ? { canonical } : {}), ...(v.hops.length > 0 ? { hops: Object.freeze([...v.hops]) } : {}), ...(unread ? { unread: true } : {}) }); } export function callTargetReadingOf(req) { const r = req.targetReading; return r !== undefined && READ_FOR.get(r) === req.args ? r : undefined; } export function callTargetViewsOf(req, direction, bare = {}) { const reading = callTargetReadingOf(req); if (reading !== undefined) { const v = direction === "tighten" ? reading.protective : reading.declared; if (v === undefined || v.kind === "refused") return undefined; if (v.kind === "unreadable") return { views: [], unreadable: v.reason }; if (direction === "allow" && (v.canonical === undefined || v.unread === true)) return { views: [], unreadable: ALLOW_UNRESOLVED }; const views = [{ path: v.lexical, kind: "lexical" }]; const seen = (p) => views.some((w) => w.path === p); if (v.canonical !== undefined && !seen(v.canonical)) views.push({ path: v.canonical, kind: "canonical" }); for (const hop of v.hops ?? []) if (!seen(hop)) views.push({ path: hop, kind: "hop" }); return { views }; } const own = req.root !== undefined || req.cwd !== undefined ? { ...(req.root !== undefined ? { root: req.root } : {}), ...(req.cwd !== undefined ? { cwd: req.cwd } : {}) } : bare; const target = effectivePathTargetOf(req, direction === "tighten" ? protectivePathTargetOf(req) : declaredPathTargetOf(req), own); if (target === undefined || target.kind === "refused") return undefined; if (target.kind === "unreadable") return { views: [], unreadable: target.reason }; if (direction === "allow") return { views: [], unreadable: ALLOW_UNRESOLVED }; return { views: [{ path: spellingOfIdentity(target.identity, target.family), kind: "lexical" }] }; } const ALLOW_UNRESOLVED = "where the target resolves was not read, so no allow by path can speak for it"; export function legCallTargetStamp(leg) { const taken = new WeakMap(); return (req, signal) => { const args = req.args; if (typeof args !== "object" || args === null || callTargetReadingOf(req) !== undefined || !leg.judgesPaths()) return undefined; const root = req.root ?? leg.root; const cwd = req.cwd ?? leg.cwd(); const prior = taken.get(args); const reading = prior !== undefined && prior.root === root && prior.cwd === cwd && prior.toolName === req.toolName && prior.toolCallId === req.toolCallId && prior.face === req.face ? prior.reading : leg.leads().then((leads) => readCallTarget(leg.env, { toolName: req.toolName, args, ...(req.face !== undefined ? { face: req.face } : {}), root, ...(cwd !== undefined ? { cwd } : {}) }, signal, leads)); if (prior?.reading !== reading) taken.set(args, { root, cwd, toolName: req.toolName, toolCallId: req.toolCallId, face: req.face, reading }); return reading.then((targetReading) => ({ ...req, targetReading })); }; }