@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
163 lines • 10.2 kB
TypeScript
import type { ByteSource } from '../../io/types.ts';
import type { AdmissionRules, Ask, Claimant, CommandContext, CommandRule, Deny, Pending } from '../../policy/index.ts';
import { type EntryGate } from '../../policy/types.ts';
import type { TSNodeLike } from '../../shell/types.ts';
import { PathSpec, type Refusal } from '../../types.ts';
import type { NamespaceLinks } from '../../view/types.ts';
import type { Namespace } from '../mount/namespace/namespace.ts';
import type { MountRegistry } from '../mount/registry.ts';
import type { SessionState } from '../session/session.ts';
import { type Word } from './inner_lines.ts';
/**
* What the command plane prints when a line does not get to run: 126 for
* a whole-command refusal or an unanswered ask, the operand code (1, tar
* 2) for a refusal about a path or carrying an error. `refusal` is the
* record the result carries beside stderr, null for the filesystem's own
* error. Mirrors the Python `Refused`.
*/
export interface Refused {
readonly stderr: Uint8Array;
readonly exitCode: number;
readonly refusal: Refusal | null;
}
/**
* A command the gate let through, and what its own I/O may touch.
*
* The gate judged the paths the line names; a walk below them reaches
* entries no rule has seen, so the dispatcher binds this to the session
* context for the command's run and the commands tier asks it before each
* read, write or listing (`EntryGate`). The paths the gate already judged
* pass, since the line was admitted on them; every other entry is judged
* by `ioRefusal` under the same precedence the gate applied to the line,
* and a refusal is the dispatcher's `PolicyDenied` (EACCES, the path, the
* reason on its record), which every command renders as GNU's
* `Permission denied`.
* `granted` holds the ask rules the line runs under a grant for: the one
* the entry point answered for this line, and the session's standing ones.
* `ctx` is what `preCommand` judged the command on, which `postCommand`
* extends with what came back.
*/
export declare class Admitted implements EntryGate {
readonly rules: AdmissionRules | null;
readonly ctx: CommandContext;
readonly judged: ReadonlySet<string>;
readonly granted: readonly CommandRule[];
constructor(init: {
rules: AdmissionRules | null;
ctx: CommandContext;
judged: ReadonlySet<string>;
granted: readonly CommandRule[];
});
check(virtual: string): void;
scopes(virtual: string): boolean;
refusal(virtual: string): string | null;
}
/**
* The paths a path-pattern guard reads for a line: the operands as
* typed and the values of path-valued flags, a command tree's group and
* leaf options among them (`treeFlags`, from `treePathScopes`), then, for
* a command that
* follows links, the targets they resolve to. `cat /data/link` reads
* `/data/secret`, so a rule protecting the target has to see it, and a
* command-scoped rule never runs at the dispatcher where the resolved path
* would otherwise be checked. The links are followed as the command's own
* session sees them (`links`, `Namespace.sessionLinks`), as the router
* follows them: on the browser fallback the ambient view folds every live
* session's hides, and a peer hiding the link must not keep its target from
* the rule; null where no link stands. The follow policy is the command's own
* (`follows`, whether `LineFacts.follows` is not NONE: rm, mv, ln, stat,
* tar ... act on the link itself, `-L` turns following back on), the same
* one the router applies to the operands before the handler runs, so a rule
* sees exactly the path the command will touch. A loop is left to that later
* step to report; here the typed paths stand. Then the operand a bare
* `ls`/`find`/`du`/`tree`/`grep -r` implies, the working directory,
* which the executor injects after the gate and which a rule on that
* directory has to see. Last come the statement's redirect targets:
* `cat < /data/secret` reads the file and `echo x > /data/secret`
* truncates it, on the shell's own fds outside the admitted command's
* gate window, so the admission is the one place a rule can see them. A
* redirect always dereferences (the shell opens the target), so its
* link targets ride along whatever the command's own follow policy
* says.
*/
export declare function policyScopes(name: string, args: readonly string[], operands: readonly (string | PathSpec)[], follows: boolean, links: NamespaceLinks | null, cwd: string, implied?: PathSpec | null, redirects?: readonly PathSpec[], treeFlags?: readonly PathSpec[]): PathSpec[];
/**
* The command plane's admission of one command: the policy chain, then
* the approval ledger. The one gate every command class passes through,
* in the tree (`runArgv`, once the words are expanded) and for a line a
* runtime takes whole (`admitLine`, per parsed command). A word the
* session's allow list leaves out is the profile's Deny, like any other
* rule's; a path the session cannot see is
* dropped before any method, so a rule never names it and the dispatcher
* answers ENOENT; a Deny renders in the outcome table's voice; an Ask
* is answered by the entry point from the session's grants or the host.
* `stdin` decides whether a bare `rg` reads the working directory.
*/
/**
* One command's literal words, classified the way the runtime would
* classify them, so the gate and the run name the same paths.
*/
export declare function classifiedWords(name: string, args: readonly string[], session: SessionState, registry: MountRegistry): Promise<(string | PathSpec)[]>;
/**
* The paths a statement's redirect targets name.
*
* Shared by admission and by the dry run, because a rule reads a
* redirect the same way in both: a target only the runtime can expand
* names no path here, and one that is not path-shaped is not a file.
*/
export declare function redirectPaths(words: readonly Word[], registry: MountRegistry, cwd: string): PathSpec[];
/**
* Everything the gate decides about one command before anything is
* spent on it: the classified context, and the policy chain's answer (the
* first Deny, else the first Ask) with the answers it came from.
*
* Split out of `admit` so a dry run can have the answer without the
* consequences. Nothing here records a request, consumes a grant or
* reaches the host, which is what makes it safe for `explain`;
* `admit` adds exactly those and renders.
*
* `intrinsic` keeps shell-provided operations subject to tool allow lists
* even when a function shadows their policy name. `unread` holds the
* virtual paths a reader of the line's text cannot vouch for: what a word
* only the runtime expands names, or a relative word after a `cd` it
* could not follow. No policy is shown them; the per-command gate reads
* the real ones and passes none. `every` asks every policy past a Deny,
* for `explain`; the gate's own answers end at the first Deny.
* `rulesOnly` asks the profile's rules alone, for a pass that judges a line
* before it runs; a policy's own `preCommand` is asked once, by the gate
* that runs the command. `line` is the line the command was parsed from,
* empty outside a line.
*/
/**
* Whether the allow list refuses a CLI that reaches a service. Such a CLI
* (an account or script CLI) reaches its service by id, past every hide, so
* under a profile that hides paths it runs only where a pattern starting
* with its own name matches the line. `*` and a pattern for another of its
* verbs do not count. Mirrors the Python `unlisted_service`.
*/
export declare function unlistedService(ctx: CommandContext, registry: MountRegistry, session: SessionState): boolean;
export declare function gate(name: string, args: readonly string[], operands: readonly (string | PathSpec)[], session: SessionState, registry: MountRegistry, namespace: Namespace | null, stdin?: ByteSource | null, redirects?: readonly PathSpec[], intrinsic?: boolean, unread?: ReadonlySet<string>, every?: boolean, rulesOnly?: boolean, line?: string): Promise<[CommandContext, Deny | Ask | null, (Deny | Ask)[]]>;
/**
* The chain's answer once the ledger has had its say. An Ask is the chain's
* answer only after every Deny had its say; the ledger answers it from the
* session's records or the host, so an answer never re-opens a deny. A wait
* the run's kill cut short is the kill landing late, not a ruling, so it
* throws as every other abandoned wait does rather than being rendered as a
* refusal the document never made. Mirrors the Python `resolved`.
*/
export declare function resolved(ctx: CommandContext, asked: Deny | Ask | null, registry: MountRegistry, signal: AbortSignal | undefined, claimant: Claimant | null): Promise<Deny | Pending | null>;
export declare function admit(name: string, args: readonly string[], operands: readonly (string | PathSpec)[], session: SessionState, registry: MountRegistry, namespace: Namespace | null, stdin?: ByteSource | null, redirects?: readonly PathSpec[], signal?: AbortSignal, claimant?: Claimant | null, intrinsic?: boolean, unread?: ReadonlySet<string>, rulesOnly?: boolean): Promise<Refused | Admitted>;
/**
* Admit a `[ ]` test, or one `[[ ]]` leaf, as the command it is: the shell
* evaluates it itself, so no command gate saw its words, and a rule, an allow
* list or a policy answers it by its own name as it answers `test` on the
* same words. `name` is `[` or `[[`; `words` what it tests, expanded,
* operators included; `signal` the run's abort signal; `claimant` the test
* and its line, null outside a line. Throws `CondRefused` where the gate
* refused it. Mirrors Python's admit_test.
*/
export declare function admitTest(name: string, words: string[], session: SessionState, registry: MountRegistry, namespace: Namespace | null, signal?: AbortSignal, claimant?: Claimant | null): Promise<Admitted>;
/** Mark redirected input without opening or expanding its source. */
export declare function statementStdin(node: TSNodeLike): Uint8Array | null;
export declare function statementRedirects(node: TSNodeLike, home: string | null): Word[];
//# sourceMappingURL=admission.d.ts.map