UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

390 lines • 20.7 kB
import type { SessionView } from '../../view/types.ts'; import { type Policies } from '../../policy/index.ts'; import type { CallStack } from '../../shell/call_stack.ts'; import { type ShellArray } from '../../shell/array.ts'; import type { ArithResult, ElementOps } from '../../shell/types.ts'; import type { ShellValue, ShellVar } from '../../shell/variable.ts'; import { VarAttr } from '../../shell/variable.ts'; import type { SessionState } from './session.ts'; /** * The one copy-out of a session's environment. * * Every tier that hands the env onward as a process view (a command's or * CLI's or a runtime's `ctx.env`, the `env` builtin) copies * through here, so the hidden-vars filter lands on all of them by * construction rather than on however many hand-rolled copies someone * remembers. The copy keeps the null prototype session records carry. * * *Exported* names only, which is what makes this the process view * rather than a second spelling of `visibleEnv`. bash puts a variable * in a child's environment when it carries the export attribute, not * when it happens to hold a string: `X=hello` is absent from `env` and * `export Y=world` is present. An unset name carrying the attribute * (`export Z`) is absent too, which falls out of the value check * rather than needing its own arm. * * Diverges from bash on one point: bash also carries each function * `export -f` marked, as a `BASH_FUNC_NAME%%` entry. mirage hands those to * a nested shell directly (`SessionState.newShell`), so neither `env` nor a * runtime lists them. */ export declare function envSnapshot(session: SessionState): Record<string, string>; /** * The name a `declare -n` reference points at, null otherwise. Null * also for a reference declared but not yet aimed (`declare -n r` * before `r=v`): bash treats the first assignment as naming the target, * so until then it stands for nothing. */ export declare function namerefTarget(session: SessionState, name: string): string | null; /** * The variable a name stands for, following `declare -n` chains. A name * that is not a reference is its own answer. A chain that comes back to * itself (`declare -n a=b; declare -n b=a`) is bash's circular name * reference, read as unset: it resolves to the empty name, which no * record has, so a reader sees unset and a writer falls back to the * reference's own record. The warning line is the one part not * reproduced. */ export declare function deref(session: SessionState, name: string): string; /** The variable's value, null when unset or hidden. Sync on purpose: * `$X` expansion is the hot path, so a read stays a record lookup plus * the hidden check. A name reference reads its target. */ export declare function envGet(session: SessionState, name: string): string | null; /** * Whether `readonly` has marked the name. * * A hidden name answers false: isReadonly speaks about the session's * visible world, and calling a name that reads as unset "readonly" * would leak it. `followRef` asks about what a `declare -n` reference * points at; a write to the reference itself (`declare -n r=w`, * `unset -n r`) asks about the reference. */ export declare function envIsReadonly(session: SessionState, name: string, followRef?: boolean): boolean; /** * The env mapping a reader tier should resolve names against. * * Always a filtered copy, never `session.env`: that getter is itself a * projection built fresh per access, so handing it out would copy the * store anyway and freeze the answer at that moment. TS diverges from * python's lazy mapping view deliberately: expansion sites read records * with plain property access, so a copy is the shape they already * consume, and env sizes make the copy cost noise. * * The *shell* view, and no longer a synonym for `envSnapshot`: this is * what `$X`, arithmetic, `[[ ]]`, `IFS` and the bare `set` listing * resolve against, and they see every variable, exported or not. The * two were the same function while the process view was also "every * string", and reusing it once the process view narrowed would have * stopped `$X` resolving a plain assignment. python kept them separate * all along (`_VisibleEnv` beside `env_snapshot`); this is TS catching * up to it. */ export declare function visibleEnv(session: SessionState): Record<string, string>; /** * The arrays mapping a reader tier should resolve names against. * * The arrays twin of `visibleEnv`: the embedder can seed * `session.arrays` before narrowing, so a hidden name can hold an * array and array reads need the same filter env reads get. */ export declare function visibleArrays(session: SessionState): Record<string, ShellArray>; /** * The associative arrays a reader tier should resolve names against. * * The third sibling beside `visibleEnv` and `visibleArrays`, for the * same reason both exist: the embedder can seed a hidden name with any * value shape, so every reader tier filters the same way. */ export declare function visibleAssocs(session: SessionState): Record<string, Record<string, string>>; /** * Write one variable through the session plane's gate. * * General over variable shapes: a string stores a scalar, a ShellArray * stores a whole array, and the two storages stay exclusive. Semantics * live here once — the hidden refusal, readonly refusal, the * `preSession` policy gate (whose context value renders an array as * its present elements joined by spaces), then the store — so every * writer states them the same way whichever tier or spelling asked. * Writers with richer mechanics (subscripts, appends, holes) compute * the resulting value on a copy and hand it here, so a denial never * leaves a half-applied write. Null policies gate nothing (a writer * outside a workspace). Throws PolicyDenied when the name is hidden * for this session (a landed write would clobber the real value the * host's wiring still reads; a swallowed one would gaslight the * writer — the vars twin of EACCES on a create into hidden path * space), ReadonlyVariableError when the name is readonly, and * PolicyDenied when a preSession policy refuses the write. */ /** * Refuse a write that names a hidden variable. * * The sync half of `setVar`'s hidden gate, shared with the * expansion-time writers that land on the raw env (`${X:=d}`, * `$((X=5))`, `printf -v`): a landed write would clobber the real * value the host's wiring still reads, and a swallowed one would * gaslight the writer; refuse loudly instead, the vars twin of EACCES * on a create into hidden path space. */ /** * Remove one surrounding quote pair from an associative subscript. * * An arithmetic reference carries its subscript verbatim, so `m["x"]` * arrives with the quotes bash would have removed; one layer comes off * and anything else is the key itself. */ export declare function stripKeyQuotes(text: string): string; /** Element callbacks bound to one session, for `evaluateArith`. */ export declare function sessionElements(session: SessionState): ElementOps; /** * An indexed subscript resolved outside an arithmetic expression: * `${a[i]}`, `a[i]=v`, `unset 'a[i]'`, `[[ -v a[i] ]]`. * * The subscript is arithmetic, so it may assign (`a[x=3]`) and seed * (`a[RANDOM=42]`), and bash binds those as it evaluates them. Each * lands through the session view once the index is known, then the `RANDOM` * reader replays the draws made after the seed. A subscript that fails * to evaluate lands what it assigned before failing and then throws, the * subscript text leading the message, since bash aborts the line on it * (`${a[1/0]}` is `1/0: division by 0`) rather than reading element 0. * `view` is the gated session view; null lands the writes ungated, outside a * workspace. Throws what the session view throws too: a PolicyDenied, or an * ArithError from a `-i` name refusing the value; a readonly name ends the * shell wherever a subscript is (`${a[R=3]}`): ExitSignal. */ export declare function subscriptIndex(session: SessionState, subscript: string, view?: SessionView | null): Promise<number>; /** * The `-i` coercion: evaluate the incoming text as arithmetic. * * Reads resolve against the visible env, so `n=x+1` sees `x`, and * element references resolve through the session's resolver, so * `n=a[1]+1` and `n=m[k]+1` see the element; an unresolvable name is 0 * (`n=abc` stores `0`), which is the arithmetic rule, not a refusal. A * malformed expression throws ArithError with the offending text led, * which is how every caller voices it (`bash: 1+: syntax error: operand * expected`), so it is spelled once here rather than at each of the * sites that catch it. */ /** Evaluate a host-supplied seed; invalid arithmetic propagates. Read * without the generator on offer: a host word naming `RANDOM` would * otherwise draw, and the draw reseed, without end. */ export declare function seedFrom(word: string, session: SessionState): number; /** Draw from the session generator, or null after RANDOM is unset. * Shell assignments validate and seed at the session view. A host-seeded * variable is consumed here on its first read. Reseeding resets repeat * suppression to zero independently of the stored word. */ export declare function nextRandom(session: SessionState, stored: string | undefined): number | null; /** * Arithmetic's reads of `$RANDOM`, bound to one session. * * A read before the expression assigns `RANDOM` draws from the session * generator. bash seeds at the instant of an assignment and every later * read draws from the new seed (`$((RANDOM=42, RANDOM))` is the first * draw after seeding with 42). Here the assignment is still pending at * the session view, which lands it gated after evaluation, so the * evaluator tells the reader of each assignment as it is made (`wrote`), * the reader seeds a scratch generator the way the session view will and draws * from that, and `settle` replays the draws on the session once the * session view has seeded it: the session ends where bash's does, seeded and * advanced by every read since the last assignment, and the write still * reaches the gate as the assignment it is. Each assignment restarts * the scratch generator and the count, since the session view lands only the * last value written, and the draws are replayed only if the session view did * land it: an assignment the caller never applied leaves the session as * it was. * * Lives beside the session view rather than with the generator because the * session view needs it too: `RANDOM=RANDOM` draws once while the seed is * evaluated, then seeds with the draw, as bash does: it reads an * assigned seed as an arithmetic expression. */ export declare class RandomReader { private readonly session; private seeded; private state; private last; private draws; constructor(session: SessionState); private special; /** The dynamic value of a name, null for a name that has none. */ readonly read: (name: string) => string | null; /** Note an assignment the expression made: the name and its value, an * integer's text. */ readonly wrote: (name: string, value: string) => void; /** Replay the scratch draws on the session generator, once the session view * has seeded it with the value the expression assigned. */ settle(): void; } /** * End `RANDOM`'s special meaning when a non-string lands on it. * * Once bash turns `RANDOM` into an array it neither draws nor seeds, * so `RANDOM=(1 2)`, `declare -a RANDOM`, `RANDOM[1]=5` and * `RANDOM+=(3)` all leave an ordinary array that `$RANDOM` reads element * 0 of, for good, as `unset RANDOM` does. Every store entry point calls this, * gated or not, since a host seeding an array onto the name means the * same thing. */ export declare function noteRandomKind(session: SessionState, name: string, value: ShellValue): void; /** * The scalar an array conversion keeps as element 0. * * When bash turns a variable into an array, its current value becomes * element 0, and for a live `RANDOM` looking the name up is what draws: * `RANDOM[1]=5` leaves `[0]` holding one draw and `declare -a RANDOM` one * alone, after which the array is ordinary. */ export declare function conversionScalar(session: SessionState, name: string): string | undefined; /** Bind arithmetic `$RANDOM` reads to a session. */ export declare function randomReader(session: SessionState): RandomReader; /** * Evaluate `text` as every arithmetic context of the shell does: against * the visible env and the session's elements, drawing through `reader`, and * stopping at a write to a readonly name, as bash's evaluation does * (`(( X=5, R=3 ))` binds X and refuses R). Throws ArithError when the text * does not evaluate, and ReadonlyVariableError, carrying the writes made before it, * for a readonly name. `added` is an integer `+=`'s added text, read after * `text` in the same evaluation and added to it. */ export declare function sessionArith(session: SessionState, text: string, reader: RandomReader, nounset?: boolean, added?: string | null): ArithResult; /** * Evaluate `text` as an `-i` write coerces it, land what it assigns through * `view`, and give back the value: a `declare -ni r=M` value, which bash * evaluates before refusing the reference (`M='X=5'` sets X), and its * `r+=M` form, whose `added` text evaluates after `text` in the same * evaluation and adds to it. Inside a `declare -g` it reads the * function's scope, as the coercion does. A hidden name throws * PolicyDenied and a readonly one ExitSignal, which ends the shell as * bash's does, the assignments before it landed; a malformed text throws * ArithError once the ones before the error land. */ export declare function evaluateInteger(session: SessionState, view: SessionView, text: string, added?: string | null): Promise<string>; /** * What a `+=` hands `setVar`: the held text then the added one, or on an * integer the held text with the added one as `added`, the two evaluating * there in turn and summing behind the store's refusals. The held value * evaluates too, so `n='x=5'; declare -i n; n+=x` stores 10, and an empty * side counts as 0. An array extends element 0 and a map key `"0"` * (`S=x; declare -a S+=y` gives `([0]="xy")`), and `held` is null when unset. */ export declare function appended(held: ShellValue | null, added: string, integer: boolean): [string, string | null]; export declare function ensureVarVisible(session: SessionState, name: string): void; /** * Put each name's global record in place for a `declare -g`, and return the * call that puts the running locals back. Outside a function, or for a name * no frame on the call path shadows, the global record is already in place. * Otherwise the running local lives in `session.vars` and the global is * what the *outermost* shadowing frame saved, so the two swap for the * declaration: its writes, marks and kind checks reach the global, and the * local comes back untouched, which is what GNU shows (`local G=5; declare * -gr G=1` leaves `$G` at 5 and writable in the function, 1 and frozen * outside, and a nested `declare -g` reaches past the caller's local too). * Arithmetic it runs still reads the locals (`stepBack`). */ export declare function reachGlobal(session: SessionState, names: readonly string[]): () => void; /** * Let a temporary-environment variable outlive its call. * * bash keeps a name that `x=1 f` put in front of a function once * something inside runs `export x` or `readonly x`: x still holds its * value after f returns, where otherwise the caller's comes back. */ export declare function outliveCall(session: SessionState, name: string): void; /** * Whether the running function's call assigned `name` in front. * * `x=1 f` puts `x` in f's temporary environment, which sits right under * f's own frame of locals. */ export declare function inCallEnv(session: SessionState, name: string): boolean; /** * The positional parameters in scope. * * Inside a function they are the function's own, even when it was called * with none: bash's `f` run bare sees `$#` as 0, never its caller's * count. Outside every function they are the shell's. */ export declare function positionalParams(session: SessionState, callStack: CallStack | null): string[]; /** * Replace the positional parameters in scope. * * `set --` and `shift` inside a function change the function's own and * leave the caller's alone, as bash's do. */ export declare function setPositionalParams(session: SessionState, callStack: CallStack | null, values: string[]): void; /** * Record the caller's record before a `local` shadows it, once per frame. * * `RANDOM` parks its generator marker too: a local `RANDOM` is an ordinary * variable for the function's extent (`local RANDOM=5; echo $RANDOM` * prints 5, and `local RANDOM=(7)` leaves the caller's generator alone), * and `restoreLocals` hands the marker back. */ export declare function shadowLocal(session: SessionState, locals: Map<string, ShellVar | null>, name: string): void; /** * Put a returning function's shadowed records back. * * Deliberate divergence: bash reseeds the global generator when a local * `RANDOM` is popped (`RANDOM=42; f(){ local RANDOM; }; f; echo $RANDOM` * prints 11074 where 17772 was next); mirage resumes the caller's * sequence where it left off. */ export declare function restoreLocals(session: SessionState, locals: Map<string, ShellVar | null>): void; /** * The session plane's view: five facts bound to one session. * * The one constructor every tier uses — builtins, the command * dispatcher, a bare unit test — so the gate cannot be skipped by * picking a different entry point. The view is the whole capability: it * carries no handle back to the raw session. */ /** * Write a variable without consulting the gate. * * For seeding a session before it is handed out -- the embedder * populating an environment, a test arranging state. `visibleArrays` * already names this case ("the embedder can seed session.arrays before * narrowing"). Anything reached from a command line goes through * `SessionView.set` instead, which is the whole point of the store being * read-only from outside. */ export declare function seedVar(session: SessionState, name: string, value: ShellValue): void; /** * Turn one attribute on or off, creating the name if needed. * * bash's `readonly NAME` / `export NAME` on a name that does not exist * yet marks it anyway, and the name stays *unset*: GNU prints * `declare -r ONLY` with no value and `${ONLY-d}` still expands to `d`. * So the record is created with no value, not with an empty string. * * A null attribute changes no attribute and only ensures the name * exists, which is what a bare `local L` / `declare D` does: GNU answers * `declare -- L` and `${L-d}` still expands to `d`, so those two cannot * route through a value writer either. */ export declare function setAttr(session: SessionState, name: string, attr: VarAttr | null, on?: boolean): void; export declare function sessionView(session: SessionState, policies?: Policies | null, diagnostics?: (string | Uint8Array)[]): SessionView; export declare const SHELL_BOOKKEEPING: ReadonlySet<string>; /** * The value a `preSession` method is shown for one variable: a scalar as * itself, an indexed array as its present elements joined by spaces, an * associative one in sorted-key order, and null for a variable that is * declared but unset. One rendering, so a rule reads the same text whether * the write came from a typed line or a restore. Mirrors Python * `gate_rendering`. */ export declare function gateRendering(value: ShellValue | null): string | null; /** * Vet a variable table a snapshot restores through the session gate. * * A snapshot is the one env input the deployment did not author, so the * `preSession` rule that refuses a name on a typed line has to see the * restore too. Every restored variable fires the gate as a `set` of its * rendered value before any of them lands, and a refusal aborts the load * with the `PolicyDenied` a live `export` of that name reports, rather * than dropping the one variable: a partial restore is a workspace whose * state matches no snapshot. The shell's own bookkeeping * (`SHELL_BOOKKEEPING`) is exempt here as it is live. Null policies gate * nothing. Mirrors Python `gate_restored_vars`. */ export declare function gateRestoredVars(policies: Policies | null, sessionId: string, table: Record<string, ShellVar>): Promise<void>; //# sourceMappingURL=state.d.ts.map