UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,048 lines • 82.1 kB
import { type LexedCommand, type LexedSegment, type LexedWord, type ShellLex, type ShellSegmentConnector, type WordPiece } from "../../shell/lex.js";
/**
 * Bare command names `bash_readonly` permits out of the box, ALSO the default reversible set for the
 * The {@link import("./fs-bash.js").bashReversibilityProbe} classifier. **Coarse first filter, NOT a security
 * boundary**: {@link coarseReadonlyCheck} matches only the command NAME, so a listed command with a
 * writing flag (e.g. `find -delete`, `sort -o`, `tee`) would still write. Such commands are therefore
 * kept OFF this default — the list is curated to commands with NO write/mutation mode under ANY args, so
 * the `effect:"read"` declaration (relied on by wake/resume reconcile + the verifier read-only boundary)
 * AND the classifier's "reversible" promise both stay truthful. The deployment's tool-policy gate is the
 * authoritative control (council blocker #1); anything that can mutate state or run arbitrary
 * code belongs on the full `bash` (effect:write, gated), not here. A deployment may widen this list,
 * accepting that responsibility.
 *
 * Deliberately ABSENT: `date` (`-s`/`--set` → CLOCK_SETTIME), `hostname` (`<name>` → kernel
 * hostname), and `file` (`-C -m` → compiles/writes a magic file) were REMOVED — each is read-only by NAME
 * but state-MUTATING with args, which an argv[0]-only filter cannot tell apart. Leaving them in defeated
 * both the `effect:read` truthfulness here and the classifier's irreversibility promise (a `date -s` would
 * auto-allow an irreversible clock jump on the full shell face).
 *
 * Rows 3-4 are the CC-anchored expansion — the members of CC's safe-command
 * set (pretty223.js:420513-420565 `e6s`; identical in the 220 corpus; the 220→245 campaign diff records
 * no change on this face) that satisfy THIS list's curation bar (no write/mutation mode under any args)
 * without new stdin-floor/option-value modeling: pure status/computation printers (`cal`…`sleep` — none
 * reads stdin when bare, none takes a path it READS except realpath/readlink, which stay boundary-judged)
 * plus the floor-2 content comparers `diff`/`cmp`/`comm` (STDIN_FILE_FLOOR rows added alongside; diff's
 * recursive form was already modeled in RECURSIVE_READ_FORMS). Still deliberately excluded, each for a
 * stated reason: `env` (an executor: `env FOO=1 cmd` runs cmd — CC excludes it too), `sort` (`-o` writes),
 * `uniq` (second positional is an OUTPUT file; CC allows a flags-only regex form this filter cannot
 * express), `printf` (builtin `-v var` assigns), `find` (CC allows only a regex excluding
 * `-delete`/`-exec`/…), and the bare-stdin text filters (`od`/`strings`/`nl`/`tsort`/`pr`/…) whose
 * value-taking options defeat the stdin-floor operand count without a per-verb option-value model
 * — those are a follow-on slice, not a silent drop.
 */
export type { ShellSegmentConnector } from "../../shell/lex.js";
export declare const BASH_READONLY_DEFAULT_ALLOW: readonly string[];
/**
 * The verbs the CLASSIFY face admits ON TOP of the shared list ({@link bashClassifyDefaultAllow}) —
 * admissible only where the compound classifier's per-verb judgment runs, and NOT on the shared list's
 * other consumer faces:
 *  · `find` — read-only only when no CC `aCy` dangerous predicate token is present (guard arm);
 *  · `sed`  — read-only only in CC's `_Ld` grammar (guard arm);
 *  · `cd`   — never touches the filesystem, but it MUTATES the persistent shell's committed cwd, so it
 *    must not reach the shared list's `isConcurrencySafe` consumer (a parallel-batch `cd` reorders
 *    every sibling's working directory) or the `effect:"read"` declaration face; the classify face's
 *    EXISTING cd boundary arm (glob/no-arg/`-` conservative, target judged against the roots) is what
 *    makes it admissible here.
 * The bash_readonly DECLARATION face checks names without argument vetting (its own header says so), so
 * none of the three may join {@link BASH_READONLY_DEFAULT_ALLOW}: `find . -delete` / `sed -i` under an
 * `effect:"read"` declaration would falsify it. Consumed as
 * {@link import("./fs-bash.js").bashReversibilityProbe}'s default when the caller passes NO allow list —
 * a caller-narrowed list is honored verbatim (naming these verbs there opts into the same guard/boundary
 * arms; omitting them keeps them refused), so the superset can never widen a narrowed deployment. The
 * poll-loop face inherits the guard arms for free: its body vetting delegates to the same compound
 * classifier.
 */
export declare const BASH_CLASSIFY_EXTRA_ALLOW: readonly string[];
/** A verb only the classify face admits. */
export type BashClassifyExtraVerb = (typeof BASH_CLASSIFY_EXTRA_ALLOW)[number];
/**
 * @contract shell.classify_allow.one_union_point — the classify face's default allow set is the union of
 * two SPELLED-OUT tables ({@link BASH_READONLY_DEFAULT_ALLOW} ∪ {@link BASH_CLASSIFY_EXTRA_ALLOW}) and
 * this function is its only assembly point; no call site composes its own. Both halves stay arrays of
 * string literals so a static reader can enumerate them: a vocabulary derived in place
 * (`[...TABLE, "extra"]`) is invisible to the release's wire-change manifest, so a verb added to or
 * dropped from this security-relevant set would move nothing a downstream reader could see.
 */
export declare function bashClassifyDefaultAllow(): readonly string[];
/**
 * G2 — options for {@link parseLeadingCommandName}. Omitting them keeps the parser byte-identical to
 * what every existing caller (the `bash_readonly` declaration face, the coarse command-name policy, the
 * permission-rule floor, the skill tool specifier) has always received.
 */
export interface LeadingCommandNameOptions {
    /**
     * Read a shell operator that sits inside a QUOTED region as ordinary argument TEXT rather than as an
     * operator, so `grep -E "a|b" f.log` parses as the single simple command it is.
     *
     * OPT-IN, read off the one lexer's result: an operator character bash reads as text (inside quotes, or
     * behind an escape) is text here. Two callers set it — the compound classify face
     * ({@link classifyCompoundReadonlyDetailed}) and the persisted permission-RULE lane (`RULE_LANE_FLOOR`),
     * whose segments come out of the same residual predicate ({@link splitShellCompoundSegments}). The
     * `bash_readonly` DECLARATION face does NOT set it: its contract is "a single simple command with no
     * operator characters at all", and it runs on raw tool input with no whole-string gate in front of it.
     *
     * Fail-closed on a command the lexer cannot delimit: every operator character then counts, exactly as
     * without this option.
     */
    quotedOperatorsAreText?: boolean;
    /**
     * Read a PATH-PREFIXED `argv[0]` (`./gradlew`, `/usr/bin/git`, `bin/tool`) as an ordinary command
     * name instead of refusing it.
     *
     * OPT-IN, and used by ONE family of callers: the persisted permission-RULE lane
     * ({@link import("../../core/permission-rule-model.js").parseRuleText} and its matcher), where
     * both sides of the comparison are literal text a person read on an approval card. The refusal this
     * option lifts exists for the argv[0]-NAME filters (the read-only allowlist, the coarse
     * command-name policy, the skill specifier): those compare a bare token against a name set, and a
     * path prefix is how a caller reaches a program the set never vetted. A rule lane compares the whole
     * command line instead, so `./gradlew` there is not a way past a name set — it IS the name that was
     * approved, and refusing it made the single most ordinary build command in a repository unable to
     * carry a standing approval at all.
     *
     * What it does NOT lift: the argv[0] metacharacter refusal (quotes/braces/globs/`~` still make the
     * parsed token differ from the program bash runs), the leading env-assignment refusal, and every
     * shell operator. And it does not make a path-prefixed name equal its basename anywhere — the rule
     * lane matches on the text as written, so `./gradlew` and `gradlew` stay two different commands. The
     * one place a basename IS taken is the rule lane's own interpreter refusal, which must read
     * `/usr/bin/node` as `node` (see
     * {@link import("../../core/permission-rule-model.js").BARE_INTERPRETER_NAMES}).
     */
    pathPrefixedNameIsText?: boolean;
}
/**
 * The SINGLE fail-closed simple-command parser shared by `bash_readonly` ({@link coarseReadonlyCheck}), the
 * `bash` reversibility classifier ({@link import("./fs-bash.js").bashReversibilityProbe}), and the coarse command-name policy
 * ({@link import("../../core/tool-policy.js").createCoarseCommandNamePolicy}). It extracts the leading
 * `argv[0]` command NAME of a SINGLE simple command, rejecting anything that could chain past or escape an
 * argv[0]-name filter: shell operators (pipes / redirects / `;` / `&&` / `$(…)` / subshells / backticks /
 * newlines / backslash), a path-prefixed command (`/usr/bin/foo`), or a leading env-assignment (`FOO=bar cmd`).
 * The path-prefix refusal is the one arm a caller may lift, opt-in and for the rule lane only — see
 * {@link LeadingCommandNameOptions.pathPrefixedNameIsText} for why it is sound exactly there.
 *
 * Returns `{ name }` for a parseable single bare command, or `{ reject }` with a human reason otherwise. It
 * does NOT inspect ARGUMENTS for write flags or consult any allowlist — that is the caller's job (the
 * allowlist for `bash_readonly`, the allow/deny lists for the coarse policy). Keeping ONE parser is the whole
 * point: a second argv[0] parser would drift from this one and silently open a circumvention.
 */
