UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

2,086 lines • 89.2 kB
import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf } from "./safety.js";
import { PARSE_FAILURE_SENTENCE, UNRESOLVED_SENTENCE, argvOf, lexShell, literalTildeSpelling, } from "../../shell/lex.js";
import { COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
export const BASH_READONLY_DEFAULT_ALLOW = [
    "ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
    "grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
    "cal", "uptime", "id", "groups", "nproc", "locale", "free", "realpath", "readlink",
    "true", "false", "seq", "expr", "type", "sleep", "diff", "cmp", "comm",
];
export const BASH_CLASSIFY_EXTRA_ALLOW = ["find", "sed", "cd"];
export function bashClassifyDefaultAllow() {
    return [...BASH_READONLY_DEFAULT_ALLOW, ...BASH_CLASSIFY_EXTRA_ALLOW];
}
const SHELL_OPERATORS = /[;&|<>$()`\n\r\\]/;
function hasBareShellOperator(s, quotedOperatorsAreText) {
    if (!quotedOperatorsAreText)
        return SHELL_OPERATORS.test(s);
    const lexed = lexShell(s);
    if ("parseFailure" in lexed)
        return SHELL_OPERATORS.test(s);
    if (lexed.segments.length > 1)
        return true;
    for (const t of lexed.segments[0].tokens) {
        if (t.kind !== "word")
            return true;
        if (expandsDollar(t) || t.assignment?.form === "compound")
            return true;
        if (t.pieces.some((p) => p.quotedBy === "\\" || (p.quotedBy === undefined && p.literal !== undefined && /[\r\\]/.test(p.literal))))
            return true;
    }
    return false;
}
function expandsDollar(w) {
    if (w.quotedSpans.some((q) => q.by === '$"'))
        return true;
    for (const p of w.pieces) {
        const k = p.expansion?.kind;
        if (k === "parameter" || k === "command" || k === "process" || k === "arithmetic")
            return true;
        if (p.literal !== undefined && (p.quotedBy === undefined || p.quotedBy === '"') && /[$`]/.test(p.literal))
            return true;
    }
    return false;
}
export function parseLeadingCommandName(command, options) {
    const trimmed = command.trim();
    if (!trimmed)
        return { reject: "empty command" };
    if (hasBareShellOperator(trimmed, options?.quotedOperatorsAreText === true)) {
        return {
            reject: "shell operators (pipes, redirects, ;, &&, command substitution, subshells, variable expansion) are not allowed — run a single simple command",
        };
    }
    const first = trimmed.split(/[ \t]+/)[0];
    if (first.includes("/") && options?.pathPrefixedNameIsText !== true) {
        return { reject: "the command must be a bare name resolved via PATH (no path prefix)" };
    }
    if (first.includes("="))
        return { reject: "leading environment-variable assignments are not allowed" };
    if (first.startsWith("#"))
        return { reject: "empty command" };
    if (/["'{}*?[\]~]/.test(first))
        return { reject: "the command name must be a bare token (no quotes, braces, globs, or ~)" };
    return { name: first };
}
export function coarseReadonlyCheck(command, allow, options) {
    const parsed = parseLeadingCommandName(command, options);
    if ("reject" in parsed)
        return parsed.reject;
    if (!allow.has(parsed.name))
        return `command "${parsed.name}" is not in the read-only allowlist`;
    return undefined;
}
const SHELL_SEGMENT_QUOTE_BLIND_REJECT = /[<>()`\n\r\\]/;
const SHELL_EXPANSION_CHARS = /[$]/;
export function carriesShellRedirection(text) {
    return SHELL_REDIRECTION_OPERATORS.test(text);
}
const SHELL_REDIRECTION_OPERATORS = /[<>]/;
function isDataFreeRedirection(r) {
    if (r.op === ">" && r.fd === 2 && r.target !== undefined && r.target.raw === "/dev/null")
        return true;
    return r.op === ">&" && r.dup !== undefined && /^[12]$/.test(r.dup) && (r.fd === undefined || r.fd === 1 || r.fd === 2);
}
const BACKGROUND_REFUSAL = "`&` backgrounding is not allowed — a backgrounded process outlives the command";
const PIPE_BOTH_REFUSAL = "`|&` pipes standard error as well as standard output — not a connector this face reads";
export function strictResidualRefusal(lexed, source, opts = {}) {
    const tolerate = opts.tolerateRedirection === true;
    const reason = tolerate
        ? "command/variable substitution, subshells, escapes, and line breaks are not allowed"
        : "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed";
    const inClass = (text) => {
        for (const ch of text)
            if (SHELL_SEGMENT_QUOTE_BLIND_REJECT.test(ch) && !(tolerate && SHELL_REDIRECTION_OPERATORS.test(ch)))
                return true;
        return false;
    };
    if ("parseFailure" in lexed)
        return inClass(source) || SHELL_EXPANSION_CHARS.test(source) ? reason : PARSE_FAILURE_SENTENCE[lexed.parseFailure.reason];
    const words = [];
    for (const s of lexed.segments) {
        if (s.connector === "\n")
            return reason;
        for (const t of s.tokens) {
            if (t.kind === "word") {
                if (inClass(t.raw))
                    return reason;
                words.push(t);
                continue;
            }
            if (t.kind !== "redirect")
                return reason;
            if (opts.dataFreeRedirections === true && isDataFreeRedirection(t))
                continue;
            if (!tolerate)
                return reason;
            if (t.target !== undefined) {
                if (inClass(t.target.raw))
                    return reason;
                words.push(t.target);
            }
        }
    }
    const home = opts.homeDir !== undefined && !homeMayBeRebound(source) ? opts.homeDir : undefined;
    for (const w of words) {
        if (expandsDollar(w) && !(home !== undefined && homeSpelledForClassify(w, home) !== undefined))
            return reason;
    }
    for (const s of lexed.segments) {
        if (s.connector === "&")
            return BACKGROUND_REFUSAL;
        if (s.pipesStderr)
            return PIPE_BOTH_REFUSAL;
    }
    for (const w of words)
        if (w.unresolved !== undefined)
            return UNRESOLVED_SENTENCE[w.unresolved];
    return undefined;
}
export function splitShellCompoundSegments(source, options) {
    const scanned = options?.trailingTerminator !== "keep" ? source.replace(/;\s*$/, "") : source;
    if (/[\n\r]/.test(source.slice(scanned.length)))
        return { reject: options?.redirection === "tolerate" ? "command/variable substitution, subshells, escapes, and line breaks are not allowed" : "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
    const lexed = lexShell(scanned);
    const refused = strictResidualRefusal(lexed, scanned, {
        tolerateRedirection: options?.redirection === "tolerate",
        dataFreeRedirections: options?.redirection === "data-free",
        ...(options?.homeDir !== undefined ? { homeDir: options.homeDir } : {}),
    });
    if (refused !== undefined)
        return { reject: refused };
    const command = lexed;
    const connectors = command.segments.map((s) => s.connector);
    return { segments: command.segments.map((s) => s.text), pipeFed: connectors.map((k) => k === "|"), connectors, lexed: command.segments };
}
export function formatOutOfRootReadApprovalOption(directory) {
    const trimmed = directory.replace(/[/\\]+$/, "");
    const cut = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
    const leaf = cut >= 0 ? trimmed.slice(cut + 1) : trimmed;
    return `Yes, allow reading from ${leaf || directory}/ from this project`;
}
export const NO_PATH_OPERAND_COMMANDS = new Set([
    "pwd", "echo", "printf", "whoami", "uname", "which", "tr", "basename", "dirname",
    "cal", "uptime", "id", "groups", "nproc", "locale", "free", "true", "false", "seq", "expr", "type", "sleep",
    "test", "[",
]);
export const NO_PATH_OPERAND_EVALUATORS = new Set(["test", "["]);
export const DECLARATION_BUILTINS = new Set(["export", "declare", "typeset", "local", "readonly"]);
export const INERT_BUILTINS = new Set(["echo", "true", "false", "pwd", ...DECLARATION_BUILTINS]);
const MESSAGE_SLOTS = {
    git: {
        globalPlain: ["-p", "--paginate", "-P", "--no-pager", "--bare", "--no-replace-objects", "--literal-pathspecs", "--glob-pathspecs", "--noglob-pathspecs", "--icase-pathspecs", "--no-optional-locks"],
        globalValue: ["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--config-env", "--super-prefix"],
        subcommands: {
            commit: { plain: "apsnvqeioz", optional: "Su", value: "mFCct", longValue: ["message", "file", "template", "pathspec-from-file", "author", "date", "cleanup", "fixup", "squash", "trailer", "reuse-message", "reedit-message"] },
            tag: { plain: "asfdvle", optional: "n", value: "mFu", longValue: ["message", "file", "local-user", "cleanup", "trailer", "sort", "format", "contains", "no-contains", "points-at", "merged", "no-merged"] },
            merge: { plain: "nqve", optional: "S", value: "mFsX", longValue: ["message", "file", "strategy", "strategy-option", "into-name", "cleanup"] },
            stash: { plain: "pkuaqS", optional: "", value: "m", longValue: ["message", "pathspec-from-file"] },
        },
    },
};
export function messageSlotWords(program, words, programIndex) {
    const out = new Set();
    const row = MESSAGE_SLOTS[program];
    if (row === undefined)
        return out;
    let j = programIndex + 1;
    for (; j < words.length; j++) {
        const w = words[j].folded;
        if (!w.startsWith("-"))
            break;
        if (row.globalValue.includes(w))
            j++;
        else if (!row.globalPlain.includes(w) && !row.globalValue.some((o) => o.startsWith("--") && w.startsWith(`${o}=`)))
            return out;
    }
    const sub = row.subcommands[words[j]?.folded ?? ""];
    if (sub === undefined)
        return out;
    const settled = (k) => words[k] !== undefined && words[k].expands === false;
    let pending;
    for (let k = j + 1; k < words.length; k++) {
        const w = words[k].folded;
        if (pending !== undefined) {
            if (pending === "message" && settled(k))
                out.add(k);
            pending = undefined;
            continue;
        }
        if (w === "--")
            break;
        if (!settled(k)) {
            pending = "other";
            continue;
        }
        if (!w.startsWith("-") || w === "-")
            continue;
        if (w.startsWith("--")) {
            const eq = w.indexOf("=");
            const name = w.slice(2, eq < 0 ? undefined : eq);
            const takers = sub.longValue.filter((n) => n.startsWith(name));
            const message = name.length > 0 && takers.length > 0 && takers.every((n) => n === "message") || name === "message";
            if (eq >= 0) {
                if (message)
                    out.add(k);
                continue;
            }
            if (takers.length > 0)
                pending = message ? "message" : "other";
            continue;
        }
        const letters = w.slice(1);
        for (let i = 0; i < letters.length; i++) {
            const c = letters[i];
            if (sub.plain.includes(c))
                continue;
            if (sub.optional.includes(c))
                break;
            if (sub.value.includes(c)) {
                if (i < letters.length - 1) {
                    if (c === "m")
                        out.add(k);
                }
                else
                    pending = c === "m" ? "message" : "other";
                break;
            }
            pending = "other";
            break;
        }
    }
    return out;
}
const XARGS_TARGETS_ACCEPTED_UNJUDGED = NO_PATH_OPERAND_COMMANDS;
export function isPathShapedToken(tok) {
    return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
}
export function resolveOperandLexically(base, operand, homeDir) {
    return resolveOperandSpellings(base, operand, homeDir)?.folded;
}
const PARENT_SEGMENT_RE = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
export function resolveOperandSpellings(base, operand, homeDir) {
    let raw = operand;
    if (raw === "~" || raw.startsWith("~/")) {
        if (homeDir === undefined || !isAbsolutePathForm(homeDir))
            return undefined;
        raw = raw === "~" ? homeDir : `${homeDir.replace(/[/\\]+$/, "")}/${raw.slice(2)}`;
    }
    else if (raw.startsWith("~")) {
        return undefined;
    }
    const family = base === undefined ? undefined : pathFamilyOf({ root: base });
    if (!win32NamespaceScreen(raw, family).ok)
        return undefined;
    if (isAbsoluteForFamily(family, raw))
        return spellingsOf(nativeUncSpellingOf(family, raw));
    if (base === undefined || family === undefined || !isAbsoluteForFamily(family, base))
        return undefined;
    if (isShellRootedSpellingUnmapped(family, raw))
        return undefined;
    return spellingsOf(joinForFamily(family, base, raw));
}
function spellingsOf(absolute) {
    const folded = normalizeAbsPathLexically(absolute);
    return PARENT_SEGMENT_RE.test(absolute) ? { folded, spelled: absolute } : { folded };
}
function reheadSegment(toks, head, from = 0) {
    return { folded: [head, ...toks.folded.slice(from + 1)], raw: [head, ...toks.raw.slice(from + 1)] };
}
export const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
function segmentSpellings(words, homeDir, homeRebound) {
    const folded = [];
    const raw = [];
    for (const w of words) {
        const spelled = homeDir !== undefined && !homeRebound ? homeSpelledForClassify(w, homeDir) : undefined;
        folded.push(spelled ?? w.folded);
        raw.push(spelled ?? w.raw);
    }
    return { folded, raw };
}
export function expansionInWord(w) {
    for (const p of w.pieces) {
        const k = p.expansion?.kind;
        if (k === "parameter" || k === "command" || k === "process" || k === "arithmetic" || k === "brace")
            return true;
        if (p.literal === undefined)
            continue;
        if (p.quotedBy === undefined && /[{}$`]/.test(p.literal))
            return true;
        if (p.quotedBy === '"' && /[$`]/.test(p.literal))
            return true;
    }
    return false;
}
const isGlobPiece = (p) => p.expansion?.kind === "glob";
export function globInWord(w) {
    for (const p of w.pieces) {
        if (isGlobPiece(p))
            return true;
        if (p.literal !== undefined && p.quotedBy === undefined && /[*?[]/.test(p.literal))
            return true;
    }
    return false;
}
function longOptionNameOf(tok) {
    if (!tok.startsWith("--") || tok.length === 2)
        return undefined;
    const body = tok.slice(2);
    const eq = body.indexOf("=");
    const name = eq >= 0 ? body.slice(0, eq) : body;
    return name.length > 0 ? name : undefined;
}
function isLongOptionAbbrevOf(name, full) {
    return full.startsWith(name);
}
export function suppliesFirstOperandSlot(slot, tok) {
    if (tok.startsWith("--")) {
        const long = longOptionNameOf(tok);
        return long !== undefined && slot.suppliedByLongNames.some((full) => isLongOptionAbbrevOf(long, full));
    }
    return new RegExp(`^-[A-Za-z0-9]*[${slot.suppliedByLetters}]`).test(tok);
}
function isGrepClusterFileStdin(tok) {
    if (tok.startsWith("--") || !/^-[A-Za-z0-9]/.test(tok))
        return false;
    for (let i = 1; i < tok.length; i++) {
        const ch = tok[i];
        if (ch === "e")
            return false;
        if (ch === "f")
            return tok.slice(i + 1) === "-";
        if (!/[A-Za-z0-9]/.test(ch))
            return false;
    }
    return false;
}
function valueNamesAFile(kind) {
    return kind === "path" || kind === "indirect" || kind === "form" || kind === "header" || kind === "url";
}
function widestValueKind(kinds) {
    if (kinds.has("indirect"))
        return "indirect";
    if (kinds.has("path"))
        return "path";
    if (kinds.has("form"))
        return "form";
    if (kinds.has("header"))
        return "header";
    if (kinds.has("url"))
        return "url";
    return [...kinds][0];
}
const VERB_OPTION_SLOTS = {
    head: {
        shortValue: { c: "count", n: "count" },
        shortPlain: "qvz",
        longValue: { bytes: "count", lines: "count" },
        longOptionalValue: {},
        longPlain: ["help", "quiet", "silent", "verbose", "version", "zero-terminated"],
    },
    tail: {
        shortValue: { b: "count", c: "count", n: "count", s: "count" },
        shortPlain: "fFqvz",
        longValue: { bytes: "count", lines: "count", "max-unchanged-stats": "count", pid: "count", "sleep-interval": "count" },
        longOptionalValue: { follow: "label" },
        longPlain: ["help", "quiet", "retry", "silent", "verbose", "version", "zero-terminated"],
    },
    cut: {
        shortValue: { b: "count", c: "count", d: "delimiter", f: "count" },
        shortPlain: "nsz",
        longValue: { bytes: "count", characters: "count", delimiter: "delimiter", fields: "count", "output-delimiter": "delimiter" },
        longOptionalValue: {},
        longPlain: ["complement", "help", "only-delimited", "version", "zero-terminated"],
    },
    grep: {
        shortValue: { A: "count", B: "count", C: "count", D: "label", d: "label", e: "pattern", f: "path", m: "count" },
        shortPlain: "0123456789abcEFGhHiIlLnoPqrRsUvVwxyzZ",
        longValue: {
            "after-context": "count",
            "before-context": "count",
            "binary-files": "label",
            devices: "label",
            directories: "label",
            exclude: "name-pattern",
            "exclude-dir": "name-pattern",
            "exclude-from": "path",
            file: "path",
            "group-separator": "label",
            include: "name-pattern",
            label: "label",
            "max-count": "count",
            regexp: "pattern",
        },
        longOptionalValue: { color: "label", colour: "label", context: "count" },
        firstOperand: { kind: "pattern", suppliedByLetters: "ef", suppliedByLongNames: ["regexp", "file"] },
        longPlain: [
            "basic-regexp", "byte-offset", "count", "dereference-recursive", "extended-regexp",
            "files-with-matches", "files-without-match", "fixed-strings", "help", "ignore-case", "initial-tab",
            "invert-match", "line-buffered", "line-number", "line-regexp", "no-filename", "no-group-separator",
            "no-messages", "null", "null-data", "only-matching", "perl-regexp", "quiet", "recursive", "silent",
            "text", "version", "with-filename", "word-regexp",
        ],
    },
    diff: {
        shortValue: { C: "count", D: "label", F: "pattern", I: "pattern", L: "label", S: "path", U: "count", W: "count", X: "path", x: "name-pattern" },
        shortPlain: "abBcdeEHilnNpqrstTuvwyZ",
        longValue: {
            exclude: "name-pattern",
            "exclude-from": "path",
            "from-file": "path",
            "horizon-lines": "count",
            ifdef: "label",
            "ignore-matching-lines": "pattern",
            label: "label",
            "show-function-line": "pattern",
            "starting-file": "path",
            tabsize: "count",
            "to-file": "path",
            width: "count",
        },
        longOptionalValue: { color: "label", context: "count", unified: "count" },
        longPlain: [
            "brief", "ed", "expand-tabs", "help", "ignore-all-space", "ignore-blank-lines", "ignore-case",
            "ignore-space-change", "ignore-tab-expansion", "ignore-trailing-space", "initial-tab", "minimal",
            "new-file", "normal", "paginate", "recursive", "report-identical-files", "rcs", "show-c-function",
            "side-by-side", "speed-large-files", "suppress-blank-empty", "text", "version",
        ],
    },
    cmp: {
        shortValue: { i: "count", n: "count" },
        shortPlain: "blsv",
        longValue: { bytes: "count", "ignore-initial": "count" },
        longOptionalValue: {},
        longPlain: ["help", "print-bytes", "quiet", "silent", "verbose", "version"],
    },
    sed: {
        shortValue: { e: "script", f: "path" },
        shortPlain: "nrEszu",
        longValue: { expression: "script", file: "path", "line-length": "count" },
        longOptionalValue: { "in-place": "label" },
        firstOperand: { kind: "script", suppliedByLetters: "ef", suppliedByLongNames: ["expression", "file"] },
        longPlain: [
            "debug", "follow-symlinks", "help", "null-data", "posix", "quiet", "regexp-extended", "sandbox",
            "separate", "silent", "unbuffered", "version", "zero-terminated",
        ],
    },
    date: {
        shortValue: { d: "count", f: "path", r: "path", s: "count" },
        shortPlain: "uR",
        longValue: { date: "count", file: "path", reference: "path", "rfc-3339": "count", set: "count" },
        longOptionalValue: { "iso-8601": "label" },
        longPlain: ["debug", "help", "rfc-822", "rfc-email", "universal", "utc", "version"],
    },
    tar: {
        shortValue: { b: "count", C: "path", f: "path", T: "path", X: "path" },
        shortPlain: "acdhjJkmOpPrStuvwWxzZ",
        longValue: {
            "blocking-factor": "count",
            directory: "path",
            exclude: "name-pattern",
            "exclude-from": "path",
            file: "path",
            "files-from": "path",
            "strip-components": "count",
        },
        longOptionalValue: {},
        longPlain: [
            "absolute-names", "append", "create", "dereference", "extract", "get", "help", "interactive",
            "keep-old-files", "list", "preserve-permissions", "sparse", "to-stdout", "touch", "update",
            "verbose", "verify", "version",
        ],
    },
    find: {
        shortValue: { D: "label", O: "count", f: "path" },
        shortPlain: "HLPdsx",
        longValue: {},
        longOptionalValue: {},
        longPlain: ["help", "version"],
        dashWordValue: {
            amin: "count", anewer: "path", atime: "count", cmin: "count", cnewer: "path", context: "label",
            ctime: "count", fstype: "label", gid: "count", group: "label", ilname: "path", iname: "name-pattern",
            inum: "count", ipath: "path", iregex: "name-pattern", iwholename: "path", links: "count",
            lname: "path", maxdepth: "count", mindepth: "count", mmin: "count", mtime: "count", name: "name-pattern",
            newer: "path", newerat: "path", neweraa: "path", neweram: "path", newerac: "path",
            newerct: "path", newerca: "path", newercm: "path", newercc: "path",
            newermt: "path", newerma: "path", newermm: "path", newermc: "path",
            newerbt: "path", newerba: "path", newerbm: "path", newerbc: "path",
            path: "path", perm: "label", printf: "label", regex: "name-pattern", regextype: "label",
            samefile: "path", size: "count", type: "label", uid: "count", used: "count", user: "label",
            wholename: "path", xtype: "label",
        },
        dashWordPlain: [
            "and", "daystart", "depth", "empty", "executable", "false", "follow", "help", "ignore_readdir_race",
            "mount", "noignore_readdir_race", "noleaf", "not", "nogroup", "nouser", "or", "print",
            "print0", "prune", "quit", "readable", "true", "version", "writable", "xdev",
        ],
        optionsArePositionIndependent: true,
    },
    curl: {
        shortValue: {
            A: "label", b: "indirect", C: "count", c: "path", D: "path", d: "indirect", E: "path", e: "label",
            F: "form", H: "header", K: "path", m: "count", o: "path", P: "label", Q: "label", r: "count",
            T: "path", t: "label", U: "label", u: "label", w: "indirect", X: "label", x: "label", Y: "count",
            y: "count", z: "path",
        },
        shortPlain: "#0123456aBfgGhiIjJklLMnNOpqRsSvVZ",
        longValue: {
            cacert: "path",
            capath: "path",
            cert: "path",
            config: "path",
            "connect-timeout": "count",
            cookie: "indirect",
            "cookie-jar": "path",
            data: "indirect",
            "data-ascii": "indirect",
            "data-binary": "indirect",
            "data-raw": "label",
            "data-urlencode": "form",
            "dump-header": "path",
            form: "form",
            "form-string": "label",
            header: "header",
            key: "path",
            "max-time": "count",
            "netrc-file": "path",
            output: "path",
            proxy: "label",
            range: "count",
            referer: "label",
            request: "label",
            resolve: "label",
            trace: "path",
            "time-cond": "path",
            "trace-ascii": "path",
            "upload-file": "path",
            url: "url",
            user: "label",
            "user-agent": "label",
            "write-out": "indirect",
        },
        longOptionalValue: {},
        longPlain: [
            "append", "compressed", "disable", "fail", "get", "globoff", "head", "help", "include", "insecure",
            "junk-session-cookies", "list-only", "location", "manual", "netrc", "next", "no-buffer", "parallel",
            "proxytunnel", "remote-header-name", "remote-name", "remote-time", "show-error", "silent",
            "use-ascii", "verbose", "version",
        ],
    },
};
export function readVerbOptionSlot(name, tok) {
    const row = VERB_OPTION_SLOTS[name];
    if (row === undefined)
        return undefined;
    if (!tok.startsWith("-") || tok === "-" || tok === "--")
        return { form: "unexplained" };
    if (tok.startsWith("--")) {
        const long = longOptionNameOf(tok);
        if (long === undefined)
            return { form: "unexplained" };
        const eq = tok.indexOf("=");
        const attached = eq >= 0 ? tok.slice(eq + 1) : undefined;
        const withValue = (kind) => attached !== undefined ? { form: "attached", value: attached, kind } : { form: "separated", kind };
        const exact = row.longValue[long];
        if (exact !== undefined)
            return withValue(exact);
        const exactOptional = row.longOptionalValue[long];
        if (exactOptional !== undefined)
            return attached !== undefined ? { form: "attached", value: attached, kind: exactOptional } : { form: "no-value" };
        if (row.longPlain.includes(long))
            return attached === undefined ? { form: "no-value" } : { form: "unexplained" };
        const valueNames = [
            ...Object.keys(row.longValue),
            ...(attached !== undefined ? Object.keys(row.longOptionalValue) : []),
        ].filter((full) => isLongOptionAbbrevOf(long, full));
        const plainNames = [
            ...row.longPlain,
            ...(attached === undefined ? Object.keys(row.longOptionalValue) : []),
        ].filter((full) => isLongOptionAbbrevOf(long, full));
        if (valueNames.length === 0)
            return plainNames.length > 0 && attached === undefined ? { form: "no-value" } : { form: "unexplained" };
        const kinds = new Set(valueNames.map((full) => (row.longValue[full] ?? row.longOptionalValue[full])));
        if (attached !== undefined)
            return { form: "attached", value: attached, kind: widestValueKind(kinds) };
        if (plainNames.length > 0 || kinds.size !== 1)
            return { form: "unexplained" };
        return withValue([...kinds][0]);
    }
    const word = row.dashWordValue !== undefined || row.dashWordPlain !== undefined ? /^-([A-Za-z_][\w-]+)$/.exec(tok)?.[1] : undefined;
    if (word !== undefined) {
        if (row.shortValue[word[0]] !== undefined)
            return { form: "unexplained" };
        const kind = row.dashWordValue?.[word];
        if (kind !== undefined)
            return { form: "separated", kind };
        if (row.dashWordPlain?.includes(word) === true)
            return { form: "no-value" };
        return { form: "unexplained" };
    }
    for (let i = 1; i < tok.length; i++) {
        const ch = tok[i];
        const kind = row.shortValue[ch];
        if (kind !== undefined) {
            const rest = tok.slice(i + 1);
            return rest !== "" ? { form: "attached", value: rest, kind } : { form: "separated", kind };
        }
        if (!row.shortPlain.includes(ch))
            return { form: "unexplained" };
    }
    return { form: "no-value" };
}
export function verbOptionsArePositionIndependent(name) {
    return VERB_OPTION_SLOTS[name]?.optionsArePositionIndependent === true;
}
export function verbFirstOperandSlot(name) {
    return VERB_OPTION_SLOTS[name]?.firstOperand;
}
function localFileUrlPath(value) {
    const m = /^file:(?:\/\/[^/?#]*)?(\/[^?#]*)/i.exec(value.trim());
    if (m === null)
        return undefined;
    const raw = m[1];
    try {
        return decodeURIComponent(raw);
    }
    catch {
        return raw;
    }
}
function formSpecFileNames(value) {
    const out = [];
    const after = (s) => {
        const at = s.search(/[@<]/);
        if (at !== -1 && at < s.length - 1)
            out.push(s.slice(at + 1));
    };
    after(value);
    for (const part of value.split(/[;,]/))
        after(part);
    return [...new Set(out)];
}
export function slotValueCandidates(value, kind) {
    if (!valueNamesAFile(kind))
        return [];
    if (kind === "url") {
        const local = localFileUrlPath(value);
        return local !== undefined ? [local] : [];
    }
    if (kind === "form")
        return formSpecFileNames(value).map(literalTildeSpelling);
    const named = value.startsWith("@") && value.length > 1 ? literalTildeSpelling(value.slice(1)) : undefined;
    if (kind === "header")
        return named !== undefined ? [value, named] : [];
    const out = [value];
    if (kind === "indirect" && named !== undefined)
        out.push(named);
    return out;
}
const SEPARATED_VALUE_OPTIONS = {
    head: { short: "cn", long: ["bytes", "lines"] },
    tail: { short: "cns", long: ["bytes", "lines", "sleep-interval", "pid", "max-unchanged-stats"] },
    cut: { short: "bcdf", long: ["bytes", "characters", "delimiter", "fields", "output-delimiter"] },
    grep: {
        ownsValueButNotSkippable: "ef",
        short: "mABCdD",
        long: [
            "max-count",
            "after-context",
            "before-context",
            "context",
            "devices",
            "directories",
            "label",
            "binary-files",
            "group-separator",
            "include",
            "exclude",
            "exclude-dir",
        ],
    },
    cmp: { short: "ni", long: ["bytes", "ignore-initial"] },
    diff: { short: "UCWIFxL", ownsValueButNotSkippable: "SX", long: ["unified", "context", "width", "ignore-matching-lines", "show-function-line", "exclude", "label", "horizon-lines", "tabsize"] },
};
function takesSeparatedValue(name, tok) {
    const model = SEPARATED_VALUE_OPTIONS[name];
    if (model === undefined)
        return false;
    if (tok.startsWith("--")) {
        if (tok.includes("="))
            return false;
        const long = longOptionNameOf(tok);
        return long !== undefined && model.long.some((full) => isLongOptionAbbrevOf(long, full));
    }
    if (!/^-[A-Za-z0-9]/.test(tok))
        return false;
    for (let i = 1; i < tok.length; i++) {
        const ch = tok[i];
        if ((model.ownsValueButNotSkippable ?? "").includes(ch))
            return false;
        if (model.short.includes(ch))
            return i === tok.length - 1;
        if (!/[A-Za-z0-9]/.test(ch))
            return false;
    }
    return false;
}
const GREP_RECURSIVE_READ_FORM = {
    discloses: "contents",
    shortLetters: "rR",
    valueOwners: "efmABCD",
    longNames: ["recursive", "dereference-recursive"],
    enumOptions: [{ shortLetter: "d", longName: "directories", recursiveValue: "recurse" }],
    dashIsStdin: true,
};
export const RECURSIVE_READ_FORMS = {
    grep: GREP_RECURSIVE_READ_FORM,
    egrep: GREP_RECURSIVE_READ_FORM,
    fgrep: GREP_RECURSIVE_READ_FORM,
    ls: { discloses: "names", shortLetters: "R", longNames: ["recursive"] },
    du: { discloses: "names", always: true },
    find: { discloses: "names", always: true },
    rg: { discloses: "contents", always: true, dashIsStdin: true },
    tree: { discloses: "names", always: true },
    ag: { discloses: "contents", always: true, dashIsStdin: true },
    ack: { discloses: "contents", always: true, dashIsStdin: true },
    tar: { discloses: "contents", shortLetters: "cru", valueOwners: "fCTXbg", longNames: ["create", "append", "update"], bundledModeLetters: "cru", dashIsStdin: true },
    diff: { discloses: "contents", shortLetters: "r", valueOwners: "UCWISFXx", longNames: ["recursive"], dashIsStdin: true },
};
const FIND_DANGEROUS_PREDICATES = new Set([
    "-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprint0", "-fls", "-fprintf", "-files0-from",
]);
const SED_READONLY_FLAGS = new Set([
    "-n", "--quiet", "--silent", "-E", "--regexp-extended", "-r", "-z", "--zero-terminated", "--posix",
]);
const SED_READONLY_CLUSTER_LETTERS = new Set(["n", "E", "r", "z"]);
const SED_READONLY_SCRIPT_PIECE = /^(?:\d+|\d+,\d+)?p$/;
const SED_FILE_OR_EXEC_COMMANDS = new Set(["r", "R", "w", "W", "e"]);
const SED_PLAIN_COMMANDS = new Set(["=", "d", "D", "F", "g", "G", "h", "H", "n", "N", "p", "P", "x", "z"]);
const SED_NUMERIC_COMMANDS = new Set(["q", "Q", "l", "L"]);
const SED_LABEL_COMMANDS = new Set(["b", "t", "T", ":", "v"]);
const SED_TEXT_COMMANDS = new Set(["a", "i", "c"]);
const SED_SUBSTITUTE_FLAGS = /[gpiImM0-9]/;
const SED_SCRIPT_LINE_LIMIT = 1024;
function utf8Length(text) {
    let bytes = 0;
    for (const ch of text) {
        const code = ch.codePointAt(0);
        bytes += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
    }
    return bytes;
}
function sedReadingKind(reading) {
    return typeof reading === "string" ? reading : "names";
}
function readSedScript(script) {
    if (script.split("\n").some((line) => utf8Length(line) >= SED_SCRIPT_LINE_LIMIT))
        return "oversized";
    let i = 0;
    let depth = 0;
    const len = script.length;
    const reads = [];
    const writes = [];
    const commands = [];
    let executesPatternSpace = false;
    const anyName = () => reads.length + writes.length + commands.length > 0 || executesPatternSpace;
    const named = () => ({ names: { reads, writes, commands, executesPatternSpace } });
    const failed = () => (anyName() ? named() : "unparsed");
    const restOfLine = () => {
        skipBlank();
        const from = i;
        while (i < len && script[i] !== "\n")
            i++;
        return script.slice(from, i);
    };
    const isBlank = (ch) => ch === " " || ch === "\t" || ch === "\v" || ch === "\f";
    const skipBlank = () => {
        while (i < len && isBlank(script[i]))
            i++;
    };
    const digits = () => {
        while (i < len && script[i] >= "0" && script[i] <= "9")
            i++;
    };
    const consumeDelimited = (delim, regex) => {
        while (i < len) {
            const ch = script[i];
            if (ch === "\\") {
                const escaped = script[i + 1];
                if (escaped === undefined || escaped === "\n" || escaped === "\r")
                    return false;
                i += 2;
                continue;
            }
            if (ch === "\n" || ch === "\r")
                return false;
            if (ch === delim) {
                i++;
                return true;
            }
            if (regex && ch === "[") {
                i++;
                if (script[i] === "^")
                    i++;
                if (script[i] === "]")
                    i++;
                while (i < len && script[i] !== "]" && script[i] !== "\n" && script[i] !== "\r") {
                    const next = script[i + 1];
                    if (script[i] === "[" && (next === ":" || next === "." || next === "=")) {
                        const at = script.indexOf(`${next}]`, i + 2);
                        if (at === -1 || script.slice(i, at).includes("\n"))
                            return false;
                        i = at + 2;
                        continue;
                    }
                    i++;
                }
                if (i >= len || script[i] !== "]")
                    return false;
                i++;
                continue;
            }
            i++;
        }
        return false;
    };
    const readAddress = () => {
        const ch = script[i];
        if (ch === undefined)
            return undefined;
        if (ch === "$") {
            i++;
            return true;
        }
        if (ch >= "0" && ch <= "9") {
            digits();
            if (script[i] === "~") {
                i++;
                digits();
            }
            return true;
        }
        if (ch === "/" || ch === "\\") {
            const delim = ch === "/" ? "/" : script[i + 1];
            i += ch === "/" ? 1 : 2;
            if (delim === undefined || !consumeDelimited(delim, true))
                return false;
            while (i < len && (script[i] === "I" || script[i] === "M"))
                i++;
            return true;
        }
        return undefined;
    };
    while (i < len) {
        const ch = script[i];
        if (isBlank(ch) || ch === "\n" || ch === "\r" || ch === ";") {
            i++;
            continue;
        }
        if (ch === "#") {
            while (i < len && script[i] !== "\n")
                i++;
            continue;
        }
        if (ch === "}") {
            if (depth === 0)
                return failed();
            depth--;
            i++;
            continue;
        }
        const first = readAddress();
        if (first === false)
            return failed();
        if (first === true) {
            skipBlank();
            if (script[i] === ",") {
                i++;
                skipBlank();
                if (script[i] === "+" || script[i] === "~") {
                    i++;
                    if (!(script[i] >= "0" && script[i] <= "9"))
                        return failed();
                    digits();
                }
                else if (readAddress() !== true)
                    return failed();
            }
            skipBlank();
        }
        while (script[i] === "!") {
            i++;
            skipBlank();
        }
        const cmd = script[i];
        if (cmd === undefined)
            return failed();
        i++;
        if (SED_FILE_OR_EXEC_COMMANDS.has(cmd)) {
            const arg = restOfLine();
            if (cmd === "e") {
                if (arg === "")
                    executesPatternSpace = true;
                else
                    commands.push(arg);
            }
            else if (cmd === "r" || cmd === "R")
                reads.push(arg);
            else
                writes.push(arg);
            continue;
        }
        if (cmd === "{") {
            depth++;
            continue;
        }
        if (cmd === "s" || cmd === "y") {
            const delim = script[i];
            if (delim === undefined || delim === "\\" || delim === "\n")
                return failed();
            i++;
            if (!consumeDelimited(delim, cmd === "s"))
                return failed();
            if (!consumeDelimited(delim, false))
                return failed();
            if (cmd === "y")
                continue;
            while (i < len) {
                const flag = script[i];
                if (flag === "w" || flag === "W") {
                    i++;
                    writes.push(restOfLine());
                    break;
                }
                if (flag === "e") {
                    executesPatternSpace = true;
                    i++;
                    continue;
                }
                if (SED_SUBSTITUTE_FLAGS.test(flag) || isBlank(flag)) {
                    i++;
                    continue;
                }
                if (flag === ";" || flag === "\n" || flag === "\r" || flag === "}" || flag === "#")
                    break;
                return failed();
            }
            continue;
        }
        if (SED_TEXT_COMMANDS.has(cmd)) {
            while (i < len && script[i] !== "\n")
                i++;
            continue;
        }
        if (SED_LABEL_COMMANDS.has(cmd)) {
            skipBlank();
            while (i < len && script[i] !== "\n" && script[i] !== ";" && script[i] !== "}" && !isBlank(script[i]))
                i++;
            continue;
        }
        if (SED_NUMERIC_COMMANDS.has(cmd)) {
            skipBlank();
            digits();
            continue;
        }
        if (!SED_PLAIN_COMMANDS.has(cmd))
            return failed();
    }
    if (depth !== 0)
        return failed();
    return anyName() ? named() : "clean";
}
const SED_OPTION_DIALECTS = [
    { separated: "efl", suffix: "iI", stopsAtFirstOperand: false },
    { separated: "ef", suffix: "iI", stopsAtFirstOperand: true },
];
function sedSeparatedSuffixTaken(tok) {
    return tok !== undefined && (tok === "" || tok.startsWith("."));
}
const SED_LONG_VALUE_OPTIONS = ["expression", "line-length"];
const SED_LONG_PLAIN_OPTIONS = [
    "debug", "follow-symlinks", "help", "in-place", "null-data", "posix", "quiet", "regexp-extended",
    "sandbox", "separate", "silent", "unbuffered", "version", "zero-terminated",
];
function readSedInvocation(args, dialect) {
    return sedReadingKind(readSedInvocationDetailed(args, dialect));
}
function readSedInvocationDetailed(args, dialect) {
    const scripts = [];
    let scriptSupplied = false;
    let endOfOptions = false;
    let firstOperand;
    for (let k = 0; k < args.length; k++) {
        const tok = args[k];
        if (!endOfOptions && tok === "--") {
            endOfOptions = true;
            continue;
        }
        if (endOfOptions || !tok.startsWith("-") || tok === "-") {
            if (firstOperand === undefined) {
                firstOperand = tok;
                if (dialect.stopsAtFirstOperand)
                    endOfOptions = true;
            }
            continue;
        }
        if (tok.startsWith("--")) {
            const long = longOptionNameOf(tok);
            if (long === undefined)
                return "unparsed";
            const eq = tok.indexOf("=");
            const attached = eq >= 0 ? tok.slice(eq + 1) : undefined;
            if (isLongOptionAbbrevOf(long, "file"))
                return "unseen";
            if (isLongOptionAbbrevOf(long, "expression")) {
                const value = attached ?? args[++k];
                if (value === undefined)
                    return "unparsed";
                scripts.push(value);
                scriptSupplied = true;
                continue;
            }
            if (SED_LONG_VALUE_OPTIONS.some((full) => isLongOptionAbbrevOf(long, full))) {
                if (attached === undefined)
                    k++;
                continue;
            }
            if (SED_LONG_PLAIN_OPTIONS.some((full) => isLongOptionAbbrevOf(long, full)))
                continue;
            return "unparsed";
        }
        for (let c = 1; c < tok.length; c++) {
            const letter = tok[c];
            if (dialect.suffix.includes(letter)) {
                if (c + 1 === tok.length && sedSeparatedSuffixTaken(args[k + 1]))
                    k++;
                break;
            }
            if (dialect.separated.includes(letter)) {
                const rest = tok.slice(c + 1);
                const value = rest !== "" ? rest : args[++k];
                if (letter === "f")
                    return "unseen";
                if (letter === "e") {
                    if (value === undefined)
                        return "unparsed";
                    scripts.push(value);
                    scriptSupplied = true;
                }
                break;
            }
            if (!SED_SHORT_PLAIN_LETTERS.includes(letter))
                return "unparsed";
        }
    }
    const script = scriptSupplied ? scripts.join("\n") : firstOperand;
    if (script === undefined)
        return "clean";
    return readSedScript(script);
}
const SED_SHORT_PLAIN_LETTERS = "nsuzEraHl";
export function sedScriptSlotFiles(args) {
    const reads = new Set();
    const writes = new Set();
    const commands = new Set();
    let executesPatternSpace = false;
    const kinds = [];
    for (const dialect of SED_OPTION_DIALECTS) {
        const reading = readSedInvocationDetailed(args, dialect);
        kinds.push(sedReadingKind(reading));
        if (typeof reading === "string")
            continue;
        for (const f of reading.names.reads)
            reads.add(f);
        for (const f of reading.names.writes)
            writes.add(f);
        for (const c of reading.names.commands)
            commands.add(c);
        executesPatternSpace ||= reading.names.executesPatternSpace;
    }
    const unread = kinds.includes("oversized")
        ? "a script line is longer than an implementation compiles whole, so what runs is not the text written here"
        : kinds.every((k) => k === "unparsed")
            ? "its script could not be read to its end"
            : undefined;
    return { reads: [...reads], writes: [...writes], commands: [...commands], executesPatternSpace, ...(unread !== undefined ? { unread } : {}) };
}
const SCRIPT_SLOT_VERBS = new Map([
    ["sed", (args) => {
            const readings = SED_OPTION_DIALECTS.map((dialect) => readSedInvocation(args, dialect));
            if (readings.includes("names") || readings.includes("unseen") || readings.includes("oversized"))
                return true;
            return readings.every((reading) => reading === "unparsed");
        }],
]);
const INLINE_PROGRAM_TEXT = "runs its argument as an interpreted program";
const INLINE_PROGRAM_STDIN = "reads the program it runs from its standard input";
const INTERPRETER_BANNER_OPTIONS = new Set(["--version", "--help", "--usage", "-version", "-help"]);
const BLANK_INLINE = {
    longText: new Set(), longFile: new Set(), longNamesProgram: new Set(), longSupplemental: new Set(),
    longValue: new Set(), longPlain: new Set(), fileOptionEndsOptions: false,
    textLetters: "", fileLetters: "", separatedValueLetters: "", attachedValueLetters: "",
    plainLetters: "", bannerLetters: "", interactiveLetters: "",
    programOperand: { at: 0, isText: false },
};
const PERL_INLINE = {
    ...BLANK_INLINE, textLetters: "eEmM", separatedValueLetters: "I", attachedValueLetters: "iCFxDV",
    plainLetters: "nplawcsStTUWX", bannerLetters: "vVh", stdinWhenUnnamed: true,
};
const RUBY_INLINE = {
    ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "IrECFKT", plainLetters: "nplawcsSU",
    bannerLetters: "vh", stdinWhenUnnamed: true,
};
const PYTHON_INLINE = {
    ...BLANK_INLINE, textLetters: "c", fileLetters: "m", separatedValueLetters: "WXQ",
    plainLetters: "uBEIsSqbdvxOtR", bannerLetters: "Vh?", interactiveLetters: "i",
    fileOptionEndsOptions: true, stdinWhenUnnamed: true,
};
const NODE_INLINE = {
    ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
    longValue: new Set(["--import", "--loader", "--experimental-loader", "--require", "--env-file", "--conditions", "--max-old-space-size"]),
    longNamesProgram: new Set(["--test"]),
    longPlain: new Set(["--inspect", "--inspect-brk", "--no-warnings", "--trace-warnings", "--watch", "--enable-source-maps", "--experimental-modules", "--no-deprecation", "--check"]),
    separatedValueLetters: "r", plainLetters: "c", bannerLetters: "vh", interactiveLetters: "i",
    stdinWhenUnnamed: true,
};
const PHP_INLINE = {
    ...BLANK_INLINE, longText: new Set(["--run", "--process-begin", "--process-code", "--process-end"]),
    textLetters: "rRBE", fileLetters: "fF", separatedValueLetters: "dcz", plainLetters: "nqswl",
    bannerLetters: "vhi", interactiveLetters: "a", stdinWhenUnnamed: true,
};
const LUA_INLINE = {
    ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "l", bannerLetters: "vh",
    interactiveLetters: "i", stdinWhenUnnamed: true,
};
const TCL_INLINE = { ...BLANK_INLINE, stdinWhenUnnamed: true };
const EXPECT_INLINE = {
    ...BLANK_INLINE, textLetters: "c", separatedValueLetters: "Db", plainLetters: "dN",
    bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: true,
};
const AWK_INLINE = {
    ...BLANK_INLINE, longText: new Set(["--source"]), textLetters: "e",
    longFile: new Set(["--file", "--exec"]), fileLetters: "fE", separatedValueLetters: "vF",
    bannerLetters: "V", programOperand: { at: 0, isText: true }, stdinWhenUnnamed: false,
};
const JQ_INLINE = {
    ...BLANK_INLINE, longFile: new Set(["--from-file"]), fileLetters: "f", separatedValueLetters: "L",
    plainLetters: "nrjcsSaCMRe", bannerLetters: "h", programOperand: { at: 0, isText: true },
    stdinWhenUnnamed: false,
};
const BC_INLINE = {
    ...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e", plainLetters: "lqsw",
    bannerLetters: "vh", interactiveLetters: "i", programOperand: undefined, stdinWhenUnnamed: true,
};
const DC_INLINE = {
    ...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e",
    longFile: new Set(["--file"]), fileLetters: "f", bannerLetters: "hV",
    programOperand: undefined, stdinWhenUnnamed: true,
};
const SQLITE_INLINE = {
    ...BLANK_INLINE, longText: new Set(["-cmd"]), longSupplemental: new Set(["-init"]),
    programOperand: { at: 1, isText: true }, stdinWhenUnnamed: true,
};
const PSQL_INLINE = {
    ...BLANK_INLINE, longText: new Set(["--command"]), textLetters: "c",
    longFile: new Set(["--file"]), fileLetters: "f", separatedValueLetters: "hpUdoF",
    plainLetters: "aeEqtxXwW", bannerLetters: "lV?", programOperand: undefined, stdinWhenUnnamed: true,
};
const OSASCRIPT_INLINE = {
    ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "ls", plainLetters: "i", bannerLetters: "h",
    stdinWhenUnnamed: true,
};
const RSCRIPT_INLINE = {
    ...BLANK_INLINE, textLetters: "e", bannerLetters: "h", stdinWhenUnnamed: false,
};
const JULIA_INLINE = {
    ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "eE",
    separatedValueLetters: "JLp", plainLetters: "q", bannerLetters: "vh", interactiveLetters: "i",
    stdinWhenUnnamed: true,
};
const GHCI_INLINE = {
    ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "io", programOperand: undefined,
    stdinWhenUnnamed: true,
};
const GROOVY_INLINE = {
    ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "Dcl", plainLetters: "n",
    bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: false,
};
const BUN_INLINE = {
    ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
    separatedValueLetters: "r", bannerLetters: "vh", stdinWhenUnnamed: false,
};
export const INTERPRETER_INLINE_PROGRAMS = new Map([
    ["perl", PERL_INLINE],
    ["ruby", RUBY_INLINE],
    ["python", PYTHON_INLINE],
    ["node", NODE_INLINE],
    ["nodejs", NODE_INLINE],
    ["php", PHP_INLINE],
    ["lua", LUA_INLINE],
    ["luajit", LUA_INLINE],
    ["tclsh", TCL_INLINE],
    ["wish", TCL_INLINE],
    ["expect", EXPECT_INLINE],
    ["awk", AWK_INLINE],
    ["gawk", AWK_INLINE],
    ["mawk", AWK_INLINE],
    ["nawk", AWK_INLINE],
    ["jq", JQ_INLINE],
    ["bc", BC_INLINE],
    ["dc", DC_INLINE],
    ["sqlite", SQLITE_INLINE],
    ["psql", PSQL_INLINE],
    ["osascript", OSASCRIPT_INLINE],
    ["Rscript", RSCRIPT_INLINE],
    ["julia", JULIA_INLINE],
    ["ghci", GHCI_INLINE],
    ["groovy", GROOVY_INLINE],
    ["bun", BUN_INLINE],
]);
export function interpreterKey(name) {
    return name.replace(/\d+(?:\.\d+)*$/, "");
}
export function inlineProgramClause(model, args) {
    let endOfOptions = false;
    let banner = false;
    let interactive = false;
    let named = false;
    let stdin = false;
    let explained = true;
    let operands = 0;
    scan: for (let i = 0; i < args.length; i++) {
        const arg = args[i];
        if (!endOfOptions && arg === "--") {
            endOfOptions = true;
            continue;
        }
        if (arg === "-") {
            if (model.programOperand?.isText !== true)
                stdin = true;
            continue;
        }
        if (!endOfOptions && arg.startsWith("-")) {
            const eq = arg.indexOf("=");
            const whole = eq < 0 ? arg : arg.slice(0, eq);
            const valueOf = () => (eq < 0 ? args[++i] : arg.slice(eq + 1));
            if (model.longText.has(whole))
                return INLINE_PROGRAM_TEXT;
            if (model.longFile.has(whole)) {
                if (valueOf() === "-") {
                    stdin = true;
                    continue;
                }
                named = true;
                if (explained && model.fileOptionEndsOptions)
                    break scan;
                continue;
            }
            if (model.longNamesProgram.has(whole)) {
                named = true;
                continue;
            }
            if (model.longSupplemental.has(whole)) {
                valueOf();
                continue;
            }
            if (INTERPRETER_BANNER_OPTIONS.has(whole)) {
                banner = true;
                continue;
            }
            if (model.longValue.has(whole)) {
                valueOf();
                continue;
            }
            if (model.longPlain.has(whole))
                continue;
            if (arg.startsWith("--")) {
                if (eq < 0)
                    explained = false;
                continue;
            }
            for (let k = 1; k < arg.length; k++) {
                const ch = arg[k];
                if (ch >= "0" && ch <= "9")
                    continue;
                if (model.textLetters.includes(ch))
                    return INLINE_PROGRAM_TEXT;
                if (model.fileLetters.includes(ch)) {
                    const value = k + 1 < arg.length ? arg.slice(k + 1) : args[++i];
                    if (value === "-") {
                        stdin = true;
                        break;
                    }
                    named = true;
                    if (explained && model.fileOptionEndsOptions)
                        break scan;
                    break;
                }
                if (model.attachedValueLetters.includes(ch)) {
                    if (model.bannerLetters.includes(ch))
                        banner = true;
                    break;
                }
                if (model.bannerLetters.includes(ch)) {
                    banner = true;
                    continue;
                }
                if (model.interactiveLetters.includes(ch)) {
                    interactive = true;
                    continue;
                }
                if (model.separatedValueLetters.includes(ch)) {
                    if (k + 1 >= arg.length)
                        i++;
                    break;
                }
                if (!model.plainLetters.includes(ch))
                    explained = false;
            }
            continue;
        }
        operands++;
        if (!named && explained && model.programOperand !== undefined && operands === model.programOperand.at + 1) {
            if (model.programOperand.isText)
                return INLINE_PROGRAM_TEXT;
            named = true;
            break;
        }
    }
    if (stdin || interactive)
        return INLINE_PROGRAM_STDIN;
    if (named || banner)
        return undefined;
    return model.stdinWhenUnnamed ? INLINE_PROGRAM_STDIN : undefined;
}
function unjudgedProgramTextRefusal(written, clause) {
    return `command "${written}" ${clause}, which this walk does not judge ${NOT_AUTO_ALLOWED}`;
}
function argumentAsProgramRefusal(written, name, args) {
    const slot = SCRIPT_SLOT_VERBS.get(name);
    if (slot !== undefined && slot(args)) {
        return unjudgedProgramTextRefusal(written, "names files or runs commands inside its script text");
    }
    const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(name));
    if (interpreter === undefined)
        return undefined;
    const clause = inlineProgramClause(interpreter, args);
    return clause === undefined ? undefined : unjudgedProgramTextRefusal(written, clause);
}
function guardedVerbFormReason(name, toks, args) {
    if (name === "find") {
        for (const t of toks) {
            if (FIND_DANGEROUS_PREDICATES.has(t)) {
                return `\`find ${t}\` executes a command or writes a file — not a read ${NOT_AUTO_ALLOWED}`;
            }
        }
        for (const w of args) {
            if (globInWord(w) || expansionInWord(w)) {
                return `\`find\` is given the unquoted pattern "${w.raw}", which the shell may expand into other arguments before find runs ${NOT_AUTO_ALLOWED}`;
            }
        }
        return undefined;
    }
    if (name === "sed") {
        for (const w of args) {
            if (globInWord(w) || expansionInWord(w)) {
                return `\`sed\` is given the unquoted pattern "${w.raw}", which the shell may expand into other arguments (a file named like a flag becomes a live option) before sed runs ${NOT_AUTO_ALLOWED}`;
            }
        }
        const flags = [];
        const operands = [];
        let endOfOptions = false;
        for (const t of toks.slice(1)) {
            if (!endOfOptions && t === "--") {
                endOfOptions = true;
                continue;
            }
            if (!endOfOptions && t.startsWith("-")) {
                flags.push(t);
                continue;
            }
            operands.push(t);
        }
        for (const f of flags) {
            if (SED_READONLY_FLAGS.has(f))
                continue;
            if (/^-[A-Za-z]+$/.test(f) && f.length > 2 && [...f.slice(1)].every((ch) => SED_READONLY_CLUSTER_LETTERS.has(ch)))
                continue;
            return `\`sed ${f}\` is outside the read-only sed form (-n plus print-only script) ${NOT_AUTO_ALLOWED}`;
        }
        const quiet = flags.some((f) => f === "-n" || f === "--quiet" || f === "--silent" || (/^-[A-Za-z]+$/.test(f) && f.includes("n")));
        if (!quiet)
            return `\`sed\` without \`-n\` echoes its whole input — only the -n print-only form is read-classified ${NOT_AUTO_ALLOWED}`;
        const script = operands[0];
        if (script === undefined)
            return `\`sed\` with no script ${NOT_AUTO_ALLOWED}`;
        for (const piece of script.split(";")) {
            if (!SED_READONLY_SCRIPT_PIECE.test(piece.trim())) {
                return `\`sed\` script "${script}" is not a pure print command (N[,M]p) ${NOT_AUTO_ALLOWED}`;
            }
        }
        return undefined;
    }
    if (name === "free") {
        for (const t of toks.slice(1)) {
            if (/^--seconds(=.*)?$/.test(t) || /^-[A-Za-z0-9]*s[A-Za-z0-9]*$/.test(t)) {
                return `\`free ${t}\` repeats until killed — it never terminates on its own ${NOT_AUTO_ALLOWED}`;
            }
        }
        return undefined;
    }
    if (name === "sleep") {
        return pollLoopSleepReason(toks);
    }
    if (name === "seq") {
        for (const t of toks.slice(1)) {
            if (t.startsWith("-") && t !== "-" && !/^-\d/.test(t))
                continue;
            if (!/^-?\d{1,6}$/.test(t)) {
                return `\`seq ${t}\` is not a small literal integer bound — the output cannot be bounded statically ${NOT_AUTO_ALLOWED}`;
            }
        }
        return undefined;
    }
    if (name === "diff") {
        for (const t of toks.slice(1)) {
            if (/^--[A-Za-z-]+=-$/.test(t) || /^-[A-Za-z]*[SX]-$/.test(t)) {
                return `\`diff ${t}\` names stdin inside a fused option — a read this check cannot see through ${NOT_AUTO_ALLOWED}`;
            }
        }
    }
    return undefined;
}
export function segmentSelectsRecursiveRead(name, args) {
    const model = RECURSIVE_READ_FORMS[name];
    if (model === undefined)
        return false;
    if (model.discloses !== "contents")
        return false;
    if (model.always === true)
        return true;
    if (model.bundledModeLetters !== undefined) {
        const first = args.find((t) => t.length > 0);
        if (first !== undefined && !first.startsWith("-") && /^[A-Za-z]+$/.test(first) && [...first].some((ch) => model.bundledModeLetters.includes(ch))) {
            return true;
        }
    }
    let endOfOptions = false;
    for (let k = 0; k < args.length; k++) {
        const t = args[k];
        if (endOfOptions)
            continue;
        if (t === "--") {
            const prev = k > 0 ? args[k - 1] : undefined;
            const prevMayOwnValue = prev !== undefined && prev.startsWith("--") && prev.length > 2 && !prev.includes("=");
            if (!prevMayOwnValue)
                endOfOptions = true;
            continue;
        }
        if (t.startsWith("--")) {
            const long = longOptionNameOf(t);
            if (long === undefined)
                continue;
            if (model.longNames?.some((full) => isLongOptionAbbrevOf(long, full)) === true)
                return true;
            for (const en of model.enumOptions ?? []) {
                if (en.longName === undefined || !isLongOptionAbbrevOf(long, en.longName))
                    continue;
                const eq = t.indexOf("=");
                const v = eq >= 0 ? t.slice(eq + 1) : args[k + 1];
                if (v !== undefined && v.length > 0 && en.recursiveValue.startsWith(v.toLowerCase()))
                    return true;
            }
            continue;
        }
        if (!t.startsWith("-") || t === "-")
            continue;
        for (let i = 1; i < t.length; i++) {
            const ch = t[i];
            if (model.shortLetters?.includes(ch) === true)
                return true;
            const en = (model.enumOptions ?? []).find((e) => e.shortLetter === ch);
            if (en !== undefined) {
                const v = i === t.length - 1 ? args[k + 1] : t.slice(i + 1);
                if (v !== undefined && v.length > 0 && en.recursiveValue.startsWith(v.toLowerCase()))
                    return true;
                if (i === t.length - 1)
                    k++;
                break;
            }
            if (model.valueOwners?.includes(ch) === true) {
                if (i === t.length - 1)
                    k++;
                break;
            }
            if (!/[A-Za-z0-9]/.test(ch))
                break;
        }
    }
    return false;
}
function isGrepFileStdinLongOption(tok) {
    const eq = tok.indexOf("=");
    if (eq < 0 || tok.slice(eq + 1) !== "-")
        return false;
    const name = longOptionNameOf(tok);
    return name !== undefined && isLongOptionAbbrevOf(name, "file");
}
const CLUSTER_OPTION_CHAR = /[A-Za-z0-9]/;
export const ASSIGNMENT_VALUE_NOT_PATHS = new Set(["make", "gmake", "bmake", "cmake", "ninja", "meson", "docker", "podman"]);
export function argvProgramOf(words) {
    const pos = readProgramPosition(words);
    if (pos.program === undefined)
        return undefined;
    const at = pos.program.index;
    return words[at]?.assignment !== undefined ? undefined : { program: pos.program.basename, from: at };
}
export function attachedOptionPayloads(tok) {
    const literal = literalTildeSpelling;
    const out = [];
    if (tok.startsWith("--")) {
        const eq = tok.indexOf("=");
        if (eq > 0 && eq + 1 < tok.length)
            out.push(literal(tok.slice(eq + 1)));
        return out;
    }
    const whole = literal(tok.slice(1));
    if (whole.length > 0 && (isPathShapedToken(whole) || isAbsolutePathForm(whole)))
        out.push(whole);
    for (let k = 2; k <= tok.length; k++) {
        if (![...tok.slice(1, k)].every((ch) => CLUSTER_OPTION_CHAR.test(ch)))
            break;
        const payload = tok.slice(k);
        if (payload.length > 0)
            out.push(literal(payload));
    }
    return out;
}
const HOME_EXPANDS_TO_ONE_WORD = /^[A-Za-z0-9_./+@:%=,-]+$/;
export function homeExpansionOf(w, home) {
    const [head, ...rest] = w.pieces;
    const e = head?.expansion;
    if (head === undefined || e === undefined || e.kind !== "parameter" || e.name !== "HOME" || (e.body !== undefined && e.body !== "HOME"))
        return undefined;
    if (head.quotedBy !== undefined && head.quotedBy !== '"')
        return undefined;
    if (head.quotedBy === undefined && !HOME_EXPANDS_TO_ONE_WORD.test(home))
        return undefined;
    if (rest.some((p) => p.expansion !== undefined || p.literal === undefined))
        return undefined;
    const next = rest[0];
    if (next !== undefined) {
        if (head.quotedBy === undefined && !(next.literal.startsWith("/") || next.quotedBy === '"'))
            return undefined;
        if (head.quotedBy === '"' && next.quotedBy === '"' && !next.literal.startsWith("/"))
            return undefined;
    }
    return { spelled: `${home}${rest.map((p) => p.literal).join("")}` };
}
const CLASSIFY_HOME_REREADABLE = /["$`\\\n\r]/;
function homeSpelledForClassify(w, home) {
    return CLASSIFY_HOME_REREADABLE.test(home) ? undefined : homeExpansionOf(w, home)?.spelled;
}
const HOME_PLAIN_READ = /\$HOME(?![A-Za-z0-9_])|\$\{HOME\}/g;
const HOME_NAME = /HOME(?![A-Za-z0-9_])/;
export function homeMayBeRebound(text) {
    const unquoted = text.replace(/\\\r?\n/g, "").replace(HOME_PLAIN_READ, " ").replace(/\$?["']|\\/g, "");
    if (HOME_NAME.test(unquoted))
        return true;
    if (hasUnquotedExtglobOpener(text))
        return true;
    if (!unquoted.includes("printf") && !/[*?[{]/.test(unquoted))
        return false;
    const lexed = lexShell(text);
    return !("parseFailure" in lexed) && lexed.segments.some((seg) => {
        const words = argvOf(seg);
        return printfNamesComputed(words) || words.some((w) => globMayMatchHome(w) || optionWordCarriesGlob(w) || wordHasBraceList(w));
    });
}
function hasUnquotedExtglobOpener(text) {
    const lexed = lexShell(text);
    return "parseFailure" in lexed || extglobOpenerIn(lexed, text);
}
export function extglobOpenerIn(lexed, text) {
    for (const seg of lexed.segments) {
        for (const [k, tok] of seg.tokens.entries()) {
            const next = seg.tokens[k + 1];
            if (tok.kind === "heredoc" && /[?*+@!]$/.test(tok.delimiter) && next?.kind === "group" && next.op === "(")
                return true;
            const t = tok.kind === "word" ? tok : tok.kind === "redirect" ? tok.target : undefined;
            if (t === undefined)
                continue;
            const last = t.pieces[t.pieces.length - 1];
            if (last === undefined || last.quotedBy !== undefined)
                continue;
            if (!/[?*+@!]$/.test(text.slice(last.span[0], last.span[1])))
                continue;
            let j = last.span[1];
            while (text[j] === "\\" && (text[j + 1] === "\n" || (text[j + 1] === "\r" && text[j + 2] === "\n")))
                j += text[j + 1] === "\r" ? 3 : 2;
            if (text[j] === "(")
                return true;
        }
    }
    return false;
}
function wordHasBraceList(w) {
    return w.pieces.some((p) => p.expansion?.kind === "brace");
}
function optionWordCarriesGlob(w) {
    if (!w.pieces.some(isGlobPiece))
        return false;
    const atoms = patternAtoms(w.pieces);
    if (atoms === undefined)
        return UNREADABLE_GLOB_IS_A_REBINDING;
    try {
        const reversed = atoms.slice().reverse();
        return closureReaches(atoms, "-") && (closureReaches(reversed, "EMOH") || closureReaches(reversed, "]"));
    }
    catch {
        return UNREADABLE_GLOB_IS_A_REBINDING;
    }
}
const UNREADABLE_GLOB_IS_A_REBINDING = true;
function escapeRegExp(s) {
    return s.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
}
function globMayMatchHome(w) {
    if (!w.pieces.some(isGlobPiece))
        return false;
    const atoms = patternAtoms(w.pieces);
    if (atoms === undefined)
        return UNREADABLE_GLOB_IS_A_REBINDING;
    try {
        if (new RegExp(`^${atoms.join("")}$`).test("HOME"))
            return true;
        return closureReaches(atoms, "HOME[") && closureReaches(atoms.slice().reverse(), "]");
    }
    catch {
        return UNREADABLE_GLOB_IS_A_REBINDING;
    }
}
function patternAtoms(pieces) {
    const atoms = [];
    for (const p of pieces) {
        if (p.literal !== undefined && p.expansion === undefined) {
            for (const ch of p.literal)
                atoms.push(caseFoldedChar(ch));
            continue;
        }
        if (isGlobPiece(p)) {
            const body = p.expansion.body;
            if (body === undefined) {
                atoms.push(RUN_ATOM);
                continue;
            }
            const cls = translateBracket(body);
            if (cls === undefined)
                return undefined;
            atoms.push(cls);
            continue;
        }
        atoms.push(RUN_ATOM);
    }
    return atoms;
}
const RUN_ATOM = ".*";
function caseFoldedChar(ch) {
    const lower = ch.toLowerCase();
    const upper = ch.toUpperCase();
    return lower !== upper ? `[${escapeRegExp(lower)}${escapeRegExp(upper)}]` : escapeRegExp(ch);
}
function closureReaches(atoms, target) {
    let re = "";
    for (let i = atoms.length - 1; i >= 0; i--)
        re = `(?:${atoms[i]}${re})?`;
    return new RegExp(`^${re}$`).test(target);
}
function scanBracketBody(body) {
    const members = [];
    let computed = false;
    for (let i = 0; i < body.length; i++) {
        const c = body[i];
        if (c === "\\") {
            if (i + 1 < body.length)
                members.push({ ch: body[++i], unquoted: false });
            continue;
        }
        if (c === "$" || c === "`" || c === "{")
            computed = true;
        members.push({ ch: c, unquoted: true });
    }
    return { members, computed };
}
function membersOpenClassExpression(members) {
    return members.some((m, i) => m.unquoted && m.ch === "[" && members[i + 1]?.unquoted === true && (members[i + 1].ch === ":" || members[i + 1].ch === "=" || members[i + 1].ch === "."));
}
function translateBracket(body) {
    if (body.includes('"') || body.includes("'"))
        return undefined;
    const { members, computed } = scanBracketBody(body);
    if (computed || membersOpenClassExpression(members))
        return undefined;
    let i = 0;
    let negated = false;
    if (members[0]?.unquoted === true && (members[0].ch === "!" || members[0].ch === "^")) {
        negated = true;
        i = 1;
    }
    const first = i;
    let cls = "";
    for (; i < members.length; i++) {
        const { ch, unquoted } = members[i];
        if (ch === "-" && unquoted && i > first && i < members.length - 1) {
            if (!negated)
                return undefined;
            cls += "-";
            continue;
        }
        const escaped = ch.replace(/[\\\]\[^-]/g, "\\$&");
        cls += !negated && ch.toLowerCase() !== ch.toUpperCase() ? `${escapeRegExp(ch.toLowerCase())}${escapeRegExp(ch.toUpperCase())}` : escaped;
    }
    return `[${negated ? "^" : ""}${cls || "\\]"}]`;
}
const literalWord = (w) => (w !== undefined && w.pieces.every((p) => p.literal !== undefined && p.expansion === undefined) ? w.pieces.map((p) => p.literal).join("") : undefined);
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
function printfNamesComputed(words) {
    for (let i = 0; i < words.length; i++) {
        const head = literalWord(words[i]);
        if (head === undefined || programBasename(head) !== "printf")
            continue;
        for (let j = i + 1; j < words.length; j++) {
            const lit = literalWord(words[j]);
            const opt = lit ?? words[j].pieces[0]?.literal ?? "";
            if (lit === "--" || !opt.startsWith("-v"))
                break;
            if (lit === undefined)
                return true;
            const name = lit === "-v" ? words[++j] : undefined;
            if (lit === "-v" && name === undefined)
                break;
            if (!IDENTIFIER.test(lit === "-v" ? (literalWord(name) ?? "") : lit.slice(2)))
                return true;
        }
    }
    return false;
}
function segmentCompoundForReadonly(command, homeDir) {
    const trimmed = command.trim();
    if (!trimmed)
        return { reject: "empty command" };
    const terminatorStripped = trimmed.endsWith(";") ? trimmed.slice(0, -1) : trimmed;
    return splitShellCompoundSegments(terminatorStripped, { trailingTerminator: "keep", redirection: "data-free", ...(homeDir !== undefined ? { homeDir } : {}) });
}
export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
    const split = segmentCompoundForReadonly(command, boundary?.homeDir);
    if ("reject" in split)
        return { reason: split.reject };
    const { segments, pipeFed } = split;
    const homeRebound = boundary?.homeDir !== undefined && homeMayBeRebound(command);
    let nameRefusal;
    const unlistedSegments = new Set();
    const reheaded = new Map();
    for (let si = 0; si < segments.length; si++) {
        const words = argvOf(split.lexed[si]);
        const toks = segmentSpellings(words, boundary?.homeDir, homeRebound);
        const folded = toks.folded;
        const pos = readProgramPosition(words);
        const from = pos.written ?? folded.length;
        const written = folded[from];
        if (written === undefined)
            return { reason: "empty command" };
        if (written === "coproc")
            return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command" };
        if (isBareControlWord(words, from, SHELL_BLOCK_WORDS)) {
            unlistedSegments.add(si);
            nameRefusal ??= `command "${written}" is not in the read-only allowlist`;
            continue;
        }
        const program = pos.program;
        if (program !== undefined) {
            const familyRefusal = argumentAsProgramRefusal(program.basename, program.basename, folded.slice(program.index + 1));
            if (familyRefusal !== undefined)
                nameRefusal ??= familyRefusal;
        }
        const parsed = parseLeadingCommandName(toks.raw.slice(from).join(" "), { quotedOperatorsAreText: true, pathPrefixedNameIsText: true });
        if ("reject" in parsed)
            return { reason: parsed.reject };
        const pathPrefixed = parsed.name.includes("/");
        const name = pathPrefixed ? parsed.name.slice(parsed.name.lastIndexOf("/") + 1) : parsed.name;
        if (pathPrefixed)
            nameRefusal ??= "the command must be a bare name resolved via PATH (no path prefix)";
        if (pos.refusal !== undefined)
            return { reason: programPositionRefusalSentence(pos.refusal) };
        if (allow.has(name)) {
            if (pathPrefixed || from > 0)
                reheaded.set(si, { from, head: name });
            if (from > 0)
                nameRefusal ??= `command "${folded[0]}" is not in the read-only allowlist`;
            continue;
        }
        nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
        if (SHELL_REENTRY_PROGRAM_RE.test(name))
            return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text") };
        if (pos.launchers.length > 0) {
            const listed = program !== undefined && (allow.has(program.basename) || program.basename === "cd");
            if (program !== undefined && !listed && SHELL_REENTRY_PROGRAM_RE.test(program.basename)) {
                return { reason: `"${written}" runs "${program.basename}", which runs its argument as shell text this classifier does not read ${NOT_AUTO_ALLOWED}` };
            }
            if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED)) {
                return { reason: `"${written}" takes the operands of the program it runs from its standard input, which this classifier does not read ${NOT_AUTO_ALLOWED}` };
            }
            if (!listed || program === undefined)
                unlistedSegments.add(si);
            else
                reheaded.set(si, { from: program.index, head: program.basename });
            continue;
        }
        unlistedSegments.add(si);
    }
    const HEAD_AUTO_ALLOW_MAX = 1_000_000;
    const headBoundIsSmall = (name, toks) => {
        if (name !== "head")
            return false;
        let byteBound = false;
        for (let k = 1; k < toks.length; k++) {
            const t = toks[k];
            if (!t.startsWith("-") || t === "-")
                continue;
            const inline = /^-[A-Za-z]*c(\d+)$/.exec(t) ?? /^--bytes=(\d+)$/.exec(t);
            if (inline) {
                if (Number(inline[1]) > HEAD_AUTO_ALLOW_MAX)
                    return false;
                byteBound = true;
                continue;
            }
            if (/^-[A-Za-z]*c$/.test(t) || t === "--bytes") {
                const v = toks[k + 1];
                if (v === undefined || !/^\d+$/.test(v) || Number(v) > HEAD_AUTO_ALLOW_MAX)
                    return false;
                byteBound = true;
                k++;
                continue;
            }
            return false;
        }
        return byteBound;
    };
    const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity, diff: 2, cmp: 2, comm: 2, sed: 2 };
    const foldedSegments = [];
    let shapeRefusal = nameRefusal;
    for (let si = 0; si < segments.length; si++) {
        if (unlistedSegments.has(si))
            continue;
        const rehead = reheaded.get(si);
        const words = argvOf(split.lexed[si]);
        const spelled = segmentSpellings(words, boundary?.homeDir, homeRebound);
        const segmentTokens = rehead === undefined ? spelled : reheadSegment(spelled, rehead.head, rehead.from);
        const argWords = words.slice((rehead?.from ?? 0) + 1);
        const toks = segmentTokens.folded;
        if (toks.length === 0)
            continue;
        foldedSegments.push(segmentTokens);
        const name = toks[0];
        const guardReason = guardedVerbFormReason(name, toks, argWords);
        if (guardReason !== undefined) {
            shapeRefusal ??= guardReason;
            continue;
        }
        if (!(pipeFed[si] ?? false)) {
            const floor = STDIN_FILE_FLOOR[name];
            const restArgs = toks.slice(1);
            const floorFirstOperandSlot = VERB_OPTION_SLOTS[name]?.firstOperand;
            const floorSlotSuppliedByFlag = floorFirstOperandSlot !== undefined && restArgs.some((t) => suppliesFirstOperandSlot(floorFirstOperandSlot, t));
            let sawNonFlagOperand = false;
            let hasStdinDash = false;
            let operandCount = 0;
            let endOfOptions = false;
            if (name !== "tr") {
                for (let k = 0; k < restArgs.length; k++) {
                    const t = restArgs[k];
                    if (endOfOptions) {
                    }
                    else if (t === "--") {
                        endOfOptions = true;
                        continue;
                    }
                    else if (takesSeparatedValue(name, t)) {
                        k++;
                        continue;
                    }
                    else {
                        if (name === "grep" && (isGrepClusterFileStdin(t) || isGrepFileStdinLongOption(t))) {
                            hasStdinDash = true;
                            break;
                        }
                        if (t.startsWith("-") && t !== "-")
                            continue;
                    }
                    const inFirstOperandSlot = floorFirstOperandSlot !== undefined && !floorSlotSuppliedByFlag && !sawNonFlagOperand;
                    sawNonFlagOperand = true;
                    operandCount++;
                    if (inFirstOperandSlot)
                        continue;
                    if (t === "-") {
                        hasStdinDash = true;
                        break;
                    }
                }
            }
            if (floor !== undefined && hasStdinDash) {
                shapeRefusal ??= `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout ${NOT_AUTO_ALLOWED}`;
                continue;
            }
            if (floor !== undefined && operandCount < floor) {
                shapeRefusal ??= `"${name}" with no file argument reads stdin and would block until the tool timeout ${NOT_AUTO_ALLOWED}`;
                continue;
            }
        }
        if (name === "tail" && toks.slice(1).some((t) => t === "--follow" || t.startsWith("--follow=") || /^[-+][^\s]*[fF]/.test(t))) {
            shapeRefusal ??= "`tail` in follow mode never terminates " + NOT_AUTO_ALLOWED;
            continue;
        }
        const GENERATOR_DEVICES = new Set(["/dev/zero", "/dev/random", "/dev/urandom", "/dev/full"]);
        const deviceCandidates = [];
        for (const t of toks.slice(1)) {
            if (t.startsWith("-") && t !== "-") {
                deviceCandidates.push(...attachedOptionPayloads(t));
                continue;
            }
            deviceCandidates.push(t);
        }
        const deviceArgs = deviceCandidates.map((t) => resolveOperandLexically(undefined, t, boundary?.homeDir) ?? normalizeAbsPathLexically(t)).filter(isBlockedDevicePath);
        const rescuedByHead = headBoundIsSmall(name, toks) && deviceArgs.every((d) => GENERATOR_DEVICES.has(d));
        if (!rescuedByHead && deviceArgs.length > 0) {
            shapeRefusal ??= "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) " + NOT_AUTO_ALLOWED;
        }
    }
    const cdSegments = foldedSegments.filter((st) => st.folded[0] === "cd");
    if (cdSegments.length > 1)
        shapeRefusal ??= `multiple directory changes in one command require approval for clarity ${NOT_AUTO_ALLOWED}`;
    return shapeRefusal !== undefined ? { reason: shapeRefusal } : {};
}
export const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
export function classifyCompoundReadonly(command, allow, boundary) {
    return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
}
export const POLL_LOOP_MAX_BEATS = 120;
export const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
const POLL_BODY_HARD_REJECT = /[<>$()`|&{}\n\r\\]/;
const NON_ASCII_WHITESPACE = /[^\S \t]/;
const NOT_A_POLL_LOOP = "not a bounded read-only poll loop (`for <v> in <literal bound>; do <read commands>; done` is the only accepted control structure)";
function pollLoopShapeOf(lexed) {
    const segments = [...lexed.segments];
    const last = segments[segments.length - 1];
    if (last !== undefined && last.tokens.length === 0 && last.connector === ";" && segments.length > 1)
        segments.pop();
    if (segments.length < 3)
        return undefined;
    const bare = (w, text) => w !== undefined && w.raw === w.folded && w.folded === text;
    const onlyWords = (s) => s.tokens.every((t) => t.kind === "word");
    const head = segments[0];
    if (!onlyWords(head))
        return undefined;
    const hw = argvOf(head);
    if (!bare(hw[0], "for") || !bare(hw[2], "in") || hw.length < 4)
        return undefined;
    const name = hw[1];
    if (name.raw !== name.folded || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name.folded))
        return undefined;
    const done = segments[segments.length - 1];
    const dw = argvOf(done);
    if (done.connector !== ";" || !onlyWords(done) || dw.length !== 1 || !bare(dw[0], "done"))
        return undefined;
    const first = segments[1];
    if (first.connector !== ";")
        return undefined;
    const fw = argvOf(first);
    if (!bare(fw[0], "do") || fw.length < 2)
        return undefined;
    const body = [{ words: fw.slice(1), segment: first }];
    for (const s of segments.slice(2, -1))
        body.push({ words: argvOf(s), segment: s });
    return { varName: name.folded, iterable: hw.slice(3), body };
}
function pollLoopBeatsOf(iterable) {
    const one = iterable.length === 1 && iterable[0].pieces.length === 1 ? iterable[0].pieces[0].expansion : undefined;
    const seq = one?.kind === "command" ? /^seq[ \t]+(\d{1,7})[ \t]+(\d{1,7})$/.exec(one.body ?? "") : null;
    const brace = seq === null && one?.kind === "brace" ? /^(\d{1,7})\.\.(\d{1,7})$/.exec(one.body ?? "") : null;
    const range = seq ?? brace;
    if (range !== null) {
        const lo = Number(range[1]);
        const hi = Number(range[2]);
        return hi >= lo ? hi - lo + 1 : undefined;
    }
    return iterable.every((w) => w.raw === w.folded && w.expansions.length === 0 && /^[A-Za-z0-9_.-]{1,64}$/.test(w.raw)) ? iterable.length : undefined;
}
function pollLoopSleepReason(toks) {
    if (toks.length !== 2)
        return "`sleep` in a poll loop must take exactly one literal numeric argument";
    const v = toks[1];
    if (!/^\d+(\.\d+)?$/.test(v) || v.length > 8 || Number(v) > POLL_LOOP_MAX_SLEEP_SECONDS) {
        return `\`sleep ${v}\` is not a literal duration within the ${POLL_LOOP_MAX_SLEEP_SECONDS}s per-beat cap ${NOT_AUTO_ALLOWED}`;
    }
    return undefined;
}
export function classifyBoundedReadonlyPollLoop(command, allow, boundary) {
    return classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary).reason;
}
export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary) {
    if (NON_ASCII_WHITESPACE.test(command)) {
        return { reason: "the command contains a non-ASCII or control whitespace character (only space and tab are allowed) — its word boundaries cannot be read the way bash would split them, so it is not auto-allowed" };
    }
    const trimmed = command.trim();
    if (!trimmed)
        return { reason: "empty command" };
    const lexed = lexShell(trimmed);
    if ("parseFailure" in lexed) {
        const r = lexed.parseFailure.reason;
        if (r === "unterminated_single_quote" || r === "unterminated_double_quote" || r === "unterminated_ansi_c") {
            return { reason: "unbalanced quote — the command's structure cannot be read reliably, so it is not auto-allowed" };
        }
        return { reason: NOT_A_POLL_LOOP };
    }
    const shape = pollLoopShapeOf(lexed);
    if (shape === undefined)
        return { reason: NOT_A_POLL_LOOP };
    if (!/^[a-z]$/.test(shape.varName)) {
        return { reason: `the loop variable "${shape.varName}" must be a single lowercase letter — a name like PATH/IFS/LD_PRELOAD would change how the body's commands resolve` };
    }
    const beats = pollLoopBeatsOf(shape.iterable);
    if (beats === undefined) {
        return { reason: `the loop bound "${shape.iterable.map((w) => w.raw).join(" ")}" is not a recognized literal — only \`$(seq <int> <int>)\`, \`{<int>..<int>}\` (ascending) or a literal word list is accepted` };
    }
    if (beats > POLL_LOOP_MAX_BEATS) {
        return { reason: `the loop runs ${beats} iterations, above the ${POLL_LOOP_MAX_BEATS}-iteration cap for auto-allow` };
    }
    const homeRebound = boundary?.homeDir !== undefined && homeMayBeRebound(command);
    const bodies = shape.body.map((b) => ({ ...b, spelled: segmentSpellings(b.words, boundary?.homeDir, homeRebound).raw }));
    for (const b of bodies) {
        if (b.segment.connector !== ";" || b.segment.tokens.some((t) => t.kind !== "word") || b.spelled.some((raw) => POLL_BODY_HARD_REJECT.test(raw))) {
            return { reason: "the loop body may not contain redirection, pipes, backgrounding, substitution, subshells, braces, escapes, or line breaks" };
        }
    }
    const readSegments = [];
    for (const b of bodies) {
        if (b.words.length === 0)
            return { reason: "empty command in the loop body" };
        const segment = b.spelled.join(" ");
        const parsed = parseLeadingCommandName(segment, { quotedOperatorsAreText: true });
        if ("reject" in parsed)
            return { reason: parsed.reject };
        if (parsed.name === "sleep") {
            const sleepReason = pollLoopSleepReason(b.spelled);
            if (sleepReason !== undefined)
                return { reason: sleepReason };
            continue;
        }
        readSegments.push(segment);
    }
    if (readSegments.length === 0) {
        return { reason: "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed" };
    }
    return classifyCompoundReadonlyDetailed(readSegments.join("; "), allow, boundary);
}