UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

440 lines • 23.9 kB
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