export declare function parseLeadingCommandName(command: string, options?: LeadingCommandNameOptions): {
    name: string;
} | {
    reject: string;
};
/** Coarse pre-execution check for `bash_readonly`: the command must be a single bare allowlisted command
 *  with no shell operators. It validates the NAME only (+ blocks operators/path-prefix/env-assignment); it
 *  does NOT inspect arguments for write flags — keeping the allowlist free of write-capable commands and
 *  the policy gate are what make `effect:"read"` truthful. Returns a reason when rejected, else undefined.
 *  This `effect:"read"` DECLARATION face deliberately stays strict-single-command; only the
 *  classify face ({@link classifyCompoundReadonly} via {@link import("./fs-bash.js").bashReversibilityProbe}) segments compounds. */
export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<string>, options?: LeadingCommandNameOptions): string | undefined;
/**
 * Does this text carry a bare redirection operator — the ONE metacharacter family
 * {@link splitShellCompoundSegments} can be asked to tolerate at its whole-string gate
 * (`redirection: "tolerate"`)?
 *
 * Exported because the tolerating caller owes a PER-SEGMENT judgement in its place, and that judgement
 * must read the same character class this module tolerated — two hand-kept lists would drift, and a
 * drift here is a segment nobody judged. Says nothing about what a caller should DO about a hit: the
 * rule lane's offer face contributes no member for such a segment; the read-only classify face passes
 * only the two data-free spellings ({@link isDataFreeRedirection}).
 */
export declare function carriesShellRedirection(text: string): boolean;
/** What a caller asks the residual predicate to pass: see {@link strictResidualRefusal}. */
export interface StrictResidualOptions {
    /** `<`/`>` are not refused and a redirection is the segment's own business (the rule lane's offer reading). */
    readonly tolerateRedirection?: boolean;
    /** Only the two data-free spellings pass ({@link isDataFreeRedirection}); every other redirection refuses. */
    readonly dataFreeRedirections?: boolean;
    /** The declared home: a word that spells it ({@link homeExpansionOf}) is not an expansion to refuse — unless the
     *  command may rebind `HOME` ({@link homeMayBeRebound}), which makes the declared home a guess. */
    readonly homeDir?: string;
}
/**
 * THE classify faces' whole-string gate, as a PREDICATE over the one lexer's result: the residual space those
 * faces live in is what this function does not refuse. Four passes, in the order the gate's sentences were
 * always minted, so a refused command is refused for the same stated reason:
 *  1. the quote-blind class ({@link SHELL_SEGMENT_QUOTE_BLIND_REJECT}) over every written spelling — a word, a
 *     redirection target under `tolerateRedirection` — plus every group, here-document, line-break connector
 *     and (unless tolerated or data-free) redirection token;
 *  2. the expansion character where bash expands it ({@link expandsDollar}), the home-directory word excepted
 *     under `homeDeclared`;
 *  3. a backgrounding `&` and a `|&`;
 *  4. a word whose value the text does not settle ({@link UNRESOLVED_SENTENCE}) — a compound array, an
 *     off-table ANSI-C escape, a subscript or parameter expansion this lexer does not read.
 * A command the lexer cannot delimit is refused for the class if its text carries one (the reading the gate
 * gave an unbalanced string), else for the parse failure. Returns the refusal REASON, or `undefined` to proceed.
 */
export declare function strictResidualRefusal(lexed: ShellLex, source: string, opts?: StrictResidualOptions): string | undefined;
/** What {@link splitShellCompoundSegments} produces: the connector-delimited segments, plus which of
 *  them a PIPE fed (the read-only classifier's stdin arm needs that distinction; a rule lane does not). */
export interface ShellCompoundSegments {
    segments: string[];
    /** Parallel to {@link segments}: was this segment preceded by `|` (rather than `;`/`&&`/`||`/nothing)?
     *  A VIEW of {@link connectors}, derived at the one place both are minted — two rows that could drift are
     *  two rows that will. */
    pipeFed: boolean[];
    /** Parallel to {@link segments}: the connector bash reached this segment through. Read by the read
     *  boundary, where it is the difference between a directory change the shell itself proved happened and
     *  one this walk cannot verify. A consumer that does not read it gets the unverified reading, which is
     *  the fail-closed one. */
    connectors: ShellSegmentConnector[];
    /** Parallel to {@link segments}: the segments as the lexer minted them — the words (with their facts) and
     *  the redirections of each. A consumer that tokenizes reads these; the text is for the sentences. */
    lexed: readonly LexedSegment[];
}
/**
 * Split a compound command into the segments bash would run, or refuse the whole string.
 *
 * THE one segmentation in this repository, and the one place the classify faces' residual predicate is
 * applied: the persisted permission-rule lane and the read-only classify face both read their segments
 * here, so a drift in what a deny rule is even looking at cannot open between them.
 *
 * Two steps: the trailing-terminator strip, then ONE lex ({@link lexShell}) judged by
 * {@link strictResidualRefusal}. The strip is OPTIONAL because it is the one step whose answer depends on
 * what the caller is asking. A face that asks "which programs does this run" wants it stripped (`ls;` runs
 * `ls`, nothing else). A face that asks "is this ONE command" must NOT strip it: `"keep"` makes `ls;` two
 * segments, the second empty, so the caller's own empty-command rule puts the whole string outside its lane
 * — which is what keeps a statement like "this rule form never matches a command containing a connector"
 * literally true instead of true-except-for-one-spelling. `redirection` picks which redirections the
 * predicate passes: none (the default, the incumbent whole-string refusal), every one (`"tolerate"` — the
 * refusal moves to the caller's per-segment vetting via {@link carriesShellRedirection}), or the two
 * data-free spellings (`"data-free"`, the read-only classify face). `homeDir` lets the word spelling the
 * declared home through the expansion refusal.
 *
 * Empty segments are NOT refused here (`;;`, a leading `;`, a trailing `&&`) — they come back as empty
 * strings, and every caller's per-segment vetting rejects them through its own empty-command rule. The
 * split does not judge segments; it says where they are.
 */
export declare function splitShellCompoundSegments(source: string, options?: {
    trailingTerminator?: "strip" | "keep";
    redirection?: "reject" | "tolerate" | "data-free";
    homeDir?: string;
}): ShellCompoundSegments | {
    reject: string;
};
/**
 * Compound read-only classification for the `bash` full-shell face (the shape tier).
 * Splits a command on the connector set `;` `&&` `||` `|` and passes every segment through
 * {@link coarseReadonlyCheck} (the single {@link parseLeadingCommandName} parser + bare-name allowlist).
 * All segments green ⇒ read-only. Returns a reason when rejected, else undefined.
 *
 * G2 — the split CARRIES QUOTE STATE (the one lexer): a connector character inside a quoted region is
 * argument text, not a boundary, so `grep -E "error:|BUILD FAILED" x.log | tail -10` is two segments
 * (`grep …` and `tail -10`) rather than four, and the phantom head `BUILD` no longer decides the verdict.
 * The whole compound face is what this fixes: a report of an every-command approval prompt on a read-only
 * monitoring pipeline traced to exactly this — a quoted alternation in a `grep -E` pattern, which is the
 * ordinary spelling of that idiom.
 *
 * Soundness — why the quote-aware split is NOT a relaxation. Backslash/`$`/backtick/subshells/redirection/
 * line breaks are refused first (the two data-free forms of {@link isDataFreeRedirection} pass, and they
 * move no data into any file), so in the residual space `'`/`"` pairing is
 * the ENTIRETY of quoting and the mask's regions are exactly bash's quoted regions. The unquoted connectors it splits on are therefore exactly bash's command boundaries:
 * every program bash runs still starts at the head of some segment this classifier vetted, and its argv[0]
 * is the head token (a token carrying a quote/brace/glob/tilde character is rejected by the parser's
 * argv[0] metachar rule, and a clean token equals the executed program name). What changes versus the
 * older quote-blind scan is only that PHANTOM segments — heads manufactured out of quoted text, which no
 * program was ever going to run — are gone. Those could only ever ADD a rejection, so removing them
 * removes false demotions and admits no additional program.
 *
 * UNBALANCED QUOTING is a parse failure, refused whole (fail-closed). A string with an unterminated quote
 * is one bash cannot parse into commands at all, so there is no "true" segmentation to be accurate about.
 *
 * A LONE `&` is hard-rejected (not a connector): it backgrounds a process that survives the shell — the node
 * execution env spools stdio and deliberately leaves descendants alive on completion — escaping TaskStop and
 * background-lifecycle governance entirely. Backgrounding is not a read-only combinator, even for a listed
 * reader (`tail -f x &` outlives the call). `|&` is refused by name.
 * One TRAILING `;` is stripped (a no-op terminator); any other empty segment (`;;`, leading `;`, trailing
 * `&&`/`|`) rejects via the parser's empty-command rule.
 *
 * NOT here by design: no git/subcommand allowlist — "zero-flag" vetting operates
 * on pre-expansion text while the shell expands quotes/braces/globs into argv, and the candidate subcommands
 * are not provably read-only under repo config anyway. Deployments needing those forms must widen via a
 * dedicated widening decision, not this classifier.
 */
/**
 * The READ-BOUNDARY face of the read-only classification (opt-in).
 *
 * The allowlist vets `argv[0]` only, so until this face existed every listed reader could name ANY
 * path: `ls /etc` and `cat /etc/passwd` classified read-only, and a deployment whose manual-mode shell
 * gate auto-allows the read-only verdict served the whole filesystem with no approval prompt. CC 2.1.220
 * demotes exactly these shapes (its per-command path extractor + working-directory containment check,
 * pretty220 @457568) and renders an approval option naming the directory (@905642 (cli250.js:66007)).
 *
 * Supplying this object turns the check ON; omitting it leaves the classifier byte-identical to its
 * name-only behaviour, so a consumer pinned to an older calling convention is unaffected.
 */
