UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

174 lines (173 loc) • 8.49 kB
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)))]; }