@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
440 lines • 23.9 kB
TypeScript
import type { ByteSource, IOResult, OpReport } from '../io/types.ts';
import type { OpRecord } from '../observe/record.ts';
import type { CommandContext, DecisionOwner, DryRun, EntryGate, HandOff, VfsExplanation } from '../policy/types.ts';
import type { ProcessHandle } from '../process/handle.ts';
import type { JobConsole } from '../shell/console/index.ts';
import type { Redirect } from '../shell/types.ts';
import type { Limit, PathSpec, Refusal, Visibility, WalkProbe } from '../types.ts';
import { MountMode } from '../types.ts';
import type { SessionAccess } from '../view/types.ts';
import type { EvaluationContext } from '../workspace/evaluation.ts';
import type { SessionState } from '../workspace/session/session.ts';
import type { ExecutionNode } from '../workspace/types.ts';
/** Whoever binds sessions: one per workspace, compared by identity. */
export declare abstract class SessionOwner {
/** The session this owner holds under `sessionId`. */
abstract get(sessionId: string): SessionState;
}
/** Where a refusal an entry point raises is noted for the line running it. */
export type RefusalSink = (refusal: Refusal) => void;
/** Runs a prepared command under ordered redirects, gating each target. */
export type RedirectResult = [ByteSource | null, IOResult, ExecutionNode];
export type RedirectRun = (stdin: ByteSource | null, sink: JobConsole | undefined, paths: readonly PathSpec[]) => Promise<RedirectResult>;
export type RedirectGuard = (paths: readonly PathSpec[], stdin: ByteSource | null) => Promise<RedirectResult | null>;
export type RedirectRunner = (run: RedirectRun, guard: RedirectGuard | undefined, name: string, args: readonly string[]) => Promise<RedirectResult>;
/**
* One running line: its signal and the sessions its statements stamp
* on (the target session and the per-call fork, one object when the
* call named no cwd or env). Bound by `shell` for the line's duration
* and read at the status write. It rides the async context rather than
* the session, so two lines on one session each see their own, and a
* statement that settles after its caller was released still reads the
* signal of the line that produced it.
*/
export interface LineAbortFrame {
signal: AbortSignal | undefined;
sessions: readonly SessionState[];
writer: symbol;
/**
* What the line's tree runs on, its deadline folded into `signal`, which
* a nested line inherits as its own; absent when it is `signal`. A
* deadline past stops the tree, not the line's own status write.
*/
stop?: AbortSignal;
}
/**
* Who makes a call, and what every check on it needs to know: the
* session, and the line, job, command and file call it runs in.
*
* One record per call, never shared. Each layer is a copy of the one
* above with its own fields set, so two calls running at once each hold
* their own, and work that runs after its command returned (a stream
* pulled later, a runtime's callback) keeps the record it was made with.
* `owner` is the session manager the session belongs to, one per
* workspace; `evaluation` the evaluation running on the session, which a
* layer bound to another session never carries. The fields whose type
* lives in a module that reads the caller (the policies, the
* recording and the cache bindings) are
* added in that module, so this one imports none of them. Mirrors
* Python's Caller.
*/
export interface Caller {
readonly lineAbort: LineAbortFrame | null;
readonly session: SessionState | null;
readonly owner: SessionOwner | null;
readonly evaluation: EvaluationContext | null;
readonly call: readonly [DecisionOwner, HandOff, string] | null;
readonly gate: EntryGate | null;
readonly ruleGate: EntryGate | null;
readonly mount: readonly [string, MountMode] | null;
readonly refusals: RefusalSink | null;
readonly explaining: VfsExplanation[] | DryRun | null;
readonly walkProbe: readonly [string, WalkProbe] | null;
readonly redirects: readonly [object, readonly PathSpec[], RedirectRunner | null, readonly Redirect[]] | null;
readonly program: SessionState | null;
readonly stop: AbortController | null;
readonly writeHeld: boolean;
readonly versionReads: boolean;
readonly commandSink: readonly [OpRecord[] | null, OpRecord[]] | null;
readonly started: number | null;
readonly command: string | null;
readonly declared: Limit | null;
readonly enclosing: JobConsole | null;
readonly execution: string | null;
readonly process: ProcessHandle | null;
readonly invocation: CommandContext | null;
readonly vfsMount: string | null;
}
/**
* A dispatch that takes who makes the op: the dispatcher's own, and each
* workspace function that hands it a caller. The runtime's DispatchFn
* leaves the caller out, as no runtime passes one; Python's carries it
* through its keywords.
*/
export type CallerDispatchFn = (name: string, path: PathSpec, args?: readonly unknown[], kwargs?: Record<string, unknown>, report?: OpReport, caller?: Caller) => Promise<[unknown, IOResult]>;
/** The caller bound to the current async context. */
export declare function currentCaller(): Caller;
/**
* Every caller live in this context, oldest first: on an isolating
* runtime the bound one alone, on the fallback storage every
* concurrently bound one.
*/
export declare function liveCallers(): readonly Caller[];
/**
* Run `fn` with `caller` bound: a layer's own copy, or the record a
* stream pulled or a callback run after its command returned was made
* with.
*/
export declare function runWithCaller<T>(caller: Caller, fn: () => Promise<T>): Promise<T>;
/**
* `fn` run as `caller` wherever it is called from: a runtime's guest asks
* its questions from a callback of its own, which carries no caller.
* Without task isolation, reject conflicting conditional sessions before
* binding the caller, so an in-flight mutation can finish under its owner.
*/
export declare function callsAs<A extends unknown[], R>(caller: Caller, fn: (...args: A) => R): (...args: A) => R;
/** `fn` run as `caller` whoever awaits it. */
export declare function awaitsAs<A extends unknown[], R>(caller: Caller, fn: (...args: A) => Promise<R>): (...args: A) => Promise<R>;
/**
* `source` pulled and closed as `caller`. A stream keeps the caller it was
* made with: whoever pulls it and however late (a reader further down a
* pipeline, a host draining a line's output), what each pull runs (a
* backend's lazy read, the record it makes, the pins and cache scope it
* reads) is that caller's, and so is the close a reader that stops early
* sends. Mirrors Python's `with_caller`.
*/
export declare function withCaller<T>(source: AsyncIterable<T>, caller: Caller): AsyncGenerator<T>;
/** The execution the current call runs in; never a sibling's on hosts
* without task isolation. */
export declare function currentExecutionId(): string | null;
/**
* Run `run` as part of the execution `executionId`, which its records name.
*
* @param process the managed runner it is, when it is one.
*/
export declare function runWithExecution<T>(executionId: string, run: () => Promise<T>, process?: ProcessHandle | null): Promise<T>;
/**
* Bind `session` for the duration of `fn`.
*
* `owner` names the manager the session belongs to; omitting it keeps the
* owner already bound, so a nested bind inside a line (a background job's
* fork) stays attributed to the workspace running it. `evaluation` is the
* evaluation running on the session; omitting it keeps the one already
* bound when it runs on this same session, and drops another session's.
*/
export declare function runWithSession<T>(session: SessionState, fn: () => Promise<T>, options?: {
owner?: SessionOwner;
evaluation?: EvaluationContext;
}): Promise<T>;
export declare function getCurrentSession(): SessionState | null;
/**
* Every line frame live in this context, oldest first, each once: on the
* fallback storage the layers of one line each copy its frame.
*/
export declare function lineFrames(): LineAbortFrame[];
/**
* The active line's cancellation channel, when it can be attributed to this
* session: the one line live on it, or the one signal every live line on it
* answers to, as an `eval` or `source` line carries the line that runs it.
*/
export declare function lineSignal(session: SessionState): AbortSignal | undefined;
/**
* The signal the current call answers to: its evaluation frame's and its
* line's, which withdraw a pipe read or copy that waits; Python cancels the
* awaiting task instead. None in a landing (`runLanding`).
*/
export declare function callerSignal(): AbortSignal | undefined;
/**
* `fn` run as a landing of what a stopped run wrote, which no abort stops,
* as a killed process's writes stay in its files: its calls answer to no
* signal, whatever caller they run as. Mirrors Python's `run_landing`.
*/
export declare function runLanding<T>(fn: () => Promise<T>): Promise<T>;
/**
* Whether the current call is part of a landing. Never without task
* isolation (a browser): there the one slot shows a landing to every call
* beside it, and a stopped guest's call must not pass as one, so the
* guest's stop wins over its landing. Mirrors Python's `in_landing`.
*/
export declare function inLanding(): boolean;
/** Bind an evaluation with its session, as one binding. */
export declare function runWithEvaluation<T>(context: EvaluationContext, fn: () => Promise<T>, owner?: SessionOwner): Promise<T>;
/**
* The evaluation bound with the current session, null outside one.
*
* On an isolating runtime that is the task's own binding. On the fallback
* storage it is the newest live binding that carries one: a bind for
* another session without an evaluation (a held dispatcher's) may be the
* newest frame, and must not answer a running line with none, which would
* skip that line's abort checks. Mirrors Python's get_current_evaluation.
*/
export declare function getCurrentEvaluation(): EvaluationContext | null;
/**
* Every session whose binding is live in this context, oldest first.
*
* On an isolating runtime this is the bound session alone, so a fold
* over it computes exactly what `getCurrentSession` implies. On the
* fallback storage it is every concurrently bound session, which is
* what lets a security predicate merge toward the most restrictive
* answer instead of trusting the newest binding: every predicate here
* fails open on "no session", so a frame another task shadows or
* settles past must keep counting while it is live.
*/
export declare function liveSessions(): SessionState[];
/**
* The bound session, but only when `owner` published it.
*
* A session carries one workspace's cwd, env and mount grants, so a
* second workspace re-entered mid-line must resolve its own session
* rather than adopt this one. The owner is the disambiguator, so every
* live binding is searched, newest first: on the fallback storage a
* concurrent workspace's bind shadowing the newest frame must not hide
* this owner's own session.
*/
export declare function getCurrentSessionFor(owner: SessionOwner): SessionState | null;
/**
* The bound session, unless another owner published it.
*
* An entry point keeps the session it is reached under, so it never widens
* a caller's view: a command's runtime, a kernel mount and a guest
* runtime all bind before they call. A binding that names an owner
* other than `owner` is another workspace's, and its session describes
* that workspace's hides and grants, so the entry point must not adopt it. A
* binding that names no owner is a deliberate placement (a kernel
* mount, a guest runtime, an embedder binding by hand) and is kept.
*/
export declare function getCurrentSessionUnlessForeign(owner: SessionOwner): SessionState | null;
export declare const DEFAULT_UMASK = 18;
/** The owner's creation mask, or the ambient mask when no owner is supplied.
* Unbound browser calls OR every live session's mask so an overlapping
* wider session cannot relax permissions. No live session means bash's default. */
export declare function sessionUmask(access?: SessionAccess): number;
/** Whether the owner's pathname expansion includes leading dots.
* Unbound browser calls require every live session to opt in; no live
* session means bash's default, off. */
export declare function dotglobActive(access?: SessionAccess): boolean;
/** Whether the owner's patterns use extglob grammar. Without an explicit
* owner, read the evaluating session; no session means bash's default, off. */
export declare function extglobActive(access?: SessionAccess): boolean;
/** The owner's visibility, or the ambient view when no owner is supplied.
* Unbound browser calls combine live sessions' hides and keep only shows
* they all grant. An explicit unrestricted owner stays unrestricted. */
export declare function sessionVisibility(access?: SessionAccess): Visibility | null;
/**
* The error a hidden path answers, in POSIX's own terms.
*
* ENOENT names a component that does not exist and EACCES an entry the
* caller may not write, so a create is EACCES only when the directory
* it lands in is visible: a hidden name under a visible parent reads
* as an existing file the session cannot write, which is the one
* answer that neither reveals the content nor invites a create that
* would clobber it. A create under a hidden directory is ENOENT, the
* same answer every read gives for that directory, so a write cannot
* detect a hide a read could not. Everything that is not a create is
* ENOENT. `create` is whether the op creates the path it names; a
* rename or copy destination is one. `links` are the namespace's links, so
* a directory reached through a link into hidden space counts as hidden.
*/
export declare function hiddenRefusal(vis: Visibility | null, virtual: string, create: boolean, links?: ReadonlyMap<string, string>): Error;
/**
* Bind the admitted command's entry gate for the duration of `fn`: the
* run of that one command.
*
* Bound by the dispatcher once the gate let the command through, so a
* nested line (`xargs`, `find -exec`, `eval`) binds its own and the outer
* command gets its gate back when it returns, and a pipeline stage in
* its own async context never sees a sibling's.
*/
export declare function runWithAdmission<T>(gate: EntryGate, fn: () => Promise<T>): Promise<T>;
/**
* The entry gate of the command running in this context, null when no
* admitted command is bound (a command constructed outside the
* dispatcher, or a line no gate judged).
*
* On an isolating runtime one gate is live and answers as bound. On
* the fallback storage several commands' gates can be live at once
* with nothing to say whose op is asking, so they merge toward
* refusal: an entry must pass every live gate's `check`, its `refusal` is
* the first live gate's that refuses it, and a rule counts
* as granted only when every live gate carries it (a once-grant nodded
* for one line must not authorize another's dispatcher), and a path as
* judged only when every live gate names it. `scopes` is true when any
* live gate scopes, keeping walks off the unfiltered native fast paths.
*/
export declare function getAdmission(): EntryGate | null;
/**
* Bind the executing mount's prefix and configured mode for the
* duration of `fn`: the run of one command.
*
* Bound by `Mount.runCommand` around the handler: it is how a command
* tells its run on one mount from a run across mounts, which binds none
* (`crossesMounts`), and the prefix a path it makes up is keyed below
* (`mountRoot`).
*/
export declare function runWithMountGate<T>(prefix: string, mode: MountMode, fn: () => Promise<T>): Promise<T>;
/**
* Bind where the entry points note a policy's refusal, for the duration of `fn`:
* one line's run.
*
* Bound by the workspace around a typed line: a command renders an op
* refusal in its own GNU words, which say nothing of the policy, so the
* entry point notes the record here and the line carries it on its result.
* Every task and nested line the line starts inherits the binding, so a
* stream drained after its command returned still reaches it. Mirrors
* Python's set_refusal_sink.
*/
export declare function runWithRefusalSink<T>(sink: RefusalSink, fn: () => Promise<T>): Promise<T>;
/**
* Whether a typed line is running in this context: the entry point it reaches is
* inside a command, not a file tool's or the host's own call. Mirrors
* Python's `line_running`.
*/
export declare function lineRunning(): boolean;
/**
* Run `fn` as a dry run: the op gate notes on `trace` what it would
* answer and stops the op before any backend or cache is touched.
* `DryRun.DECIDING` is what a policy does while it decides the op
* explained: its reads run, nothing changes. A null trace runs `fn` for
* real. Mirrors Python's `set_explaining`.
*/
export declare function runExplaining<T>(trace: VfsExplanation[] | DryRun | null, fn: () => Promise<T>): Promise<T>;
/**
* The dry run's trace when the calls in this context only explain,
* DECIDING while its policies decide, null when they run.
*/
export declare function explaining(): VfsExplanation[] | DryRun | null;
/**
* Hand an entry point's refusal to the line running in this context; an entry point
* reached outside any line (a programmatic op) has no line to tell, and
* the record rides the thrown error alone.
*/
export declare function noteRefusal(refusal: Refusal): void;
/**
* The walk probe bound to the command serving `virtual`, null outside a
* mount's command (a generic invoked directly in a test).
*
* Selected among every live binding by the path itself, the longest prefix
* covering it winning as the mount table routes, so on the fallback storage
* a concurrent command on another mount cannot lend its probe to this one.
* Mirrors Python's get_walk_probe.
*/
export declare function walkProbeFor(virtual: string): WalkProbe | null;
/**
* Bind a statement's expanded redirect targets to the command node they
* belong to, for the duration of `fn` (that node's run).
*
* The redirect layer expands the targets before the command executes (a
* `$()` in one runs exactly once there), so the admission gate deep in
* command dispatch cannot re-derive them; it reads them here instead.
* Keyed by the node object itself so a nested line expanded on the way
* to the command (a `$()` operand, an `eval`) never inherits the outer
* statement's targets.
*
* The runner applies redirects after command words expand, admitting each
* resolved target before opening it. Nested expansions keep their own identity.
*/
export declare function runWithRedirectPaths<T>(node: object, paths: readonly PathSpec[], fn: () => Promise<T>, runner?: RedirectRunner | null, syntax?: readonly Redirect[]): Promise<T>;
/** The caller a deferred workspace callback runs as: the bound one, as
* `session` when one is named, whose evaluation comes along only when it
* is the session bound now. */
export declare function sessionCaller(session: SessionState, owner?: SessionOwner): Caller;
/**
* Run a line as a program run in a session: `find -exec` hands its words
* to `execvp`, so the head it runs is the coreutils program, not the
* shell's builtin of the same name (`printf -v` is a format string there,
* not an assignment). Keyed by the session object, which the child shells
* it starts inherit, and cleared again by a nested shell the line starts
* (`-exec sh -c ...`), so that shell's builtins are its own.
*/
export declare function runAsProgram<T>(session: SessionState, fn: () => Promise<T>): Promise<T>;
/** Run a nested shell's script as the shell's own again: a nested shell
* is a program, and the builtins it runs are its builtins, `printf -v`
* included. */
export declare function runAsShell<T>(fn: () => Promise<T>): Promise<T>;
/** Whether the line running in this session is a program run. */
export declare function isProgramInvocation(session: SessionState): boolean;
/**
* The redirect targets bound to this command node, empty for any other
* node or when none are bound.
*
* The node is the disambiguator, so every live binding is searched,
* newest first: on the fallback storage a concurrent statement's bind
* shadowing the newest frame must not hide this node's own targets.
*/
export declare function redirectPathsFor(node: object): readonly PathSpec[];
/** Pending redirects, before a rewritten command reaches admission. */
export declare function redirectSyntaxFor(node: object): readonly Redirect[];
/**
* The runner bound with this command node's redirect targets, null for any
* other node or when none is bound.
*/
export declare function redirectRunnerFor(node: object): RedirectRunner | null;
/**
* Whether a path is a redirect target command admission already judged
* for the statement writing it now.
*
* The dispatcher asks this, and unlike `redirectPathsFor` it takes no node,
* because by the time the shell writes the file the node has returned
* and the dispatcher sees only a path. The binding is what keeps that honest: it
* exists only while one statement's targets are being written, and a
* statement whose targets a rule refused never reaches the write at all.
* So a bound target is one the line was admitted with, and re-deriving a
* verdict for it from the dispatcher, which knows neither the line nor the nod it
* holds, can only get it wrong. Every live binding counts, because a
* target is only ever bound after its own line's admission judged it: on the
* fallback storage a concurrent statement's frame must not shadow this
* one's targets into a re-derived refusal.
*/
export declare function redirectTargetJudged(virtual: string): boolean;
/**
* The mount mode after narrowing by the owner's profile, or the live
* sessions' when no owner is supplied. The mount's own mode is the
* strongest one available; a profile's mode can only weaken it (a READ
* mount stays read-only whatever the profile says). A mount the profile
* does not name keeps its own mode.
*/
export declare function effectiveMountMode(mountPrefix: string, mountMode: MountMode, access?: SessionAccess): MountMode;
export declare function effectivePathMode(virtual: string, mountPrefix: string, mountMode: MountMode, access?: SessionAccess): MountMode;
/**
* The strongest mode one session reaches anywhere under a mount: its
* mount-wide mode, or a deeper show grant, still capped by the mount's
* configured mode. Mirrors the Python `strongest_under_session`.
*/
export declare function strongestUnderSession(sess: SessionAccess, mountPrefix: string, mountMode: MountMode): MountMode;
/**
* The strongest mode the live sessions reach anywhere under a mount
* (`strongestUnderSession`), folded to the weakest across them: a command
* runs only when every live session would let it.
*
* What the whole-mount gates read: a write command stays runnable on a
* mount whose only writable region is a show entry (the dispatcher then
* refuses per path), and the interpreters' any-`x` rule counts a show
* grant the way it counts a whole mount.
*/
export declare function strongestModeUnder(mountPrefix: string, mountMode: MountMode, access?: SessionAccess): MountMode;
export declare function readonlyBelow(virtual: string, mountPrefix: string, mountMode: MountMode, access?: SessionAccess): string | null;
/** Apply the same mode ceiling to command, dispatcher and namespace writes. */
export declare function requirePathsWritable(paths: readonly PathSpec[], mountPrefix: string, mountMode: MountMode, subtree?: boolean, access?: SessionAccess): void;
/** Bind access to its owner before an asynchronous operation can suspend. */
export declare function sessionAccess(session: SessionAccess | null): SessionAccess;
//# sourceMappingURL=session_context.d.ts.map