export interface BashReadonlyRootBoundary {
    /** Canonical containment roots — the primary root plus any additional allowed directories. An EMPTY
     *  array means "no directory is allowed", so every path-shaped operand demotes (a bare `cat a.txt`
     *  still does not: a name with no separator can only reach the working directory, which is the
     *  caller's to govern). A caller that wants the check OFF omits the whole object instead. */
    roots: readonly string[];
    /** Base for resolving relative operands. Defaults to `roots[0]`. Callers tracking an observable `cd`
     *  should pass the LIVE working directory, or a `..` operand will be judged against the wrong base. */
    cwd?: string;
    /** Expansion for a leading `~` — and the value the read faces substitute for the `$HOME` / `${HOME}` word
     *  spellings ({@link homeExpansionOf}). Without it a `~`-rooted operand is unresolvable
     *  and demotes (with no out-of-root signal — the classifier does not know where it points, which is a
     *  different verdict from knowing it points outside). */
    homeDir?: string;
    /** The sensitive-path read deny judge over LEXICALLY RESOLVED operands: a hit demotes the command (ask,
     *  never auto-allow), independently of the roots — in-root operands are judged too. Returns the matched
     *  pattern, or null. TWO named residuals, both inherited from this classifier's declared purity
     *  (synchronous, zero I/O — the same scope stated for the containment half): ① operand TARGET matching
     *  only, no ancestor intersection: the judge sees the operand's own resolved spelling, never its subtree.
     *  The RECURSIVE-reach half of ① is narrowed — a listed recursive verb's path operand under this wired
     *  seat rides {@link CompoundReadonlyVerdict.recursiveReadPaths} (⊂ undecidedPaths), so `grep -r x ~/` no
     *  longer auto-allows; what REMAINS is the form table's stated open set (the KNOWN OPEN SET note at
     *  {@link RECURSIVE_READ_FORMS}: a traversal verb absent there that a deployment allowlists classifies by
     *  its ordinary form and can auto-run a traversal unjudged). ② LEXICAL only HERE — an in-root symlink
     *  whose target is guarded reads as its innocent spelling at this seat, as it does for the containment
     *  half, because the classify auto-allow lane has no I/O seat by contract; the CANONICAL view is judged by
     *  an execute-time recheck that asks THIS judge (the same function object, forwarded) where each candidate
     *  really lands, on a `checkedPaths` literal and a pattern's expansions alike.
     *  EVERY read face wires this one seat, the `bash_readonly` shell included, at BOTH of its judging
     *  moments — roots answer WHERE a command may read and the allowlist WHICH verb runs, neither answers
     *  WHICH FILES are off the table. What this seat is and is not is stated once, at
     *  `shell.read_boundary.friction_floor`. `walked` — the path is a member a traversal enumerated, not a
     *  spelling of the command ({@link import("./read-deny.js").ReadDenyMatcher.matchPath}). */
    denyMatch?: (resolvedPath: string, opts?: {
        readonly walked?: boolean;
    }) => string | null;
    /** What the wired {@link denyMatch} judge would apply — the ACTIVE deny patterns. Read for ONE question:
     *  is there anything for a traversal to defeat? EMPTY ⇒ the judge refuses nothing, so a walk under it
     *  bypasses nothing and {@link CompoundReadonlyVerdict.recursiveReadPaths} is not minted. ABSENT ⇒ the
     *  caller did not say, which is not "none": the walk is reported, the reading every caller had before
     *  this seat existed. It is deliberately NOT the reference implementation's question at the same place —
     *  its deny rules are ROOTED, so "is the guarded place inside this traversal?" is lexically answerable
     *  there; these patterns match a segment run ANYWHERE in a path ({@link
     *  import("./read-deny.js").ReadDenyEntry}), so only emptiness is. */
    denyEntries?: readonly string[];
    /** The resolved read-face containment state. Under "open" the CONTAINMENT half of
     *  this boundary is structurally satisfied (an out-of-roots operand is not a demotion; it reports
     *  as a checked candidate instead), while the deny half above keeps judging in both faces.
     *  Absent ⇒ "roots" (byte-compat). The `bash_readonly` face never passes this seat — its
     *  containment is load-bearing and never opens. */
    face?: "open" | "roots";
}
/**
 * The structured verdict of {@link classifyCompoundReadonlyDetailed}. `reason === undefined`
 * is the read-only verdict; everything else is a demotion.
 */
export interface CompoundReadonlyVerdict {
    /** Demotion reason (the same string {@link classifyCompoundReadonly} returns), or undefined: the
     *  command's SHAPE is not one the shape tier auto-allows. WHERE the command reads is not this
     *  verdict's business — that is the gate's shell read station (`shell-read-check.ts`). */
    reason?: string;
}
/**
 * The single minting point for the out-of-root-read approval option text, so a gate rendering
 * the prompt and any other consumer cannot drift apart. Shape follows CC: the option
 * shows the LAST segment of the grantable directory followed by a separator, not the full path.
 *
 * `directory` is the directory to be granted — a caller holding a filesystem (this classifier does not)
 * resolves a file operand to its parent first; CC does the same with a stat-then-dirname step.
 * The separator is always `/`: this classifier's path handling is POSIX-lexical throughout, where CC
 * uses the host's native separator.
 */
export declare function formatOutOfRootReadApprovalOption(directory: string): string;
/** Programs whose operands are NEVER filesystem paths, so no operand of theirs is checked:
 *  `echo`'s words are literal text, `which`'s are PATH lookups, `tr`'s are character sets, and
 *  `basename`/`dirname` do pure string surgery on a path without ever opening it. Everything else —
 *  including a deployment-widened name this file does not know — goes through the path-shaped
 *  operand scan below, which is safe for an unknown command because a bare word can only ever name
 *  something under the working directory. */
export declare const NO_PATH_OPERAND_COMMANDS: ReadonlySet<string>;
/** The no-path-operand programs that EVALUATE a word (`test -v 'a[$(…)]'` runs the substitution in the subscript): a word
 *  the shell or that evaluation rewrites is handed over as any program's is; only their settled words name nothing. */
export declare const NO_PATH_OPERAND_EVALUATORS: ReadonlySet<string>;
/** The builtins whose `NAME=value` arguments are assignments bash performs (and exports, for `export`), never paths. */
export declare const DECLARATION_BUILTINS: ReadonlySet<string>;
/** The shell BUILTINS that open no file, start no program and evaluate nothing: an external program's loader reads its
 *  environment (`LD_PRELOAD` for `sleep`), `printf` reads `TZ` for `%(…)T` and assigns through `-v`, `test -v` and an
 *  array subscript evaluate arithmetic (which runs a `$(…)` inside it). A declaration builtin counts only bare — no option
 *  word (`-i` evaluates the value) — and every inert segment's words carry no `[` `]` `$` or backquote. */
export declare const INERT_BUILTINS: ReadonlySet<string>;
/** The indices of the SETTLED words a {@link MESSAGE_SLOTS} row reads as message text under the subcommand's grammar:
 *  the value of `-m` (attached, or the next word) or of `--message` (`=`, or the next word; an unambiguous prefix of it
 *  counts). A word the shell rewrites is never claimed, and neither is any word another option consumes — those words
 *  stay the verb-blind walk's. */
export declare function messageSlotWords(program: string, words: readonly {
    readonly folded: string;
    readonly expands: unknown;
}[], programIndex: number): Set<number>;
/** True for a token that can denote a filesystem location OUTSIDE the working directory. A bare word
 *  (`foo`, `a.txt`, a grep pattern, an awk program) cannot: with no separator and no `..` it always
 *  resolves under the cwd, which the caller's cwd governance already covers.
 *
 *  The one spelling that would break that argument is a win-form absolute written with
 *  backslashes only (`\\host\share\secret`, `C:\other\secret`): no `/`, so not path-shaped, yet it names
 *  something the cwd does not contain. Two separate facts, once written as one:
 *   · on the COMPOUND faces it is indeed unreachable — {@link SHELL_SEGMENT_QUOTE_BLIND_REJECT} refuses the
 *     whole command as soon as it contains a backslash, wherever it stands, so no such token reaches this scan;
 *   · on {@link classifySimpleCommandReadBoundary} — one command, no compound grammar, and therefore no
 *     such gate — it DOES reach it. Measured on a win32-rooted boundary, before the lane split below: the operand
 *     resolved to nothing, was dropped, and the verdict was empty, i.e. a read outside every root
 *     AUTO-ALLOWED on the one face whose doctrine is hard refusal in exactly that direction.
 *  The second fact is why the lane split in
 *  {@link collectSegmentBoundaryFindings} tests {@link isAbsolutePathForm} BESIDE this predicate,
 *  which is what the option-payload arm always did; the two now read one rule rather than two. */
