UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

221 lines (220 loc) • 11.3 kB
import { literalHeredocBody, literalTextOf } from "../../shell/lex.js"; import { programBasename, readProgramPosition } from "./bash-program-position.js"; import { implementationRejects, implementationsOf, programRow, readArgRoles, selectedOptions, targetUserOf } from "./program-read-model.js"; import { namesStandardInput } from "./safety.js"; export function codeBodies(basename, args) { const row = programRow(basename); switch (row?.body) { case "first-operand": return firstOperandBody(row, args); case "assignment-values": return aliasValues(args); case "option-values": return shellTextOptionValues(row, args); case undefined: return []; } } function firstOperandBody(row, args) { const first = readArgRoles(row, args).find((e) => e.kind === "operand"); const action = first === undefined ? undefined : args[first.at]; return action === undefined || action === "" || action === "-" ? [] : [action]; } function aliasValues(args) { const values = []; for (const a of args) { const eq = a.indexOf("="); if (eq > 0) values.push(a.slice(eq + 1)); } return values; } function shellTextOptionValues(row, args) { const callbacks = []; for (const e of optionSection(readArgRoles(row, args))) { const text = shellTextOf(e, args); if (text !== undefined) callbacks.push(text); } return callbacks; } function optionSection(events) { const end = events.findIndex((e) => e.kind === "operand" || e.kind === "dash-word-operand" || e.kind === "end-of-options"); return end === -1 ? [...events] : events.slice(0, end); } function shellTextOf(e, args) { if (e.kind === "value" && e.role?.kind === "shell-text") return args[e.at]; if (e.kind === "option" && typeof e.reading === "object" && e.reading.role?.kind === "shell-text") return e.reading.attached; return undefined; } export function reentryText(basename, args, stdin) { const row = programRow(basename); const r = row?.reentry; if (row === undefined || r?.kind !== "shell") return undefined; const text = argvText(row, r.text, args); if (text !== undefined || stdin === undefined) return text; const source = scriptSourceOf(row, args); return (r.stdin === "script" && source.stdin) || (source.file !== undefined && namesStandardInput(args[source.file])) ? stdin : undefined; } function argvText(row, at, args) { if (at === "words") return args.length === 0 ? undefined : args.join(" "); if (at === "non-option-words") return args.filter((a) => !a.startsWith("-")).join(" ") || undefined; if (at === "file-operand") return undefined; for (const e of optionSection(readArgRoles(row, args))) { const text = shellTextOf(e, args); if (text !== undefined) return text; } return undefined; } export function launchedCommands(argv, pos = readProgramPosition(argv)) { const runs = pos.refusal?.kind === "control_syntax_at_program_position" ? pos.refusal.runsShell : undefined; if (runs === undefined) return []; const row = programRow(programBasename(argv[runs.launcher].folded)); const words = argv.slice(runs.from).map((w) => w.folded); const at = (k) => runs.from + k; if (row?.launcher?.shellText === "user") return userCommands(row, words, at); const user = row === undefined ? undefined : targetUserOf(row, argv.slice(runs.launcher + 1, runs.from).map((w) => w.folded)); const as = user !== undefined ? { as: user } : {}; if (words.length === 0 || row?.launcher === undefined) return [{ args: [], ...as }]; if (row.launcher.shellText === "joined") { const texts = new Map(); for (const view of implementationsOf(row)) { const events = readArgRoles(view, words); const first = events.find((e) => e.kind === "operand" || e.kind === "dash-word-operand")?.at ?? words.length; if (implementationRejects(row, view, words.slice(0, first))) continue; const joined = words.slice(first).map((_, k) => at(first + k)); const values = events.flatMap((e) => (e.kind === "option" || (e.kind === "value" && e.at < first) ? roleValue(e, "shell-text", words, at) ?? [] : [])); for (const t of [...(joined.length === 0 ? [] : [{ joined }]), ...values]) texts.set(JSON.stringify(t), t); } return texts.size === 0 ? [{ args: [], ...as }] : [...texts.values()].map((t) => ({ args: [{ fixed: "-c" }, t], ...as })); } return [{ args: [{ fixed: "-c" }, { joined: words.map((_, k) => at(k)), escaped: true }], ...as }]; } function roleValue(e, kind, words, at) { if (e.kind === "value" && e.role?.kind === kind) return { at: at(e.at) }; if (e.kind === "option" && typeof e.reading === "object" && e.reading.role?.kind === kind) return { at: at(e.at), from: words[e.at].length - e.reading.attached.length }; return undefined; } function userCommands(row, words, at) { const out = []; const valued = (e, kind) => roleValue(e, kind, words, at); for (const view of implementationsOf(row)) { const events = readArgRoles(view, words); const operands = events.filter((e) => e.kind === "operand"); const user = (operands[0]?.dash !== undefined ? operands[1] : operands[0])?.at; const end = user ?? words.length; const stops = view.grammars[0]?.afterFirstOperand === "operands"; const own = stops ? events.filter((e) => e.at < end) : events; const named = own.flatMap((e) => valued(e, "command") ?? []).at(-1); const fromEnvironment = selectedOptions(view, stops ? words.slice(0, end) : words, "one-implementation").some((o) => o.mode === "names-program"); const program = named !== undefined ? { program: named } : fromEnvironment ? { program: { spelled: "\"$SHELL\"" } } : {}; const target = targetUserOf(view, stops ? words.slice(0, end) : words.filter((_, k) => k !== user), { name: user === undefined ? "root" : words[user], dash: operands[0]?.dash !== undefined }); const runs = { ...program, ...(target !== undefined ? { as: target } : {}) }; if (stops) { out.push({ ...runs, args: user === undefined ? [] : words.slice(user + 1).map((_, k) => ({ at: at(user + 1 + k) })) }); continue; } const rest = operands.filter((e) => user !== undefined && e.at > user).map((e) => ({ at: at(e.at) })); const texts = own.flatMap((e) => valued(e, "shell-text") ?? []); if (texts.length === 0) out.push({ ...runs, args: rest }); for (const text of texts) out.push({ ...runs, args: [{ fixed: "-c" }, text, ...rest] }); } return out; } export function launchedReadings(argv, command) { const p = command.program; if (p === undefined) return { shell: "sh" }; const raw = (k, from = 0) => { if (from === 0) return argv[k].raw; const literal = literalTextOf(argv[k].pieces); return literal === undefined ? argv[k].folded.slice(from) : `'${literal.slice(from).replace(/'/g, "'\\''")}'`; }; const line = [("at" in p ? raw(p.at, p.from) : p.spelled), ...command.args.map((a) => ("fixed" in a ? a.fixed : "at" in a ? raw(a.at, a.from) : a.joined.map((k) => raw(k)).join(" ")))].join(" "); const literal = "at" in p ? literalTextOf(argv[p.at].pieces)?.slice(p.from ?? 0) : undefined; if (literal === undefined) return { shell: "sh", line }; const basename = programBasename(literal); const reentry = programRow(basename)?.reentry; return reentry?.kind === "shell" && reentry.text === "c-option" ? { shell: basename } : { line }; } export function launchedInput(argv, pos, spell) { const spelled = (word) => (args) => spellLaunched(argv, args, { word: (k, from) => word(k).slice(from), values: spell.values }); const asTexts = spelled(spell.text); const asArgs = spelled(spell.folded); const lines = new Set(); const programWords = []; const shells = launchedCommands(argv, pos).flatMap((c) => { const { shell, line } = launchedReadings(argv, c); if (line !== undefined) lines.add(line); if (shell === undefined) return []; if (line === undefined && c.program !== undefined && "at" in c.program && c.program.from === undefined) programWords.push(c.program.at); const words = c.args.map((x) => ("at" in x && x.from === undefined ? x.at : undefined)); return [{ shell, texts: asTexts(c.args), args: asArgs(c.args), words, ...(c.as !== undefined ? { as: c.as } : {}) }]; }); return { shells, programWords, lines: [...lines] }; } function sudoEscaped(field) { return field.replace(/[^A-Za-z0-9_$-]/gu, (c) => `\\${c}`); } function launchedSpellingOf(v) { return v.glob === true ? v.parts.map((p) => (p.pattern ? p.text : sudoEscaped(p.text))).join("") : sudoEscaped(v.spelled); } export function spellLaunched(argv, args, spell) { const field = (k) => { const literal = literalTextOf(argv[k].pieces); if (literal !== undefined) return sudoEscaped(literal); const values = spell.values?.(k); return values?.length === 1 ? launchedSpellingOf(values[0]) : argv[k].raw; }; return args.map((a) => ("fixed" in a ? a.fixed : "at" in a ? spell.word(a.at, a.from ?? 0) : a.joined.map((k) => (a.escaped === true ? field(k) : spell.word(k, 0))).join(" "))); } export function scriptFileAt(basename, args) { const row = programRow(basename); return row?.reentry?.kind === "shell" && argvText(row, row.reentry.text, args) === undefined ? scriptSourceOf(row, args).file : undefined; } function scriptSourceOf(row, args) { const events = readArgRoles(row, args); const options = optionSection(events); const end = events[options.length]?.at ?? args.length; if (selectedOptions(row, args.slice(0, end), "one-implementation").some((o) => o.mode === "interactive")) return { stdin: true }; const first = events.find((e) => e.kind === "operand"); if (first?.kind !== "operand" || first.dash !== undefined) return { stdin: true }; return { file: first.at, stdin: options.some((e) => e.kind === "option" && e.reading === "speculative") }; } export function stdinTextsOf(segment, fed) { const out = []; let replacesPipe = false; for (const t of segment.tokens) { if (t.kind === "heredoc") { const literal = t.quoted ? t.body : literalHeredocBody(t.body); out.push(literal === undefined ? { from: t.body } : { from: t.body, literal }); } else if (t.kind === "redirect" && (t.fd ?? 0) === 0 && (t.op === "<<<" || t.op === "<" || (t.op === "<&" && t.dup !== "-"))) { const literal = t.op === "<<<" && t.target !== undefined ? literalTextOf(t.target.pieces) : undefined; out.push({ from: `${t.op}${t.target?.raw ?? t.dup ?? ""}`, ...(literal !== undefined ? { literal } : {}) }); replacesPipe = true; } } return replacesPipe ? out : [...fed, ...out]; }