@sema-agent/core
Version:
Stateless, task-oriented AI agent core
174 lines (173 loc) • 8.49 kB
JavaScript
import { denyFloorShadowsOf } from "./shell-read-judge.js";
export const SHELL_WRITE_COMMANDS = Object.freeze(["mkdir", "touch", "rm", "rmdir", "mv", "cp", "tee", "sed"]);
const WRITE_COMMAND_SET = new Set(SHELL_WRITE_COMMANDS);
export function spellsOption(word, long, short) {
if (word.startsWith("--")) {
const name = word.slice(2).split("=")[0];
return name.length > 0 && long.startsWith(name);
}
return new RegExp(`^-[A-Za-z]*${short}`).test(word);
}
export function programArgs(segment) {
const words = segment.argv.map((w) => ({ raw: w.raw, spelled: w.folded }));
const at = words.findIndex((w) => w.spelled === segment.verb?.name);
return at < 0 ? [] : words.slice(at + 1);
}
const SED_GRAMMARS = [
{ required: "efl", optional: "i", longRequired: ["expression", "file", "line-length"] },
{ required: "efiI", optional: "" },
];
const COPY_GRAMMARS = [
{ required: "St", optional: "", longRequired: ["target-directory", "suffix", "no-preserve", "sparse"] },
{ required: "", optional: "" },
];
function readArgv(words, grammar) {
const options = [];
const operands = [];
let endOfOptions = false;
for (let k = 0; k < words.length; k++) {
const w = words[k];
const s = w.spelled;
if (endOfOptions || !s.startsWith("-") || s === "-") {
operands.push(w);
continue;
}
if (s === "--") {
endOfOptions = true;
continue;
}
if (s.startsWith("--")) {
const eq = s.indexOf("=");
const name = s.slice(2, eq < 0 ? undefined : eq);
if (grammar.longRequired === undefined || name.length === 0)
continue;
const value = eq >= 0 ? { raw: w.raw, spelled: s.slice(eq + 1) } : grammar.longRequired.some((long) => long.startsWith(name)) ? words[++k] : undefined;
options.push({ name, long: true, ...(value !== undefined ? { value } : {}) });
continue;
}
for (let c = 1; c < s.length; c++) {
const letter = s[c];
const rest = s.slice(c + 1);
if (grammar.required.includes(letter)) {
const value = rest.length > 0 ? { raw: w.raw, spelled: rest } : words[++k];
options.push({ name: letter, long: false, ...(value !== undefined ? { value } : {}) });
break;
}
if (grammar.optional.includes(letter)) {
options.push({ name: letter, long: false, ...(rest.length > 0 ? { value: { raw: w.raw, spelled: rest } } : {}) });
break;
}
options.push({ name: letter, long: false });
}
}
return { options, operands };
}
export function sedOptionsOf(segment) {
const words = programArgs(segment);
let inPlace = false;
let followsLinks = false;
const suffixes = new Set();
for (const grammar of SED_GRAMMARS) {
for (const o of readArgv(words, grammar).options) {
if (o.long ? "in-place".startsWith(o.name) : o.name === "i" || o.name === "I") {
inPlace = true;
if (o.value !== undefined && o.value.spelled.length > 0)
suffixes.add(o.value.spelled);
}
if (o.long && "follow-symlinks".startsWith(o.name))
followsLinks = true;
}
}
return { inPlace, suffixes: [...suffixes], followsLinks };
}
export function sedBackupNames(name, suffix) {
const appended = `${name}${suffix}`;
return suffix.includes("*") ? [suffix.replace(/\*/g, () => name), appended] : [appended];
}
const OPERAND_SEATS = {
touch: (s) => (programArgs(s).some((w) => w.spelled.startsWith("-") && spellsOption(w.spelled, "no-dereference", "h")) ? ["landing", "entry"] : ["landing"]),
rm: () => ["entry"],
rmdir: () => ["entry"],
sed: (s) => (sedOptionsOf(s).followsLinks ? ["landing"] : ["entry"]),
mv: () => ["landing", "entry"],
cp: () => ["landing", "entry"],
};
const SOURCE_WRITTEN = { cp: [], mv: ["entry"] };
function sedBackupTargets(segment) {
const { suffixes, followsLinks } = sedOptionsOf(segment);
if (suffixes.length === 0)
return [];
const files = segment.reads.filter((c) => c.provenance === "operand" && c.expands === false);
return files.flatMap((f) => suffixes.flatMap((suffix) => followsLinks
? [{ candidate: { ...f, spelled: `${f.spelled}${suffix}`, face: "write" }, seats: ["entry"], followed: { operand: f, suffix } }]
: sedBackupNames(f.spelled, suffix).map((spelled) => ({ candidate: { ...f, spelled, face: "write" }, seats: ["entry"] }))));
}
function copyReadingsOf(segment) {
const out = [];
for (const grammar of COPY_GRAMMARS) {
const { options, operands } = readArgv(programArgs(segment), grammar);
const sources = [...operands];
const dest = options.findLast((o) => o.value !== undefined && (o.long ? "target-directory".startsWith(o.name) : o.name === "t"))?.value ?? sources.pop();
if (dest !== undefined)
out.push({ dest, sources });
}
return out;
}
function copySourceWordsOf(segment) {
const readings = copyReadingsOf(segment);
if (readings.length < COPY_GRAMMARS.length || programArgs(segment).some((w) => w.spelled.startsWith("-")))
return new Set();
const [first, ...rest] = readings.map((r) => new Set(r.sources.map((w) => w.raw)));
const dests = new Set(readings.map((r) => r.dest.raw));
return new Set([...first].filter((raw) => !dests.has(raw) && rest.every((s) => s.has(raw))));
}
function copyDestinationChildren(segment) {
const candidateOf = (raw) => segment.reads.find((c) => c.raw === raw && (c.provenance === "unlisted_word" || c.provenance === "operand"));
const children = new Map();
for (const { dest, sources } of copyReadingsOf(segment)) {
const destCandidate = candidateOf(dest.raw);
if (destCandidate !== undefined && destCandidate.expands !== false)
continue;
for (const src of sources) {
const source = candidateOf(src.raw);
if (source !== undefined && source.expands !== false)
continue;
const child = `${dest.spelled.replace(/\/+$/, "")}/${src.spelled.replace(/\/+$/, "").split("/").pop() ?? ""}`;
if (child.endsWith("/") || children.has(child))
continue;
children.set(child, destCandidate !== undefined && source !== undefined ? { ...destCandidate, spelled: child } : { spelled: child, raw: src.raw, provenance: "unlisted_word", expands: "unresolvable", bases: [] });
}
}
return [...children.values()];
}
function writeTargetsOf(segment) {
const out = [];
const write = (c, seats, written, child) => void out.push({ candidate: { ...c, face: "write" }, ...(seats !== undefined ? { seats } : {}), ...(written !== undefined ? { written } : {}), ...(child === true ? { child } : {}) });
for (const r of segment.redirects)
if (r.direction === "out" && r.target !== undefined)
write(r.target);
for (const c of segment.reads)
if (c.provenance === "write_redirect_target")
write(c);
const verb = segment.verb?.basename;
if (verb !== undefined && WRITE_COMMAND_SET.has(verb) && (verb !== "sed" || sedOptionsOf(segment).inPlace)) {
const seats = OPERAND_SEATS[verb]?.(segment);
const sourceWritten = SOURCE_WRITTEN[verb];
const sources = sourceWritten !== undefined ? copySourceWordsOf(segment) : undefined;
for (const c of segment.reads)
if (c.provenance === "operand" || c.provenance === "unlisted_word" || c.provenance === "option_value")
write(c, seats, sources?.has(c.raw) === true ? sourceWritten : undefined);
if (verb === "cp" || verb === "mv")
for (const c of copyDestinationChildren(segment))
write(c, seats, undefined, true);
if (verb === "sed")
out.push(...sedBackupTargets(segment));
}
return out;
}
export function judgedCandidatesOf(segment) {
const reads = segment.reads.filter((c) => c.provenance !== "write_redirect_target");
const writes = writeTargetsOf(segment);
const plain = (cs) => cs.map((candidate) => ({ candidate }));
return [...plain(reads), ...plain(denyFloorShadowsOf(reads)), ...writes, ...plain(denyFloorShadowsOf(writes.map((w) => w.candidate)))];
}