export declare function isPathShapedToken(tok: string): boolean;
/** Resolve one operand against the boundary's base, WITHOUT touching the filesystem. Returns undefined
 *  when the token cannot be resolved statically — the fail-closed direction, reported as its own
 *  demotion rather than silently treated as in-root.
 *
 *  The resolution is TEXTUAL, and its limitation runs in BOTH directions (only the first shows up as
 *  a refusal, so only that half tends to get reported):
 *   · false REFUSAL — a non-canonical spelling of an in-root path (a symlinked `/tmp` → `/private/tmp`
 *     prefix) does not textually match the canonical roots and is refused;
 *   · false ALLOW — a symlink INSIDE a root that points outside it (`<root>/vendor` → `/etc`) resolves
 *     to a textual prefix match, so `cat vendor/passwd` is judged in-root and the real command follows
 *     the link. This face is therefore lexical containment, NOT filesystem containment: it is weaker
 *     than the structured Read/Grep tools, which canonicalize through `realpath` before comparing
 *     (see safety.ts's `canonicalizeAndEnforceRoot`). Closing it means resolving each candidate against
 *     the live filesystem, which this function deliberately cannot do (it is synchronous, pure, and
 *     shared with the approval-gate face); the seam for it is the ENFORCING caller, which holds the
 *     ExecutionEnv.
 *
 *  The verdict now carries {@link CompoundReadonlyVerdict.checkedPaths} (the
 *  candidates that landed inside), and `createBashReadonlyTool` canonicalizes them on a LOCAL env
 *  before running — closing BOTH directions above for that face (an escaping symlink is refused; a
 *  non-canonical spelling that resolves back inside is no longer refused). Two residuals stay here by
 *  construction, and belong to the caller rather than to this function: the compound/approval-gate face
 *  has no filesystem and stays lexical, and a REMOTE env keeps the lexical behaviour (its
 *  `canonicalPath` is an RPC per candidate). */
export declare function resolveOperandLexically(base: string | undefined, operand: string, homeDir: string | undefined): string | undefined;
/** One operand's two absolute spellings. `folded` has its `..` collapsed in the TEXT and is the
 *  containment answer — every root comparison, deny judgment and verdict field here is built from it.
 *  `spelled` keeps those segments, the walk the KERNEL performs: each prefix resolves (symlinks included)
 *  BEFORE a parent step applies, so `<root>/link/../x` names `dirname(realpath(link))/x`. Present ONLY
 *  when the operand traverses `..`; a filesystem-resolving caller judges it BESIDE the folded form. */
export interface OperandSpellings {
    /** The `..`-collapsed absolute path — what containment is decided on. */
    folded: string;
    /** The uncollapsed absolute path, when the operand traverses `..`. */
    spelled?: string;
}
/** Resolve one operand into {@link OperandSpellings}, or `undefined` on the same unresolvable operands
 *  {@link resolveOperandLexically} reports (it is this function's folded half). */
export declare function resolveOperandSpellings(base: string | undefined, operand: string, homeDir: string | undefined): OperandSpellings | undefined;
/** Programs that run their argument AS SHELL TEXT (the sh family, `eval`, `source`, `.`): what they read is decided
 *  by text nobody here parsed. Spelled as a pattern, not a name list — these are program names, never tool names.
 *  The prefix alternation is the SPELLINGS of the shell, not a curated subset of them: the Almquist shell and
 *  its descendants (`ash`, `mksh`, `yash`, `pdksh`) and the C shell family (`csh`, `tcsh`) all run `-c` text
 *  exactly as `sh` does, and a deployment that meets one of them meets the whole member. GNU `parallel` is a member
 *  for the reason it is not a launcher: it hands a BUILT command line to a shell. */
export declare const SHELL_REENTRY_PROGRAM_RE: RegExp;
/**
 * Does the shell REWRITE this word before the program sees it — a brace list, a parameter, command, process or
 * arithmetic expansion, or a `$` / backtick / brace character where bash reads it (unquoted; `$` and a backtick
 * also inside double quotes)? Read off the lexer's pieces. Brace expansion runs BEFORE tilde expansion and
 * before the word ever reaches the program, so `cat {<abs>,}` hands the program an absolute path no spelling
 * shows, and `ls {~,}` a home directory the `~` arm of {@link resolveOperandLexically} never got to refuse —
 * both were real out-of-root reads. Rather than emulate expansion (the classifier resolves paths lexically and
 * has no filesystem), such a word is declared unresolvable — the direction the whole file errs in.
 *
 * Deliberately NOT here: the GLOB characters ({@link globInWord}) — a glob only ever names entries of a
 * directory that is already on the command line — and a tilde, which the resolver reads.
 */
export declare function expansionInWord(w: {
    readonly pieces: readonly WordPiece[];
}): boolean;
/**
 * Does this word carry PATHNAME expansion (`*`, `?`, `[`), unquoted? Split from {@link expansionInWord} because
 * the two have different consequences, not different detection: a brace/variable/command expansion can produce
 * ANY word, so it is unresolvable; a glob only ever names entries of a directory that is already on the command
 * line, so a caller holding a filesystem can decide it. Both quoting forms suppress globbing in bash.
 */
export declare function globInWord(w: {
    readonly pieces: readonly WordPiece[];
}): boolean;
/**
 * Does this token supply the verb's FIRST-OPERAND slot, so that the first operand is a FILE rather than the
 * slot ({@link VerbOptionSlots.firstOperand})? One reader, every verb with such a row.
 *
 * Read over the whole leading letter RUN, not a prefix: GNU clusters short options (`-if` is `-i -f`) and a
 * prefix reading left the slot to swallow — and therefore skip — a real file operand. Digits belong to the run
 * (grep's obsolete numeric context spec: `-2ebeta` is `-C2 -e beta`), and a long option is matched as an
 * ABBREVIATION of the declared name (`grep --reg=X <outside>` is `--regexp`). Over-triggering is harmless in
 * the direction that matters: a token wrongly counted here makes the first operand a CHECKED path candidate.
 */
export declare function suppliesFirstOperandSlot(slot: NonNullable<VerbOptionSlots["firstOperand"]>, tok: string): boolean;
/**
 * WHAT ONE OPTION'S VALUE IS, per verb — the one declaration every reader of an option token shares.
 *
 * The question asked of a value is always "can this name a file?", and the answer is a property of the
 * OPTION and its own grammar, never of the value's shape: `sed -e 's/a/b/'` and `sed -f s.sed` differ in
 * nothing a shape test can see. The kinds spell that answer out per slot so a row is auditable letter by
 * letter, and {@link slotValueCandidates} is where every consumer gets it — a kind that cannot name a
 * file yields nothing there, which is the same answer as "this value named none".
 */
export type OptionValueKind = "path" | "indirect" | "pattern" | "script" | "count" | "delimiter" | "name-pattern" | "form" | "header" | "url" | "label";
/**
 * One verb's option grammar: which short letters and long names take a VALUE, and what that value is.
 *
 * FAIL-SAFE BY CONSTRUCTION, which is what makes a PARTIAL row sound: the reader must account for every
 * option character it walks, so the first character a row does not declare ends the reading as
 * `unexplained` and hands the token back to the verb-blind payload scan ({@link attachedOptionPayloads})
 * — today's over-approximation, unchanged. A row therefore only ever makes an answer MORE precise where
 * it recognizes the option; omitting one costs precision, never a judgement.
 *
 * THREE ARITIES, because `getopt_long` has three. An option that takes no value, one that requires a
 * value (attached or the next token), and one whose value is OPTIONAL — and the third is not the second
 * with a caveat: `getopt_long` gives an optional value NO separated spelling, so `diff --unified a b`
 * hands diff two FILES while `diff --unified=3 a b` hands it a width. A row that files an optional-value
 * option under {@link VerbOptionSlots.longValue} therefore eats `a`, and the file operand it swallowed
 * is judged by nobody — the one direction this table may never move. They go in
 * {@link VerbOptionSlots.longOptionalValue}, which reads the attached spelling and consumes nothing in
 * the separated one. A short letter whose value is optional (`sed -i[SUFFIX]`) is left out of BOTH short
 * maps for the same reason: an optional value can only ever be attached, and the blind scan is the safe
 * reading of a token this row will not explain.
 *
 * WHAT A ROW MAY NOT GET WRONG, stated as the rule for adding one: declaring a FILE-valued option
 * as something else hides a read, and so does declaring an OPTIONAL-value option as a required one —
 * the value it eats in the separated spelling is a file operand. Declaring a value-taking option as
 * plain is safe (the reading ends unexplained at its value), and declaring the non-file slots is what
 * removes the phantom candidates a blind split invents — admitting one more option character moves the
 * split right, and a split that lands on a `/` manufactures an absolute path the command never names.
 *
 * AND A ROW IS ABOUT A NAME, WHICH TWO IMPLEMENTATIONS MAY NOT SPELL ALIKE. `sed -l` takes a required
 * line length in one widely deployed sed and NO value in another; `grep --context` is optional in one
 * and required in another; and the engine runs whichever the host ships. A disputed arity is resolved
 * towards the reading that CONSUMES NOTHING, because only consuming can swallow a file operand: a
 * disputed LONG name is filed as optional-value (right under one reading, merely imprecise under the
 * other), and a disputed SHORT letter is left out of both short maps, since the short maps have no way
 * to say "attached only". Where implementations disagree about a value's KIND rather than its arity,
 * the file-naming reading is the one to declare — over-approximating a value costs an ask and
 * under-approximating one costs the read. Every row here is checked against the binaries this
 * repository's own tools run on; a row added from a manual page alone is a guess.
 *
 * AND A ROW APPLIES WHILE THE COMMAND IS STILL IN ITS OPTION SECTION. POSIX ends option parsing at the
 * first operand, GNU permutes, and a transfer program parses its own options anywhere — so once the
 * operands have begun, whether a dash-leading token is an option at all is implementation-dependent and
 * BOTH readings are taken: the token is not consumed (it is judged as the operand it may be) and the
 * slot's candidates are judged too (it may name a file). Adding a candidate is always safe; consuming
 * is what hides one.
 *
 * @contract shell.read_boundary.option_value_slots — where this table declares a verb's option, the
 * read boundary reads that option's value from the DECLARED slot and nowhere else: a slot that can name
 * a file yields exactly its value as a path candidate (an `indirect` slot also yields what the value
 * names past its `@`, a `header` slot only for a whole-value `@<file>`, and a `url` slot the local path
 * a `file:` URL names), and a slot that cannot yields none — the attached and separated spellings of one
 * option read the same row and can never disagree, except where the row declares the value OPTIONAL,
 * which has no separated spelling at all. Past the command's first operand the separated spelling
 * consumes nothing and its value is judged BOTH as that value and as an operand, because which of the
 * two it is, is implementation-dependent. Where it declares nothing — no row for the verb, an
 * option character the row does not account for, a long-option prefix whose reachable names leave this
 * spelling's value unsettled —
 * the verb-blind payload scan judges the token exactly as it did before, so an absent or partial row
 * costs precision and never a judgement. A row is DATA about a PROGRAM and not about the read-only
 * allowlist: it says what that program does with its own argument, never that the program may be read
 * from, and it grants no per-verb arm to a program a deployment has not listed.
 */
