UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

405 lines (404 loc) • 18.1 kB
import { userInfo } from "node:os"; import { canonicalizeTarget, isAbsolutePathForm, joinForFamily, normalizeAbsPathLexically, pathFamilyOf } from "./safety.js"; import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "../../core/gate-bounded-await.js"; import { thrownText } from "../../core/thrown-value.js"; import { readDenyPatternRun, readDenyRunHead } from "./read-deny.js"; export const AUXILIARY_HOME_PROBE_MS = 2_000; const said = new Set(); function sayOnce(kind, line) { if (said.has(kind)) return; said.add(kind); try { console.warn(line); } catch { } } function isNoAccountRow(e) { try { const info = typeof e === "object" && e !== null ? e.info : undefined; return typeof info === "object" && info !== null && info.code === "ENOENT"; } catch { return false; } } function valueText(v) { try { return JSON.stringify(v) ?? String(v); } catch { return typeof v; } } function accountHomeOf() { try { const home = userInfo().homedir; if (typeof home === "string" && isAbsolutePathForm(home)) return { state: "home", home }; sayOnce("bad_value", `read-deny homes: the account home lookup answered ${valueText(home)}, not an absolute path — home-anchored read-deny rows are placed under the environment's declared home only.`); return { state: "none" }; } catch (e) { if (isNoAccountRow(e)) return { state: "none" }; sayOnce("unanswered", `read-deny homes: the account home could not be read (${thrownText(e)}) — home-anchored read-deny rows hold their run at every location until the lookup answers.`); return { state: "unanswered" }; } } function declaredHomeOf(env) { const home = env.homeDir; return home !== undefined && isAbsolutePathForm(home) ? home : undefined; } function declaredReadDenyHomesAnswered(env) { const home = declaredHomeOf(env); if (home === undefined) return { homes: [], whole: true }; const account = accountHomeOf(); if (account.state === "unanswered") return { homes: [], whole: false }; if (account.state === "none" || normalizeAbsPathLexically(account.home) === normalizeAbsPathLexically(home)) return { homes: [home], whole: true }; return { homes: [home, account.home], whole: true }; } export function declaredReadDenyHomes(env) { return declaredReadDenyHomesAnswered(env).homes; } function isDeclared(declared, home) { return declared !== undefined && normalizeAbsPathLexically(home) === normalizeAbsPathLexically(declared); } async function homeSpellingsOf(env, home, signal) { try { const canonical = await env.canonicalPath(home, signal); if (canonical.ok) return { spellings: canonical.value !== home ? [home, canonical.value] : [home], canonical: canonical.value, answered: true }; return { spellings: [home], answered: canonical.error.code === "not_found" || canonical.error.code === "not_directory" }; } catch { return { spellings: [home], answered: false }; } } export async function readDenyHomesOf(env, signal) { const listed = declaredReadDenyHomesAnswered(env); const [declared, ...auxiliary] = listed.homes; if (!listed.whole || declared === undefined) return []; const [own, ...others] = await Promise.all([ homeSpellingsOf(env, declared, signal), ...auxiliary.map((home) => until(homeSpellingsOf(env, home, signal), Date.now() + AUXILIARY_HOME_PROBE_MS, signal).then((r) => r ?? { spellings: [home], answered: false })), ]); if (!own.answered) return []; if (others.some((r) => !r.answered)) sayAuxiliary("its path probe did not answer"); return [...new Set([...own.spellings, ...others.flatMap((r) => r.spellings)])]; } async function until(work, deadline, signal) { return await withGateTimeout(work, Math.max(0, deadline - Date.now()), signal, "auxiliaryHome").catch(() => undefined); } function sayAuxiliary(what) { sayOnce("auxiliary", `read-deny homes: a home beside the environment's declared home did not answer its probes within ${AUXILIARY_HOME_PROBE_MS} ms (${what}) — its home-anchored read-deny rows sit at its spelling only, and a path through an alias of it is judged as spelled, until the next placement.`); } export async function readDenyHomeLinksOf(env, entries, homes, signal, deadline) { const rows = []; const familyOf = new Map(); for (const { pattern, caseSensitive } of entries) { const { homeAnchored, run } = readDenyPatternRun(pattern); if (!homeAnchored) continue; const head = run.split("/").filter(Boolean).slice(0, readDenyRunHead(run)).join("/"); if (head.length === 0) continue; const spelled = homes.map((home) => { const family = pathFamilyOf({ root: home }); const at = normalizeAbsPathLexically(joinForFamily(family, home, head)); familyOf.set(at, family); return at; }); rows.push({ pattern, caseSensitive: caseSensitive === true, spelled }); } const leadOf = async (at, family) => (await whereItLeads(env, at, family, signal)) ?? ((await holdsNothingBelow(env, at, signal)) ? at : undefined); const resolved = new Map(await Promise.all([...familyOf].map(async ([at, family]) => [at, deadline === undefined ? await leadOf(at, family) : await until(leadOf(at, family), deadline, signal)]))); const out = new Map(); const whole = [...resolved.values()].every((location) => location !== undefined); for (const { pattern, caseSensitive, spelled } of rows) { const fold = (p) => (caseSensitive ? p : p.replace(/[A-Z]/g, (c) => c.toLowerCase())); const own = new Set(spelled.map(fold)); const locations = []; for (const at of spelled) { const location = resolved.get(at); if (location !== undefined && !own.has(fold(location)) && !locations.includes(location)) locations.push(location); } if (locations.length > 0) out.set(pattern, locations); } return { linked: out, whole }; } async function holdsNothingBelow(env, at, signal) { const probe = await withGateTimeout(Promise.resolve().then(() => env.canonicalPath(at, signal)), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "readDenyHead").catch(() => undefined); return probe !== undefined && !probe.ok && probe.error.code === "not_directory"; } export async function whereItLeads(env, at, family = pathFamilyOf({ root: at }), signal) { try { const key = await canonicalizeTarget(env, at, signal, family); return key.ok ? normalizeAbsPathLexically(key.key) : undefined; } catch { return undefined; } } export async function whereItsAnsweredPrefixLeads(env, at, family = pathFamilyOf({ root: at }), signal) { const walk = async () => { const rest = []; for (let here = normalizeAbsPathLexically(at);;) { const led = await whereItLeads(env, here, family, signal); if (led !== undefined) return rest.reduce((p, name) => joinForFamily(family, p, name), led); const parent = normalizeAbsPathLexically(`${here}/..`); if (parent === here) return undefined; rest.unshift(here.slice(parent.length).replace(/^[\\/]+/, "")); here = parent; } }; return await withGateTimeout(walk(), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "ruleRowHead").catch(() => undefined); } const NO_CHAIN = { ancestors: [], whole: false }; async function chainAbove(env, identity, canonical, signal, deadline) { let own; const ancestors = []; let whole = true; for (let dir = normalizeAbsPathLexically(canonical), first = true;; first = false) { const id = deadline === undefined ? await identityOf(env, identity, dir, signal) : await until(identityOf(env, identity, dir, signal), deadline, signal); if (id === undefined && deadline !== undefined && Date.now() >= deadline) { whole = false; break; } if (id?.ok !== true) whole = false; else if (first) own = { dir, id: id.value }; else ancestors.push({ dir, id: id.value }); const parent = normalizeAbsPathLexically(`${dir}/..`); if (parent === dir) break; dir = parent; } return { ...(own !== undefined ? { own } : {}), ancestors, whole }; } async function identityOf(env, identity, path, signal) { try { return await identity.call(env, path, signal); } catch { return undefined; } } async function readAuxiliaryHome(env, home, opts, signal) { const deadline = Date.now() + AUXILIARY_HOME_PROBE_MS; const spelled = await until(homeSpellingsOf(env, home, signal), deadline, signal); let reading; if (spelled?.canonical === undefined) { const answered = spelled?.answered === true; reading = { place: [home], chain: answered ? { ancestors: [], whole: true } : NO_CHAIN, linked: new Map(), answered }; } else { const place = spelled.spellings; const identity = env.fileIdentity; const [chain, heads] = await Promise.all([ typeof identity === "function" ? chainAbove(env, identity, spelled.canonical, signal, deadline) : Promise.resolve({ ancestors: [], whole: true }), opts.entries !== undefined ? readDenyHomeLinksOf(env, opts.entries, place, signal, deadline) : Promise.resolve({ linked: new Map(), whole: true }), ]); reading = { place, chain, linked: heads.linked, answered: chain.whole && heads.whole }; } if (!reading.answered) sayAuxiliary("a probe did not answer"); return reading; } function homeViewSourceOver(env, auxiliary) { const declared = declaredHomeOf(env); const identity = env.fileIdentity; let declaredChain; const readDeclared = () => { if (declared === undefined || typeof identity !== "function") return Promise.resolve(NO_CHAIN); if (declaredChain === undefined) { const reading = withGateTimeout((async () => { const spelled = await homeSpellingsOf(env, declared); return spelled.canonical === undefined ? NO_CHAIN : await chainAbove(env, identity, spelled.canonical); })(), DEFAULT_GATE_AWAIT_TIMEOUT_MS, undefined, "homeChain").catch(() => NO_CHAIN); declaredChain = reading; void reading.then((c) => { if (!c.whole && declaredChain === reading) declaredChain = undefined; }); } return declaredChain; }; return { views(signal) { let index; const indexOf = () => (index ??= (async () => { const chains = [await readDeclared(), ...(await Promise.all(auxiliary)).map((a) => a.chain)]; const out = new Map(); for (const { own } of chains) if (own !== undefined && !out.has(own.id)) out.set(own.id, own.dir); for (const { ancestors } of chains) for (const { dir, id } of ancestors) if (!out.has(id)) out.set(id, dir); return out.size > 0 ? out : undefined; })()); const views = new Map(); const viewOf = (path, leafIsFile) => { const key = `${leafIsFile ? "f" : "d"}:${path}`; let v = views.get(key); if (v === undefined) { v = (async () => { const index = await indexOf(); if (index === undefined || typeof identity !== "function") return undefined; if (!leafIsFile) { const id = await identityOf(env, identity, path, signal); if (id === undefined) return undefined; if (id.ok) { const at = index.get(id.value); if (at !== undefined) return at; } else if (id.error.code !== "not_found" && id.error.code !== "not_directory") return undefined; } const parent = normalizeAbsPathLexically(`${path}/..`); if (parent === path) return undefined; const up = await viewOf(parent, false); return up === undefined ? undefined : joinForFamily(pathFamilyOf({ root: path }), up, path.slice(parent.length).replace(/^[\\/]+/, "")); })(); views.set(key, v); } return v; }; return { async of(path, opts) { if (typeof identity !== "function" || (declared === undefined && auxiliary.length === 0) || !isAbsolutePathForm(path)) return undefined; const spelled = normalizeAbsPathLexically(path); const view = await viewOf(spelled, opts?.leaf === "file"); return view === undefined || view === spelled ? undefined : view; }, }; }, }; } const PLACEMENT_VIEWS = new WeakMap(); function placementOf(matcher, homes, linked, whole) { const list = Object.freeze([...homes]); PLACEMENT_VIEWS.set(list, matcher.homeViews); return { matcher, homes: list, linked, whole }; } export function placementViewsOf(homes) { return PLACEMENT_VIEWS.get(homes); } export function sayPlacementLost() { try { console.warn("read-deny homes: this tool band was handed a home list that is not the placement's own (an engine defect) — its home-anchored read-deny rows hold their run at every location, and only the declared home's views are read."); } catch { } } function readAuxiliaryHomes(env, homes, opts, signal) { const declared = declaredHomeOf(env); return [...new Set(homes.filter((h) => isAbsolutePathForm(h) && !isDeclared(declared, h)))].map((home) => readAuxiliaryHome(env, home, opts, signal)); } export function homeViewsAt(env, homes) { return homeViewSourceOver(env, readAuxiliaryHomes(env, homes, {})); } export function homeViewsOf(env, judge, signal) { return (judge?.homeViews ?? homeViewsAt(env, [])).views(signal); } export function carryHomeViews(judge, source) { return { ...judge, homeViews: source }; } export async function placeReadDenyAtHome(env, compile, homes, signal) { const entries = compile([]).entries; const declared = declaredHomeOf(env); const heldAnywhere = (whole) => placementOf(carryHomeViews(compile([]), homeViewSourceOver(env, [])), [], new Map(), whole); const listed = homes === undefined ? declaredReadDenyHomesAnswered(env) : { homes, whole: true }; if (!listed.whole) return heldAnywhere(false); const own = listed.homes.filter((h) => isDeclared(declared, h)); let declaredAt = []; if (own.length > 0) { const ownSpellings = await withGateTimeout(homeSpellingsOf(env, own[0], signal), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "readDenyHomes"); if (!ownSpellings.answered) return heldAnywhere(false); declaredAt = [...new Set([...own, ...ownSpellings.spellings])]; } const auxiliary = readAuxiliaryHomes(env, listed.homes, { entries }, signal); const [heads, readings] = await Promise.all([readDenyHomeLinksOf(env, entries, declaredAt, signal), Promise.all(auxiliary)]); const at = [...new Set([...declaredAt, ...readings.flatMap((r) => r.place)])]; const linked = new Map(); for (const map of [heads.linked, ...readings.map((r) => r.linked)]) { for (const [pattern, locations] of map) linked.set(pattern, [...new Set([...(linked.get(pattern) ?? []), ...locations])]); } const matcher = linked.size === 0 ? compile(at) : compile(at, linked); return placementOf(carryHomeViews(matcher, homeViewSourceOver(env, auxiliary)), at, linked, heads.whole); } export function readDenyPlacedLater(unplaced, place) { let current = unplaced; let whole = false; let pending; const again = () => (pending ??= place() .then((placed) => { current = placed.matcher; whole = placed.whole; }, () => undefined) .finally(() => { pending = undefined; })); void again(); const ready = async () => { if (pending !== undefined) await pending; if (!whole) await again(); }; const settled = async () => { if (pending !== undefined) await pending; return current; }; const homeViews = { views(signal) { let views; return { of: async (path, opts) => await (await (views ??= settled().then((m) => m.homeViews.views(signal)))).of(path, opts) }; }, }; const matcher = { get entries() { return current.entries; }, get ruleRows() { return current.ruleRows; }, matchPath: (path, opts) => current.matchPath(path, opts), matchTarget: (canonicalKey, lexicalView, opts) => current.matchTarget(canonicalKey, lexicalView, opts), get rgExclusionGlobs() { return current.rgExclusionGlobs; }, get rgProbeGlobs() { return current.rgProbeGlobs; }, unexcludableUnder: (scope) => current.unexcludableUnder?.(scope) ?? false, rowAtOrBelow: (path) => current.rowAtOrBelow?.(path) ?? null, homeViews, }; return { matcher, ready }; }