@sema-agent/core
Version:
Stateless, task-oriented AI agent core
1,048 lines • 82.1 kB
TypeScript
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;