export interface VerbOptionSlots {
    /** Short letters whose value is the rest of the token, or the NEXT token when the letter ends it. */
    readonly shortValue: Readonly<Record<string, OptionValueKind>>;
    /** Short letters taking no value. */
    readonly shortPlain: string;
    /** Long option FULL names whose value is `=<value>` or the next token. */
    readonly longValue: Readonly<Record<string, OptionValueKind>>;
    /** Long option FULL names whose value exists ONLY as `=<value>`: `getopt_long`'s `optional_argument`,
     *  which never reaches for the next token. Reading one of these as {@link longValue} would consume the
     *  operand that follows the bare spelling, so the operand goes unjudged — see the THREE ARITIES note. */
    readonly longOptionalValue: Readonly<Record<string, OptionValueKind>>;
    /** Long option FULL names taking no value — declared so that an abbreviation reaching one is seen to be
     *  ambiguous rather than read as its value-taking neighbour. */
    readonly longPlain: readonly string[];
    /** SINGLE-dash WORD options — a grammar getopt does not have and a cluster walk cannot read: `-name` is not
     *  `-n -a -m -e`, and splitting it that way manufactures a path candidate per suffix. Asked before the cluster
     *  walk, only for a dash plus MORE THAN ONE character; a name in neither map falls to the verb-blind scan. */
    readonly dashWordValue?: Readonly<Record<string, OptionValueKind>>;
    /** The value-less half of {@link dashWordValue}. */
    readonly dashWordPlain?: readonly string[];
    /** The FIRST positional operand, where this verb's grammar gives it a meaning that is NOT a file: grep's
     *  PATTERN, sed's SCRIPT. Read with the same {@link OptionValueKind} vocabulary the option slots use, so
     *  {@link slotValueCandidates} answers "can this word name a file" once for both spellings — `sed -e '/x/p' f`
     *  and `sed '/x/p' f` are one invocation and were two readings. `suppliedBy*` name the options that deliver
     *  the slot instead, leaving the first operand a file after all. No row ⇒ an ordinary first operand. */
    readonly firstOperand?: {
        readonly kind: OptionValueKind;
        /** Short letters, matched inside a cluster and with digits allowed in front (grep's obsolete numeric
         *  context spec: `-2ebeta` is `-C2 -e beta`). */
        readonly suppliedByLetters: string;
        /** Long option FULL names; an abbreviation reaching one counts ({@link isLongOptionAbbrevOf}). */
        readonly suppliedByLongNames: readonly string[];
    };
    /** This program parses its options WHEREVER they appear, so the permute ambiguity the both-readings rule
     *  below answers (POSIX/GNU/BSD disagree whether a dash-token past the first operand is still an option) does
     *  not exist for it. Declared only where the program's manual states the grammar, and never to rescue a
     *  spelling into auto-allow: a token the row does not explain still goes to the verb-blind scan. */
    readonly optionsArePositionIndependent?: true;
}
/** What one row makes of ONE option token. `unexplained` = the row cannot account for the token, and the
 *  caller reads it the verb-blind way it always did. */
export type VerbOptionSlotReading = {
    readonly form: "unexplained";
} | {
    readonly form: "no-value";
} | {
    readonly form: "attached";
    readonly value: string;
    readonly kind: OptionValueKind;
} | {
    readonly form: "separated";
    readonly kind: OptionValueKind;
};
/**
 * Read one option token of `name` through its row. `undefined` ⇒ this verb has no row.
 *
 * Short form is getopt: scanning the cluster left to right, the FIRST value-taking letter owns the
 * remainder of the token, or the next token when it ends there. Long form is `getopt_long`'s
 * unambiguous-prefix rule with an EXACT name resolved first; a prefix reaching several names is settled
 * only where they agree about what THIS spelling does with its value, which is one question for an
 * attached `=value` (can it name a file?) and two for a separated one (does the option take the next
 * token at all?) — see the branch below.
 *
 * An unsettled prefix falls back rather than guessing, because skipping a value that is really a FILE
 * hides a read: the verb-blind scan that judges every suffix is the safe answer to "this row does not
 * know". That direction is why this reading is NOT the stdin floor's ({@link SEPARATED_VALUE_OPTIONS}),
 * which asks a different question — does this value fill one of the SLOTS the floor counts — and answers
 * it in the other direction. Measured rather than argued: reading the floor off this table moved twelve
 * of its verdicts, eight of them wrongly (`grep -e pat file` blocks on nothing, and its pattern value is
 * exactly what makes grep's floor of two `<pattern> <file>`).
 */
export declare function readVerbOptionSlot(name: string, tok: string): VerbOptionSlotReading | undefined;
/** Does this verb's row declare that its options are parsed WHEREVER they appear — i.e. that the permute
 *  ambiguity the both-readings rule answers does not exist for it? See
 *  {@link VerbOptionSlots.optionsArePositionIndependent}. */
export declare function verbOptionsArePositionIndependent(name: string): boolean;
/** The verb's first-operand slot row, for a reader outside this module that walks operands with the same tables. */
export declare function verbFirstOperandSlot(name: string): VerbOptionSlots["firstOperand"];
/**
 * The path candidates one DECLARED value offers, which is also the answer to "did this value name a
 * file?" — an empty list IS the no.
 *
 * Three readings, one per grammar the declared slots have: a value that always names a file offers
 * itself; an `indirect` one may name a file either way, so it offers itself AND what it names past an
 * `@`; and a `header` names a file ONLY as a whole-value `@<file>` — `curl -H 'X: /etc/x'` sends that
 * text and opens nothing, while `curl -H @<file>` adds a header per line of the file (measured against
 * the binary, both directions). Reading `header` the `indirect` way would refuse a header whose text
 * merely looks like a path; reading it as a non-file kind hides the `@` spelling, which is the direction
 * that may not be taken.
 */
export declare function slotValueCandidates(value: string, kind: OptionValueKind): string[];
/**
 * The per-command model of RECURSIVE/EXPANDING read forms: invocations that read the
 * SUBTREE of a path operand, not the operand itself. Consumed by {@link collectSegmentBoundaryFindings}
 * only when a {@link BashReadonlyRootBoundary.denyMatch} seat is wired and has entries to apply (see
 * {@link CompoundReadonlyVerdict.recursiveReadPaths} for the contract and why the deny seat is the
 * trigger), and then only for the rows whose {@link RecursiveReadFormModel.discloses} column reads
 * "contents". The reference implementation draws the same line as a verb set: its recursive-reach breaker
 * fires for `grep`/`egrep`/`fgrep`/`rg`/`diff` (plus two copy verbs a WRITE face answers for here) and for no
 * name that merely lists a tree; `tar` is a contents row here and absent there, because that implementation
 * ships no built-in deny table and this one does.
 *
 * Membership covers every {@link BASH_READONLY_DEFAULT_ALLOW} verb with a recursive form, PLUS the
 * common tree-reading verbs a deployment plausibly widens its allowlist with (`find`, `rg`, …): the
 * allow set is a caller parameter, and a table pinned to the default list would silently go blind the
 * day a deployment adds `rg` — whose recursion needs no flag at all. A widened verb this table does
 * not know stays name-only classified (recorded residual: the deployment owns that widening, same
 * posture as the allowlist's own header note).
 *
 * DIRECTION. Detection over-approximates deliberately: a flag mis-read as recursive costs one ask on
 * a command that is usually a real-getopt error anyway (`grep -dr …` — ambiguous enum value), while an
 * under-read auto-allows a traversal the deny judge never saw — the leaking direction. The cluster
 * walk therefore honours getopt value-owner semantics ONLY where skipping prevents a false fire on a
 * value AND the ownership holds on every supported implementation (`grep -eRfoo` is a pattern under
 * GNU and BSD alike), never to rescue a spelling into auto-allow — a letter whose ownership DIVERGES
 * across implementations is not modeled at all (see the `ls` row), because a scan stopped on the
 * wrong platform's table is an auto-allowed real traversal.
 */
