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