@struktoai/mirage-dsh 0.0.1 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +49 -0
- package/dist/{chunk-CATVBGGR.js → chunk-2F2PJ7D7.js} +66 -4
- package/dist/{chunk-CATVBGGR.js.map → chunk-2F2PJ7D7.js.map} +1 -1
- package/dist/chunk-4CZV24HW.js +196 -0
- package/dist/chunk-4CZV24HW.js.map +1 -0
- package/dist/{chunk-C67TSSYB.js → chunk-M4JOF35U.js} +108 -15
- package/dist/chunk-M4JOF35U.js.map +1 -0
- package/dist/chunk-QTBMLMKT.js +698 -0
- package/dist/chunk-QTBMLMKT.js.map +1 -0
- package/dist/fs-B3Zi9oo6.d.ts +104 -0
- package/dist/index.d.ts +137 -5
- package/dist/index.js +14 -6
- package/dist/plugin/fs.d.ts +1 -1
- package/dist/plugin/fs.js +2 -2
- package/dist/plugin/service.d.ts +3 -2
- package/dist/plugin/service.js +1 -1
- package/dist/plugin/shell.d.ts +1 -1
- package/dist/plugin/shell.js +2 -2
- package/dist/{service-CxFYAvoV.d.ts → service-BK6NR_RX.d.ts} +51 -1
- package/dist/shell-BTf2wsDy.d.ts +258 -0
- package/package.json +10 -7
- package/dist/chunk-C67TSSYB.js.map +0 -1
- package/dist/chunk-J6F4OX6Z.js +0 -102
- package/dist/chunk-J6F4OX6Z.js.map +0 -1
- package/dist/chunk-ZSZVHBSL.js +0 -455
- package/dist/chunk-ZSZVHBSL.js.map +0 -1
- package/dist/fs-CuiUEA7A.d.ts +0 -54
- package/dist/shell-B0N_jppN.d.ts +0 -108
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shell.ts","../src/spill.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport { DSH_ENV_PREFIX, ShellExecutor } from '@deepseek-ai/dsh-shell'\nimport type {\n CollectedOutput,\n ShellExecRequest,\n ShellExecSpec,\n ShellProcess,\n ShellProcessRead,\n ShellProcessStatus,\n ShellRunResult,\n ShellSandboxInfo,\n} from '@deepseek-ai/dsh-shell'\nimport {\n Channel,\n JobConsole,\n KILLED_OUTCOME,\n RAMConsoleStore,\n exitOutcome,\n} from '@struktoai/mirage-core/shell/console/index'\nimport type { ConsoleChunk } from '@struktoai/mirage-core/shell/console/index'\nimport { setCwd } from '@struktoai/mirage-core/workspace/session/shell_dirs'\nimport { sessionView } from '@struktoai/mirage-core/workspace/session/state'\nimport type {\n ExecuteOptions,\n ExecuteResult,\n} from '@struktoai/mirage-core/workspace/workspace/workspace'\nimport type { Workspace } from '@struktoai/mirage-node'\nimport { TailBuffer, tailCap } from './text.ts'\nimport { SpillSink, ensureDirPath, type SpillTarget } from './spill.ts'\nimport type {} from './service.ts'\nimport type { Refusal } from '@struktoai/mirage-core/types'\n\nconst DEFAULT_TIMEOUT_MS = 120_000\nconst MAX_TIMEOUT_MS = 600_000\nconst DEFAULT_STDOUT_MAX_BYTES = 200_000\nconst DEFAULT_STDERR_MAX_BYTES = 64_000\nconst STDERR_MARKER = new TextEncoder().encode('\\n--- stderr ---\\n')\n// What a console may hold that its reader has not consumed yet. Capping\n// the delta does not bound this: a reader's cursor advances but frees\n// nothing, so an uncapped store keeps every chunk of a noisy command for\n// the life of the process. Five deltas' worth, because a drain awaits the\n// spill's own writes and has to be free to fall briefly behind, and\n// bounded, so a command that outruns it forever cannot grow the heap.\n// Derived from the call's own budget rather than fixed: a retention\n// smaller than one delta would make a slow reader lossy by construction.\nconst CONSOLE_RETENTION_DELTAS = 5\n\n// Monotonic within the process, so concurrent background commands never\n// collide on a spill filename. Not reset, so it needs no time or randomness.\nlet spillCounter = 0\n\n// The mount whose writability a `read-only` policy keeps, because dsh's\n// own definition of that mode keeps it: \"permits only required sinks such\n// as /dev/null\". Narrowing it too would make `cmd > /dev/null` fail, which\n// no read-only sandbox anywhere does.\nconst SINK_PREFIX = '/dev'\n\n// How mirage refuses a write the session's mount grants do not allow.\n// Three spellings, because the refusal is raised in two places: a command\n// names the mount it would not write to, while a redirection or an op below\n// the granted mode is refused in the read-only voice as the file opens.\n// Hide refusals keep `Permission denied`. Only consulted for a call that\n// ran under `read-only`, so the only permission error these can catch is\n// the one this executor just imposed.\nconst DENIAL_SIGNATURES = ['read-only mount at ', ': Permission denied', ': Read-only file system']\n\n/** Configuration for the mirage shell executor. */\nexport interface MirageShellConfig {\n /**\n * Default working directory for commands. Defaults to `/`. With\n * `sessionId` it instead seeds the bound session's initial cwd, and the\n * session's own cwd is the default from then on.\n */\n workdir?: string\n /** Default foreground timeout in milliseconds. Defaults to 120000. */\n defaultTimeoutMs?: number\n /** Upper cap on any requested timeout. Defaults to 600000. */\n maxTimeoutMs?: number\n /** Default stdout capture budget in bytes. Defaults to 200000. */\n stdoutMaxBytes?: number\n /** stderr capture budget in bytes. Defaults to 64000. */\n stderrMaxBytes?: number\n /**\n * Bind every command to this named workspace session. By default each\n * command runs in an ephemeral fork of the workspace's default session,\n * so nothing persists between calls, which is the one-shot contract of\n * dsh's bash tool. With a session bound, `cd`, `export`, and function\n * definitions persist across calls, the persistent-shell contract. The\n * session is created on first use if the workspace does not have it; an\n * existing session is adopted as is.\n *\n * A spec carrying an explicit `env`, or a `workdir` that names a real\n * directory in this world, still runs as a one-call subshell of the\n * bound session, per mirage's `ExecuteOptions` semantics: both say\n * \"just for this command\". The two things dsh injects on every call\n * are deliberately not read that way, since neither carries that\n * intent and either would fork every command and leave the binding\n * with nothing to persist. A workdir resolved on the harness's own\n * machine names nothing here and is dropped; the managed `DSH_*`\n * snapshot is seeded into the session instead.\n */\n sessionId?: string\n /**\n * When set, a background command whose streamed output overruns its\n * delta budget spills its full stdout and stderr to files under this\n * workspace directory, and `readOutput()` points at them so a reader\n * can recover what the delta dropped. The directory is a workspace\n * path (e.g. `/tmp` on a ram mount), so the agent reads the spill\n * through the same VFS as everything else; the writes go through the\n * workspace, so they appear in history like any other write. Unset\n * (the default) means no spill: output that overruns is simply\n * flagged `lossy`, the honest \"no safe path available\" answer.\n */\n spillDir?: string\n}\n\nfunction collect(text: string, maxBytes: number): CollectedOutput {\n const capped = tailCap(text, maxBytes)\n return { text: capped.text, truncated: capped.truncated }\n}\n\nfunction executeOptions(\n spec: ShellExecSpec,\n workdir: string,\n signal: AbortSignal,\n sessionId: string | undefined,\n bound: boolean,\n fallbackWorkdir: string,\n sink?: JobConsole,\n): ExecuteOptions & { provision?: false } {\n // A per-call `env` makes mirage fork a subshell, exactly as `cwd` does,\n // so what goes in it decides whether anything can persist. Bound to a\n // session, only a genuine per-call override belongs here: dsh sends a\n // non-empty managed `DSH_*` snapshot on every single call, and carrying\n // that per call would fork every command and quietly undo the binding.\n // Those facts are seeded into the session instead, by `applyManagedEnv`.\n const managed = (spec.dshEnv as Record<string, string> | undefined) ?? {}\n const env = bound ? { ...(spec.env ?? {}) } : { ...(spec.env ?? {}), ...managed }\n // Unbound, `cwd` is always present so every command runs in an ephemeral\n // fork: isolation must not hinge on a spec happening to carry a workdir,\n // and a read-only call runs in a *named* twin session, so without a cwd\n // its `cd` and exports would persist into the next nominally one-shot\n // call. Bound, an absent workdir runs in the session itself, which is\n // what lets its state persist. Either way the decision is the binding's,\n // never the session this particular call happens to land in.\n const cwd = workdir !== '' ? workdir : bound ? undefined : fallbackWorkdir\n return {\n signal,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(cwd !== undefined ? { cwd } : {}),\n ...(Object.keys(env).length > 0 ? { env } : {}),\n ...(spec.stdin !== undefined ? { stdin: new TextEncoder().encode(spec.stdin) } : {}),\n ...(sink !== undefined ? { sink } : {}),\n }\n}\n\n/**\n * A background command over the workspace executor, streamed through a\n * `JobConsole`. The command runs with the console as its `sink`, so each\n * statement of a compound line lands as it finishes rather than the whole\n * line arriving at the end (a single command still shows up in one chunk,\n * having nothing to emit before it completes). A background follow loop\n * drains the console into `pending`, which `readOutput()` hands back and\n * clears — consuming, so consecutive reads never re-deliver. Unread output\n * is bounded to `budget` bytes: the head is dropped and `lossy` set once\n * it overruns, keeping the tail, which is where the full stream spills to\n * a file. Both ends of the conduit are bounded, because draining the\n * console does not free it: the console holds a retention budget of its\n * own, and a command that outruns this loop by that much loses chunks,\n * which arrives here as a gap in the sequence. `kill()` aborts\n * cooperatively (the executor observes the signal between pipeline stages\n * and inside sleep).\n */\nclass MirageShellProcess implements ShellProcess {\n status: ShellProcessStatus = 'running'\n exitCode: number | null = null\n signal: NodeJS.Signals | null = null\n sandbox?: ShellSandboxInfo\n readonly done: Promise<void>\n\n private readonly controller: AbortController\n private readonly console: JobConsole\n private readonly budget: number\n private readonly spill: SpillSink | null\n private readonly sandboxInfo: ShellSandboxInfo | undefined\n private readonly consumed: Promise<void>\n private readonly pending: TailBuffer\n private lossy = false\n private inStderr = false\n private settled = false\n private expectSeq = 0\n\n constructor(\n run: Promise<ExecuteResult>,\n controller: AbortController,\n console_: JobConsole,\n budget: number,\n spill: SpillSink | null,\n sandboxInfo: ShellSandboxInfo | undefined,\n ) {\n this.controller = controller\n this.console = console_\n this.budget = budget\n this.pending = new TailBuffer(budget)\n this.spill = spill\n this.sandboxInfo = sandboxInfo\n this.consumed = this.consume()\n this.done = run.then(\n (result) => this.settle(result, null),\n (err: unknown) => this.settle(null, err),\n )\n }\n\n private async consume(): Promise<void> {\n // follow() yields every chunk in sequence and ends on the CONTROL\n // chunk that finish() appends.\n for await (const chunk of this.console.follow(0)) {\n if (chunk.channel === Channel.CONTROL) return\n // A seq that skips means the console trimmed chunks this loop had\n // not read: the command outran the drain by a whole retention\n // budget. Those bytes are gone for good, so say so, and stop the\n // spill rather than let a file with a hole in it be handed back\n // as the full stream.\n if (chunk.seq !== this.expectSeq) {\n this.lossy = true\n this.spill?.disable()\n }\n this.expectSeq = chunk.seq + 1\n await this.appendChunk(chunk)\n }\n }\n\n private async appendChunk(chunk: ConsoleChunk): Promise<void> {\n // The full, uncapped stream goes to the spill sink (if enabled)\n // before the delta is capped, so nothing dropped from the delta is\n // lost to a reader that follows the spill path.\n if (this.spill !== null) await this.spill.ingest(chunk.channel, chunk.data)\n // stderr rides the same delta as stdout, opened by a marker so the\n // reader can tell the two apart; a run of stderr chunks marks once.\n let dropped = false\n if (chunk.channel === Channel.STDERR) {\n if (!this.inStderr) {\n dropped = this.pending.append(STDERR_MARKER)\n this.inStderr = true\n }\n } else {\n this.inStderr = false\n }\n // The backlog bounds itself as it grows, so a reader that never\n // drains cannot grow it without limit and an append costs the chunk\n // rather than everything buffered before it. The tail is kept (the\n // freshest output), matching what the buffered path did at completion.\n dropped = this.pending.append(chunk.data) || dropped\n if (dropped) {\n this.lossy = true\n // The delta just dropped bytes; move the full stream to files so\n // the reader can still recover them from the spill path.\n if (this.spill !== null) await this.spill.begin()\n }\n }\n\n private async settle(result: ExecuteResult | null, err: unknown): Promise<void> {\n this.settled = true\n if (this.sandboxInfo !== undefined) this.sandbox = this.sandboxInfo\n let outcome: string\n if (result !== null) {\n this.status = 'completed'\n this.exitCode = result.exitCode\n outcome = exitOutcome(result.exitCode)\n } else {\n this.status = 'killed'\n this.signal = 'SIGTERM'\n const message = err instanceof Error ? err.message : String(err)\n await this.console.emit(Channel.STDERR, new TextEncoder().encode(message))\n outcome = KILLED_OUTCOME\n }\n // The CONTROL chunk ends the follow loop; awaiting `consumed`\n // guarantees every chunk (the last one included) has landed in\n // `pending` before `done` resolves, so a read after `done` is whole.\n await this.console.finish(outcome)\n await this.consumed\n }\n\n readOutput(): ShellProcessRead {\n const delta = this.pending.take()\n const lossy = this.lossy\n this.lossy = false\n return {\n delta,\n lossy,\n ...(this.spill?.stdoutPath !== undefined ? { stdoutSpillPath: this.spill.stdoutPath } : {}),\n ...(this.spill?.stderrPath !== undefined ? { stderrSpillPath: this.spill.stderrPath } : {}),\n }\n }\n\n kill(): boolean {\n if (this.settled) return false\n this.controller.abort()\n return true\n }\n}\n\n/**\n * Mirage-backed implementation of `ctx.shell`: `run` executes the command\n * line with mirage's own shell (coreutils-faithful commands, installed\n * CLIs, the policy layer) against the shared `ctx.mirage` workspace, so a\n * path from `ctx.fs` means the same file here. There is no OS process\n * behind a command: `signal` in results is a compatibility value for kills,\n * and abort/timeout act cooperatively at the executor's own boundaries.\n *\n * Every command runs in an ephemeral fork of the workspace's default\n * session, so no shell state survives from one call to the next, matching\n * the one-shot contract of dsh's bash tool. Configuring a `sessionId`\n * binds all commands to one named session instead, whose cwd, exports,\n * and functions persist across calls.\n */\nexport class MirageShellExecutor extends ShellExecutor {\n static readonly inject = ['mirage']\n\n private readonly workdir: string\n private readonly defaultTimeoutMs: number\n private readonly maxTimeoutMs: number\n private readonly stdoutMaxBytes: number\n private readonly stderrMaxBytes: number\n private readonly sessionId: string | undefined\n private readonly spillDir: string | undefined\n private sessionReady: Promise<void> | null = null\n private readOnlyReady: Promise<string> | null = null\n\n constructor(ctx: Context, config: MirageShellConfig = {}) {\n super(ctx)\n this.workdir = config.workdir ?? '/'\n this.defaultTimeoutMs = config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS\n this.maxTimeoutMs = config.maxTimeoutMs ?? MAX_TIMEOUT_MS\n this.stdoutMaxBytes = config.stdoutMaxBytes ?? DEFAULT_STDOUT_MAX_BYTES\n this.stderrMaxBytes = config.stderrMaxBytes ?? DEFAULT_STDERR_MAX_BYTES\n this.sessionId = config.sessionId\n this.spillDir = config.spillDir\n }\n\n // The workspace may still be building (declarative mounts resolve\n // asynchronously), so every execution awaits the service's `ready`.\n private workspace(): Promise<Workspace> {\n return this.ctx.mirage.ready\n }\n\n /**\n * The directory this command actually runs in.\n *\n * dsh fills an unspecified workdir from the calling session's cwd (by\n * way of the sandbox policy's workspace root), and that is a directory\n * on the harness's own machine, which names nothing here. Running\n * there leaves `pwd` reporting a path the agent cannot reach and every\n * relative path failing, and, because a per-call cwd forks a subshell,\n * it also defeats a bound session on every call. So a workdir that is\n * not a directory in this world is treated as unset: the configured\n * default when unbound, the session's own cwd when bound.\n *\n * @param spec the resolved spec whose workdir is being placed.\n * @returns the workdir to execute under, `''` meaning the session's own.\n */\n private async worldWorkdir(spec: ShellExecSpec): Promise<string> {\n if (spec.workdir === '') return ''\n const ws = await this.workspace()\n if (await ws.fs.isDir(spec.workdir)) return spec.workdir\n return this.sessionId === undefined ? this.workdir : ''\n }\n\n // A spill sink for one background command, or null when no spill\n // directory is configured. The target reaches the live workspace so\n // the full stream lands on a mount the agent can read back.\n private newSpill(): SpillSink | null {\n const dir = this.spillDir\n if (dir === undefined) return null\n const target: SpillTarget = {\n ensureDir: async (d) => {\n const ws = await this.workspace()\n await ensureDirPath({ exists: (p) => ws.fs.exists(p), mkdir: (p) => ws.fs.mkdir(p) }, d)\n },\n write: async (p, bytes) => {\n const ws = await this.workspace()\n await ws.fs.writeFile(p, bytes)\n },\n append: async (p, bytes) => {\n const ws = await this.workspace()\n await ws.fs.append(p, bytes)\n },\n }\n spillCounter += 1\n const log = this.ctx.logger('mirage-dsh')\n return new SpillSink(target, dir, `mirage-shell-${spillCounter.toString()}`, (err: unknown) => {\n log.debug('spill to %s failed, output will not be recoverable: %o', dir, err)\n })\n }\n\n /**\n * With every runtime in the world reaching only the vfs\n * (`ctx.mirage.vfsOnly`), the workspace dispatch is the single gate\n * for anything a command can do, so this executor behaves like a\n * workspace-write sandbox: reads and writes land only where mounts\n * (and their modes) allow. Declaring it lets sandbox-aware plugins\n * (dsh's permission presets) compose over this executor. A world\n * holding a runtime with doors around the gate (the host `local`\n * python, a remote sandbox) voids that claim, so this answers\n * undefined then (the base contract's \"does not sandbox\") and those\n * plugins refuse to compose instead of trusting a lie.\n */\n override get sandboxMode(): ShellExecutor['sandboxMode'] {\n return this.ctx.mirage.vfsOnly ? 'workspace-write' : undefined\n }\n\n /**\n * The mode this one call runs under: the policy the caller resolved\n * for it, or this executor's own default when the call carried none.\n * Undefined keeps the \"no claim\" answer for a world some runtime can\n * act outside of, where no mode would be true.\n *\n * @param spec the resolved spec whose policy is being read.\n * @returns the effective mode, or undefined when nothing is claimed.\n */\n private modeFor(spec: ShellExecSpec): ShellExecutor['sandboxMode'] {\n const declared = this.sandboxMode\n if (declared === undefined) return undefined\n return spec.sandboxPolicy?.mode ?? declared\n }\n\n /**\n * The session this call runs in: the read-only twin when the policy\n * confines it to reads, else this executor's own binding.\n *\n * `workspace-write` and `danger-full-access` both run in the ordinary\n * session, because the mounts and their modes already are the\n * workspace boundary and mirage has nothing wider to grant.\n *\n * @param spec the resolved spec whose policy selects the session.\n * @returns the session id to execute under, or undefined for the default.\n */\n private async sessionFor(spec: ShellExecSpec): Promise<string | undefined> {\n if (this.modeFor(spec) !== 'read-only') return this.sessionId\n return this.readOnlySession()\n }\n\n /**\n * The sandbox facts to stamp on this run's result and process handle,\n * or undefined when the world is not fully workspace-bound (no claim).\n *\n * `enforcement` is 'full': when every runtime reaches only the vfs, the\n * workspace gate cannot be bypassed, so unlike an OS sandbox on an older\n * kernel there is no promised effect it fails to govern. `runnerFailed`\n * is false because the workspace executor is the runner and a failure to\n * run surfaces as a rejected/aborted execution, not a runner that never\n * started.\n *\n * @param spec the resolved spec this run was built from.\n * @param denied whether the run was refused a write by the narrowing.\n * @returns the facts to stamp, or undefined when nothing is claimed.\n */\n private sandboxInfo(spec: ShellExecSpec, denied = false): ShellSandboxInfo | undefined {\n const mode = this.modeFor(spec)\n if (mode === undefined) return undefined\n return { mode, denied, enforcement: 'full', runnerFailed: false }\n }\n\n /**\n * The retention budget of a background command's console.\n *\n * Only the background path caps retention: there a follow loop drains\n * the console while the command still runs, so the budget bounds what\n * the loop has not reached yet, and a chunk lost to it is reported as\n * a gap in the sequence. A foreground console is read once, after the\n * fact, with nothing to bound.\n *\n * @param spec the resolved spec carrying the stdout budget.\n * @returns the retention budget in bytes.\n */\n private retentionFor(spec: ShellExecSpec): number {\n return Math.max(spec.stdoutMaxBytes, this.stderrMaxBytes) * CONSOLE_RETENTION_DELTAS\n }\n\n /**\n * Take a finished run's two streams off its console, capped, and spill\n * whichever one lost bytes.\n *\n * A foreground run streams into a console rather than returning its\n * output whole, because mirage throws on abort: bytes a killed command\n * had already printed are recoverable from a sink and nowhere else.\n * Only a truncated stream spills, since an untruncated one is already\n * whole in `text` and writing a file for it would put a copy of every\n * command's output on a mount. The console this reads is untrimmed, so\n * a spill is the whole stream rather than the tail of one.\n *\n * @param console_ the console this run streamed into.\n * @param spec the resolved spec carrying the stdout budget.\n * @returns the capped streams, each with a spill path when it lost bytes.\n */\n private async collectFrom(\n console_: JobConsole,\n spec: ShellExecSpec,\n ): Promise<{ stdout: CollectedOutput; stderr: CollectedOutput }> {\n const decoder = new TextDecoder()\n const outBytes = await console_.snapshot(Channel.STDOUT)\n const errBytes = await console_.snapshot(Channel.STDERR)\n const stdout = collect(decoder.decode(outBytes), spec.stdoutMaxBytes)\n const stderr = collect(decoder.decode(errBytes), this.stderrMaxBytes)\n if (!stdout.truncated && !stderr.truncated) return { stdout, stderr }\n const spill = this.newSpill()\n if (spill === null) return { stdout, stderr }\n if (stdout.truncated) await spill.ingest(Channel.STDOUT, outBytes)\n if (stderr.truncated) await spill.ingest(Channel.STDERR, errBytes)\n await spill.begin()\n return {\n stdout: {\n ...stdout,\n ...(spill.stdoutPath !== undefined ? { spillPath: spill.stdoutPath } : {}),\n },\n stderr: {\n ...stderr,\n ...(spill.stderrPath !== undefined ? { spillPath: spill.stderrPath } : {}),\n },\n }\n }\n\n /**\n * Whether this run was refused, by the session's permission document\n * or by the read-only narrowing.\n *\n * The document's refusals ride the result itself: a `Deny`, an\n * unanswered ask and a policy that raised all leave `refusal` on the\n * `ExecuteResult`, whatever the line did with its streams (`2>&1`, a\n * trailing command that owns the status). That record is read for\n * every call, because a role's `commands.deny` and `commands.ask`\n * rules bind under `workspace-write` and `danger-full-access` alike:\n * a mode says what the mounts allow, and says nothing about whether a\n * rule forbids the line. The narrowing has no record, since it is\n * EROFS/EACCES from the mounts, so its signatures are still read off\n * stderr, and only for a call that ran read-only, where this executor\n * is what imposed it.\n *\n * @param spec the resolved spec this run was built from.\n * @param result what the workspace answered.\n * @param stderr the run's captured standard error.\n * @returns true when something refused the run.\n */\n private wasDenied(\n spec: ShellExecSpec,\n result: { readonly refusal: Refusal | null },\n stderr: string,\n ): boolean {\n if (result.refusal !== null) return true\n if (this.modeFor(spec) !== 'read-only') return false\n return DENIAL_SIGNATURES.some((signature) => stderr.includes(signature))\n }\n\n resolve(request: ShellExecRequest): ShellExecSpec {\n // Bound to a session, an unspecified workdir stays empty so the\n // session's own cwd governs; filling the default here would turn\n // every call into a subshell and nothing would ever persist.\n const workdir = request.workdir ?? (this.sessionId === undefined ? this.workdir : '')\n return {\n command: request.command,\n workdir,\n timeoutMs: Math.min(request.timeoutMs ?? this.defaultTimeoutMs, this.maxTimeoutMs),\n stdoutMaxBytes: request.stdoutMaxBytes ?? this.stdoutMaxBytes,\n signal: request.signal,\n stdin: request.stdin,\n env: request.env,\n dshEnv: request.dshEnv,\n sandboxPolicy: request.sandboxPolicy,\n }\n }\n\n private ensureSession(): Promise<void> {\n if (this.sessionId === undefined) return Promise.resolve()\n this.sessionReady ??= this.provisionSession(this.sessionId).catch((err: unknown) => {\n this.sessionReady = null\n throw err\n })\n return this.sessionReady\n }\n\n /**\n * Seed this call's managed `DSH_*` snapshot into the bound session.\n *\n * These are harness facts about the session (its home, its id), not\n * overrides for one command, and on a bound session they have to live\n * in the session: handed over as a per-call `env` they would fork a\n * subshell on every call, since dsh never sends an empty snapshot.\n *\n * The snapshot replaces rather than merges, per the seam's own rule\n * that a fact absent from the current snapshot must not inherit a\n * stale value from an earlier one. Only the managed namespace is\n * touched, so a variable the agent exported itself is left alone.\n *\n * @param ws the live workspace holding the session.\n * @param sessionId the session this call runs in.\n * @param spec the resolved spec carrying the snapshot.\n */\n private async applyManagedEnv(\n ws: Workspace,\n sessionId: string,\n spec: ShellExecSpec,\n ): Promise<void> {\n const managed = spec.dshEnv as Record<string, string> | undefined\n if (managed === undefined) return\n const session = ws.getSession(sessionId)\n const view = sessionView(session)\n for (const key of Object.keys(session.env)) {\n if (key.startsWith(DSH_ENV_PREFIX) && !(key in managed)) await view.unset(key)\n }\n for (const [key, value] of Object.entries(managed)) await view.set(key, value)\n }\n\n private readOnlySession(): Promise<string> {\n this.readOnlyReady ??= this.provisionReadOnly().catch((err: unknown) => {\n this.readOnlyReady = null\n throw err\n })\n return this.readOnlyReady\n }\n\n /**\n * Create (once) the session a read-only call runs in: a twin of the\n * session this executor would otherwise use, with every grant it holds\n * narrowed to `read`, so mirage's own dispatch is what refuses the\n * write rather than a second permission layer bolted on here.\n *\n * The twin narrows, never widens, and that takes every part of the\n * source's view, which is what `narrow` in core stamps: modes, hidden\n * paths, hidden variables, command rules. Its modes cover every mount\n * at `read` (the one exception is the null sink, per\n * {@link SINK_PREFIX}), which is at least as narrow as whatever the\n * source held, since `read` is the weakest mode there is; naming a\n * mount only narrows it, so a prefix the map omits would keep its own\n * mode rather than disappear. The other three are copied from the\n * source session rather than recompiled, because the profile it was\n * created under is not something a session records.\n *\n * Leaving any of them behind widens. Hides are the obvious one: a\n * binding confined to `/allowed` would read `/secret` in read-only\n * mode although the same command is refused outside it. Command rules\n * are the one modes cannot stand in for, because a mode bounds a\n * mount and an account CLI reaches a service: a profile that denies\n * `slack message send` or `git push` still denies it here, where\n * every mount being `read` says nothing at all about it.\n *\n * The policy's `workspaceRoot` is deliberately not consulted anywhere:\n * it is a directory on the harness's machine, so containment against\n * it says nothing about this world. The mounts are the boundary.\n *\n * @returns the id of the read-only session.\n */\n private async provisionReadOnly(): Promise<string> {\n const ws = await this.workspace()\n const sessionId = `${this.sessionId ?? 'mirage-dsh'}::read-only`\n await ws.ensureSessionsLoaded()\n if (ws.listSessions().some((s) => s.sessionId === sessionId)) return sessionId\n const source = ws.getSession(this.sessionId ?? ws.defaultSessionId)\n const grants: Record<string, string> = {}\n for (const entry of ws.mounts()) {\n grants[entry.prefix] = entry.prefix.replace(/\\/+$/, '') === SINK_PREFIX ? 'exec' : 'read'\n }\n const hide = [...(source.hiddenPaths?.paths ?? []), ...(source.hiddenPaths?.patterns ?? [])]\n const twin = ws.createSession(sessionId, {\n mounts: grants,\n ...(hide.length > 0 ? { permissions: { paths: { hide } } } : {}),\n })\n twin.commands = source.commands\n twin.hiddenVars = source.hiddenVars\n setCwd(twin, this.workdir)\n return sessionId\n }\n\n private async provisionSession(sessionId: string): Promise<void> {\n const ws = await this.workspace()\n await ws.ensureSessionsLoaded()\n if (ws.listSessions().some((s) => s.sessionId === sessionId)) return\n setCwd(ws.createSession(sessionId), this.workdir)\n }\n\n async run(spec: ShellExecSpec): Promise<ShellRunResult> {\n const sandbox = this.sandboxInfo(spec)\n // An already-aborted signal never fires its listener, so answer before\n // dispatch: the command must not run at all.\n if (spec.signal?.aborted === true) {\n return {\n exitCode: null,\n signal: 'SIGTERM',\n timedOut: false,\n aborted: true,\n timeoutMs: spec.timeoutMs,\n stdout: { text: '', truncated: false },\n stderr: { text: '', truncated: false },\n ...(sandbox !== undefined ? { sandbox } : {}),\n }\n }\n const controller = new AbortController()\n // The command streams into a console instead of returning its output\n // whole, because mirage answers an abort by throwing: what a killed\n // command already printed survives only in a sink.\n //\n // Retention is deliberately unbounded here, unlike the background\n // console: nothing drains this one, `collectFrom` reads it once the\n // command is over, and the store evicts whole chunks, so a budget\n // would silently drop an entire buffered stream larger than itself\n // and report empty output as untruncated. Holding the full stream\n // for the length of one call is what the executor did anyway before\n // a sink was attached, and it is what lets a truncated run spill\n // every byte rather than only the tail a budget kept.\n const console_ = new JobConsole(new RAMConsoleStore(null))\n let timedOut = false\n let aborted = false\n const timer = setTimeout(() => {\n timedOut = true\n controller.abort()\n }, spec.timeoutMs)\n const onAbort = (): void => {\n if (!timedOut && !aborted) {\n aborted = true\n controller.abort()\n }\n }\n spec.signal?.addEventListener('abort', onAbort, { once: true })\n try {\n await this.ensureSession()\n const ws = await this.workspace()\n const sessionId = await this.sessionFor(spec)\n const bound = this.sessionId !== undefined\n if (bound && sessionId !== undefined) await this.applyManagedEnv(ws, sessionId, spec)\n const workdir = await this.worldWorkdir(spec)\n const result = await ws.execute(\n spec.command,\n executeOptions(spec, workdir, controller.signal, sessionId, bound, this.workdir, console_),\n )\n const captured = await this.collectFrom(console_, spec)\n const settled = this.sandboxInfo(spec, this.wasDenied(spec, result, captured.stderr.text))\n return {\n exitCode: result.exitCode,\n signal: null,\n timedOut: false,\n aborted: false,\n timeoutMs: spec.timeoutMs,\n ...captured,\n ...(settled !== undefined ? { sandbox: settled } : {}),\n }\n } catch (err) {\n if (!controller.signal.aborted) throw err\n // The fused deadline was the first cause: report the kill as a\n // result, never a rejection, per the seam contract.\n return {\n exitCode: null,\n signal: 'SIGTERM',\n timedOut,\n aborted,\n timeoutMs: spec.timeoutMs,\n // Whatever the command printed before the kill landed. GNU's own\n // timeout keeps it, and a model that watched a build run for two\n // minutes is owed the log rather than an empty string.\n ...(await this.collectFrom(console_, spec)),\n ...(sandbox !== undefined ? { sandbox } : {}),\n }\n } finally {\n clearTimeout(timer)\n spec.signal?.removeEventListener('abort', onAbort)\n }\n }\n\n start(spec: ShellExecSpec): ShellProcess {\n // A background handle has no aggregated stderr to read a denial back\n // off, so its facts carry the mode without the verdict; a reader that\n // needs it sees the refusal in the streamed output.\n const sandbox = this.sandboxInfo(spec)\n const controller = new AbortController()\n // The console is the streaming conduit, holding what the follow loop\n // has not drained yet (nothing, when the loop keeps up). Its own\n // retention budget is what bounds that, since reading a chunk does\n // not release it.\n const console_ = new JobConsole(new RAMConsoleStore(this.retentionFor(spec)))\n const spill = this.newSpill()\n if (spec.signal?.aborted === true) {\n controller.abort()\n return new MirageShellProcess(\n Promise.reject(new Error('command aborted before start')),\n controller,\n console_,\n spec.stdoutMaxBytes,\n spill,\n sandbox,\n )\n }\n const onAbort = (): void => {\n controller.abort()\n }\n spec.signal?.addEventListener('abort', onAbort, { once: true })\n const run = this.ensureSession()\n .then(async () => {\n const sessionId = await this.sessionFor(spec)\n const ws = await this.workspace()\n const bound = this.sessionId !== undefined\n if (bound && sessionId !== undefined) await this.applyManagedEnv(ws, sessionId, spec)\n return { ws, sessionId, bound, workdir: await this.worldWorkdir(spec) }\n })\n .then(({ ws, sessionId, bound, workdir }) =>\n ws.execute(\n spec.command,\n executeOptions(\n spec,\n workdir,\n controller.signal,\n sessionId,\n bound,\n this.workdir,\n console_,\n ),\n ),\n )\n .finally(() => spec.signal?.removeEventListener('abort', onAbort))\n return new MirageShellProcess(run, controller, console_, spec.stdoutMaxBytes, spill, sandbox)\n }\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { Channel } from '@struktoai/mirage-core/shell/console/index'\n\n/** Where a spill sink writes: a workspace directory it can create and extend. */\nexport interface SpillTarget {\n ensureDir(dir: string): Promise<void>\n write(path: string, bytes: Uint8Array): Promise<void>\n append(path: string, bytes: Uint8Array): Promise<void>\n}\n\n/** The two directory facts `ensureDirPath` needs from a workspace. */\nexport interface DirMaker {\n exists(path: string): Promise<boolean>\n mkdir(path: string): Promise<void>\n}\n\n/**\n * Create `dir` and any missing ancestor, treating an existing one as\n * done.\n *\n * `mkdir` is one level and is not idempotent, so a probe alone is not\n * enough: two commands that first overrun at the same moment both see\n * the spill directory missing, one creates it and the other is refused\n * for a directory that is now exactly what it wanted. Losing that race\n * must not cost a command its spill, so the refusal is re-checked\n * against existence and only a directory that is still missing is a\n * real failure.\n *\n * @param dirs the workspace's `exists`/`mkdir`.\n * @param dir absolute workspace path to create.\n */\nexport async function ensureDirPath(dirs: DirMaker, dir: string): Promise<void> {\n let path = ''\n for (const part of dir.split('/').filter((p) => p !== '')) {\n path += `/${part}`\n if (await dirs.exists(path)) continue\n try {\n await dirs.mkdir(path)\n } catch (err) {\n if (!(await dirs.exists(path))) throw err\n }\n }\n}\n\nfunction totalLength(parts: Uint8Array[]): number {\n return parts.reduce((sum, p) => sum + p.byteLength, 0)\n}\n\nfunction concat(parts: Uint8Array[]): Uint8Array {\n const out = new Uint8Array(totalLength(parts))\n let at = 0\n for (const p of parts) {\n out.set(p, at)\n at += p.byteLength\n }\n return out\n}\n\n/**\n * Keeps the full, uncapped stdout and stderr of one command in workspace\n * files so a reader can recover output the delta budget dropped.\n *\n * Memory stays bounded: each channel buffers in memory only until the\n * delivered delta first overruns its budget, at which point `begin()`\n * flushes both buffers to their files and every later chunk appends\n * straight to the file. A write failure (no writable mount at the\n * configured path) disables the sink and leaves the paths undefined, the\n * honest \"no safe path available\" answer, so the process still streams,\n * just without a spill to point at.\n */\nexport class SpillSink {\n stdoutPath: string | undefined\n stderrPath: string | undefined\n\n private readonly target: SpillTarget\n private readonly dir: string\n private readonly base: string\n private readonly onFailure: (err: unknown) => void\n private started = false\n private failed = false\n private stdoutParts: Uint8Array[] = []\n private stderrParts: Uint8Array[] = []\n\n constructor(\n target: SpillTarget,\n dir: string,\n base: string,\n onFailure: (err: unknown) => void = () => undefined,\n ) {\n this.target = target\n this.dir = dir\n this.base = base\n this.onFailure = onFailure\n }\n\n /** Buffer a chunk before spill starts, or append it to the file after. */\n async ingest(channel: Channel, data: Uint8Array): Promise<void> {\n if (this.failed) return\n if (!this.started) {\n if (channel === Channel.STDERR) this.stderrParts.push(data)\n else this.stdoutParts.push(data)\n return\n }\n await this.appendFile(channel, data)\n }\n\n /** Flush the buffered streams to files; called once, on the first overrun. */\n async begin(): Promise<void> {\n if (this.started || this.failed) return\n try {\n await this.target.ensureDir(this.dir)\n if (totalLength(this.stdoutParts) > 0) {\n this.stdoutPath = `${this.dir}/${this.base}.stdout`\n await this.target.write(this.stdoutPath, concat(this.stdoutParts))\n }\n if (totalLength(this.stderrParts) > 0) {\n this.stderrPath = `${this.dir}/${this.base}.stderr`\n await this.target.write(this.stderrPath, concat(this.stderrParts))\n }\n this.started = true\n this.stdoutParts = []\n this.stderrParts = []\n } catch (err) {\n // Reported, never swallowed: a spill quietly vanishing reads later\n // as \"the harness lost my output\" with nothing to explain it.\n this.onFailure(err)\n this.disable()\n }\n }\n\n private async appendFile(channel: Channel, data: Uint8Array): Promise<void> {\n try {\n if (channel === Channel.STDERR) {\n if (this.stderrPath === undefined) {\n this.stderrPath = `${this.dir}/${this.base}.stderr`\n await this.target.write(this.stderrPath, data)\n } else {\n await this.target.append(this.stderrPath, data)\n }\n } else {\n if (this.stdoutPath === undefined) {\n this.stdoutPath = `${this.dir}/${this.base}.stdout`\n await this.target.write(this.stdoutPath, data)\n } else {\n await this.target.append(this.stdoutPath, data)\n }\n }\n } catch (err) {\n this.onFailure(err)\n this.disable()\n }\n }\n\n /**\n * Give up on spilling and report no path.\n *\n * Called on a write failure, and by a reader that discovers it lost\n * bytes before they ever reached here: a file missing the middle of\n * the stream is worse than no file, because the path presents it as\n * the whole one.\n */\n disable(): void {\n this.failed = true\n this.stdoutPath = undefined\n this.stderrPath = undefined\n this.stdoutParts = []\n this.stderrParts = []\n }\n}\n"],"mappings":";;;;;;AAeA,SAAS,gBAAgB,qBAAqB;AAW9C;AAAA,EACE,WAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,cAAc;AACvB,SAAS,mBAAmB;;;ACrB5B,SAAS,eAAe;AA8BxB,eAAsB,cAAc,MAAgB,KAA4B;AAC9E,MAAI,OAAO;AACX,aAAW,QAAQ,IAAI,MAAM,GAAG,EAAE,OAAO,CAAC,MAAM,MAAM,EAAE,GAAG;AACzD,YAAQ,IAAI,IAAI;AAChB,QAAI,MAAM,KAAK,OAAO,IAAI,EAAG;AAC7B,QAAI;AACF,YAAM,KAAK,MAAM,IAAI;AAAA,IACvB,SAAS,KAAK;AACZ,UAAI,CAAE,MAAM,KAAK,OAAO,IAAI,EAAI,OAAM;AAAA,IACxC;AAAA,EACF;AACF;AAEA,SAAS,YAAY,OAA6B;AAChD,SAAO,MAAM,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,YAAY,CAAC;AACvD;AAEA,SAAS,OAAO,OAAiC;AAC/C,QAAM,MAAM,IAAI,WAAW,YAAY,KAAK,CAAC;AAC7C,MAAI,KAAK;AACT,aAAW,KAAK,OAAO;AACrB,QAAI,IAAI,GAAG,EAAE;AACb,UAAM,EAAE;AAAA,EACV;AACA,SAAO;AACT;AAcO,IAAM,YAAN,MAAgB;AAAA,EACrB;AAAA,EACA;AAAA,EAEiB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT,UAAU;AAAA,EACV,SAAS;AAAA,EACT,cAA4B,CAAC;AAAA,EAC7B,cAA4B,CAAC;AAAA,EAErC,YACE,QACA,KACA,MACA,YAAoC,MAAM,QAC1C;AACA,SAAK,SAAS;AACd,SAAK,MAAM;AACX,SAAK,OAAO;AACZ,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA,EAGA,MAAM,OAAO,SAAkB,MAAiC;AAC9D,QAAI,KAAK,OAAQ;AACjB,QAAI,CAAC,KAAK,SAAS;AACjB,UAAI,YAAY,QAAQ,OAAQ,MAAK,YAAY,KAAK,IAAI;AAAA,UACrD,MAAK,YAAY,KAAK,IAAI;AAC/B;AAAA,IACF;AACA,UAAM,KAAK,WAAW,SAAS,IAAI;AAAA,EACrC;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,QAAI,KAAK,WAAW,KAAK,OAAQ;AACjC,QAAI;AACF,YAAM,KAAK,OAAO,UAAU,KAAK,GAAG;AACpC,UAAI,YAAY,KAAK,WAAW,IAAI,GAAG;AACrC,aAAK,aAAa,GAAG,KAAK,GAAG,IAAI,KAAK,IAAI;AAC1C,cAAM,KAAK,OAAO,MAAM,KAAK,YAAY,OAAO,KAAK,WAAW,CAAC;AAAA,MACnE;AACA,UAAI,YAAY,KAAK,WAAW,IAAI,GAAG;AACrC,aAAK,aAAa,GAAG,KAAK,GAAG,IAAI,KAAK,IAAI;AAC1C,cAAM,KAAK,OAAO,MAAM,KAAK,YAAY,OAAO,KAAK,WAAW,CAAC;AAAA,MACnE;AACA,WAAK,UAAU;AACf,WAAK,cAAc,CAAC;AACpB,WAAK,cAAc,CAAC;AAAA,IACtB,SAAS,KAAK;AAGZ,WAAK,UAAU,GAAG;AAClB,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAAA,EAEA,MAAc,WAAW,SAAkB,MAAiC;AAC1E,QAAI;AACF,UAAI,YAAY,QAAQ,QAAQ;AAC9B,YAAI,KAAK,eAAe,QAAW;AACjC,eAAK,aAAa,GAAG,KAAK,GAAG,IAAI,KAAK,IAAI;AAC1C,gBAAM,KAAK,OAAO,MAAM,KAAK,YAAY,IAAI;AAAA,QAC/C,OAAO;AACL,gBAAM,KAAK,OAAO,OAAO,KAAK,YAAY,IAAI;AAAA,QAChD;AAAA,MACF,OAAO;AACL,YAAI,KAAK,eAAe,QAAW;AACjC,eAAK,aAAa,GAAG,KAAK,GAAG,IAAI,KAAK,IAAI;AAC1C,gBAAM,KAAK,OAAO,MAAM,KAAK,YAAY,IAAI;AAAA,QAC/C,OAAO;AACL,gBAAM,KAAK,OAAO,OAAO,KAAK,YAAY,IAAI;AAAA,QAChD;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,UAAU,GAAG;AAClB,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,UAAgB;AACd,SAAK,SAAS;AACd,SAAK,aAAa;AAClB,SAAK,aAAa;AAClB,SAAK,cAAc,CAAC;AACpB,SAAK,cAAc,CAAC;AAAA,EACtB;AACF;;;ADvIA,IAAM,qBAAqB;AAC3B,IAAM,iBAAiB;AACvB,IAAM,2BAA2B;AACjC,IAAM,2BAA2B;AACjC,IAAM,gBAAgB,IAAI,YAAY,EAAE,OAAO,oBAAoB;AASnE,IAAM,2BAA2B;AAIjC,IAAI,eAAe;AAMnB,IAAM,cAAc;AASpB,IAAM,oBAAoB,CAAC,uBAAuB,uBAAuB,yBAAyB;AAoDlG,SAAS,QAAQ,MAAc,UAAmC;AAChE,QAAM,SAAS,QAAQ,MAAM,QAAQ;AACrC,SAAO,EAAE,MAAM,OAAO,MAAM,WAAW,OAAO,UAAU;AAC1D;AAEA,SAAS,eACP,MACA,SACA,QACA,WACA,OACA,iBACA,MACwC;AAOxC,QAAM,UAAW,KAAK,UAAiD,CAAC;AACxE,QAAM,MAAM,QAAQ,EAAE,GAAI,KAAK,OAAO,CAAC,EAAG,IAAI,EAAE,GAAI,KAAK,OAAO,CAAC,GAAI,GAAG,QAAQ;AAQhF,QAAM,MAAM,YAAY,KAAK,UAAU,QAAQ,SAAY;AAC3D,SAAO;AAAA,IACL;AAAA,IACA,GAAI,cAAc,SAAY,EAAE,UAAU,IAAI,CAAC;AAAA,IAC/C,GAAI,QAAQ,SAAY,EAAE,IAAI,IAAI,CAAC;AAAA,IACnC,GAAI,OAAO,KAAK,GAAG,EAAE,SAAS,IAAI,EAAE,IAAI,IAAI,CAAC;AAAA,IAC7C,GAAI,KAAK,UAAU,SAAY,EAAE,OAAO,IAAI,YAAY,EAAE,OAAO,KAAK,KAAK,EAAE,IAAI,CAAC;AAAA,IAClF,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,EACvC;AACF;AAmBA,IAAM,qBAAN,MAAiD;AAAA,EAC/C,SAA6B;AAAA,EAC7B,WAA0B;AAAA,EAC1B,SAAgC;AAAA,EAChC;AAAA,EACS;AAAA,EAEQ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,UAAU;AAAA,EACV,YAAY;AAAA,EAEpB,YACE,KACA,YACA,UACA,QACA,OACA,aACA;AACA,SAAK,aAAa;AAClB,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,UAAU,IAAI,WAAW,MAAM;AACpC,SAAK,QAAQ;AACb,SAAK,cAAc;AACnB,SAAK,WAAW,KAAK,QAAQ;AAC7B,SAAK,OAAO,IAAI;AAAA,MACd,CAAC,WAAW,KAAK,OAAO,QAAQ,IAAI;AAAA,MACpC,CAAC,QAAiB,KAAK,OAAO,MAAM,GAAG;AAAA,IACzC;AAAA,EACF;AAAA,EAEA,MAAc,UAAyB;AAGrC,qBAAiB,SAAS,KAAK,QAAQ,OAAO,CAAC,GAAG;AAChD,UAAI,MAAM,YAAYC,SAAQ,QAAS;AAMvC,UAAI,MAAM,QAAQ,KAAK,WAAW;AAChC,aAAK,QAAQ;AACb,aAAK,OAAO,QAAQ;AAAA,MACtB;AACA,WAAK,YAAY,MAAM,MAAM;AAC7B,YAAM,KAAK,YAAY,KAAK;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,MAAc,YAAY,OAAoC;AAI5D,QAAI,KAAK,UAAU,KAAM,OAAM,KAAK,MAAM,OAAO,MAAM,SAAS,MAAM,IAAI;AAG1E,QAAI,UAAU;AACd,QAAI,MAAM,YAAYA,SAAQ,QAAQ;AACpC,UAAI,CAAC,KAAK,UAAU;AAClB,kBAAU,KAAK,QAAQ,OAAO,aAAa;AAC3C,aAAK,WAAW;AAAA,MAClB;AAAA,IACF,OAAO;AACL,WAAK,WAAW;AAAA,IAClB;AAKA,cAAU,KAAK,QAAQ,OAAO,MAAM,IAAI,KAAK;AAC7C,QAAI,SAAS;AACX,WAAK,QAAQ;AAGb,UAAI,KAAK,UAAU,KAAM,OAAM,KAAK,MAAM,MAAM;AAAA,IAClD;AAAA,EACF;AAAA,EAEA,MAAc,OAAO,QAA8B,KAA6B;AAC9E,SAAK,UAAU;AACf,QAAI,KAAK,gBAAgB,OAAW,MAAK,UAAU,KAAK;AACxD,QAAI;AACJ,QAAI,WAAW,MAAM;AACnB,WAAK,SAAS;AACd,WAAK,WAAW,OAAO;AACvB,gBAAU,YAAY,OAAO,QAAQ;AAAA,IACvC,OAAO;AACL,WAAK,SAAS;AACd,WAAK,SAAS;AACd,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,YAAM,KAAK,QAAQ,KAAKA,SAAQ,QAAQ,IAAI,YAAY,EAAE,OAAO,OAAO,CAAC;AACzE,gBAAU;AAAA,IACZ;AAIA,UAAM,KAAK,QAAQ,OAAO,OAAO;AACjC,UAAM,KAAK;AAAA,EACb;AAAA,EAEA,aAA+B;AAC7B,UAAM,QAAQ,KAAK,QAAQ,KAAK;AAChC,UAAM,QAAQ,KAAK;AACnB,SAAK,QAAQ;AACb,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,GAAI,KAAK,OAAO,eAAe,SAAY,EAAE,iBAAiB,KAAK,MAAM,WAAW,IAAI,CAAC;AAAA,MACzF,GAAI,KAAK,OAAO,eAAe,SAAY,EAAE,iBAAiB,KAAK,MAAM,WAAW,IAAI,CAAC;AAAA,IAC3F;AAAA,EACF;AAAA,EAEA,OAAgB;AACd,QAAI,KAAK,QAAS,QAAO;AACzB,SAAK,WAAW,MAAM;AACtB,WAAO;AAAA,EACT;AACF;AAgBO,IAAM,sBAAN,cAAkC,cAAc;AAAA,EACrD,OAAgB,SAAS,CAAC,QAAQ;AAAA,EAEjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT,eAAqC;AAAA,EACrC,gBAAwC;AAAA,EAEhD,YAAY,KAAc,SAA4B,CAAC,GAAG;AACxD,UAAM,GAAG;AACT,SAAK,UAAU,OAAO,WAAW;AACjC,SAAK,mBAAmB,OAAO,oBAAoB;AACnD,SAAK,eAAe,OAAO,gBAAgB;AAC3C,SAAK,iBAAiB,OAAO,kBAAkB;AAC/C,SAAK,iBAAiB,OAAO,kBAAkB;AAC/C,SAAK,YAAY,OAAO;AACxB,SAAK,WAAW,OAAO;AAAA,EACzB;AAAA;AAAA;AAAA,EAIQ,YAAgC;AACtC,WAAO,KAAK,IAAI,OAAO;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,MAAc,aAAa,MAAsC;AAC/D,QAAI,KAAK,YAAY,GAAI,QAAO;AAChC,UAAM,KAAK,MAAM,KAAK,UAAU;AAChC,QAAI,MAAM,GAAG,GAAG,MAAM,KAAK,OAAO,EAAG,QAAO,KAAK;AACjD,WAAO,KAAK,cAAc,SAAY,KAAK,UAAU;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA,EAKQ,WAA6B;AACnC,UAAM,MAAM,KAAK;AACjB,QAAI,QAAQ,OAAW,QAAO;AAC9B,UAAM,SAAsB;AAAA,MAC1B,WAAW,OAAO,MAAM;AACtB,cAAM,KAAK,MAAM,KAAK,UAAU;AAChC,cAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,GAAG,CAAC;AAAA,MACzF;AAAA,MACA,OAAO,OAAO,GAAG,UAAU;AACzB,cAAM,KAAK,MAAM,KAAK,UAAU;AAChC,cAAM,GAAG,GAAG,UAAU,GAAG,KAAK;AAAA,MAChC;AAAA,MACA,QAAQ,OAAO,GAAG,UAAU;AAC1B,cAAM,KAAK,MAAM,KAAK,UAAU;AAChC,cAAM,GAAG,GAAG,OAAO,GAAG,KAAK;AAAA,MAC7B;AAAA,IACF;AACA,oBAAgB;AAChB,UAAM,MAAM,KAAK,IAAI,OAAO,YAAY;AACxC,WAAO,IAAI,UAAU,QAAQ,KAAK,gBAAgB,aAAa,SAAS,CAAC,IAAI,CAAC,QAAiB;AAC7F,UAAI,MAAM,0DAA0D,KAAK,GAAG;AAAA,IAC9E,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,IAAa,cAA4C;AACvD,WAAO,KAAK,IAAI,OAAO,UAAU,oBAAoB;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,QAAQ,MAAmD;AACjE,UAAM,WAAW,KAAK;AACtB,QAAI,aAAa,OAAW,QAAO;AACnC,WAAO,KAAK,eAAe,QAAQ;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAc,WAAW,MAAkD;AACzE,QAAI,KAAK,QAAQ,IAAI,MAAM,YAAa,QAAO,KAAK;AACpD,WAAO,KAAK,gBAAgB;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBQ,YAAY,MAAqB,SAAS,OAAqC;AACrF,UAAM,OAAO,KAAK,QAAQ,IAAI;AAC9B,QAAI,SAAS,OAAW,QAAO;AAC/B,WAAO,EAAE,MAAM,QAAQ,aAAa,QAAQ,cAAc,MAAM;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcQ,aAAa,MAA6B;AAChD,WAAO,KAAK,IAAI,KAAK,gBAAgB,KAAK,cAAc,IAAI;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,MAAc,YACZ,UACA,MAC+D;AAC/D,UAAM,UAAU,IAAI,YAAY;AAChC,UAAM,WAAW,MAAM,SAAS,SAASA,SAAQ,MAAM;AACvD,UAAM,WAAW,MAAM,SAAS,SAASA,SAAQ,MAAM;AACvD,UAAM,SAAS,QAAQ,QAAQ,OAAO,QAAQ,GAAG,KAAK,cAAc;AACpE,UAAM,SAAS,QAAQ,QAAQ,OAAO,QAAQ,GAAG,KAAK,cAAc;AACpE,QAAI,CAAC,OAAO,aAAa,CAAC,OAAO,UAAW,QAAO,EAAE,QAAQ,OAAO;AACpE,UAAM,QAAQ,KAAK,SAAS;AAC5B,QAAI,UAAU,KAAM,QAAO,EAAE,QAAQ,OAAO;AAC5C,QAAI,OAAO,UAAW,OAAM,MAAM,OAAOA,SAAQ,QAAQ,QAAQ;AACjE,QAAI,OAAO,UAAW,OAAM,MAAM,OAAOA,SAAQ,QAAQ,QAAQ;AACjE,UAAM,MAAM,MAAM;AAClB,WAAO;AAAA,MACL,QAAQ;AAAA,QACN,GAAG;AAAA,QACH,GAAI,MAAM,eAAe,SAAY,EAAE,WAAW,MAAM,WAAW,IAAI,CAAC;AAAA,MAC1E;AAAA,MACA,QAAQ;AAAA,QACN,GAAG;AAAA,QACH,GAAI,MAAM,eAAe,SAAY,EAAE,WAAW,MAAM,WAAW,IAAI,CAAC;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBQ,UACN,MACA,QACA,QACS;AACT,QAAI,OAAO,YAAY,KAAM,QAAO;AACpC,QAAI,KAAK,QAAQ,IAAI,MAAM,YAAa,QAAO;AAC/C,WAAO,kBAAkB,KAAK,CAAC,cAAc,OAAO,SAAS,SAAS,CAAC;AAAA,EACzE;AAAA,EAEA,QAAQ,SAA0C;AAIhD,UAAM,UAAU,QAAQ,YAAY,KAAK,cAAc,SAAY,KAAK,UAAU;AAClF,WAAO;AAAA,MACL,SAAS,QAAQ;AAAA,MACjB;AAAA,MACA,WAAW,KAAK,IAAI,QAAQ,aAAa,KAAK,kBAAkB,KAAK,YAAY;AAAA,MACjF,gBAAgB,QAAQ,kBAAkB,KAAK;AAAA,MAC/C,QAAQ,QAAQ;AAAA,MAChB,OAAO,QAAQ;AAAA,MACf,KAAK,QAAQ;AAAA,MACb,QAAQ,QAAQ;AAAA,MAChB,eAAe,QAAQ;AAAA,IACzB;AAAA,EACF;AAAA,EAEQ,gBAA+B;AACrC,QAAI,KAAK,cAAc,OAAW,QAAO,QAAQ,QAAQ;AACzD,SAAK,iBAAiB,KAAK,iBAAiB,KAAK,SAAS,EAAE,MAAM,CAAC,QAAiB;AAClF,WAAK,eAAe;AACpB,YAAM;AAAA,IACR,CAAC;AACD,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,MAAc,gBACZ,IACA,WACA,MACe;AACf,UAAM,UAAU,KAAK;AACrB,QAAI,YAAY,OAAW;AAC3B,UAAM,UAAU,GAAG,WAAW,SAAS;AACvC,UAAM,OAAO,YAAY,OAAO;AAChC,eAAW,OAAO,OAAO,KAAK,QAAQ,GAAG,GAAG;AAC1C,UAAI,IAAI,WAAW,cAAc,KAAK,EAAE,OAAO,SAAU,OAAM,KAAK,MAAM,GAAG;AAAA,IAC/E;AACA,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,EAAG,OAAM,KAAK,IAAI,KAAK,KAAK;AAAA,EAC/E;AAAA,EAEQ,kBAAmC;AACzC,SAAK,kBAAkB,KAAK,kBAAkB,EAAE,MAAM,CAAC,QAAiB;AACtE,WAAK,gBAAgB;AACrB,YAAM;AAAA,IACR,CAAC;AACD,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiCA,MAAc,oBAAqC;AACjD,UAAM,KAAK,MAAM,KAAK,UAAU;AAChC,UAAM,YAAY,GAAG,KAAK,aAAa,YAAY;AACnD,UAAM,GAAG,qBAAqB;AAC9B,QAAI,GAAG,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,cAAc,SAAS,EAAG,QAAO;AACrE,UAAM,SAAS,GAAG,WAAW,KAAK,aAAa,GAAG,gBAAgB;AAClE,UAAM,SAAiC,CAAC;AACxC,eAAW,SAAS,GAAG,OAAO,GAAG;AAC/B,aAAO,MAAM,MAAM,IAAI,MAAM,OAAO,QAAQ,QAAQ,EAAE,MAAM,cAAc,SAAS;AAAA,IACrF;AACA,UAAM,OAAO,CAAC,GAAI,OAAO,aAAa,SAAS,CAAC,GAAI,GAAI,OAAO,aAAa,YAAY,CAAC,CAAE;AAC3F,UAAM,OAAO,GAAG,cAAc,WAAW;AAAA,MACvC,QAAQ;AAAA,MACR,GAAI,KAAK,SAAS,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC;AAAA,IAChE,CAAC;AACD,SAAK,WAAW,OAAO;AACvB,SAAK,aAAa,OAAO;AACzB,WAAO,MAAM,KAAK,OAAO;AACzB,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,iBAAiB,WAAkC;AAC/D,UAAM,KAAK,MAAM,KAAK,UAAU;AAChC,UAAM,GAAG,qBAAqB;AAC9B,QAAI,GAAG,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,cAAc,SAAS,EAAG;AAC9D,WAAO,GAAG,cAAc,SAAS,GAAG,KAAK,OAAO;AAAA,EAClD;AAAA,EAEA,MAAM,IAAI,MAA8C;AACtD,UAAM,UAAU,KAAK,YAAY,IAAI;AAGrC,QAAI,KAAK,QAAQ,YAAY,MAAM;AACjC,aAAO;AAAA,QACL,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,SAAS;AAAA,QACT,WAAW,KAAK;AAAA,QAChB,QAAQ,EAAE,MAAM,IAAI,WAAW,MAAM;AAAA,QACrC,QAAQ,EAAE,MAAM,IAAI,WAAW,MAAM;AAAA,QACrC,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC7C;AAAA,IACF;AACA,UAAM,aAAa,IAAI,gBAAgB;AAavC,UAAM,WAAW,IAAI,WAAW,IAAI,gBAAgB,IAAI,CAAC;AACzD,QAAI,WAAW;AACf,QAAI,UAAU;AACd,UAAM,QAAQ,WAAW,MAAM;AAC7B,iBAAW;AACX,iBAAW,MAAM;AAAA,IACnB,GAAG,KAAK,SAAS;AACjB,UAAM,UAAU,MAAY;AAC1B,UAAI,CAAC,YAAY,CAAC,SAAS;AACzB,kBAAU;AACV,mBAAW,MAAM;AAAA,MACnB;AAAA,IACF;AACA,SAAK,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAC9D,QAAI;AACF,YAAM,KAAK,cAAc;AACzB,YAAM,KAAK,MAAM,KAAK,UAAU;AAChC,YAAM,YAAY,MAAM,KAAK,WAAW,IAAI;AAC5C,YAAM,QAAQ,KAAK,cAAc;AACjC,UAAI,SAAS,cAAc,OAAW,OAAM,KAAK,gBAAgB,IAAI,WAAW,IAAI;AACpF,YAAM,UAAU,MAAM,KAAK,aAAa,IAAI;AAC5C,YAAM,SAAS,MAAM,GAAG;AAAA,QACtB,KAAK;AAAA,QACL,eAAe,MAAM,SAAS,WAAW,QAAQ,WAAW,OAAO,KAAK,SAAS,QAAQ;AAAA,MAC3F;AACA,YAAM,WAAW,MAAM,KAAK,YAAY,UAAU,IAAI;AACtD,YAAM,UAAU,KAAK,YAAY,MAAM,KAAK,UAAU,MAAM,QAAQ,SAAS,OAAO,IAAI,CAAC;AACzF,aAAO;AAAA,QACL,UAAU,OAAO;AAAA,QACjB,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,SAAS;AAAA,QACT,WAAW,KAAK;AAAA,QAChB,GAAG;AAAA,QACH,GAAI,YAAY,SAAY,EAAE,SAAS,QAAQ,IAAI,CAAC;AAAA,MACtD;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,CAAC,WAAW,OAAO,QAAS,OAAM;AAGtC,aAAO;AAAA,QACL,UAAU;AAAA,QACV,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA,WAAW,KAAK;AAAA;AAAA;AAAA;AAAA,QAIhB,GAAI,MAAM,KAAK,YAAY,UAAU,IAAI;AAAA,QACzC,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC7C;AAAA,IACF,UAAE;AACA,mBAAa,KAAK;AAClB,WAAK,QAAQ,oBAAoB,SAAS,OAAO;AAAA,IACnD;AAAA,EACF;AAAA,EAEA,MAAM,MAAmC;AAIvC,UAAM,UAAU,KAAK,YAAY,IAAI;AACrC,UAAM,aAAa,IAAI,gBAAgB;AAKvC,UAAM,WAAW,IAAI,WAAW,IAAI,gBAAgB,KAAK,aAAa,IAAI,CAAC,CAAC;AAC5E,UAAM,QAAQ,KAAK,SAAS;AAC5B,QAAI,KAAK,QAAQ,YAAY,MAAM;AACjC,iBAAW,MAAM;AACjB,aAAO,IAAI;AAAA,QACT,QAAQ,OAAO,IAAI,MAAM,8BAA8B,CAAC;AAAA,QACxD;AAAA,QACA;AAAA,QACA,KAAK;AAAA,QACL;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,UAAM,UAAU,MAAY;AAC1B,iBAAW,MAAM;AAAA,IACnB;AACA,SAAK,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAC9D,UAAM,MAAM,KAAK,cAAc,EAC5B,KAAK,YAAY;AAChB,YAAM,YAAY,MAAM,KAAK,WAAW,IAAI;AAC5C,YAAM,KAAK,MAAM,KAAK,UAAU;AAChC,YAAM,QAAQ,KAAK,cAAc;AACjC,UAAI,SAAS,cAAc,OAAW,OAAM,KAAK,gBAAgB,IAAI,WAAW,IAAI;AACpF,aAAO,EAAE,IAAI,WAAW,OAAO,SAAS,MAAM,KAAK,aAAa,IAAI,EAAE;AAAA,IACxE,CAAC,EACA;AAAA,MAAK,CAAC,EAAE,IAAI,WAAW,OAAO,QAAQ,MACrC,GAAG;AAAA,QACD,KAAK;AAAA,QACL;AAAA,UACE;AAAA,UACA;AAAA,UACA,WAAW;AAAA,UACX;AAAA,UACA;AAAA,UACA,KAAK;AAAA,UACL;AAAA,QACF;AAAA,MACF;AAAA,IACF,EACC,QAAQ,MAAM,KAAK,QAAQ,oBAAoB,SAAS,OAAO,CAAC;AACnE,WAAO,IAAI,mBAAmB,KAAK,YAAY,UAAU,KAAK,gBAAgB,OAAO,OAAO;AAAA,EAC9F;AACF;","names":["Channel","Channel"]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { FileSystem, FsTarget, FsInfo, FsPathInfo, FsDirEntry, FsWriteIntent, FsWriteOutcome, FsEditRequest, FsVersion, FsEditOutcome } from '@deepseek-ai/dsh-fs';
|
|
3
|
+
|
|
4
|
+
type SandboxPolicy = Parameters<FileSystem['writeText']>[4];
|
|
5
|
+
/** Configuration for the mirage filesystem backend. */
|
|
6
|
+
interface MirageFsConfig {
|
|
7
|
+
/** Virtual base directory for relative paths. Defaults to `/`. */
|
|
8
|
+
cwd?: string;
|
|
9
|
+
/** Exclusive byte limit on each overwrite-diff side. Defaults to 10 MiB. */
|
|
10
|
+
diffBasisMaxBytes?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Mirage-backed implementation of `ctx.fs`. Targets are canonical virtual
|
|
14
|
+
* paths (namespace symlinks followed), every operation walks the workspace
|
|
15
|
+
* op door — session grants, admission policies, cache read-through and
|
|
16
|
+
* post-write invalidation all fire exactly as they do for a shell command —
|
|
17
|
+
* and `processPath` answers in the same virtual path space the mirage shell
|
|
18
|
+
* executes in, so the two providers share one execution world.
|
|
19
|
+
*
|
|
20
|
+
* One limit worth stating: mirage's op facade takes no `AbortSignal`, so
|
|
21
|
+
* cancellation is honored at this adapter's own boundaries (before a
|
|
22
|
+
* dispatch, between listing entries) and not inside a single op. A long
|
|
23
|
+
* read from a remote backend therefore runs to completion after the
|
|
24
|
+
* signal fires, and the caller learns of the abort when it returns.
|
|
25
|
+
*/
|
|
26
|
+
declare class MirageFileSystem extends FileSystem {
|
|
27
|
+
static readonly inject: string[];
|
|
28
|
+
private fsOps;
|
|
29
|
+
private readonly cwd;
|
|
30
|
+
private readonly diffBasisMaxBytes;
|
|
31
|
+
private readonly locks;
|
|
32
|
+
constructor(ctx: Context, config?: MirageFsConfig);
|
|
33
|
+
private ops;
|
|
34
|
+
private get links();
|
|
35
|
+
/**
|
|
36
|
+
* The same confinement claim `MirageShellExecutor` makes, off the same
|
|
37
|
+
* fact and for the same reason: with every runtime reaching only the
|
|
38
|
+
* vfs, a mutation cannot land anywhere but a mount, under its mode.
|
|
39
|
+
*
|
|
40
|
+
* The two seams sit over one world, so answering differently here
|
|
41
|
+
* would let dsh fence a bash write and wave an identical `ctx.fs`
|
|
42
|
+
* write straight through.
|
|
43
|
+
*/
|
|
44
|
+
get sandboxMode(): FileSystem['sandboxMode'];
|
|
45
|
+
/**
|
|
46
|
+
* Refuse a mutation the call's sandbox policy does not allow.
|
|
47
|
+
*
|
|
48
|
+
* `workspaceRoot` is deliberately not consulted: it is a directory on
|
|
49
|
+
* the harness's own machine, so containment against it says nothing
|
|
50
|
+
* about this world. The mounts and their modes are the boundary, and
|
|
51
|
+
* `read-only` is the one mode that narrows them further. Wording and
|
|
52
|
+
* code mirror `dsh-fs-sandbox`, so the tool layer renders one denial
|
|
53
|
+
* marker whichever backend refused.
|
|
54
|
+
*
|
|
55
|
+
* @param policy the per-call policy, absent for an unguarded mutation.
|
|
56
|
+
* @param displayPath the path to name in the refusal.
|
|
57
|
+
*/
|
|
58
|
+
private assertMutable;
|
|
59
|
+
/**
|
|
60
|
+
* The base a relative path resolves against.
|
|
61
|
+
*
|
|
62
|
+
* dsh hands `ctx.fs` either the calling session's cwd or the sandbox
|
|
63
|
+
* policy's workspace root, and both are directories on the harness's
|
|
64
|
+
* own machine that name nothing here. Resolving `notes.txt` against
|
|
65
|
+
* one yields `/Users/.../notes.txt`, which every read then reports as
|
|
66
|
+
* absent. So a base that is not a directory in this world falls back
|
|
67
|
+
* to the configured one, the same rule the shell executor applies to
|
|
68
|
+
* a workdir.
|
|
69
|
+
*
|
|
70
|
+
* An absolute path ignores its base, so it never pays for the probe.
|
|
71
|
+
*
|
|
72
|
+
* @param path the path being resolved.
|
|
73
|
+
* @param cwd the caller's base, if any.
|
|
74
|
+
* @returns the base to resolve against.
|
|
75
|
+
*/
|
|
76
|
+
private resolveBase;
|
|
77
|
+
private normalize;
|
|
78
|
+
private follow;
|
|
79
|
+
private withLock;
|
|
80
|
+
resolve(path: string, opts?: {
|
|
81
|
+
cwd?: string;
|
|
82
|
+
signal?: AbortSignal;
|
|
83
|
+
}): Promise<FsTarget>;
|
|
84
|
+
processPath(target: FsTarget): string;
|
|
85
|
+
fileUrl(target: FsTarget): string;
|
|
86
|
+
contains(parent: FsTarget, child: FsTarget): boolean;
|
|
87
|
+
stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>;
|
|
88
|
+
lstat(path: string, opts?: {
|
|
89
|
+
cwd?: string;
|
|
90
|
+
}, signal?: AbortSignal): Promise<FsPathInfo | undefined>;
|
|
91
|
+
readText(target: FsTarget, signal?: AbortSignal): Promise<string>;
|
|
92
|
+
streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>;
|
|
93
|
+
readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array>;
|
|
94
|
+
listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>;
|
|
95
|
+
private dirEntry;
|
|
96
|
+
writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxPolicy): Promise<FsWriteOutcome>;
|
|
97
|
+
private diffBasis;
|
|
98
|
+
private versionAfterWrite;
|
|
99
|
+
editText(target: FsTarget, edit: FsEditRequest, expected?: {
|
|
100
|
+
version: FsVersion;
|
|
101
|
+
}, signal?: AbortSignal, sandboxPolicy?: SandboxPolicy): Promise<FsEditOutcome>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { MirageFileSystem as M, type MirageFsConfig as a };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,140 @@
|
|
|
1
|
-
export { M as MirageConfig, a as MirageMount, b as MirageMountBlock, c as MirageRuntimeBlock, d as MirageService } from './service-
|
|
2
|
-
export { M as MirageFileSystem, a as MirageFsConfig } from './fs-
|
|
3
|
-
export { M as MirageShellConfig, a as MirageShellExecutor } from './shell-
|
|
4
|
-
import '@deepseek-ai/cordis';
|
|
5
|
-
import '@struktoai/mirage-core';
|
|
1
|
+
export { M as MirageConfig, a as MirageMount, b as MirageMountBlock, c as MirageRuntimeBlock, d as MirageService } from './service-BK6NR_RX.js';
|
|
2
|
+
export { M as MirageFileSystem, a as MirageFsConfig } from './fs-B3Zi9oo6.js';
|
|
3
|
+
export { M as MirageShellConfig, a as MirageShellExecutor } from './shell-BTf2wsDy.js';
|
|
4
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
5
|
+
import { AskHandler } from '@struktoai/mirage-core/policy/decisions';
|
|
6
|
+
import { Decision } from '@struktoai/mirage-core/policy/types';
|
|
7
|
+
import '@struktoai/mirage-core/workspace/workspace/workspace';
|
|
6
8
|
import '@struktoai/mirage-node';
|
|
7
9
|
import '@deepseek-ai/dsh-fs';
|
|
8
10
|
import '@deepseek-ai/dsh-shell';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The tool an approval request is attributed to. A mirage ask is raised
|
|
14
|
+
* by the command plane, which is what dsh's bash tool called, so the
|
|
15
|
+
* audit trail names that tool rather than the head word of the line
|
|
16
|
+
* (which the request's own reason already carries).
|
|
17
|
+
*/
|
|
18
|
+
declare const APPROVAL_TOOL_NAME = "bash";
|
|
19
|
+
/**
|
|
20
|
+
* The outcomes dsh's approval channel answers with.
|
|
21
|
+
*
|
|
22
|
+
* Declared here rather than imported: the type lives in
|
|
23
|
+
* `@deepseek-ai/dsh-sandbox`, which this package does not depend on, and
|
|
24
|
+
* dsh itself treats the channel structurally for the same reason (its
|
|
25
|
+
* `EscalationApprover` is "a minimal STRUCTURAL function shape, not the
|
|
26
|
+
* approval service type"). Staying structural is what lets any approver
|
|
27
|
+
* be wired, dsh's own included.
|
|
28
|
+
*/
|
|
29
|
+
type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
30
|
+
/**
|
|
31
|
+
* The one question this bridge asks of dsh: `ctx.approval` narrowed to
|
|
32
|
+
* the call it makes. An `ApprovalService` satisfies it structurally,
|
|
33
|
+
* without this package importing the approval or agent packages.
|
|
34
|
+
*/
|
|
35
|
+
interface Approver {
|
|
36
|
+
request(req: {
|
|
37
|
+
agent: object | undefined;
|
|
38
|
+
toolName: string;
|
|
39
|
+
callId: string;
|
|
40
|
+
reason: string;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}): Promise<ApprovalOutcome>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* dsh's approval channel, or null when the composition has none.
|
|
46
|
+
*
|
|
47
|
+
* Read through `ctx.get`, not as `ctx.approval`. A plain property read
|
|
48
|
+
* of a service this plugin does not `inject` does not answer undefined,
|
|
49
|
+
* it throws (`cannot get property "approval" without inject`), so
|
|
50
|
+
* probing that way would turn every ask in a composition without an
|
|
51
|
+
* approver into a thrown error instead of a pending one. `ctx.get` is
|
|
52
|
+
* cordis's documented read "without the inject requirement", and its
|
|
53
|
+
* default strictness answers only a provider whose fiber is live, so an
|
|
54
|
+
* unloaded approval plugin reads as absent rather than as a stale
|
|
55
|
+
* channel nobody is listening on.
|
|
56
|
+
*
|
|
57
|
+
* Declaring `inject` instead would be wrong in the other direction: it
|
|
58
|
+
* would make an approval channel a load requirement, and a headless
|
|
59
|
+
* composition that legitimately has none would fail to compose at all.
|
|
60
|
+
*
|
|
61
|
+
* Read at ask time rather than captured when the plugin loads, because
|
|
62
|
+
* cordis composes plugins in whatever order the profile lists them and a
|
|
63
|
+
* snapshot taken at load would miss an approver registered after this.
|
|
64
|
+
*
|
|
65
|
+
* @param ctx the cordis context to read.
|
|
66
|
+
* @returns the approver, or null when nothing provides one.
|
|
67
|
+
*/
|
|
68
|
+
declare function approverOf(ctx: Context): Approver | null;
|
|
69
|
+
/**
|
|
70
|
+
* What the human is shown: the rule's reason, and the line it is about.
|
|
71
|
+
*
|
|
72
|
+
* The reason alone is the operator's sentence ("deletes are reviewed")
|
|
73
|
+
* and names nothing being deleted, so the line is quoted beside it. The
|
|
74
|
+
* words come off the record rather than being re-rendered, so what was
|
|
75
|
+
* approved is what was asked.
|
|
76
|
+
*
|
|
77
|
+
* Each word is rendered as a shell would read it back, because this
|
|
78
|
+
* string is the whole of what the human authorizes and the words are
|
|
79
|
+
* already-expanded values, not source. Joined raw, `rm -- 'quarterly
|
|
80
|
+
* report'` reads as two operands and a newline inside a name forges a
|
|
81
|
+
* line break in the prompt, so the human would be nodding at something
|
|
82
|
+
* other than the run. `shellQuote` is GNU's diagnostic rendering: an
|
|
83
|
+
* ordinary name stays bare, and only a word a shell would read as
|
|
84
|
+
* something else is dressed.
|
|
85
|
+
*
|
|
86
|
+
* @param record the ledger record being asked about.
|
|
87
|
+
* @returns the prompt text.
|
|
88
|
+
*/
|
|
89
|
+
declare function approvalReason(record: Decision): string;
|
|
90
|
+
/**
|
|
91
|
+
* Bridge mirage's ask to dsh's approval channel: the handler that fills
|
|
92
|
+
* `WorkspaceOptions.onAsk`.
|
|
93
|
+
*
|
|
94
|
+
* Mirage owns every outcome (`policy/decisions.ts`); this only decides
|
|
95
|
+
* which of its branches an approval answer lands on. Every answer from a
|
|
96
|
+
* live channel is one of mirage's two settled verbs, because
|
|
97
|
+
* `request` blocks until the channel answers and so never reports "no
|
|
98
|
+
* answer yet":
|
|
99
|
+
*
|
|
100
|
+
* * `allowed-once` is dsh's only yes and grants exactly the line that
|
|
101
|
+
* asked, so it maps to ALLOW at ONCE and never SESSION. Widening one
|
|
102
|
+
* nod into a standing grant is the one thing the human did not
|
|
103
|
+
* consent to; a host that wants SESSION answers writes them to the
|
|
104
|
+
* ledger itself (`ws.decisions.answer`), the surface built for it.
|
|
105
|
+
* * `rejected` is an explicit refusal.
|
|
106
|
+
* * `cancelled` is a refusal too: the human was shown the prompt and
|
|
107
|
+
* dismissed it, which is an answer rather than the absence of one.
|
|
108
|
+
* Leaving it pending would re-prompt them, since a retry reuses the
|
|
109
|
+
* waiting record and would call `request` again.
|
|
110
|
+
* * `unavailable` is a permission check that could not run, so it
|
|
111
|
+
* fails closed. Pending here would be fail-open-into-retry.
|
|
112
|
+
*
|
|
113
|
+
* The three refusals match what dsh does with the same vocabulary for
|
|
114
|
+
* its own escalations, where `approveEscalation` throws on a rejection,
|
|
115
|
+
* a cancellation and an unanswerable ask alike.
|
|
116
|
+
*
|
|
117
|
+
* The run's own signal rides along, so a prompt raised for a line whose
|
|
118
|
+
* run is then killed or times out is dismissed with it rather than left
|
|
119
|
+
* on somebody's screen for a command that no longer exists. The ledger
|
|
120
|
+
* stops waiting either way — it bounds the wait by the same signal — but
|
|
121
|
+
* only the channel can take its prompt down.
|
|
122
|
+
*
|
|
123
|
+
* With no approver on the context the handler answers null, which is
|
|
124
|
+
* mirage's "nobody has answered yet": the ask stays pending in the
|
|
125
|
+
* ledger. That is deliberate, and it is why the service installs this
|
|
126
|
+
* unconditionally rather than only under a composed approver — the two
|
|
127
|
+
* are the same answer to mirage (`onAsk` unset and a null return both
|
|
128
|
+
* render pending), and installing always is what lets an approver
|
|
129
|
+
* composed after this plugin still be found.
|
|
130
|
+
*
|
|
131
|
+
* Refusing instead would rewrite an operator's `ask` rule as a `deny`,
|
|
132
|
+
* and the embedder may well be answering `ws.decisions.pending()` out
|
|
133
|
+
* of band.
|
|
134
|
+
*
|
|
135
|
+
* @param ctx the cordis context carrying `ctx.approval`.
|
|
136
|
+
* @returns the handler to pass as `onAsk`.
|
|
137
|
+
*/
|
|
138
|
+
declare function askThroughApproval(ctx: Context): AskHandler;
|
|
139
|
+
|
|
140
|
+
export { APPROVAL_TOOL_NAME, type ApprovalOutcome, type Approver, approvalReason, approverOf, askThroughApproval };
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MirageFileSystem
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-M4JOF35U.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
APPROVAL_TOOL_NAME,
|
|
6
|
+
MirageService,
|
|
7
|
+
approvalReason,
|
|
8
|
+
approverOf,
|
|
9
|
+
askThroughApproval
|
|
10
|
+
} from "./chunk-4CZV24HW.js";
|
|
7
11
|
import {
|
|
8
12
|
MirageShellExecutor
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
13
|
+
} from "./chunk-QTBMLMKT.js";
|
|
14
|
+
import "./chunk-2F2PJ7D7.js";
|
|
11
15
|
export {
|
|
16
|
+
APPROVAL_TOOL_NAME,
|
|
12
17
|
MirageFileSystem,
|
|
13
18
|
MirageService,
|
|
14
|
-
MirageShellExecutor
|
|
19
|
+
MirageShellExecutor,
|
|
20
|
+
approvalReason,
|
|
21
|
+
approverOf,
|
|
22
|
+
askThroughApproval
|
|
15
23
|
};
|
|
16
24
|
//# sourceMappingURL=index.js.map
|
package/dist/plugin/fs.d.ts
CHANGED
package/dist/plugin/fs.js
CHANGED
package/dist/plugin/service.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { d as default } from '../service-
|
|
1
|
+
export { d as default } from '../service-BK6NR_RX.js';
|
|
2
2
|
import '@deepseek-ai/cordis';
|
|
3
|
-
import '@struktoai/mirage-core';
|
|
3
|
+
import '@struktoai/mirage-core/policy/types';
|
|
4
|
+
import '@struktoai/mirage-core/workspace/workspace/workspace';
|
|
4
5
|
import '@struktoai/mirage-node';
|
package/dist/plugin/service.js
CHANGED
package/dist/plugin/shell.d.ts
CHANGED
package/dist/plugin/shell.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Service, Context } from '@deepseek-ai/cordis';
|
|
2
|
-
import {
|
|
2
|
+
import { Explanation } from '@struktoai/mirage-core/policy/types';
|
|
3
|
+
import { MountSpec } from '@struktoai/mirage-core/workspace/workspace/workspace';
|
|
3
4
|
import { Workspace, Mount, NodeWorkspaceOptions } from '@struktoai/mirage-node';
|
|
4
5
|
|
|
5
6
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -48,6 +49,27 @@ interface MirageConfig {
|
|
|
48
49
|
* the other, not both.
|
|
49
50
|
*/
|
|
50
51
|
runtimes?: (string | MirageRuntimeBlock)[];
|
|
52
|
+
/**
|
|
53
|
+
* The roles (`profiles:` in mirage config), each a whole permission
|
|
54
|
+
* document: its `commands.allow` list, its `commands.ask` and
|
|
55
|
+
* `commands.deny` rules, the mount sections carrying rules of their
|
|
56
|
+
* own, and the paths and vars it hides. The YAML-friendly twin of
|
|
57
|
+
* `workspaceOptions.profiles`; pass one or the other, not both.
|
|
58
|
+
*
|
|
59
|
+
* Written as the document, not as an already-parsed profile: every
|
|
60
|
+
* entry goes through `parseSessionProfile`, so a misspelled field is a
|
|
61
|
+
* load error rather than a first-session one. That parse is what lets
|
|
62
|
+
* a dsh bundle patch carry a policy at all, since
|
|
63
|
+
* `workspaceOptions.profiles` wants profiles already parsed and a
|
|
64
|
+
* patch file holds plain YAML.
|
|
65
|
+
*/
|
|
66
|
+
profiles?: Record<string, unknown>;
|
|
67
|
+
/**
|
|
68
|
+
* Which role shapes a session created without one, the workspace's own
|
|
69
|
+
* session included. A name `profiles` does not define is an error. The
|
|
70
|
+
* YAML-friendly twin of `workspaceOptions.profile`.
|
|
71
|
+
*/
|
|
72
|
+
profile?: string;
|
|
51
73
|
/** Options forwarded to the service-owned workspace's constructor. */
|
|
52
74
|
workspaceOptions?: NodeWorkspaceOptions;
|
|
53
75
|
}
|
|
@@ -72,6 +94,34 @@ declare class MirageService extends Service {
|
|
|
72
94
|
constructor(ctx: Context, config?: MirageConfig);
|
|
73
95
|
/** The live workspace; throws until `ready` resolves. */
|
|
74
96
|
get workspace(): Workspace;
|
|
97
|
+
/**
|
|
98
|
+
* The decision ledger: every ask this world has raised and every
|
|
99
|
+
* answer given, questions still waiting included.
|
|
100
|
+
*
|
|
101
|
+
* The host's handle on the ask layer. A composition with an approval
|
|
102
|
+
* channel answers inline and rarely reads this; one without it finds
|
|
103
|
+
* its pending questions here (`pending()`) and answers them out of
|
|
104
|
+
* band (`answer(id, outcome, scope)`), which is what makes an `ask`
|
|
105
|
+
* rule usable with no human at the keyboard.
|
|
106
|
+
*/
|
|
107
|
+
get decisions(): Workspace['decisions'];
|
|
108
|
+
/**
|
|
109
|
+
* What a line would do here, without doing it: one `Explanation` per
|
|
110
|
+
* command, carrying the outcome, the rule that spoke and the exact
|
|
111
|
+
* exit code and stderr the agent would get.
|
|
112
|
+
*
|
|
113
|
+
* A dry run through the same gate the dispatcher uses, so a host
|
|
114
|
+
* reading this and an agent typing the line cannot be told different
|
|
115
|
+
* things. It spends no ONCE answer, records no question and never
|
|
116
|
+
* reaches the approval channel, which is what makes it safe to call
|
|
117
|
+
* about a line nobody typed — a pre-execute check, or an operator
|
|
118
|
+
* asking why a command was refused.
|
|
119
|
+
*
|
|
120
|
+
* @param line the command line to judge.
|
|
121
|
+
* @param sessionId the session to judge it in; the default when empty.
|
|
122
|
+
* @returns one explanation per command of the line.
|
|
123
|
+
*/
|
|
124
|
+
explain(line: string, sessionId?: string): Promise<Explanation[]>;
|
|
75
125
|
/**
|
|
76
126
|
* True while every runtime in the world reaches only the vfs
|
|
77
127
|
* (`Runtime.reach`), meaning the workspace dispatch is the single
|