export interface RecursiveReadFormModel {
    /** What the traversal puts in front of the CALLER: the file CONTENTS it walked over, or only their
     *  NAMES and sizes. The demotion arm reads "contents" rows only — the deny judge cannot pre-check a
     *  subtree, and handing back the bytes it guards is the hazard; handing back names is a different
     *  disclosure with a different answer. WHERE THE BYTES GO is part of the question and not always a
     *  property of the verb (`tar -cf -` writes the archive to stdout), and reading that per invocation is a
     *  per-program option model this zero-IO pass does not carry — so an ambiguous row resolves demote-ward,
     *  as every ambiguity here does. A "names" row's other fields have no behavioural consumer today; the
     *  row stays because it is a DECISION, and a verb deleted from this table reads as one nobody made. */
    readonly discloses: "contents" | "names";
    /** The verb's ordinary operation IS a tree walk — every invocation with a path operand recurses,
     *  no flag required (`du`, `find`, `rg`, `tree`, `ag`, `ack`). */
    readonly always?: true;
    /** Short option letters selecting recursion, judged over the getopt option-letter run. */
    readonly shortLetters?: string;
    /** Value-taking short letters: the FIRST one hit owns the token remainder (or, when it ends the
     *  token, the next token) — letters inside that value are not options and must not fire. List a
     *  letter ONLY when it takes a value on EVERY supported implementation (GNU and BSD): stopping the
     *  scan on one platform's table is an under-read on the other, the leaking direction. */
    readonly valueOwners?: string;
    /** Long option names selecting recursion, abbreviation-aware ({@link isLongOptionAbbrevOf} — an
     *  abbreviation real getopt_long would call ambiguous over-fires into an ask, the safe side). */
    readonly longNames?: readonly string[];
    /** Enum-valued options where one VALUE selects recursion (grep's `-d recurse` /
     *  `--directories=recurse`). The value is matched as a CASE-FOLDED unambiguous-prefix
     *  over-approximation: GNU argmatch accepts prefixes, and BSD grep accepts the value
     *  case-insensitively (`-d RECURSE` recurses — verified on the real binary), so
     *  both laxities fold in. A spelling only one implementation accepts over-fires on the other
     *  into an ask on an erroring command — the required side. */
    readonly enumOptions?: readonly {
        shortLetter?: string;
        longName?: string;
        recursiveValue: string;
    }[];
    /** tar's traditional dash-less FIRST-operand key word (`tar cf out.tar dir`, the "old option
     *  style" in tar's own manual): mode letters found there select recursion exactly as their dashed
     *  spellings do. */
    readonly bundledModeLetters?: string;
    /** `true` when a bare `-` operand denotes STDIN for this command (content readers: grep's file
     *  slot, diff, tar's `-f -`). Absent ⇒ `-` is an ordinary filename (ls/du/find/tree have no stdin
     *  sense) and stays a positional operand for the recursion demotion. */
    readonly dashIsStdin?: boolean;
}
export declare const RECURSIVE_READ_FORMS: Readonly<Record<string, RecursiveReadFormModel>>;
/**
 * Read ONE sed script and answer whether it can name a file or run a command.
 *
 * sed's command letters are a CLOSED set, which is what makes this a small recognizer over a known
 * grammar rather than a second hand-written parser over an open one: address → optional negation →
 * command letter → the argument that letter takes. Anything the walk cannot finish reading — an
 * unbalanced `s` delimiter, an unclosed `{`, a letter outside the set — answers `unparsed`, which
 * its caller treats exactly as it treats `names`: sed itself rejects those scripts, so the cost of the
 * conservative answer is one approval on a script that was not going to run.
 */
/** What one sed script NAMES: the files its `r`/`R` commands read, the files its `w`/`W` commands (and `s///w`)
 *  write, the command lines its `e` commands run, and whether an `s///e` flag executes the pattern space. */
export interface SedScriptNames {
    readonly reads: readonly string[];
    readonly writes: readonly string[];
    readonly commands: readonly string[];
    readonly executesPatternSpace: boolean;
    /** Why the script's names are NOT all here, when they are not: the script is supplied from a FILE whose text is not
     *  in the command (one reading), or no reading could parse it to its end. Absent when every name is listed. */
    readonly unread?: string;
}
/**
 * Does this verb's SCRIPT SLOT put a file name or a command line somewhere the walk's operand reading
 * cannot see? Table-driven by verb so a second such verb is a row rather than a second branch.
 *
 * EVERY implementation reading is asked, and the answers combined: an affirmative `names` under any one
 * refuses; a script no reading could SEE refuses, because file-supplied program text may carry anything;
 * and a script no reading could finish refuses. A reading that lands on a clean script is what clears the
 * invocation — which is what keeps the everyday spellings, whose other reading lands on a plain file name,
 * at their old verdict.
 */
/** Every file and command line a sed invocation's script slot names, under BOTH option dialects (the union,
 *  deduplicated): what a candidate extraction hands the read judge (`reads`), the write face (`writes`) and a
 *  nested plan (`commands`). A reading that saw no text names nothing here — that fact rides the verdict face. */
export declare function sedScriptSlotFiles(args: readonly string[]): SedScriptNames;
/**
 * How an INTERPRETER receives the program it runs, declared per interpreter rather than guessed.
 *
 * The member that completes the ARGUMENT-AS-PROGRAM family. An interpreter's program can arrive three
 * ways: as an OPTION's value (`perl -e '...'`, `python3 -c '...'`, `node --eval '...'`), as the first
 * OPERAND (the awk family, whose program is mandatory and is not a file unless `-f` named one), or on
 * STANDARD INPUT when the command line names none at all. Only the third and the first are this arm's
 * business together with the second; a program named as a FILE operand (`perl x.pl`, `python3 app.py`)
 * is a path the ordinary operand walk already judges, and stays exactly where it was.
 *
 * WHAT IS JUDGED IS THE PRESENCE OF PROGRAM TEXT, NEVER ITS CONTENT. A quoted program is ONE word to
 * this file's lexer and its contents are never parsed — so the answer cannot be "does this program read
 * a file", only "is there a program this walk did not read". That is why the everyday harmless
 * spellings (`node -e 'console.log(1)'`) refuse along with the rest: telling them apart needs a parser
 * per language, which is exactly the thing a lexical floor must not grow.
 *
 * A row is DATA, so a further interpreter is a row rather than a branch, and a versioned spelling
 * (`python3.11`, `perl5.36`, `tclsh8.6`) is the same row via {@link interpreterKey}.
 */
export interface InlineProgramModel {
    /** Options whose value is program TEXT, separated (`--eval prog`) or fused (`--eval=prog`). Matched on
     *  the whole token, so a single-dash long option (`-cmd`) is one of these rather than a letter bundle. */
    readonly longText: ReadonlySet<string>;
    /** Options whose value NAMES the program (a script file, or a module the interpreter resolves). */
    readonly longFile: ReadonlySet<string>;
    /** Options that NAME the program while taking NO value of their own — a mode that selects what runs
     *  (`node --test` runs the test files it discovers, and reads no program from its input). */
    readonly longNamesProgram: ReadonlySet<string>;
    /** Options whose value is a file the interpreter reads BESIDE the program it is given — an
     *  initialization script, a library to preload. Their value is consumed so it is not counted as an
     *  operand, and they do NOT name the program: what follows still has to. */
    readonly longSupplemental: ReadonlySet<string>;
    /** Options whose value is the NEXT token and means none of the above. */
    readonly longValue: ReadonlySet<string>;
    /** Long options this row declares to take NO value. Declaring them is what keeps an invocation
     *  EXPLAINED past them, so a later script operand still ends the interpreter's own option scanning. */
    readonly longPlain: ReadonlySet<string>;
    /** Whether an option that NAMES the program also ends this interpreter's option processing, because
     *  what follows is the named program's own argument list (`python3 -m <module> -c` passes `-c` to the
     *  module). False where a named source is PRELIMINARY and further sources still follow (`dc -f <file>
     *  -e '<program>'` runs both). */
    readonly fileOptionEndsOptions: boolean;
    /** Short-option letters that introduce program text — alone (`-e prog`), fused (`-eprog`) or inside a
     *  bundle (`-pe`, `-lne`, `-uc`, `-0777e`). */
    readonly textLetters: string;
    /** Short-option letters whose value names the program (`-f prog.awk`, `-fprog.awk`, `-m <module>`). */
    readonly fileLetters: string;
    /** Short-option letters whose value is the rest of the token, or the NEXT token when the token ends on
     *  them (`perl -I lib`). The bundle scan stops there — what follows in the token is that value. */
    readonly separatedValueLetters: string;
    /** Short-option letters whose value can only be ATTACHED (`perl -i.bak`, `-C<flags>`): the bundle scan
     *  stops there too, but a bare spelling takes NOTHING from the next token — reading `perl -C -e '…'`
     *  as "-C whose value is -e" swallowed the option that carries the program. */
    readonly attachedValueLetters: string;
    /** Short-option letters that take no value at all. Declaring them is what makes an invocation EXPLAINED
     *  (see `explained` in the recognizer); they change nothing else. */
    readonly plainLetters: string;
    /** Short-option letters that make this interpreter PRINT AND EXIT. Per interpreter, because the same
     *  letter is a banner in one and something else entirely in another (`-v` is the version in ruby and
     *  VERBOSE EXECUTION in python; `-h` is the version banner in lua and the HOST in psql). */
    readonly bannerLetters: string;
    /** Short-option letters that make the interpreter read commands INTERACTIVELY after whatever else it
     *  was given — a banner or a named script does not end the session, so the program still arrives on
     *  standard input (`lua -v -i`, `lua -i script.lua`). */
    readonly interactiveLetters: string;
    /** Which OPERAND names the program, when any does, and whether it is program TEXT (the awk family, a
     *  jq filter, the SQL after a database) or a program FILE (every script interpreter). Absent when no
     *  operand ever names the program — psql's are connection arguments, and an interactive compiler's are
     *  modules it loads before reading commands from its input. */
    readonly programOperand?: {
        readonly at: number;
        readonly isText: boolean;
    };
    /** Whether an invocation that names no program anywhere reads one from its standard input. */
    readonly stdinWhenUnnamed: boolean;
}
/** The interpreters this file has a row for, keyed by the basename bash resolves, with the trailing
 *  version dropped ({@link interpreterKey} — which is also why the key for `sqlite3` is `sqlite`). */
