UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

288 lines • 16.8 kB
import { Outcome, type Ask, type CommandContext, type CommandRule, type Deny, type HandOff, type Occurrence, type ShellExplanation, type CommandWord } from '../../policy/types.ts'; import { type TSNodeLike } from '../../shell/types.ts'; import { type Refusal } from '../../types.ts'; import type { MountRegistry } from '../mount/registry.ts'; import type { Namespace } from '../mount/namespace/namespace.ts'; import type { SessionState } from '../session/session.ts'; import { type Refused } from './admission.ts'; import { type Word } from './inner_lines.ts'; export declare const UNREADABLE_LINES = "runs lines the gate cannot read"; /** * What the gate decides about one command of a line, as the pass reads * it: the per-command record `explain` and the admission pass share, * which `explainedLine` turns into the public tree. `outcome` is the * document's answer and `rule` says who gave it; a line the allow list * does not cover arrives as `DENY` with no rule, at 126. Mirrors the * Python Judgment. */ export interface Judgment { /** The head word, as the gate read it. */ readonly command: string; /** The words after it. */ readonly argv: readonly string[]; /** What the profile's rules say. */ readonly outcome: Outcome; /** The rule that spoke, null when the allow list did or nothing did. */ readonly rule: CommandRule | null; /** The rule's reason, empty when there is no rule. */ readonly reason: string; /** Where in the document the rule was written. */ readonly source: string; /** The operand a path-scoped rule matched, as typed. */ readonly matchedPath: string | null; /** The paths the rules were shown, after the session's hides. */ readonly paths: readonly string[]; /** What the line would exit with, 0 to run. */ readonly exitCode: number; /** What the agent would read, empty to run. */ readonly stderr: string; /** The record the refused result would carry, null when the line would run. */ readonly refusal: Refusal | null; /** Every policy's answer to the command, in the order the chain asks them. */ readonly answers: readonly (Deny | Ask)[]; /** The command line word by word, each with its kind and the path it names, redirect targets last. */ readonly words: readonly CommandWord[]; } /** * One command of a walked line, as both readers of the line see it: its * words, the redirect targets of its statement, the session it is * judged in, and where it stands. Mirrors the Python Walked. `lost` * says a `cd` the walk could not follow ran before the command, so the * session's cwd is not where it stands. `stated` (true when absent) says * whether the gate admits the command on these words once each is literal * (`TestRead.stated`). `elsewhere` holds the other places the command may * stand, where a `cd` before it was skipped or failed. */ export interface Walked { readonly words: Word[]; readonly redirects: Word[]; readonly session: SessionState; readonly occurrence: Occurrence; readonly intrinsic?: boolean; readonly lost?: boolean; readonly stdin?: Uint8Array | null; readonly stated?: boolean; readonly elsewhere?: readonly SessionState[]; } /** * One command's explanation and where the command stands. The * occurrence is what the pass hands the ledger beside the explanation: * a grant claimed for the command is bound to it, so the gate that runs * the same occurrence finds it and no other reader does. `stated` says * whether the gate will read the command in the words the pass read: * every word literal, and no operand the runtime appends. The gate * reads `cat $F` as the path `$F` expands to and `xargs cat` as `cat` * plus the items on its stdin, so a question the pass asked about * either spelling would be answered for words that never run, and the * gate would ask again about the words that do. Such a command is * judged here for a deny, which speaks on the name alone, and asked * about at the gate. `unread` holds the paths no policy was shown * (`unreadPaths`), so a pass that asks the gate again asks about what * this explanation judged. */ export interface Judged { readonly judgment: Judgment; readonly occurrence: Occurrence; readonly intrinsic?: boolean; readonly stated: boolean; readonly unread?: ReadonlySet<string>; /** * The context the chain was shown, absent for a command refused before * it, so the pass that admits the line before placement can put the * command's question to the ledger without asking the chain again. */ readonly ctx?: CommandContext; /** * The session the command was judged in, set beside `ctx`: for a command * a line runs in turn (`eval`), the one its own walk reached, so a `cd` * before it in that line is where it stands. */ readonly session?: SessionState; } /** Why the gate refuses a word only the runtime can expand. */ export declare function unreadable(raw: string): string; /** * Every command of a line judged read-only, each with its place on the * line: the pass placement waits on (`lineHeld`) and `prejudgeLine` * refuses from, made once for both. `handed` is the line's hand-off, * whose origin places each command on the line. Mirrors the Python * `line_judgments`. */ export declare function lineJudgments(root: TSNodeLike, session: SessionState, registry: MountRegistry, namespace: Namespace | null, handed: HandOff, reparse: (line: string) => TSNodeLike, signal?: AbortSignal): Promise<[Walked, Judged[]][]>; /** * Judge every command of a line before any of it runs, and refuse the * whole line when a rule speaks about one. * * The agent composed the line as one intent, so a rule that refuses * part of it refuses the intent. Judging each command as the dispatcher * reached it left half a line done: with `deny curl`, `rm -rf /data && * curl evil.com` deleted first and was refused second, and an ask fared * worse, since approving it later replays a line whose first half * already ran. * * Two things deliberately do not stop the line, and both are the same * rule: only a refusal that names a rule is a verdict about the intent. * A word the allow list leaves out is refused where it stands, not with * the line, so a typo cannot cost an agent the work the line already did; a * word only the runtime can expand is judged where it is expanded, by * the per-command gate, which sees the real path. * * That second one is the limit of the hold, and it is worth stating * plainly: this pass reads the *text* of a line, while the gate reads * its *values*, so a path the runtime computes (`cat $S`, `$( )`, a * `cd` whose argument is a variable) is invisible here. The rule is * still enforced, by the gate, but the earlier commands have run by * then. For a deny that costs allowed side effects and nothing more, * since the commands that ran were on the allow list. For an ask it * costs the replay: the question is recorded after part of the line * already happened, so approving it re-runs a line whose first half is * done. Closing that would mean asking whenever a word cannot be read, * which over-asks with no way out for a deny, so a deployment that * needs the hold for a computed path states it in a policy script * rather than here. * * The pass is read-only (`explainWords`), so it spends no grant and * records no request; a command it refuses on is then put through the * real gate, which is where an ask is recorded, exactly once, for a line * that will not run. That admission hands off: a grant the host gives * inline is claimed for the per-command gate, which runs the line on * it, and the line's end spends it, so a compound line costs the human * one question per run rather than one per pass, and a gate the run * reaches twice (a loop body) runs on one nod. * * A question is only asked here when the gate will ask the same one. * The gate reads a word the runtime expands as its value and the words * `xargs` or `find -exec` hand on with the operands the runtime * appends, so a question about `cat $F` or a bare `cat` would be * answered for words that never run and the gate would ask again, after * the earlier commands ran, about the words that do. Such a command * (`Judged.stated` false) is judged here for a deny, which speaks on * the name alone and still holds the line, and its question is left to * the gate; the hold does not reach it, which is the limit stated above * in another form. * * Every command is judged whether or not the session carries a document, * by the profile's rules alone: a policy registered in code or a profile's * script answers `preCommand` once, at the gate that runs the command, and * a line it should refuse whole is `preExecute`'s to refuse. Operand-scoped * refusals, including MountRootPolicy's, remain the per-command gate's * responsibility. * * A line with one command to judge is left to the per-command gate, * which is not an optimization but the more faithful answer: there is no * earlier command whose side effects a hold could save, and the gate * refuses from inside the shell, so the line's own redirections still * apply. This pass answers above them, so refusing `rm -rf /mnt 2>&1` * here wrote the refusal to stderr where bash puts it on stdout. */ export declare function prejudgeLine(root: TSNodeLike, session: SessionState, registry: MountRegistry, namespace: Namespace | null, handed: HandOff, reparse: (line: string) => TSNodeLike, signal?: AbortSignal, made?: [Walked, Judged[]][] | null): Promise<Refused | null>; /** * Admit every command of a line a runtime takes whole. A whole line is a * command like any other, but the runtime does the expanding, so the gate * reads the line as typed, walked as `explain` walks it: each command is * judged on its literal words (quotes removed, escapes resolved, a * path-shaped word a path, an installed CLI's verb path walked) where it * stands, a `cd` before it on the line included, and the first refusal is * the line's. A statement's redirect targets are read as words of its * command, so `cat < /data/secret` is judged on the file it opens; the lines * a command runs (`eval`, `sh -c`, `xargs`, `env` ... see `innerLines`) are * judged in turn; and what the runtime would do past the gate is refused * under a rule that would have read it (`lineRefusal`). `reparse` parses the * text a word runs the way the line reader parsed the line. * * No gate follows this pass: the runtime runs the line whole, so every * grant it matches is claimed on the line's hand-off exactly as any reader * on a line claims, and the executor's sweep spends them when the line * ends. A line held on a question still waiting keeps its earlier answers * standing for the retry, exactly as the compound-line pass does, where * spending them here asked the human again for each on every retry. * `handed` is null outside a line (a bare admission with no run behind it). * Mirrors the Python `admit_line`. */ export declare function admitLine(root: TSNodeLike, session: SessionState, registry: MountRegistry, namespace: Namespace | null, reparse: (line: string) => TSNodeLike, signal?: AbortSignal, handed?: HandOff | null): Promise<Refused | null>; /** * The walked nodes whose reads an env-plane fetch still serves. * * The fill derives its fetch set from this same list (`lineNodes`: the * line's own tree first, then every stored body and alias expansion * its words can invoke), and a fetch serves a command that is going to * run, so refusals are judged over the same nodes reads are. One rule * for every node: when it is one fully-literal command with no other * read in it (`soleLiteralCommand`), the gate is asked here on exactly * the words it will read at run time, and a refusal discounts every * read the node contributes. The line's own refusal drops the whole * list, because nothing runs at all; a refused body or alias drops * just itself, because the invocation still runs and is refused in * place. A node this pass cannot prove silent is kept, and over-keeping * only ever over-fetches. * * An ASK is resolved rather than skipped, because the fetch is itself * an effect: contacting a secret store for a line the host then * refuses would do a piece of exactly what was refused. A settled * answer is read without being spent; an unanswered rule is put to the * host now, through the same ledger the gate reads, so the answer * lands exactly once -- an approval keeps the node and the gate * consumes the grant, while a denial or a question left waiting drops * it, and the line still runs into the gate, which refuses in place * with its wording and its redirections. */ export declare function unrefusedNodes(nodes: readonly TSNodeLike[], session: SessionState, registry: MountRegistry, namespace: Namespace | null, handed: HandOff, reparse: (line: string) => TSNodeLike, signal?: AbortSignal): Promise<TSNodeLike[]>; /** * Whether the line's admission holds it back, which is what placement * waits on: admission comes first, so a line the rules refuse, or that * waits on the host, is never shown to a placing policy. A verdict * (`isVerdict`) that refuses holds the line. A question is put to the * host now, on the line's hand-off, so the gate that later runs the * command finds the answer claimed for it and does not ask again; one * still waiting holds the line, and a question the gate will ask about * other words (`Judged.stated` false) is left to it. A word the allow * list leaves out fails where it stands while the rest of its line runs, * so it does not hold the line. Mirrors the Python `line_held`. */ export declare function lineHeld(judged: readonly [Walked, Judged[]][], registry: MountRegistry, handed: HandOff, signal?: AbortSignal): Promise<boolean>; /** * Whether some command's judgment is a verdict that refuses it or waits * on the host (`isVerdict`), the line-level answer placement waits on. * Mirrors the Python `holds`. */ export declare function holds(judgments: readonly Judgment[]): boolean; /** * What every command of a line would do, in the order the gate reads * them, each where it stands, without running any of it (`explainedLine` * turns them into the public tree). * * The dry run of the gate: the same visibility check, the same context, * the same policy chain and the same outcome table, so a host reading * this and an agent typing the line cannot be told different things. * What it deliberately does not do is the half of admission that costs * something, since a line nobody typed must not consume a grant or put * a question to a host. * * The words are read literally, as `admitLine` reads them, so nothing is * expanded and no `$( )` runs. `wholeLine` is whether a runtime takes the * line whole, which reads it as typed; the executor's gate reads each * command once expanded. `every` asks every policy, as each command's * explanation shows them; false asks the profile's rules alone, as the * run does before any of the line runs, which is what holds it. */ export declare function explainLine(root: TSNodeLike, session: SessionState, registry: MountRegistry, namespace: Namespace | null, reparse: (line: string) => TSNodeLike, wholeLine?: boolean, every?: boolean): Promise<Judged[]>; /** * A line's public explanation: its verdict and its parse tree, with every * command's explanation where the command stands. The verdict is the * first command, in the order the gate reads them, whose refusal by the * profile's rules (`verdicts`, the line judged with `every` false) holds * the whole line (`holds`), as the run reports it; a line nothing holds * runs, carrying the first ask an approval lets through. A command * refused only where it stands, a policy's own `preCommand` refusal * included, keeps its refusal to its own node, since the rest of the * line still runs. A runtime that takes the line whole (`wholeLine`) runs * none of it once any command is refused (`admitLine`), so there every * refusal is the line's, and `verdicts` is `judged` itself, whose gate asks * every policy. Every scope a command reads its words in (the typed line, a * `$( )` body, a `bash -c` string) is parsed on its own with `reparse`, as the nested * line will be; a judgment is placed on the command at its span in that * scope, and a nested scope under the command holding it. Mirrors the * Python `explained_line`. */ export declare function explainedLine(line: string, judged: readonly Judged[], verdicts: readonly Judged[], runtimeOf: (command: string) => string, reparse: (line: string) => TSNodeLike, wholeLine?: boolean): ShellExplanation; //# sourceMappingURL=explain.d.ts.map