@sema-agent/core
Version:
Stateless, task-oriented AI agent core
236 lines (235 loc) • 13.3 kB
JavaScript
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 }));
};
}