export declare const INTERPRETER_INLINE_PROGRAMS: ReadonlyMap<string, InlineProgramModel>;
/** The row a versioned spelling belongs to: `python3`, `python3.11` and `python` are one program with
 *  one program-delivery grammar, so the trailing version is dropped before the lookup. */
export declare function interpreterKey(name: string): string;
/**
 * How this invocation receives its program, or `undefined` when it NAMES one (a script file, a module,
 * a program file option) or names none at all on an interpreter that then reads none.
 *
 * Read over the folded arguments with the program word excluded. Three sources in one pass — an option
 * whose value is program text, the operand a row declares as the program, and standard input — and one
 * rule for every doubt: an option this file has no row for never ENDS the search for program text. The
 * word that names the program stops the scan (past a script, `-p` belongs to the SCRIPT), but only while
 * every option so far was one the row EXPLAINS: after an option of unknown arity the following word may
 * be that option's value rather than the script, so the search has to run to the end.
 */
export declare function inlineProgramClause(model: InlineProgramModel, args: readonly string[]): string | undefined;
/** Does this already-vetted segment (FOLDED tokens — quoting does not change what getopt sees) invoke
 *  a {@link RECURSIVE_READ_FORMS} verb in a recursive form that DISCLOSES CONTENTS? `args` excludes the
 *  command name. Names-only traversals (`find`, `ls -R`, `du`, `tree`, an archive write) are in the table
 *  and answer `false` here — see {@link RecursiveReadFormModel.discloses}. */
export declare function segmentSelectsRecursiveRead(name: string, args: readonly string[]): boolean;
/** WHICH programs are DECLARED not to open the value of a `NAME=value` ARGUMENT — an allow-list, because
 *  the other polarity was measured false. `make PREFIX=/usr/local`, `cmake -D…`, `docker run -e HOME=/root`
 *  name a build prefix and an environment value their program never opens, and judging those was the one
 *  shape of the verb-blind rule that MANDATED the everyday build line. The exemption is theirs alone.
 *
 *  The polarity is the whole point. Listing the programs that DO open such a value means anything the walk
 *  fails to recognise is exempt — and what it fails to recognise is WHICH PROGRAM RUNS: an undeclared
 *  wrapper is read as the program (`caffeinate dd if=<path>`), and a launcher can carry the program inside
 *  an option payload with no word equal to it (`env -Sdd if=<path>`). Both were verified to execute the
 *  read. Inverted, none of those is on this list, so none is exempt — nor is a fifth spelling of the same
 *  trick. A further program is a member, not a branch, and adding one is a claim about it alone.
 *
 *  NOT its business: the LEADING environment assignments and an OPTION word's payload, each with its own arm. */
export declare const ASSIGNMENT_VALUE_NOT_PATHS: ReadonlySet<string>;
/** The program the shell will RUN here and where its OWN argv begins — the two seats
 *  {@link HeadedSegment.argvProgram} / {@link HeadedSegment.argvFrom} carry, from one reader so they cannot
 *  disagree. The LEADING assignments (both spellings) are the program-position reader's own step, so this
 *  seat no longer strips them a second time: a second strip read the quoted `'A+=x'` as an assignment the
 *  one reader calls a program name, which is two readers with two answers. */
export declare function argvProgramOf(words: readonly LexedWord[]): {
    program: string;
    from: number;
} | undefined;
export declare function attachedOptionPayloads(tok: string): string[];
/**
 * The home directory's VARIABLE spelling as a word FACT: `$HOME` / `${HOME}` as the word's FIRST piece,
 * unquoted or double-quoted, followed by `/`, by a double-quoted piece, or by nothing (unquoted), or — inside
 * the quotes — by `/` or the closing quote; every later piece literal. Those are exactly the words whose expansion
 * is the DECLARED home and nothing else, so `spelled` is what bash hands the program: `<home><rest>`. Everything
 * else is left to be the expansion it is: a single-quoted spelling does not expand, a mid-word or suffixed one
 * (`x$HOME`, `$HOME.bak`, `$HOMES`) is a substitution the classify faces refuse and the read station cannot
 * place, and an unquoted spelling of a home bash would split or glob ({@link HOME_EXPANDS_TO_ONE_WORD}) is not
 * one word. The caller reads it only against a declared home (with none declared the child's `$HOME` may differ
 * from what `~` falls back to) and only when the command cannot rebind `HOME` ({@link homeMayBeRebound}).
 */
export declare function homeExpansionOf(w: LexedWord, home: string): {
    readonly spelled: string;
} | undefined;
/** May this command text REBIND `HOME` before a later word expands it? Read LEXICALLY, not by grammar: the text
 *  spells the name `HOME` somewhere other than as a plain read ({@link HOME_PLAIN_READ}) — after `\<newline>`
 *  continuations are joined and quotes (with the `$` of a `$'…'` / `$"…"` opener) and backslashes are removed, so
 *  `"HO"ME`, `HO\ME`, `HO$'ME'` and an `eval` string spell it the same as a bare word. That covers an assignment at ANY nesting depth (a `{ … }` group, a
 *  function body, a `case` arm, a loop), a declaration builtin's operand, a `for` / `select` variable, a `read` /
 *  `printf -v` / `mapfile` / `getopts` / `unset` target, a nameref value, an arithmetic or `${HOME:=…}` assignment,
 *  without naming any of them. A reader that substitutes the declared home for `$HOME` / `${HOME}` / a leading `~`
 *  asks this first; a text it answers yes for leaves those words unplaced (the fail-closed side).
 *  WHY not a grammar reading: every "where does the name stand" reading of this question (the leading assignment
 *  words, a declaration builtin's operands) has had a nesting level or a builtin it did not see.
 *  ACCEPTED COST: a text that merely spells the name while reading through the home — `echo HOME; cat ~/x`, a longer
 *  name ending in it (`MY_HOME=…`), a comment — asks instead of running.
 *  A name the shell COMPUTES spells no `HOME` to find, so four more readings count as a rebinding: an unquoted GLOB
 *  word whose pattern can match the name `HOME` or an array element `HOME[…]` of it ({@link globMayMatchHome} —
 *  `read H?ME`, `unset [H]OME`, `read H?ME[0]`, `unset *` glob against the working directory when an entry named
 *  `HOME` sits there); an unquoted EXTGLOB opener ({@link hasUnquotedExtglobOpener} — `read !(x)` matches nearly
 *  every name); an unquoted BRACE list ({@link wordHasBraceList} — `read H{O,X}ME` expands to a list whose first
 *  word is the name); an option word that carries a glob ({@link optionWordCarriesGlob} — `read -aH?ME`, where a
 *  matched entry named `-aHOME` hands the builtin `-a HOME`); and a `printf -v` name that is not a literal
 *  identifier ({@link printfNamesComputed} — `printf` opens no path, so its words are no candidate of their own).
 *  Elsewhere a computed word is its own unresolvable candidate, and an escape the lexer does not decode is
 *  unresolved on its own (asked). */
export declare function homeMayBeRebound(text: string): boolean;
/** {@link hasUnquotedExtglobOpener} over an already-lexed text. A here-document delimiter counts too: the lexer ends
 *  it at the `(` (`<<@(x)` reads as the delimiter `@` and a group), where bash with `extglob` on reads the one word
 *  `@(x)` — a different delimiter, so the body the lexer read as data is not the body bash reads. The token keeps no
 *  per-character quoting, so a delimiter quoted anywhere (`<<'E'@(X)`) counts as well: a quoted opener is bash's
 *  syntax error, where reading it as one costs a card and runs nothing. */
export declare function extglobOpenerIn(lexed: LexedCommand, text: string): boolean;
/**
 * {@link classifyCompoundReadonly} with the structured verdict. The name/operator/blocking arms
 * run FIRST and unchanged, so an existing demotion keeps its exact reason and never acquires the
 * out-of-root signal; the boundary scan below only ever converts a would-be READ-ONLY verdict into a
 * demotion, which is why omitting `boundary` reproduces the name-only behaviour exactly.
 */
export declare function classifyCompoundReadonlyDetailed(command: string, allow: ReadonlySet<string>, 
/** The environment's declared home, so the `$HOME` / `${HOME}` word spellings fold the way the read station
 *  folds them before the shape is read. Nothing else is read off it: WHERE a command reads is the station's. */
boundary?: {
    readonly homeDir?: string;
}): CompoundReadonlyVerdict;
/** Working-directory moves {@link evaluateReadBoundary} does NOT model. `cd` is deliberately absent —
 *  it IS modelled (the walk threads the base through it), which is the whole difference: a move the
 *  walk follows keeps the verdict decidable, and one it cannot follow does not. */
export declare const UNMODELLED_BASE_MOVERS: ReadonlySet<string>;
/** Test seam for the lockstep pin that the launcher table (the ONE launcher truth, in the program-position
 *  reader's module) stays a SUBSET of the rule lane's curated `BARE_INTERPRETER_NAMES`. Exported for that one
 *  assertion — production code reads the table directly. The containment face ({@link classifyOutOfRootReadGate})
 *  does NOT look through a launcher: there every segment whose program is not a listed reader is judged whole,
 *  launcher or not, so looking through one would only narrow what it reads. */
