UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

199 lines (198 loc) • 10.9 kB
import { SHELL_CONTROL_PREFIXES } from "../../shell/lex.js"; export const NOT_AUTO_ALLOWED = "— not auto-allowed"; export function programBasename(word) { return word.includes("/") ? (word.split("/").pop() ?? word) : word; } export const ASSIGNMENT_PREFIX = /^[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]\s]*\])?\+?=/; export function writtenAssignmentPrefix(token) { const prefix = ASSIGNMENT_PREFIX.exec(token)?.[0]; if (prefix === undefined) return undefined; const appends = prefix.endsWith("+="); const written = prefix.slice(0, prefix.length - (appends ? 2 : 1)); const bracket = written.indexOf("["); return { prefix, name: bracket < 0 ? written : written.slice(0, bracket), appends, settlesWholeName: !appends && bracket < 0 }; } export { SHELL_CONTROL_PREFIXES }; export const SHELL_BLOCK_WORDS = new Set(["for", "select", "case", "esac", "fi", "done", "function", "coproc", "[[", "]]", "in"]); export function isBareControlWord(words, i, set) { const w = words[i]; return w !== undefined && w.raw === w.folded && set.has(w.folded); } export function isLeadingAssignmentWord(words, i) { const w = words[i]; return w !== undefined && w.assignment !== undefined && w.assignment.bare && w.atCommandPosition; } export const ARGV_REENTRY_PROGRAM_RE = /^xargs$/; export const ARGV_REENTRY_DEFAULT_PROGRAM = "echo"; const NO_DECLARED_OPTIONS = { optionArity: new Map() }; const launcherRow = (pairs, extra = {}) => ({ optionArity: new Map(pairs), ...extra }); export const LAUNCHER_TABLE = new Map([ ["env", launcherRow([["-i", 0], ["-0", 0], ["-v", 0], ["--ignore-environment", 0], ["--null", 0], ["--debug", 0], ["-u", 1], ["--unset", 1]], { assignmentOperands: true, commandString: ["-S", "--split-string"] })], ["command", launcherRow([["-p", 0]])], ["builtin", NO_DECLARED_OPTIONS], ["exec", NO_DECLARED_OPTIONS], ["noglob", NO_DECLARED_OPTIONS], ["xargs", NO_DECLARED_OPTIONS], ["busybox", NO_DECLARED_OPTIONS], ["sudo", { optionArity: new Map(), assignmentOperands: true }], ["doas", NO_DECLARED_OPTIONS], ["pkexec", { optionArity: new Map(), whenNoProgram: "shell" }], ["su", { optionArity: new Map(), shellText: true }], ["runuser", { optionArity: new Map(), shellText: true }], ["chroot", { optionArity: new Map(), requiredOperands: 1, whenNoProgram: "shell" }], ["setpriv", NO_DECLARED_OPTIONS], ["nohup", NO_DECLARED_OPTIONS], ["nice", launcherRow([["-n", 1], ["--adjustment", 1]], { negativeIntegerIsOwnArgument: true })], ["ionice", NO_DECLARED_OPTIONS], ["chrt", { optionArity: new Map(), requiredOperands: 1 }], ["taskset", { optionArity: new Map(), requiredOperands: 1 }], ["stdbuf", launcherRow([["-i", 1], ["-o", 1], ["-e", 1], ["--input", 1], ["--output", 1], ["--error", 1]])], ["timeout", launcherRow([ ["--foreground", 0], ["--preserve-status", 0], ["--verbose", 0], ["-v", 0], ["-k", 1], ["-s", 1], ["--kill-after", 1], ["--signal", 1], ], { requiredOperands: 1 })], ["time", { optionArity: new Map([ ["-p", 0], ["-a", 0], ["-v", 0], ["-q", 0], ["--portability", 0], ["--append", 0], ["--verbose", 0], ["--quiet", 0], ["--help", 0], ["--version", 0], ["--", 0], ["-f", 1], ["-o", 1], ["--format", 1], ["--output", 1], ]), }], ["watch", { optionArity: new Map(), shellText: true }], ["setsid", NO_DECLARED_OPTIONS], ["flock", { optionArity: new Map(), requiredOperands: 1 }], ["unshare", { optionArity: new Map(), whenNoProgram: "shell" }], ["nsenter", { optionArity: new Map(), whenNoProgram: "shell" }], ["script", { optionArity: new Map(), shellText: true }], ["numactl", NO_DECLARED_OPTIONS], ["prlimit", NO_DECLARED_OPTIONS], ["systemd-run", NO_DECLARED_OPTIONS], ["strace", NO_DECLARED_OPTIONS], ["ltrace", NO_DECLARED_OPTIONS], ["valgrind", NO_DECLARED_OPTIONS], ["firejail", { optionArity: new Map(), whenNoProgram: "shell" }], ["bwrap", NO_DECLARED_OPTIONS], ]); export const COMMAND_LAUNCHERS = new Set(LAUNCHER_TABLE.keys()); function pastLauncherOptions(launcher, folded, i) { const row = LAUNCHER_TABLE.get(launcher); if (row === undefined) return i; for (;;) { const tok = folded[i]; if (tok === undefined) return i; if (row.negativeIntegerIsOwnArgument === true && /^-\d+$/.test(tok)) { i += 1; continue; } const step = launcherOptionStep(row, tok); if (step === undefined) return i; if (step === 2 && folded[i + 1] === undefined) return i; i += step; } } function launcherOptionStep(row, tok) { const exact = row.optionArity.get(tok); if (exact !== undefined) return exact === 1 ? 2 : 1; const eq = tok.startsWith("--") ? tok.indexOf("=") : -1; if (eq > 2 && row.optionArity.get(tok.slice(0, eq)) === 1) return 1; if (/^-[A-Za-z]./.test(tok) && row.optionArity.get(tok.slice(0, 2)) === 1) return 1; if (/^-[A-Za-z]+$/.test(tok) && [...tok.slice(1)].every((ch) => row.optionArity.get(`-${ch}`) === 0)) return 1; return undefined; } export const PROGRAM_POSITION_REFUSAL_KINDS = Object.freeze(["option_at_program_position", "control_syntax_at_program_position"]); export function readProgramPosition(words) { const folded = words.map((w) => w.folded); const controlWords = []; let i = 0; while (i < folded.length && isBareControlWord(words, i, SHELL_CONTROL_PREFIXES)) { const controlWord = folded[i]; controlWords.push(i); i = pastLauncherOptions(controlWord, folded, i + 1); } const none = (written, assignments, refusal) => ({ controlWords, written, assignments, launchers: [], argvFromStdin: false, runsDefaultProgram: false, program: undefined, refusal }); if (i >= folded.length) return none(undefined, []); const written = i; const assignments = []; while (i < folded.length && isLeadingAssignmentWord(words, i)) { assignments.push(i); i++; } const head = folded[i]; if (head === undefined) return none(written, assignments); const basename = programBasename(head); if (basename.startsWith("-") && basename !== "-") { const owner = controlWords.length > 0 ? folded[controlWords[controlWords.length - 1]] : head; return none(written, assignments, { kind: "option_at_program_position", owner, option: head }); } if (!COMMAND_LAUNCHERS.has(basename)) { return { controlWords, written, assignments, launchers: [], argvFromStdin: false, runsDefaultProgram: false, program: { index: i, name: head, basename }, refusal: undefined }; } const launchers = [{ index: i, name: basename }]; const owner = head; const shellRefusal = () => ({ kind: "control_syntax_at_program_position", owner, word: "sh", runsShell: true }); let owedOperands = LAUNCHER_TABLE.get(basename)?.requiredOperands ?? 0; let lastRow = LAUNCHER_TABLE.get(basename); const chain0 = (program, refusal) => ({ controlWords, written, assignments, launchers, argvFromStdin: false, runsDefaultProgram: false, program, refusal }); if (lastRow?.shellText === true) return chain0(undefined, shellRefusal()); let argvFromStdin = ARGV_REENTRY_PROGRAM_RE.test(basename); let lastReentry = argvFromStdin ? i : -1; const chain = (program, refusal) => ({ controlWords, written, assignments, launchers, argvFromStdin, runsDefaultProgram: lastReentry === folded.length - 1, program, refusal }); for (let j = pastLauncherOptions(basename, folded, i + 1); j < folded.length; j++) { const t = folded[j]; if (owedOperands > 0 && !t.startsWith("-")) { owedOperands -= 1; continue; } if (lastRow?.assignmentOperands === true && !t.startsWith("-") && t.includes("=")) continue; const wordBasename = programBasename(t); if (ARGV_REENTRY_PROGRAM_RE.test(wordBasename)) { argvFromStdin = true; lastReentry = j; lastRow = undefined; owedOperands = 0; continue; } if (COMMAND_LAUNCHERS.has(wordBasename)) { launchers.push({ index: j, name: wordBasename }); lastRow = LAUNCHER_TABLE.get(wordBasename); if (lastRow?.shellText === true) return chain(undefined, shellRefusal()); owedOperands = lastRow?.requiredOperands ?? 0; j = pastLauncherOptions(wordBasename, folded, j + 1) - 1; continue; } if (t.startsWith("-")) return chain(undefined, { kind: "option_at_program_position", owner, option: t }); if (SHELL_CONTROL_PREFIXES.has(wordBasename) || SHELL_BLOCK_WORDS.has(wordBasename)) { return chain(undefined, { kind: "control_syntax_at_program_position", owner, word: wordBasename }); } return chain({ index: j, name: t, basename: wordBasename }); } if (lastRow?.whenNoProgram === "shell") return chain(undefined, shellRefusal()); return chain(undefined); } export function argvReentryUnjudged(pos, acceptedTargets) { if (!pos.argvFromStdin) return false; const runs = pos.program?.basename ?? (pos.runsDefaultProgram ? ARGV_REENTRY_DEFAULT_PROGRAM : undefined); return runs === undefined || !acceptedTargets.has(runs); } export const SHELL_SCAN_DECLINES = Object.freeze(["unreadable_program_position", "stream_fed_operands"]); export function programPositionRunsShell(pos, shellProgram) { if (pos.refusal !== undefined) return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true; return pos.program !== undefined && shellProgram.test(pos.program.basename); } export function shellScanDeclineOf(pos, acceptedStreamTargets) { if (pos.refusal !== undefined) { return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true ? undefined : "unreadable_program_position"; } return argvReentryUnjudged(pos, acceptedStreamTargets) ? "stream_fed_operands" : undefined; } export function programPositionRefusalSentence(refusal) { switch (refusal.kind) { case "option_at_program_position": return `"${refusal.owner}" is given the option "${refusal.option}" where the program it runs would be — which program runs is not readable without that word's option table ${NOT_AUTO_ALLOWED}`; case "control_syntax_at_program_position": return `"${refusal.owner}" is given shell control syntax ("${refusal.word}") where the program it runs would be ${NOT_AUTO_ALLOWED}`; default: { const exhaustive = refusal; return exhaustive; } } }