export declare const COMMAND_LAUNCHERS_FOR_TEST: ReadonlySet<string>;
/**
 * Compound read-only classification, reason-only face. Returns the demotion reason, or
 * undefined when the command classifies read-only. ⚠️ `undefined` is NOT "safe to auto-execute":
 * the detailed verdict may still carry `undecidedPaths` (operands whose unexpanded spelling — a
 * glob — is what got checked), and this face discards that field. An auto-allow decision must read
 * {@link classifyCompoundReadonlyDetailed} and treat a non-empty `undecidedPaths` as ask — the
 * engine's own probe does exactly that (fs-bash.ts). The optional `boundary`: with it,
 * an allowlisted reader whose path arguments leave the allowed directories is demoted too (use the
 * detailed face when the caller wants to know WHY, e.g. to offer the narrow "allow reading from
 * <dir>" approval); without it the verdict is exactly what it always was.
 */
export declare function classifyCompoundReadonly(command: string, allow: ReadonlySet<string>, boundary?: {
    readonly homeDir?: string;
}): string | undefined;
/**
 * Iteration cap for {@link classifyBoundedReadonlyPollLoop}. The bound must be a LITERAL in the
 * command text (that is what makes "bounded" provable without executing anything); this cap keeps a
 * literal-but-huge bound (`$(seq 1 999999)`) from turning "bounded" into "unbounded in practice".
 * 120 beats × the sleep cap below ≈ 20h worst case — the walltime axis governs actual runtime; this
 * arm's job is only that termination is structurally guaranteed.
 */
export declare const POLL_LOOP_MAX_BEATS = 120;
/**
 * Per-`sleep` cap (seconds) for {@link classifyBoundedReadonlyPollLoop}. A literal `sleep 99999999`
 * is technically bounded but practically a hang — the same hazard class the availability arms
 * demote (a green verdict that blocks until the tool timeout is green in name only).
 */
export declare const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
/**
 * The BOUNDED READ-ONLY POLL LOOP arm of the classify face. The compound classifier above rejects any
 * control structure (`for` is not an allowlisted name), which makes the shape tier behave like
 * the `"always"` floor for a monitoring consumer whose whole idiom is `for i in $(seq 1 8); do tail -n 5 x.log;
 * sleep 2; done`. This arm accepts EXACTLY that grammar and nothing else. Returns the rejection
 * reason, or undefined when the command matches the grammar (⇒ safe to treat as reversible).
 *
 * It is ADDITIVE and consulted only after {@link classifyCompoundReadonly} rejected (see
 * {@link import("./fs-bash.js").bashReversibilityProbe}): it can never change a verdict the plain face
 * already gives, and every unparseable shape falls through to a rejection — the default path is
 * untouched.
 *
 * SAFETY ARGUMENT — why this allowance does not increase irreversibility/egress risk. Each pillar
 * maps to a test group in test/poll-loop-classifier.test.ts (G1-G6):
 *  G1 BOUNDED TERMINATION. The loop head must be one of three LITERAL forms with a computable
 *     iteration count ≤ {@link POLL_LOOP_MAX_BEATS} ({@link pollLoopBeatsOf}); `while`/`until` never
 *     match the skeleton. Every body command terminates: non-`sleep` segments inherit the availability/
 *     108/153 availability arms via {@link classifyCompoundReadonlyDetailed} (blocking stdin, `tail
 *     -f`, unbounded devices all reject), and `sleep` must be a literal ≤
 *     {@link POLL_LOOP_MAX_SLEEP_SECONDS}. Bounded beats × terminating body ⇒ the loop halts.
 *  G2 NO WRITE OR EGRESS CHANNEL. Every non-`sleep` body segment must pass the SAME
 *     {@link coarseReadonlyCheck} parser + allowlist as the plain face (one parser, no drift);
 *     `sleep` touches neither filesystem nor network. No redirection (`>` `<`), no `$`/backtick
 *     substitution, no subshell exists anywhere: the body bans them wholesale
 *     ({@link POLL_BODY_HARD_REJECT}), and the head's single `$(seq <int> <int>)` exemption is an
 *     exact-spelling match whose two arguments are literal digits.
 *  G3 CONNECTOR CLOSURE. `;` is the only connector in the body; `|`/`&`/`&&`/`||` are banned there
 *     outright, and a nested control structure surfaces as a `for`/`do`/`done` head token that fails
 *     the allowlist. Nothing can chain past the vetted segment heads.
 *  G4 READ BOUNDARY UNCHANGED. The reassembled body goes through the boundary scan of
 *     {@link classifyCompoundReadonlyDetailed} with the caller's boundary — out-of-root operands
 *     reject exactly as on the plain face (not relaxed by one rule). STRICTER here: a
 *     verdict carrying {@link CompoundReadonlyVerdict.undecidedPaths} (an unexpanded glob) rejects,
 *     because the probe face is reason-only and a repeated read of a runtime-decided target must not
 *     auto-allow.
 *  G5 STRUCTURE SOUNDNESS (the parser sees what bash executes). Any non-ASCII / control whitespace
 *     ({@link NON_ASCII_WHITESPACE}) rejects the whole command up front, the reference implementation's
 *     form. The skeleton is then read off the ONE lexer's segments and words ({@link pollLoopShapeOf}) — the
 *     same reading bash gives the text: an unterminated quote is a parse failure (reject), a `done` hidden in
 *     quotes is a word that is not `done`, and the var/iterable words must be written bare. The body's
 *     segments are the lexer's, so a connector inside quotes is text, never a boundary.
 *  G6 ENVIRONMENT INTEGRITY. The loop variable must be a SINGLE LOWERCASE LETTER: the `for`
 *     assignment mutates a shell variable, and a name like `PATH`/`IFS`/`LD_PRELOAD`/`BASH_ENV`
 *     would change how the body's commands RESOLVE (`for PATH in dir; do cat x; done` would run
 *     `dir/cat` — arbitrary code under an auto-allow). No known resolution-affecting variable is a
 *     single lowercase letter, and the body cannot read the variable anyway (`$` is banned there),
 *     so the counter is write-only and inert.
 *
 * ACCEPTED-COST NOTES (the residuals, recorded rather than silently tolerated — same posture as the
 * `head` note above):
 *  · `$(seq …)` RESOLVES A COMMAND. This is the ONE command-substitution form the ruling exempts, and
 *    it does run whatever `seq` resolves to on PATH (or a shell function/alias of that name). Two
 *    facts bound the exposure. FIRST, the trust assumption is ISOMORPHIC to the allowlist itself:
 *    `ls`/`cat`/`grep` are equally PATH-resolved, so a party that can shadow `seq` can already shadow
 *    `cat` — this arm adds no trust assumption the plain face does not already make, and the file's
 *    standing posture is that the tool-policy gate is the authoritative control and command-name
 *    resolution to the real coreutils binaries is assumed (see {@link BASH_READONLY_DEFAULT_ALLOW}).
 *    SECOND, the beat count is taken from the LITERAL digits, not from seq's output, so a shadowed
 *    `seq` emitting more items only loosens the ITERATION bound (never the read-only / no-write /
 *    no-egress envelope — every beat still runs allowlisted readers + literal `sleep`), and the
 *    walltime axis cascades a hard kill regardless. A deployment wanting NO command-substitution at
 *    all can prefer `{<lo>..<hi>}` or a literal list, which bash expands internally with no command
 *    run. (The substitution is a flagged, high-severity shape kept per the ruling that names
 *    `$(seq <int> <int>)` as the one exemption — recorded for a
 *    tightening decision, since the brace form is a drop-in equivalent.)
 *  · STDIN-BLOCKING BODY (an availability residual, not a safety one). What REMAINS residual is the general
 *    statement: this arm inherits whatever the shared availability arms can see, they are best-effort
 *    (an operand that only turns out to be unreadable at run time is not in the text), and a hang is
 *    bounded by the tool timeout — outside the irreversibility / egress envelope this arm governs. The
 *    G1 termination guarantee is therefore stated for the NON-stdin-blocking body.
 *
 * NON-RESIDUAL (a divergence this arm CLOSES rather than accepts): the JS-`\s`-vs-bash-`$IFS` gap.
 * Because the shared parser splits on JS `\s` (which includes NBSP/U+2000-block/U+3000/form-feed/
 * vertical-tab) but bash's default `$IFS` does not, `ls x` reads as head `ls` here yet is ONE
 * command word to bash. Rather than reason about the (harmless — nonexistent program, exit 127)
 * consequence case by case, the whole command is rejected up front if it carries any such character
 * ({@link NON_ASCII_WHITESPACE}), so the grammar's tokenization and bash's stay identical.
 */
export declare function classifyBoundedReadonlyPollLoop(command: string, allow: ReadonlySet<string>, boundary?: {
    readonly homeDir?: string;
}): string | undefined;
/**
 * The poll-loop face's STRUCTURED verdict — the same {@link CompoundReadonlyVerdict} the compound face
 * returns, so a consumer reads the boundary's structural findings (deny row, containment, recursive or
 * undecided operands) off the loop body exactly as off a plain compound, beside whatever sentence the
 * loop grammar or the body's shape produced. {@link classifyBoundedReadonlyPollLoop} is its `reason`
 * projection. A refusal of the LOOP GRAMMAR itself (not a bounded loop, an unlisted body program, an
 * unvetted sleep) carries no structure: no reader segment was walked.
 */
export declare function classifyBoundedReadonlyPollLoopDetailed(command: string, allow: ReadonlySet<string>, boundary?: {
    readonly homeDir?: string;
}): CompoundReadonlyVerdict;